likes
comments
collection
share

Vue-Router核心原理的简单实现

作者站长头像
站长
· 阅读数 15

github

实现过程会涉及以下vue内容

  • 插件
  • 混入
  • Vue.observable()
  • 插槽
  • render函数
  • 运行时和完整版的Vue

实现一个VueRouter

VueRouter类图

  • 属性

    • options:存储路由相关的配置moderoutes
    • data:响应式对象,使用observable API处理
    • routeMap:以键值对的形式存储 {路径:组件}
  • 方法

    • installVue 插件需要对外暴露的 install 方法,将来会被Vue.use()使用。
    • constructor:构造函数
    • init 函数:全局初始化。
    • createRouteMap:创建路径path与组件component的映射。
    • initComponents:初始化 Router-ViewRouter-Link 组件。
    • initEvent: 监听浏览器 historypopstate 事件,当页面 URL 变化时,将会触发监听事件。

1. 新建VueRouter

使用Vue-Cli 创建一个Vue项目vue create vue2-router3,并安装vue-router

新建一个vuerouter文件夹,然后在里面新建index.js,当前js文件默认导出一个VueRouter类。

// vuerouter/index.js
export default class VueRouter { }

2. 实现install方法

首先我们思考install方法要实现什么功能?

  • 功能①:判断插件是否已经被安装,如果已经安装则不需要重复安装了
  • 功能②:记录Vue的构造函数到全局变量中,给VueRouter中的实例方法使用,例如router-linkrouter-view这两个组件需要调用Vue.component创建。
  • 功能③:把创建Vue实例时传入的router对象,注入到Vue实例上。我们使用的this.$router就是在此时入住到Vue实例上的。

要实现install方法,也就是实现这三个功能:

// vuerouter/index.js

let _Vue = null
export default class VueRouter{

  // 在VueRouter类里定义一个静态方法install
  static install(Vue) {
    // 实现功能① 
    // 执行install方法时添加一个installed属性,记录插件被安装了
    // 判断该属性为true时,说明已经安装过了,直接return
    if(VueRouter.install.installed){
      return 
    }
    VueRouter.install.installed = true
    
    // 实现功能②
    // 在VueRouter类外面定义_Vue变量
    // 用于接收传过来的Vue构造函数
    _Vue = Vue
    
    // 实现功能③
    // 把创建Vue实例的时候传入的router对象注入到Vue实例上  
    // 这里要用到混入,具体原因如果不懂,可以看下后面的解释  
    _Vue.mixin({
      beforeCreate() {
        if(this.$options.router){
          _Vue.prototype.$router = this.$options.router
        }
      },
    }) 
  }
}

install方法中功能①和功能②比较简单易懂,功能③可能稍微难懂一些。所以再梳理一遍实现思路。

我们在创建Vue项目时,在main.js文件中会使用new Vue({router,render: h => h(App)}).$mount('#app')来创建Vue根组件实例,在这时会传入router对象,我们install方法中的功能③需要做的就是将router注入到所有的Vue实例上,命名$router,即我们经常用的this.$router

想让所有的实例共享一个成员,首先考虑到的是将$router设置到构造函数的原型上,在当前VueRouter/index.js文件中我们存储的Vue构造函数是_Vue,即最终挂载到_Vue.prototype上。

但是我们现在还没有获取到new Vue传过来的routerrouter是在new Vue创建实例时传入的选项,因此想要获取到router必须要在能获取到Vue实例的时候。所以这里要用到混入,给所有的Vue实例混入一个选项,而选项里设置一个beforeCreate钩子函数,这样我们在beforeCreate钩子函数中就可以获取到Vue实例,也就可以获取到传入的options里的router

至此我们就可以在Vue构造函数的prototype上挂载$router ,并赋值为router

补充:

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。详情可以查看Vue.js2官网文档插件部分

3. 实现构造函数constructor

 // vuerouter/index.js

let _Vue = null
export default class VueRouter{

  static install(Vue) {
    ...
  }
  
  constructor (options) {
    this.options = options 
    this.routeMap = {} // 以键值对的形式存储options中传入的routes;键 - 路由地址,值 - 路由组件
    // data应该是响应式的对象 - 使用vue的observable
    this.data = _Vue.observable({
      current: '/'
    })
  }
}

4. 实现createRouteMap()

createRouteMap函数的主要功能就是遍历所有的路由规则(routes),把路由规则解析成键值对的形式,然后存储到routeMap中。

 // vuerouter/index.js

let _Vue = null
export default class VueRouter{

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

5. 实现initComponents()

constructor函数用来注册<Router-Link></Router-Link><Router-View></Router-View>组件。

router-link

  • router-link最终渲染成1个a标签
  • router-linkto属性设置到a标签的herf属性(默认使用history方法)
  • 使用slot插槽,将router-link的内容,插入a标签中
// vuerouter/index.js

let _Vue = null
export default class VueRouter{

  static install(Vue) {
    ...
  }
  
  constructor (options) {
    ...
  }
  
  createRouteMap () {
    ...
  }

  initComponents (Vue) {
    // 实现router-link组件,router-link 最终被渲染成 a 标签
    Vue.component('router-link',{
      props:{
        to:String
      },     
      // 使用render函数实现
      // 这里也可以用template模版实现,
      // 但是需要配置runtimeCompiler为true,使用包含运行时编译器的 Vue 构建版本
      // template:'<a :href="to"><slot></slot></a>'
      render(h) {
        return h('a',{
          attrs:{
            href: this.to
          },
          // 给a标签对应的dom对象注册事件
          on:{
            click: this.clickHandler
          }
        },[this.$slots.default]) 
      },
     methods: {
        clickHandler (e) {
          history.pushState({},'',this.to)
          this.$router.data.current = this.to
          // 阻止默认行为
          e.preventDefault();
        }
      },
    })
  }
}

这里我使用的是render函数,没有用template模版,因为templatevue运行时的版本里不支持,需要使用vue完整版即包含运行时编译器的vue构建版本,但这会应用额外增加 10kb 左右。具体内容可以查看vue-cli官方文档

// vue.config.js
// 配置使用包含运行时编译器的Vue构建版本,在vue.config.js文件里配置runtimeCompiler属性为true即可。
module.exports = {
  runtimeCompiler:true
}

router-view

router-view这里要实现的功能就是根据path路径显示对应的component组件,这里要用到之前的routeMap对象,所以这里需要获取到vue-router的实例,Vue.component里的this指向当前Vue组件并不是VueRouter实例,所以我们先声明一个变量self 用来保存vue-router的实例。

// vuerouter/index.js

let _Vue = null
export default class VueRouter{

  static install(Vue) {
    ...
  }
  
  constructor (options) {
    ...
  }
  
  createRouteMap () {
    ...
  }

  initComponents (Vue) {
    Vue.component('router-link',{...})
    
    const self = this // vue-router的实例
    Vue.component('router-view',{
      render(h){
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }
}

6. 实现init

init就是一个全局的初始化方法,包装了createRouteMapinitComponents两个初始化方法

// vuerouter/index.js

let _Vue = null
export default class VueRouter{

 static install(Vue) {
    if(VueRouter.install.installed){
      return 
    }
    VueRouter.install.installed = true
    _Vue = Vue
    _Vue.mixin({
      beforeCreate() {
        if(this.$options.router){
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()  // ++ 在install里调用全局init方法
        }
      },
    }) 
  }

  
  constructor (options) {
    ...
  }
  
  // 用一个init 方法包装createRouteMap和initComponents两个初始化方法
  init(){
    this.createRouteMap()
    this.initComponents(_Vue)
  }
  
  createRouteMap () {
    ...
  }

  initComponents (Vue) {
    Vue.component('router-link',{...})
    
    const self = this
    Vue.component('router-view',{...})
  }
}

7.体验自己实现的Vue-Router

修改vue-router引入路径,将引入的官方的vue-router改成我们自己写的VueRouter

// router/index.js
...
// import VueRouter from 'vue-router'  // 官方的
import VueRouter from '../vueRouter'   // 我们的
...

最终实现的代码

// vuerouter/index.js

let _Vue = null

export default class VueRouter {
  
  static install(Vue) {
    if(VueRouter.install.installed){
      return 
    }
    VueRouter.install.installed = true
    
    // 把Vue构造函数记录到全局变量
    _Vue = Vue
        
    // 混入
    _Vue.mixin({
      beforeCreate() {
        if(this.$options.router){
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()
        }
      },
    }) 
  }

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

  init(){
    this.createRouteMap()
    this.initComponents(_Vue)
  }

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

  initComponents (Vue) {
    Vue.component('router-link',{
      props:{
        to:String
      },
      render(h) {
        return h('a',{
          attrs:{
            href: this.to
          },
          on:{
            click: this.clickHandler
          }
        },[this.$slots.default]) 
      },
      methods: {
        clickHandler (e) {
          history.pushState({},'',this.to)
          this.$router.data.current = this.to
          // 阻止默认行为
          e.preventDefault();
        }
      },
    })

    const self = this 
    Vue.component('router-view',{
      render(h){
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }
}

启动项目 并 打开浏览器

Vue-Router核心原理的简单实现

Vue-Router核心原理的简单实现

通过点击AboutHome可以实现页面切换,至此我们的VueRouter已经完成大半,但是还是有一些地方需要处理的。

我们当前默认按照history模式实现的,并没有实现根据new Router时传入的mode来使用对应的模式;另外我们点击浏览器的返回前进按钮,会发现浏览器地址发生了变化,但是页面却没有变化。