如何手写一个Vue-Router

171 阅读3分钟

1.准备工作

在开始书写代码前我们需要搞清楚一个问题

Vue.use是干什么用的?

Vue.use官方说明: 通过全局方法 Vue.use() 使用插件,Vue.use 会自动阻止多次注册相同插件,它需要在你调用 new Vue() 启动应用之前完成,Vue.use() 方法至少传入一个参数,该参数类型必须是 Object 或 Function,如果是 Object 那么这个 Object 需要定义一个 install 方法,如果是 Function 那么这个函数就被当做 install 方法。在 Vue.use() 执行时 install 会默认执行,当 install 执行时第一个参数就是 Vue,其他参数是 Vue.use() 执行时传入的其他参数。就是说使用它之后调用的是该组件的install 方法。

其实可以理解为 Vue.use是Vue实例与插件的一座桥梁。 既然是需要手写一个Vue-router自然也就离不开 Vue实例的方法支持。接下来我们整理下代码结构与思路

2.代码结构

手写一个Vue-Router都需要实现那些方法

  1. install 判断当前插件是否被安装,获取到Vue实例将router对象注入到Vue实例
  1. createRouteMap 遍历路由规则 把路由规则解析成键值对的形式存储到routeMap中
  1. initComponent 注册全局组件 router-link router-view
  1. initEvent 监听浏览器窗口回退前进事件动态更新路由信息

4.代码实现

项目基本结构代码我就不再赘述可以参考gitee项目源码

vue-router: 手写vue-router实现 (gitee.com)

首先为VueRouter实现静态方法 install

// MyRouter.jslet _Vue = null
class VueRouter {
    static install(Vue){
        //1 判断当前插件是否被安装
        if(VueRouter.install.installed){
            return;
        }
        VueRouter.install.installed = true
        //2 把Vue的构造函数记录在全局
        _Vue = Vue
        //3 把创建Vue的实例传入的router对象注入到Vue实例
        
        // 此处mixin代表为所有的vue实例注入mixin 也就说明每个vue组件都会执行一次 
        // 但是我们为Vue实例原型上挂载router只需要执行一次 
        _Vue.mixin({
            beforeCreate(){
               // 只有Vue的$options会有router属性 组件实例是没有这个属性的 此处做下判断 避免重复执行
                if(this.$options.router){
                    _Vue.prototype.$router = this.$options.router
                    // 可以在此处添加一个自定义表示属性查看代码是否生效
                    _Vue.prototype.$router.type="MyRouter"
                    // 此处的this指向vue实例
                    console.log(this)
                }
               
            }
        })
    }
​
}
​

1.gif

可以观察到组件实例的 $options 是没有router属性的 并且在Vue实例的router属性上也成功挂载了自定义标识符type:"MyRouter"

接下来继续实现createRouteMap 方法

// MyRouter.js
​
let _Vue = null
class VueRouter {
   ......
   
   constructor (options) {
    // 接收路由信息
    this.options = options
    // 创建map对象存储路由映射关系
    this.routeMap = {}
       
    // observable 创建一个响应式对象存储当前路由信息
    this.data = _Vue.observable({
      current: '/'
    })
  }
​
  createRouteMap () {
    // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }
​
}
​

实现initComponent方法在Vue实例上注册全局组件 router-link router-view

// MyRouter.js
​
let _Vue = null
class VueRouter {
   ......
  constructor (options) {
    this.options = options
    this.routeMap = {}
    this.data = _Vue.observable({
      current: '/'
    })
    this.init()
  }
​
   // 此处采用了render函数的写法构建组件
  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()
        }
      }
     
    })
    const self = this
    Vue.component('router-view', {
      render (h) {
        // 获取当前路由地址所对应的组件
        const cm = self.routeMap[self.data.current]
        // 渲染至路由出口 router-view
        return h(cm)
      }
    })
  }
​
  // 路由初始化
  init () {
    this.createRouteMap()
    this.initComponent(_Vue)
  }
​
}

2.gif 此时手写路由已经可以满足基本路由效果 但是会发现一个问题那就是在浏览器进行前进或者回退操作时路由页面并没有实时更新

接下来实现initEvent 方法监听浏览器前进回退操作同步更新路由页面信息 下面是完整实现代码

let _Vue = nullclass VueRouter {
  static install (Vue) {
    // 1 判断当前插件是否被安装
    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    // 2 把Vue的构造函数记录在全局
    _Vue = Vue
    // 3 把创建Vue的实例传入的router对象注入到Vue实例
    // _Vue.prototype.$router = this.$options.router
    _Vue.mixin({
      beforeCreate () {
        console.log(this)
​
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
​
          // 可以在此处添加一个自定义表示属性查看代码是否生效
          _Vue.prototype.$router.type = 'MyRouter'
          // 此处的this指向vue实例
        }
      }
    })
  }
​
  constructor (options) {
    this.options = options
    this.routeMap = {}
    // observable
    this.data = _Vue.observable({
      current: '/'
    })
    this.init()
  }
​
  init () {
    this.createRouteMap()
    this.initComponent(_Vue)
    this.initEvent()
  }
​
  createRouteMap () {
    // 遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }
​
  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)
      }
    })
  }
  
  
  // 监听浏览器前进回退操作同步更新路由信息 
  initEvent () {
    window.addEventListener('popstate', () => {
      this.data.current = window.location.pathname
    })
  }
}
​
export default VueRouter
​

完整效果展示

3.gif


以上只是个人在代码学习中的一些心得体会和笔记分享 如有错误还望大家能够指出批评