zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Vue2.x - Vue Router

Vue Router Vue2
2023-09-14 09:13:41 时间

目录

Vue与SPA

什么是SPA

简单了解SPA

什么是MPA

SPA相较于MPA的优点

实现SPA的几个关键点

理解SPA单页面架构

什么是前端路由

锚链接与URL中的hash值

通过hashchange事件实现前端路由

编程式导航

浏览器历史记录&前进后退功能

Vue与SPA

通过Vue认识SPA单页面丰富内容的来源

在Vue中实现前端路由

Vue Router

安装和配置Vue Router

下载vue-router

引入vue-router到项目中

注册VueRouter插件

 创建VueRouter实例并将其关联vm.$options.router

路由模块

认识VueRouter

路由器和路由

routes配置属性

路由器的第二个工作

router-view

认识路由组件

什么是路由组件

路由组件的创建和销毁时机

keep-alive组件

两个新的生命周期钩子actived,deactived

路由组件实例上的$route属性

路由组件上的$router属性 

嵌套路由

什么是嵌套路由 

嵌套路由的实现

路由重定向 redirect属性

默认子路由

动态路由

什么是动态路由

$route.params

将$route.params转为组件props

路由配置属性props的三种用法

查询参数路由

什么是查询参数路由

$route.query

通过路由配置属性props简化$route.query的获取

命名路由

什么是命名路由

为什么需要命名路由

router-link to属性的对象式写法

命名router-view

路由配置属性components的对象式写法

命名router-view 

编程式导航

什么是导航

声明式导航

编程式导航

router.push

router.replace

router.go

导航守卫

什么是导航守卫

为什么需要导航守卫

导航守卫的分类

全局导航前置守卫

关于next函数的四种调用方式

全局导航后置守卫

路由元信息属性 meta

路由独享守卫

组件内守卫

完整的导航解析流程 

路由器的两种工作模式

hash模式

histroy模式

最终代码案例


Vue与SPA

什么是SPA

简单了解SPA

SPA(Single Page Web Application)单页面网站应用,顾名思义,只有一个网页的网站。

最简单的SPA其实就是,把一个index.html放在网站服务器上,如下

但是这种SPA网站应该不会有人光顾。因为太单调了,太简单了。

而实际上的企业级SPA网站,是要能够基本替代MPA网站,并超越MPA的网站的。

什么是MPA

MPA(Multiple Page Web Application)多页面网站应用,顾名思义,由多个网页构成的网站。

目前网络上能访问到的大部分网站都是MPA的。

MPA网站的最大特点就是,从一个网页跳转到另一个网页。

MPA网站的网页间跳转,其实本质是变更浏览器地址栏的URL,促使浏览器刷新网页,来发起HTTP GET请求,到服务器获取到最新的网页资源,这将导致两个问题:

  • 消耗大量服务器性能:首先MPA是多页面网站,而浏览器每次只能展示一个页面,所以MPA意味着浏览器会向服务器多次请求不同页面资源。其次,浏览器可能会重复刷新某个页面,导致服务器收到重复页面资源的请求(PS:虽然服务器可以在响应中设置缓存头,促使浏览器缓存请求到的页面资源,防止无意义的重复的页面资源的请求,但是这并不意味着服务器不需要处理重复的页面资源请求,因为当浏览器意识到请求的页面资源已经在自身缓存中时,浏览器还是需要发送请求到服务器验证缓存的页面资源是否过期,如果过期,则服务器需要重新响应页面资源,如果未过期,则需要响应状态码304,告知浏览器可以继续使用缓存的页面资源)
  • 消耗大量浏览器性能:每次网页间跳转,都会导致网页刷新,而网页刷新本质是从服务器请求到新的网页资源,或者从自身缓存中获取网页资源,然后浏览器需要重新渲染整个网页。(网页的渲染是一个极其复杂的流程,首先是构建DOM树,然后是构建CSSOM树,然后将DOM树和CSSOM树结合形成渲染树,然后....反正就是很复杂,所以渲染整个网页是非常消耗浏览器性能的,特别是大量重复无意义的网页刷新行为)

MPA网站还有一些其他的小问题,比如跳转的网页可能会在新页签中打开,这不仅会占用大量浏览器资源,而且会给用户一种混乱的感觉,再比如,每次网页跳转的默认刷新行为都是需要时间的,这段时间会去请求服务器,重新渲染网页等,浪费用户的时间,还有网页刷新会造成网页内容的抖动。

所以MPA的弊端还是挺严重的,但是目前WEB网站,还是MPA模式居多,这是为啥呢?

因为MPA网站开发简单,或者说我们前端开发更熟悉多页面的开发模式。

SPA相较于MPA的优点

那么SPA要如何取代MPA,解决MPA的诸多问题呢?

首先我们来谈谈SPA的优点

  • SPA只有一个页面,当我们在浏览器初次加载SPA网站时,就已经完成了SPA所有(唯一)页面的加载,所以相较于MPA而言,对于服务器的综合性能消耗更小。
  • SPA只有一个页面,所以不存在从一个网页跳转到另一个网页,SPA的都是网页内的内容切换。即SPA用单页面中内容切换,代替了,MPA多页面间跳转。而SPA单页面中的内容切换,其实就是网页的局部DOM更新。这将降低浏览器的渲染压力。

因为SPA不存在多页面间跳转,只存在单页面中内容切换(局部DOM更新),所以不会导致网页刷新,也就不会发生到服务器请求网页资源的行为,所以SPA内容切换的耗时(请求服务器+渲染)将远远小于MPA网页间跳转的耗时。并且由于没有网页刷新发生,所以也不存在网页抖动。

实现SPA的几个关键点

看来SPA无论对于服务器,还是浏览器来说都是友好的嘛!

那么最最最关键的东西来了,SPA如何实现呢?

其实SPA的实现,就是几个关键点的实现:

  • 理解SPA的单页面架构(导航区+展示区)
  • 前端路由的实现(浏览器地址栏URL的改变会引起展示区局部DOM的更新)

理解SPA单页面架构

我们可以将SPA单页面划分出两个基础区域:导航区、展示区。

比如下图中,我们点击导航区的“用户中心”超链接,就会导致展示区的内容改变(局部DOM更新)为用户中心的内容。

如果是MPA的话,我们点击导航区“用户中心”,则用户中心的内容会以一个独立网页的形式呈现,而不是被更新当前网页的局部。

什么是前端路由

什么是路由?

在前端中,路由其实就是:输入在浏览器地址栏的URL  与  浏览器窗口呈现的内容(网页或HTML片段)的对应关系

什么是前端路由,什么是后端路由?

当我们在浏览器地址栏输入URL后,导致网页刷新,促使浏览器发送HTTP GET请求到后端服务器获取对应网页资源。即URL对应的网页来自于后端服务器。此时路由称为后端路由。

当我们在浏览器地址栏输入URL后不会导致网页刷新而是触发了浏览器原生的事件,导致对应事件回调会将准备好的HTML片段更新到网页指定位置。即URL对应的HTML片段来自前端自身。此时路由称为前端路由。(PS:SPA网站虽然只有一个网页,但是同时会存在多个HTML片段,这些HTML片段是作为SPA单页面展示区的内容的,一般来说这些HTML片段会和URL组成路由保存在js文件中,然后SPA单页面再引入该js文件即可。)

锚链接与URL中的hash值

那么输入什么样的URL,才不会导致网页刷新呢?

之前我们学习a标签时,知道a标签的href支持传入一个锚链接,所谓锚链接就是一个#id,点击超链接就可以将网页首部定位到对应id的标签上。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    ul {position:fixed;}
    a {color: white;}
    
    div {height: 950px;}
    #box1 {background-color: skyblue;}
    #box2 {background-color: seagreen;}
    #box3 {background-color: pink;}
  </style>
</head>
<body>
  <ul>
    <li><a href="#box1">box1-skyblue</a></li>
    <li><a href="#box2">box2-seagreen</a></li>
    <li><a href="#box3">box3-pink</a></li>
  </ul>

  <div id="box1"></div>
  <div id="box2"></div>
  <div id="box3"></div>
</body>
</html>

 通过上面例子我们可以发现, 当我们点击锚链接时,不仅网页首部定位到对应id的标签上,而且浏览器的地址栏的URL也发生了变化,比如

初始时,浏览器地址栏的URL为 

http://127.0.0.1:5500/index.html

当我们点击锚链接#box2后,浏览器地址栏的URL为 

http://127.0.0.1:5500/index.html#box2

并且,地址栏URL改变了,并没有导致网页刷新。

⭐这是因为URL中#以后的部分(包含#)URL的hash值,单纯变更URL的hash值,浏览器是不会刷新网页的。这是浏览器内置的一个特性。

⭐另外,如果我们强制刷新对应带有hash的URL的网页,浏览器虽然会发送HTTP GET请求到服务器,但是请求URL中不会携带hash值,即服务器不会收到URL的hash值

 ⭐我们可以通过window.location对象获取到当前地址栏URL的hash值

通过hashchange事件实现前端路由

浏览器有一个原生内置事件hashchange,如果当前网页的URL的hash值改变,则会触发hashchange事件,此时我们就可以实现前端路由了

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul>
    <li><a href="#/home">主页</a></li>
    <li><a href="#/about">关于</a></li>
  </ul>

  <div id="router-view"></div>

  <script>
    // 定义路由
    const routes = [
      {
        hash: '',
        html: '<h1>这是DEFAULT内容</h1>'
      },
      {
        hash: '#/home',
        html: '<h1>这是HOME内容</h1>'
      },
      {
        hash: '#/about',
        html: '<h1>这是ABOUT内容</h1>'
      }
    ]

    window.addEventListener('load', ()=>{alert('页面刷新了')}) // 检测网页是否刷新
    window.addEventListener('DOMContentLoaded', onHashChange)
    window.addEventListener('hashchange', onHashChange)

    function onHashChange(){
      // 当URL的hash改变时,找打对应hash的HTML片段
      const {html} = routes.find(route => {
        return route.hash === location.hash
      })

      // 将对应HTML片段更新到router-view中
      const routerView = document.querySelector('#router-view')
      routerView.innerHTML = html
    }
  </script>
</body>
</html>

编程式导航

目前我们已经根据hashchange事件简单实现了前端路由,但是还有一点美中不足的就是:我们当前只能依赖于a标签锚链接来改变URL中的hash,难道我们所有的导航都要用a标签写吗?

其实我们将a标签锚链接改变URL的hash的方式,叫做“声明式导航”。

除了声明式导航,还有“编程式导航”,编程式导航指的是通过编写代码来改变地址栏中的URL。

基于window.location.hash可读可写属性来实现URL hash的变更

 可以发现,window.location.hash不仅可以(网页无刷新地)改变地址栏的URL的hash,还可以触发hashchange事件

基于window.history.pushState方法或者replaceState方法实现URL hash的变更

 可以发现,window.history.pushState方法或者replaceState方法只能(网页无刷新地)改变地址栏的URL值,但是不能触发hashchange事件。

为什么window.history.pushState或replaceState不能触发hashchange事件呢?因为它们改变的是整个URL的值,而不是URL的hash值。

 下面代码用编程式导航,代替了声明式导航,最终效果一致

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul>
    <!-- <li><a href="#/home">主页</a></li> -->
    <!-- <li><a href="#/about">关于</a></li> -->
    <li><button onclick="location.hash = '#/home'">主页</button></li>
    <li><button onclick="changeURL('#/about')">关于</button></li>
  </ul>

  <div id="router-view"></div>

  <script>
    // 定义路由
    const routes = [
      {
        hash: '',
        html: '<h1>这是DEFAULT内容</h1>'
      },
      {
        hash: '#/home',
        html: '<h1>这是HOME内容</h1>'
      },
      {
        hash: '#/about',
        html: '<h1>这是ABOUT内容</h1>'
      }
    ]

    window.addEventListener('load', ()=>{alert('页面刷新了')}) // 检测网页是否刷新
    window.addEventListener('DOMContentLoaded', onHashChange)
    window.addEventListener('hashchange', onHashChange)

    function onHashChange(){
      // 当URL的hash改变时,找打对应hash的HTML片段
      const {html} = routes.find(route => {
        return route.hash === location.hash
      })

      // 将对应HTML片段更新到router-view中
      const routerView = document.querySelector('#router-view')
      routerView.innerHTML = html
    }

    function changeURL(hash){
      history.pushState(null, null, location.origin + hash) 
      // 由于window.history.pushState或replaceState无法变更URL的hash后无法触发hashchange事件
      // 而我们触发hashchange事件是为了执行其事件回调
      // 所以我们可以直接在pushState或replaceState修改完hash后,自己执行hashchange事件回调
      onHashChange()
    }
  </script>
</body>
</html>

浏览器历史记录&前进后退功能

我们都知道浏览器有一个历史记录,每当我们在浏览器访问一个网页,该网页的URL就会被记录到浏览器的历史记录中。

那么仅变更了hash的URL会被放入历史记录吗?

通过实践,我们可以发现,无论是a标签锚链接,还是location.hash,还是history.pushState修改了URL的hash后,对应的URL都会被加入历史记录中。

浏览器的历史记录本质是一个栈结构,最早访问的网页的URL存储在栈底,最后访问的网页的URL存储在栈顶。

我们可以利用浏览器的前进、后退菜单键,来调出历史栈中的URL。

而URL入历史栈的方式有两种:

  • push模式:该模式URL正常压入栈顶
  • replace模式:该模式URL会覆盖已存在的栈顶URL

而我们通过 

  • a标签锚链接
  • location.hash 
  • history.pushState

修改的URL,其实都是以push模式入历史栈。

而只有history.replaceState支持修改的URL以replace模式入历史栈

通过上面例子,我们可以发现历史记录中:hash为#/home的URL 被 hash为#/about的URL覆盖了 

Vue与SPA

通过Vue认识SPA单页面丰富内容的来源

我们基于Vue脚手架打包后的项目,其实就是一个SPA项目,因为打包代码中只有一个index.html。

 我们来通过Vue创建的SPA项目来验证几个SPA的疑问:

SPA网站虽然只有一个网页,但是有非常多的HTML代码片段,这些HTML代码片段一般保存在js文件中,然后被SPA单页面引入,被在适当时机插入到SPA单页面指定位置。

Vue项目打包后的index.html,原始内容非常简单,但是当被浏览器加载后,则会执行其引入的js文件:app.a0561165.js

 在Vue项目中,HTML代码片段是包含在组件模板中的,而组件模板最终都会变转化为虚拟DOM对象

比如上面文件app.a0561165.js就是Vue项目打包后的一个js文件,在该文件中就能找到对应的虚拟DOM对象的痕迹。 

而这里的虚拟DOM对象,最终会被转化为真实DOM对象,插入index.html的指定位置。

这就是SPA网站虽然只有一个网页,但是依旧可以展示大量内容的原因。

在Vue中实现前端路由

App.vue

<template>
  <div>
    <ul>
      <li><a href="#/home">home</a></li>
      <li><a href="#/about">about</a></li>
    </ul>
    <component :is="componentId"></component>
  </div>
</template>

<script>
  import MyHome from '@/components/MyHome.vue'
  import MyAbout from '@/components/MyAbout.vue'

  export default {
    name: 'App',
    components: {MyHome, MyAbout},
    data(){
      return {
        componentId: null
      }
    },
    methods: {
      onHashChange(){
        switch(location.hash) {
          case '#/home': this.componentId = MyHome; break;
          case '#/about': this.componentId = MyAbout; break;
        }
      }
    },
    created(){
      window.addEventListener('DOMContentLoaded', this.onHashChange)
      window.addEventListener('hashchange', this.onHashChange)
    }
  }
</script>

MyAbout.vue

<template>
  <h1>这是ABOUT内容</h1>
</template>

<script>
  export default {
    name: 'MyAbout'
  }
</script>

MyHome.vue

<template>
  <h1>这是HOME内容</h1>
</template>

<script>
  export default {
    name: 'MyHome'
  }
</script>

怎么说呢,虽然上面代码也可以在Vue项目中实现前端路由,但是非常地不规范,兼容性也很差,所以为了弥补Vue项目的前端路由能力,Vue官方推出了vue-router包。 

Vue Router

安装和配置Vue Router

下载vue-router

Vue2.x的项目需要使用vue-router@3 版本的包,Vue3.x的项目需要使用vue-router@4 版本以上的包。

vue-router包需要被Vue项目在生产环境使用

引入vue-router到项目中

vue-router对外暴露的VueRouter,不仅是一个构造函数,还是一个Vue插件

注册VueRouter插件

注册VueRouter插件,本质是调用其VueRouter构造函数对象的install方法

 本次注册插件,会带来如下影响:

 创建VueRouter实例并将其关联vm.$options.router

Vue构造函数入参配置对象,默认是不支持router属性,但是当我们Vue.use(VueRouter)后,Vue构造函数就被追加处理入参options.router属性的能力了。

路由模块

由于Vue Router最终会处理所有的前端路由逻辑,所以安装和配置Vue Router的逻辑写在main.js中,会让main.js变得臃肿,所以Vue官方推荐我们将安装和配置Vue Router的代码写到路由模块 /src/router/index.js中

认识VueRouter

路由器和路由

VueRouter是一个构造函数,它用于创建一个路由器实例。

路由器的第一个工作就是管理SPA前端路由。

在Vue项目中,前端路由指的是:URL中hash地址 与  组件 的对应关系。

http://localhost:5500/index.html#/home

上面URL中:

#/home  是 URL的 hash值

/home  是 URL的 hash地址

routes配置属性

VueRouter构造函数入参配置对象有一个routes配置属性,它是一个数组,数组元素就是被VueRouter实例管理的前端路由对象。

路由对象有两个基础属性path和component,分别标识 hash地址 和 组件

路由器的第二个工作

路由器除了管理路由外,还会时刻监听浏览器地址栏URL的hash地址的变化,一旦hash地址发生改变,则从自身管理的路由中,找到hash地址对应的component组件,并将其渲染到导致hash地址改变的组件中。 

此时有两个问题:

  • 如何更改浏览器地址栏的URL的hash地址?
  • 路由组件应该被渲染到哪里?

我们知道通过a标签的锚链接可以更改浏览器地址栏中的URL的hash地址,而VueRouter在内部对a标签进行了封装,推出了router-link组件。

router-link组件标签有一个to属性,该属性的值将在router-link标签被点击后,更新为浏览器地址栏URL新的hash地址

 

根据实践结果可以发现,router-link标签本质就是一个a标签,router-link的to属性 是对 a标签的href属性的封装。

router-link除了to属性外,还有一个active-class属性,该属性的作用是:为被点击激活的router-link标签添加指定样式(active-class属性值),且具有排他性。

另外router-link组件还有一个tag属性,该属性默认值为'a',表示router-link组件被编译后会变为一个a标签,我们可以自定义tag属性为任何HTML标签,这样最终router-link组件被渲染出来的就是我们指定的HTML标签

router-link组件标签还有一个replace属性,该属性值默认为false,表示router-link标签修改的浏览器地址栏URL后,URL会被以push模式加入浏览器历史记录栈中,而如果设置replace属性为true,则表示URL会被以replace模式加入历史记录栈。

router-view

router-view组件用于占位。

A组件中router-link修改了浏览器地址栏的URL的hash地址后,路由器就会监听到最新的hash地址,并到自身管理路由中,根据hash地址找到对应的路由组件,最终将路由组件渲染到A组件中router-view占位的地方。

认识路由组件

什么是路由组件

路由中与hash地址对应的组件,就是路由组件。

路由组件与非路由组件最大的区别在于,路由组件不会直接当成组件标签使用,而是完全交给路由器管理。路由器会将路由组件渲染到指定的router-view组件标签占位的地方。

为了在开发过程中,有效区分路由组件和非路由组件,官方推荐将

  • 路由组件 放在 /src/pages 文件夹下
  • 非路由组件 放在 /src/components 文件夹下

路由组件的创建和销毁时机

路由组件被切入时创建,被切出时销毁。

keep-alive组件

keep-alive组件是Vue的内置组件,keep-alive组件可以缓存内容组件的实例,避免组件实例被销毁。

如果我们使用keep-alive组件包裹router-view,则相当于keep-alive组件包裹了路由组件,这样可以缓存路由组件的实例,避免其被切出后销毁。

但是keep-alive默认会缓存所有渲染进router-view的组件,比上面例子中,MyAbout和MyHome组件都会被缓存。

如果我们只想缓存一个指定的组件,则可以使用keep-alive组件标签的include属性的字符串写法 

需要注意,include中写的是组件定义时的name属性值。

如果我们想缓存多个组件的实例,则可以使用include的数组写法 

 keep-alive组件标签还有一个exclude属性,用法和inclue一致,功能相反。

两个新的生命周期钩子actived,deactived

由于keep-alive可以缓存其内容组件的实例,导致组件被卸载后,组件的实例不会销毁,而是进入一种失活状态,当组件被重新装载时,组件的实例不会创建,而是直接去激活keep-alive缓存的组件实例。

而keep-alive缓存的组件实例的失活时机,和激活时机,也被加入了钩子。

 对于路由组件来说,当路由组件被切入时,其actived钩子调用,当路由组件被切出时,其deactived钩子调用。

路由组件实例上的$route属性

当我们Vue.use(VueRouter)后,Vue.prototype上就会被挂载一个$route属性,则所有组件实例上就都有了一个$route属性,该属性是一个Object对象,用于收集组件的路由信息。

路由组件上的$router属性 

当我们Vue.use(VueRouter)后,Vue.prototype上就会被挂载一个$router属性,则所有组件实例上就都有了一个$router属性,并且所有组件实例的$router都指向了同一个VueRouter实例对象,那就是我们传递给vm.$options.router的路由器对象。路由器对象上主要包含了各种导航方法,用于编程式导航。

嵌套路由

什么是嵌套路由 

 嵌套路由,即子路由

  • 子路由的hash地址必然以父路由的hash地址为前缀,如父路由hash地址是/home,子路由hash地址为/home/message
  • 子路由组件必然要在父路由组件中展示,表现为组件嵌套。

比如下面例子中,

当在App组件中router-link到/home时,App组件中router-view中展示就是Home组件。

当在Home组件中router-link到/home/news时,Home组件中router-view展示的就是News组件。

嵌套路由的实现

1、子路由配置属性children

VueRouter构造函数入参配置对象的属性:routes数组的元素,就是一级路由。

路由对象都有一个children属性,该属性也是一个数组,用于配置子级路由,例如一级路由对象的children属性中配置的就是二级路由;二级路由对象的children属性中配置的就是三级路由;

 需要注意的是children中路由对象的path是不需要加 / 前缀的,如上,path: 'news',中news并不需要加/前缀,VueRouter内部会为我们自动添加。

 2、子路由组件的渲染

由于子路由组件,必须要定义在父路由的组件中,比如/home/news就是/home的子路由,所以MyNews组件必然要定义到MyHome组件中,而路由组件都是交给路由器管理的,所以我们只需要在MyHome组件中router-link to /home/news,让路由器找/home/news对应的子组件,然后渲染到router-view中即可

路由重定向 redirect属性

在上面例子中,我们发现了一个问题: 

 当hash地址为/home时,其实等价于/home/,即此时子路由为 ''

而/home的子路由中并没有配置 path: ‘’ 的路由,所以没有对应的子组件被渲染,导致MyHome组件的展示区空白。

 此时,我们可以设置,当路由地址为 /home 时,让其重定向到 /home/news,保证MyHome组件的展示区始终有内容展示。

默认子路由

通过路由重定向,我们可以让 /home 和 /home/news 的展示结果一致。

而 /home 又等价于 /home/,所以路由重定向的作用是,让 /home/ 和 /home/news 路由对应同一个路由组件。

相当于:

所以此时,/home/news路由就没有存在必要了,可以简化为

我们将子路由中 path: '' 的路由,称为默认子路由。 

动态路由

什么是动态路由

在Vue Router中,每一个路由hash地址都要对应一个组件,但是存在一种情况,就是hash地址中存在动态的路径参数

  • /about/001
  • /about/002
  • /about/003

那么此时我们是否需要定义三个组件去分别展示001,002,003呢?如下

虽然可行,但是这种路由定义是有弊端的,如果有001~100的about详情,我们是否要定义100个路由组件呢?

所以Vue Router支持动态路由hash地址。

所谓动态路由hash地址,即将路由hash地址中动态的路径参数用 :参数名 代替,此时动态路由hash地址就可以匹配到 所有复合要求的路由hash了。

$route.params

此时我们已经将多个路由hash,对应到了一个路由组件中。

如:/about/001、/about/002、/about/003 都对应路由组件MyDetail。

那么路由组件应该如何获取 动态路由hash地址中动态路径参数呢?

此时我们需要借助 组件实例上的$route属性,该属性是一个对象,包含当前路由组件的所有路由相关信息

我们可以通过 动态路径参数 会挂载为当前路由组件实例身上$route.params对象的属性。

我们需要注意$route.params对象的属性的名字取决于  定义的动态路径参数名字。

将$route.params转为组件props

当前我们需要通过 $route.params.xxx 获取路径参数,形式比较繁琐。所以Vue Router支持将$route.params.xxx 转为 组件的props属性。而组件的props中属性都将挂载为组件实例的直接属性。

为啥可以将$route.params.xxx 当成 props属性 使用呢?

我们其实可以将 父路由组件中 router-link to 导航到 子路由组件的过程,看成是父路由组件给子路由组件传参的过程

 而to的hash地址中的路径参数,其实就是父路由组件,传递给子路由组件的参数值。

所以这种父子组件传参,子组件通过props配置属性接收,在理论上是合情合理的。

实现props获取动态路由路径参数,我们需要走两步

1、在动态路由中开启props支持,即设置props:true

 2、在动态路由对应的路由组件中,使用props配置属性接收路径参数

路由配置属性props的三种用法

前面已经介绍路由配置属性props的第一种用法,即设置路由对象的props属性为true,这样对应路由组件上的$route.params.xxx就会被交给路由组件的props,简化了我们在路由组件中获取动态路由路径参数的形式。

路由配置属性props还有另外两种用法:

  • 对象式
  • 函数式

我们可以利用路由配置属性props对象 传递一些固定数据给路由组件

 路由配置属性props还可以写成函数,此时props函数可以接收一个默认入参 $route,该入参就是当前路由组件实例的$route属性

查询参数路由

什么是查询参数路由

即 路由hash地址中带有查询参数,比如 /home/news?id=001

hash地址中的查询参数不会对hash地址造成影响,即 /home/news 和 /home/news?id=001 都会匹配到 /home下的子路由news。

$route.query

其实带有查询参数的路由,也可以看作是一种 父路由组件 向 子路由组件 传参的方式。

在子路由组件中,我们可以通过 组件实例上的$route.query属性获取到查询参数

通过路由配置属性props简化$route.query的获取

目前在路由中查询参数的获取,要通过 $route.query.xxx ,形式非常繁琐,此时我们可以通过路由配置属性props来简化其获取形式

我们知道 路由配置属性props 支持三种模式:布尔式、对象式、函数式

其中props函数可以接收到对于路由组件实例的$route属性作为入参,所以

命名路由

什么是命名路由

简单说,就是给路由配置对象增加一个name属性,即给路由定义一个别名。

如下面例子,给/home/news定义了一个别名xinwen

为什么需要命名路由

随着嵌套的路由越来越多,router-link的to属性指向的hash地址也就越来越长,这将使得router-link的to属性的书写非常繁琐。

所以 Vue Router 支持给路由定义别名,让router-link的to属性指向路由的别名  来替代 冗长的hash地址。

命名路由需要结合router-link组件标签的to属性的对象式写法使用。

router-link to属性的对象式写法

router-link to属性的值是一个hash地址,而目前hash地址包括三个部分:

  • 路由静态路径,如/home
  • 动态路径参数,如/home/about/001
  • 查询参数,/home/news?id=001

目前,我们都是将这三个部分通过字符串拼接的方式定义在一起的,可读性很差。

所以to属性支持对象式写法,将这三部分作为to对象的属性

 需要注意的是,如果router-link to的是动态路由,则to对象式写法只能导航到 路由别名,而不能导航到 路由hash地址。因为Vue Router不会将to对象中的path和params组合后进行路由查找,而是直接查找path对应的路由

 而to对象使用name查找路由的话,则会考虑进params

命名router-view

路由配置属性components的对象式写法

router-view的作用是占位,当router-link改变了地址栏hash地址后,路由器就会将对应hash地址的路由组件component渲染到router-view中。

但是一个router-view只能渲染一个路由组件,即使我们将router-view定义多次,也只是将一个路由组件渲染多次而已。

所以,为了能够在提供多种路由组件给router-view,可以使用路由配置属性components

命名router-view 

路由器根据hash地址找到了对应路由配置对象的components属性,然后路由器会将:

  • components.default对应的组件  渲染到  未命名的router-view中
  • components.xxx对应的组件  渲染到  命名为xxx的router-view中

编程式导航

什么是导航

Vue Router中导航的意思就是改变浏览器地址栏的URL的hash地址,来促使内容切换。

声明式导航

  • router-link组件的to属性可以改变浏览器地址栏的URL的hash地址
  • a标签的href属性可以改变浏览器地址栏的URL的hash地址

上面这种通过标签点击改变hash地址的方式,称为声明式导航

编程式导航

使用代码来改变浏览器地址栏的URL的hash地址 的方式就是编程式导航。

之前介绍SPA路由时,我们尝试了:

  • location.hash
  • history.pushState,history.replaceState

来改变hash地址,这些方式就是编程式导航。

而Vue Router对这些方式进行了更加完整的封装,并定义为了 路由器实例 router的方法

  • router.push(hash | routeObj,onComplete, onAbort)
  • router.replace(hash | routeObj,onComplete, onAbort)
  • router.go(n)

编程式导航相较于声明式导航的自由度更高,可以进行异步导航。

router.push

router.push有三个入参

  • hash | routeObj:hash即hash地址,routeObj即相当于router-link to的对象
  • onComplete:可选,成功回调
  • onAbort:可选,中止回调

router.replace

router.replace与router.push用法相同,只是在URL存入历史记录栈的方式不同。

router.push是将URL压入栈顶

router.replace是先将栈顶URL出栈,再将replace的URL压入栈顶,表现为替换掉栈顶URL。

router.go

router.go只有一个入参,入参是一个数值。

该方法是模拟浏览器的前进、后退菜单键功能。

入参正数值,表示前进n个,入参负数值,表示后退n个。

导航守卫

什么是导航守卫

所谓导航守卫,就是导航拦截。

即,当你访问某hash地址对应的路由组件时,路由器会先拦截你的访问,然后校验你的权限信息,如果权限合法,则放开拦截,让你访问对应路由组件,否则就保持拦截。

为什么需要导航守卫

可能大家一听到权限校验相关的东西,下意识决定应该是后端的事情,但是实际企业开发中,基本上所有的校验都要求前后端保持一致。

如果前端未加校验,只在后端加校验,则虽然有后端兜底,不会发生安全问题,但是前端给用户的体验会变差,让用户怀疑网站的安全性。

如果前端加了校验,后端没加校验,则黑客可以绕过前端的校验,直接与后端对话,造成安全问题。

说白了,前端校验就是个样子工程,不做虽然不会导致大问题,但是面子过不去。

所以,前端是需要导航守卫的,虽然是个面子工程。

导航守卫的分类

  • 全局导航守卫:前置、后置
  • 路由独享守卫
  • 组件内的守卫:路由进入前,路由离开后

全局导航前置守卫

全局导航前置守卫,就是路由器实例的一个方法beforeEach,该方法接收一个回调函数作为入参。

当导航触发时,beforeEach的参数回调函数就会被调用,底层会依次传入

  • to:到哪个路由去
  • from:从哪个路由来
  • next:放行函数

作为beforeEach的参数回调函数的入参。

下面例子,定义了一个全局导航前置守卫

 全局导航前置守卫的回调函数执行时机是:

1、项目首次加载时,此时相当于hash地址从 /  到  /

 2、hash地址发生改变时,如下例中,hash地址从 / 变为 /home/news

关于next函数的四种调用方式

next函数是放行函数,它有四种调用方式:

  • next():放行到目的路由
  • next(hash):放行到指定hash地址对应的路由
  • next(false):不放行,阻止路由跳转,保持在本路由
  • next(err):err是一个Error对象,此时next执行会触发router.onError的参数回调函数
router.beforeEach((to, from, next) => {
  const token = JSON.parse(localStorage.getItem('token'))
  let permission = 0; // 未登录

  if (token) {
    const { name, pass } = token
    if (name == 'qfc' && pass == '123456') {
      permission = 2 // 已登录,最高权限
    } else {
      permission = 1 // 已登录,普通权限
    }
  }

  if (to.path === '/login' || permission === 2) {
    return next()
  }

  if (permission === 0) {
    alert('未登录,请先登录')
    return next('/login')
  }

  if (permission === 1 && to.path.startsWith('/about')) {
    alert('权限不足,访问被中止')
    return next(false)
  }

  next()
})

 上面例子分别演示了next放行函数的三种常用用法。

我们需要注意的是,导航守卫中next方法能且只能严格调用一次。

  • 如果导航守卫中next方法被一次也没被调用,则导航流程阻塞
  • 如果导航守卫中next方法被调用了多次,理论上只有最后一次有效

全局导航后置守卫

全局导航后置守卫,就是路由器实例的一个方法afterEach,该方法接收一个回调函数作为入参。

当导航结束时,afterEach的参数回调函数就会被调用,底层会依次传入

  • to:目的路由对象
  • from:源路由对象

作为afterEach的参数回调函数的入参

和beforeEach参数回调函数入参不同的是,afterEach的参数回调函数没有next函数,这表明了afterEach并不会起到拦截作用

afterEach参数回调的调用时机是:

  • 当路由切换成功时,afterEach参数回调被调用。
  • 当路由切换失败或者被阻止,则afterEach参数回调不会被调用。

  

通过上面例子,我们可以发现,

  • 只要触发导航,则beforeEach的参数回调就会调用,无论导航最终成功还是中止
  • 而afterEach的参数回调只有在导航成功完成时,才会被调用

afterEach的作用是,当导航到目标路由后,做一些beforeEach中不好做的统一收尾工作。

比如我们想要变更document.title为当前路由的对应的信息,如果我们在beforeEach中处理的话

可以发现在beforeEach中需要在每次next调用前进行document.title的设置,即需要设置多次,并且要考虑路由跳转中止的话,不能使用to.path作为document.title,只能使用from.path作为document.title。

而在afterEach中,只需要设置一次,且由于afterEach的参数回调只有在路由跳转成功后才会调用,所以可以仅使用to.path作为document.title。

路由元信息属性 meta

前面在实现路由校验时,除了要校验用户登录状态,还要校验用户是否有访问对应路由的权限。而当前,我们只能在router.beforeEach中判断各种路由path是否需要权限,此时router.beforeEach中的逻辑非常难看,且后期不容易维护

 此时我们可以在路由对象中为路由设置元信息,来标记该路由是否需要权限校验

然后在router.beforeEach参数回调中就不需要维护固定path的权限校验了,而是改为统一权限校验

 

路由独享守卫

路由独享守卫 只作用于 当前路由,即当导航到当前路由时,会触发当前路由的独享守卫。

我们可以在路由配置对象中,定义路由独享守卫属性beforeEnter,它是一个函数,底层依次传入

  • to
  • from
  • next

三个入参给beforeEnter。

beforeEnter的调用时机为:当导航到beforeEnter所在路由时,就会触发beforeEnter的执行。

我们可以发现beforeEnter入参的to,永远都是自己所在的路由。 

路由独享守卫,可以做一些路由私有的权限校验。

而如果一个路由,既被路由独享守卫作用,也被全局导航守卫作用,则各守卫执行顺序如下

 可以发现,执行顺序是:全局导航前置守卫 => 路由独享守卫 => 全局导航后置守卫

组件内守卫

组件内路由守卫有三个

  • beforeRouteEnter(to, from, next)
  • beforeRouteUpdate(to, from, next)
  • beforeRouteLeave(to, from, next)

而组件内路由守卫,既可以当成导航守卫使用,也可以当成路由组件特有的生命周期钩子使用。

需要注意的是,这三个钩子函数,只有路由组件才有,非路由组件没有

其实一个组件既可以用作路由组件,也可以用作非路由组件。如下例中,MyLogin既当成了非路由组件使用,又当成了路由组件使用。

  • 作为非路由组件的MyLogin来说,它无法触发上面三个钩子。
  • 作为路由组件的MyLogin来说,它可以触发上面三个钩子。

其根本原因是,当路由组件是由路由器管理渲染的,而路由器在渲染路由组件的过程中,会调用上面三个钩子。

而非路由组件不是由路由器管理渲染的,所以即使非路由组件定义了上面三个钩子,也没有路由器去调用。 

总结,只有组件被路由器渲染时,才会触发组件的beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave三个钩子。

beforeRouteEnter是在路由组件被渲染之前,即被创建之前调用的,因此此时beforeRouteEnter中是无法访问到组件实例this的。

beforeRouteUpdate 是在路由组件更新时触发的,这里的更新特指:动态路由之间切换时,比如/about/001到/about/002

 并且,此时beforeRouteUpdate 中可以访问到组件实例。

beforeRouteLeave是在路由组件被卸载之前调用的,即从当前路由 导航到 其他路由前调用

需要注意的是,beforeRouteLeave会在全局导航前置守卫前执行。

beforeRouteLeave可以用来提示用户输入未保存,阻止其跳转到其他路由.

完整的导航解析流程 

当一个导航触发后,必然是从一个路由from 跳转到 另一个路由to;

如果from、to都是同一个动态路由,则

  1. beforeEach触发
  2. to路由的beforeRouteUpdate触发
  3. afterEach触发

如果from、to有一个不是动态路由,则

  1. from路由的beforeRouteLeave触发
  2. beforeEach触发
  3. to路由的beforeEnter触发
  4. to路由的beforeRouteEnter触发
  5. afterEach触发

路由器的两种工作模式

hash模式

VueRouter的默认工作模式就是hash模式。

而hash工作模式,指的是:路由器通过监听浏览器地址栏中URL的hash地址的变化,来实现前端路由。

此模式的特点是,浏览器地址栏中URL中必定有一个#,如

 不适合有强迫症的人群。但是此时模式兼容性好。

并且URL中的hash地址仅用于前端路由,不会发送给后端服务器。

histroy模式

所谓history模式,即路由器监听的是浏览器地址栏URL的变化,此时URL不需要携带#。

比如 localhost:8080/about

我们可以在定义路由器实例的VueRouter入参配置对象中增加一个属性:mode: 'history'

来开启路由器的history工作模式。

注意观察上面例子中,浏览器地址栏的变化,可以发现此时地址栏URL不再带有#。

这种工作模式的优点是:提升了强迫症患者的视觉感受

但是history模式的缺点却十分致命:

  • 兼容性不好,一些老版本的浏览器不支持history模式
  •  history模式下的网页刷新可能会导致发送未知的请求到服务器,如果服务器未做特殊处理的话,则SPA应用将发生错误。

对于第二种缺点,我们可以将Vue项目打包后部署到一个服务器上

 如上图,我们利用node+express创建一个静态资源托管的服务器,并将SPA项目部署到了服务器静态资源目录,之后启动了服务器。

可以发现,如果我们按照SPA单页面中定义好的导航进行跳转,则没有任何问题。但是当我们刷新网页时,或者通过浏览器地址栏访问某SPA路由时,SPA应用就出错了。

原因很简单,由于是history模式,所以此时前端路由的URL,和后端路由的URL没有任何区别,当我们刷新网页时,或者通过浏览器地址访问某SPA路由时,会触发两种行为:

  • 前端路由:路由器监听到URL的变化,开始准备对应组件渲染到展示区
  • 后端路由:浏览器监听到地址栏URL的变化,开始发送HTTP GET请求到服务器获取对应资源

其中后端路由会失败,因为虽然此时地址栏URL本质还是前端路由,在服务器上并没有对应路由的接口。

比如:localhost/home/news?id=001

当我们刷新网页时,浏览器就会发送一个HTTP GET请求到服务器获取对应资源

此时的解决方案是,让服务器定制对应的前端路由URL对应的接口,然后响应200给前端,让前端不会报错即可。

而express有一个第三方中间件connect-history-api-fallback - npm (npmjs.com)

 就可以帮助后端express服务器自动处理这些前端路由URL。

最终代码案例

GitHub - qwx427219/Vue_SPA_Routericon-default.png?t=M5H6https://github.com/qwx427219/Vue_SPA_Router.git