Vue源碼系列:Vue-Router

172 阅读2分钟

整理一下在拉勾前端训练营的Vue的知识,所以写下这篇文章。

回顾

Vue-Router是Vue中常用的插件。先简单回顾一下Vue-Router是做什么和怎样用。

Vue-Router是官方的路由器,用来构建单页面应用。所谓单页面应用,就是当用户点击超链接时,浏覧器不会向服务端发起请求,然后跳转到下一个网页,而是如平常的桌面的应用般,自然切换界面。

简单回顾下如何使用。首先用vue的脚手架创建项目,该项目要有vue-router插件,然后在src/view里添加一些vue文件作为界面,然后在src/router里建立index.js,构建路由。代码如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'

Vue.use(VueRouter)


const routes = [
  {
    path: '/',
    name: 'Index',
    component: Index
  },
  {
    path: '/blog',
    name: 'Blog',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import('../views/Blog.vue')
  },
  {
    path: '/photo',
    name: 'Photo',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import('../views/Photo.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

首先Vue要注册一个Vue-Router插件,创建路由规则,component用箭头函数动态加载作为view的vue文件,即当切换至该页面才加载。最后创建一个VueRouter实例,引入路由规则,输出实例。项目的主文件中引入路由器。

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

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

关于路由JS的部分已经完成,剩下就是在html显示出来,这要用到router-link和router-view。router-link类似于超链接,不同的是它不会使页面跳转,而是根据它指向的路径切换router-view的界面。 html页面中的路由部分如下所示:

<div>
  <router-link to="/">Index</router-link> |
  <router-link to="/blog">Blog</router-link> |
  <router-link to="/photo">Photo</router-link>
</div>
<router-view/>

Vue-Router插件实现

Install

现在开始实现插件。在Vue里,所有的插件都必须有install静态方法,这样Vue才能通过执行它,加载插件。我们可以把install理解为一种规范,或如果在面向对象语言,则是以接口形式出现。 实现install方法前,看一下加载了路由器的Vue实例有什么特别。 首先会在options属性里,多了router,而里面又有一个options,含有routes属性,它带有我们之前设定的路由规则。

Vue_Instance

所以install要做的是三件事:

  1. 判断是否安装。如果安装了,则不用再安装
  2. 把引入的Vue作为全局对象
  3. 把Vue实例所加载的路由注册至全局的Vue

第一件事比较容易实现,我们只要用一个boolean变量和判断就能做到。

class VueRouter {
  static install(Vue) {
    if(VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
  }
}

如果有一个VueRouter.install.installed为true,表示已经安装了,退出方法。 我们在class外面定义一个变量,之后可以全局引用传来的vue实例。

let _vue = null
...
  _vue = Vue

接下来要做的是把vue实例的router引入至vue。这要怎样做呢? 这里要运用mixin(混入)。所谓混合,就是把一部分功能拿出来,可以在不同的vue实例中重複使用。

...
_Vue.mixin({
  beforeCreate(){
    if(this.$options.router){
       _Vue.prototype.$router = this.$options.router
                    
    }
               
  }
})
...

我们用mixin全局注册一个生命周期事件,当一个vue实例创建前,看一下当前实例有没有router属性,有的话,则在原型上添加该路由,全局可以引用。$options是用来读取vue实例中自定义的属性,自定义的属性都放在options属性里。注意一下这里的this,这里的this是vue实例的this,而不是vue-router,因为this的指向是以引用时的位置决定,beforeCreate是在vue实例中执行,所以是指向该vue实例。

Constructor 构造函数

现在开始写构造函数。代码如下:

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

构造函数的形参是一个对象,传来一系列属性,如mode和routes等,routeMap用来保存路径与组件之间的映射,data保存当前路径,使用Vue的observable方法,把它变成响应式数据,如果data发生变化,vue会自动作出相应更新。 之后我们要研究Vue-Router的初始化方法。

createRouteMap

这个方法是解析options,建立路径与组件之间的映射,存入routeMap。

createRouteMap() {
  this.options.routes.forEach(route => {
    this.routeMap[route.path] = route.component 
  })
}

initComponent(Vue)

initComponent创建router-link和router-view组件。router-link组件在浏覧器显示的是超链接,所以我们大概知道这组件要怎样写。

initComponent(Vue) {
  _Vue.component("router-link", {
    props: {
      to: String
    },
    template: '<a :href="to"><slot></slot></a>'
  })
}

不过这样写,当一在html里使用,发生报错,原因是Vue的脚手架生成的Vue模块不是完整版的,运行时没有编译器进行编译 (平时我们写的vue文件,运行项目前已经预先编译)。解决方法有两种,一是添加一个运行时编译器,不过要多加载10kb内容,二是用渲染函数。这里用第二种方法:

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()
      }
    }
})

渲染函数render有一个参数,是一个函数,它用来创造元素,它接收三个参数,第一个是创造元素类型,这里是'a',第二个是元素的设定,如属性,方法等,它是一个对象。attrs里可以设定元素的属性,而on可以挂载方法。attrs的不多说,一目了然,重点说一下on里的方法。

当用户点撃router-link,不会发生跳转,但浏覧器的路径发生改变,而且router的data属性也要改变。不发生跳转可以用preventDefault,而data修改为to的值就可以了。浏覧器的路径要怎样发生改变?可以使用history.pushState。

history.pushState简单来说,就是为浏覧器历史添加记录。前两个参数用不上就不管,关键是最后一个,它可以修改浏覧器路径。

好啦,现在router-link的属性和方法已经设定好,最后一个参数可以传入一个数组,作为文本的值。this.$slots.default可以获取没有名字的插糟的默认值。

最后创建router-view组件,它的作用就是把当前路径对应的组件渲染出来。 我们要做的事:

  1. 找出当前路径对应的组件
  2. 渲染组件出来 第一件简单,用this.data.current得到当前路径,然后在routeMap得到组件。 第二件事可以用渲染函数直接把组件渲染出来。
const self = this
Vue.component("router-view",{
  render(h){
    const cm=self.routeMap[self.data.current]
    return h(cm)
  }
})

注意self是保存Vue-Router的引用,因为在Vue.component的this是指向Vue。

initEvent

Vue-Router插件基本已经完成,然而如果用户返回前一页,浏覧器的界面没有什何反应,因为浏覧器只是改变路径,所以我们还需要加上事件,使vue有所反应,所应渲染界面,而popstate则是为此而生,当返回上下页,调用popState注册的事件。

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

init

基本所有初始化方法已经完成,用init方法封装。

init(){
  this.createRouteMap()
  this.initComponent(_Vue)
  this.initEvent()
}

最后放在构造函数里。简单的Vue-Router就完成啦。