Vue-router原理揭秘

114 阅读2分钟

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 展示由 Vue.js 的过渡系统提供的过渡效果
  • 细致的导航控制
  • 自动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

接下来了解下其基本用法

基本用法

利用vue-cli生成项目,项目配置中选择vue-router,生成完成后运行npm run serve打开项目,在src/router/index.js中引入VueRouter,并且配置好路由对象:

import Vue from 'vue'
import VueRouter from 'vue-router'
import pageA from '../views/pageA.vue'
import pageB from '../views/pageB.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'pageA',
    component: pageA
  },
  {
    path: '/pageB',
    name: 'pageB',
    component: pageB
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Vue.use(VueRouter)用来注册路由插件,Vue.use会调用传入对象的install方法,routes为我们定义的路由规则,再通过new VueRouter()方法创建路由对象,最后导出router路由对象。

在view文件下定义两个组件:

import pageA from '../views/pageA.vue'
import pageB from '../views/pageB.vue'

src/main.js的vue构造函数中传入路由对象:

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这里会在vue实例上加上routeroute和router两个属性,其中route里面存储了一些路由规则参数等路由信息,route里面存储了一些路由规则参数等路由信息,router上定义了路由的各种方法。

最后再src/app.vue中加入<router-view/>占位符,当匹配到对应的路由时会加载对应的组件来替换掉占位符。同时创建链接:

<router-link to="/">Home</router-link> |
<router-link to="/pageB">About</router-link>

运行npm run serve点击按钮既可以看到路由切换。

动态路由

在开发一些详情页时,可能需要这样的路由detail/1detail/2,这就需要使用到动态路由,可以这样定义路由规则:

{
    path: '/detail/:id',
    name: 'Detail',
    props:true,    
    component: () => import ('../views/Detail.vue')
}

这里定义组件时使用了路由懒加载,只有用户访问组件时才会加载该组件,接收id有两种方式:

  • 通过$route.params.id
  • 第二种通过props接收
<template>
  <div class="about">
    <span>当前路由id:{{$route.params.id}}</span>
    <h2>{{id}}</h2>
    <h1>This is Detail</h1>
  </div>
</template>

<script>
export default {
  props:['id'],
  name:'Detail'
}
</script>
嵌套路由

如同官网所说,一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:

/user/johnny/profile                     /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

这里定义一个layout组件,里面写上了公共的头部和尾部,其余部分需要路由匹配。

//layout.vue
<template>
  <div>
    <h1>这里是头部</h1>
    <router-view></router-view>
    <h1>这里是底部</h1>
  </div>
</template>

<script>
export default {
  name:'layout',
}
</script>

这样定义下路由规则:

{
    path: '/',
    component: layout,
    children:[
      {
        path: '/pageB',
        name: 'pageB',
        component: pageB
      },
      {
        path: '/pageA',
        name: 'pageA',
        component: pageA
      },
      {
        path: '/detail/:id',
        name: 'Detail',
        props:true,
        component: () => import ('../views/Detail.vue')
      }
    ]
  },

这样当路由匹配到pageB或者pageA时就能加载layout组件内的东西了。

编程式导航

跳转路由除了上面的<router-link to="/">Home</router-link> ,在$router上定义了其他的方法:

  • $route.push()

    这个方法可以通过路由跳转,

    this.$router.push('/pageA')
    

    也可以通过定义路由规则时的name跳转,这种方式可以加个params给路由传参

    this.$router.push({name:'Detail',params:{
       id:15
    }})
    
  • $route.replace()

    这个方法同样也可以实现路由跳转

    this.$router.replace('/pageA')
    

    区别不同的就是replace方法是在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。

    同样我们也可以返回上一条历史记录:

    this.$router.go(-1)
    

    或者是

    this.$router.back()
    
路由守卫
  • router.beforeEach 全局前置守卫

接受两个参数to和from,to是即将进入的目标,from是导航离开的路由。

如果返回false,表明取消本次导航,如果导航地址更改了,会重定向到from路由对应的地址;如果返回的是一个路由地址,就相当于调用router.push一样,跳转一个新的路由。

  • router.beforeResolve 全局解析守卫, 这和 router.beforeEach 类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。
  • router.afterEach() 全局后置钩子, 可以做一些分析、更改页面标题、声明页面等辅助功能 ,无法更改路由。
  • beforeEnter(),这个是路由独享的守卫,可以再配置路由规则时定义,会在路由进入时触发
  • beforeRouteEnter(),beforeRouteUpdate(),beforeRouteLeave()这三个都是组件内的守卫,用法基本类似。
Hash模式和History模式

在我们利用VueRouter创建路由组建时,有一个mode字段,这个就是申明我们的路由以何种模式来运行的,

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

这里使用的是history模式来创建路由,还可以使用mode:hash来创建hash模式的路由,这两种模式不管表现形式还是原理差距都很大,接下来一一介绍下:

  • hash

    监听浏览器地址hash值变化,执行相应的js切换网页 。使用window.location.hash属性及窗口的onhashchange事件,可以实现监听浏览器地址hash值变化。其主要有以下几个特点:

    • hash指的是地址中#号以及后面的字符,也称为散列值。hash也称作锚点,本身是用来做页面跳转定位的。如[http://localhost/index.html#abc,这里的#abc就是hash;]

    • 散列值是不会随请求发送到服务器端的,所以改变hash,不会重新加载页面;

    • 监听 window 的 hashchange 事件,当散列值改变时,可以通过 location.hash 来获取和设置hash值;

    • location.hash值的变化会直接反应到浏览器地址栏;

  • history

    利用history API实现url地址改变,根据当前路由地址找到对应组件重新渲染。 window.history 属性指向 History 对象,它表示当前窗口的浏览历史。当发生改变时,只会改变页面的路径,不会刷新页面 。而且History 对象保存了当前窗口访问过的所有页面网址。通过 history.length 可以得出当前窗口一共访问过几个网址。浏览器工具栏的前进和后退按钮,其实就是对History对象进行操作。

    History主要有两个属性:

    • History.length:当前窗口访问过的网址数量
    • History.state:History堆栈最上层的状态值

    History对象主要由五个静态方法:

    • History.back(): 移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果
    • History.forward(): 移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
    • History.go(): 接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。
    • History.pushState(): 该方法用于在历史中添加一条记录,不会刷星页面,会导致History对象和地址栏发生变化,接受三个参数:
      • object,一个对象, 通过 pushState 方法可以将该对象内容传递到新页面中。如果不需要这个对象,此处可以填 null。
      • title,指标题,几乎没有浏览器支持该参数,传一个空字符串比较安全。
      • url, 新的网址,必须与当前页面处在同一个域。
    • History.replaceState(): 该方法用来修改 History 对象的当前记录,用法与 pushState() 方法一样

    而history模式还有一个问题需要注意下,单页面应用实际上就是我们在第一次进入页面时,向服务器发送请求,获得数据,后续的路由更改并不会向服务器发送请求,而是通过路由匹配对应的组件来实现页面更新,而history部署在不同的服务器上,需要配置下服务器,不然就会请求一个不存在的地址并不会匹配到404组件,而是一个会返回一个404的状态码,用一个例子更加情绪的描述下问题:

    首先将上述项目运行npm run build打包,新建一个web目录,将打包的dist目录下的文件全部拖入到改web文件下,新建一个fuwu目录,安装两个模块expressconnect-history-api-fallback,简单的搭建一个静态服务器

    //app.js
    const path = require('path')
    // 导入处理 history 模式的模块
    const history = require('connect-history-api-fallback')
    // 导入 express
    const express = require('express')
    
    const app = express()
    // 注册处理 history 模式的中间件
    // app.use(history())
    // 处理静态资源的中间件,网站根目录 ../web
    app.use(express.static(path.join(__dirname, '../web')))
    
    // 开启服务器,端口是 3000
    app.listen(3001, () => {
      console.log('服务器开启,端口:3001')
    })
    
    

    node app.js启动下这个服务,就可以看到我们的项目了,点击按钮也能跳转对应的路由,在路由规则中添加一个404的路由:

    {
        path: '*',
        component: nothing
    },
    

    当我们在导航栏中输入pageB1,这是一个不存在的路由,并不会命中nothing组件,而是直接返回一个404的状态码,这时就需要在我们的服务器中添加一个中间件,app.use(history()),再重新启动下我们的服务器,再次输入不存在的路径时,就会匹配我们的nothing组件了。

    如果是nginx服务的话,就需要修改下conf/nginx.conf文件:

    location / {
    	root html;
    	index index.html index.htm;
    	#新添加内容
    	#尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页
    	#如果都获取不到返回根目录中的 index.html
    	try_files $uri $uri/ /index.html;
    }
    
    
手写vue-router

首先安装vue-cli,使用脚手架创建一个vue项目,准备在这个项目中验证我们的路由系统。在项目的根目录下新建vue-router目录,目录下新建index.js,首先编写下install方法:

let _Vue = null
export default class VueRouter {
    static install(Vue){
        //1 判断当前插件是否被安装
        if(VueRouter.install.installed){
            return;
        }
        VueRouter.install.installed = true
        //2 把Vue的构造函数记录在全局
        _Vue = Vue
        //3 把创建Vue实例传入的router对象注入到Vue实例
        _Vue.mixin({
            beforeCreate(){
                if(this.$options.router){
                    _Vue.prototype.$router = this.$options.router
                }
               
            }
        })
    }

}

这里首先判断了当前插件是否被安装,用一个变量记录,如果被安装了就不在走后续流程;随后将install函数中传入的vue实例记录到全局,后续需要用到实例上的很多方法;最后将创建Vue实例时传入的router对象挂载到Vue实例,这里借用了混入方法,在beforeCreate()钩子函数里做一下判断,只给vue实例去挂载,而组件就不需要了。

接着编写下类的构造器:

constructor(options){
    this.options = options
    this.routeMap = {}
    // observable
    this.data = _Vue.observable({
        current:"/"
    })
    this.init()
}

该构造器挂在了三个属性到路由实例上,options就是传入的route数组,包含路由和组件,routeMap就是路由与对应组件的依赖,而data是由_Vue.observable创建的响应式对象,里面的current代表当前路由。

紧接着解析下传入的options,将对应的路由和组件存入routeMap

createRouteMap(){
    //遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
        this.routeMap[route.path] = route.component
    });
}

接下来就开始编写route-link组件和route-view组件

initComponent(Vue){
    Vue.component("router-link",{
        props:{
            to:String
        },
        render(h){
            return h("a",{
                attrs:{
                    href:this.to
                },
                on:{
                    click:this.clickhander
                }
            },[this.$slots.default])
        },
        methods:{
            clickhander(e){
                history.pushState({},"",this.to)
                this.$router.data.current=this.to
                e.preventDefault()
            }
        }
        // template:"<a :href='to'><slot></slot><>"
    })
    const self = this
    Vue.component("router-view",{
        render(h){
            // self.data.current
            const cm=self.routeMap[self.data.current]
            return h(cm)
        }
    })
}

编写组件时使用render函数,render函数接受一个函数作为参数,根据该函数来渲染成虚拟dom。该函数接受三个参数,第一个创建元素的选择器;第二个设置元素的属性,这里通过attrs设置了元素的href属性和通过on绑定了的click事件;第三个设置元素的内容,这里是获取了默认插槽的内容。

在最后监听了popstate事件,根据前言,点击浏览器的前进和后退就会触发该函数,在回调中设置了data中的current,由于data是响应式数据,一旦更改就会重新更新视图。

initEvent(){
    //
    window.addEventListener("popstate",()=>{
        this.data.current = window.location.pathname
    })
}

代码地址