Vue-router源码解读与实现

在手撕之前,可以先了解前端路由到底是什么,前端路由都有哪些方式实现?
推荐阅读大佬的解析:https://zhuanlan.zhihu.com/p/27588422

这里是我摘抄的一部分解析:

前端路由

随着前端应用的业务功能越来越复杂、用户对于使用体验的要求越来越高,单页应用(SPA)成为前端应用的主流形式。大型单页应用最显著特点之一就是采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

“更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式:

  • 利用URL中的hash(“#”)

    • hash(“#”)符号的本来作用是加在URL中指示网页中的位置(也就是锚点),hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
    • 读取:window.location.hash
    • 监听:hashchange事件
    • 每次改变hash,都会在浏览器的访问历史中增加一个记录
  • 利用History interfaceHTML5中新增的方法
    hash差不多,表现形式上少了个丑丑的“#”,具体移步大佬的解析

经过大佬的解析,加之自己的理解,这里会实现一个简易版的VueRouter

功能分析

在手撕vue-router源码前,我们先看一下它的基本用法和要实现的功能:

import Vue from 'vue';
// 第1步,要导入并安装插件
import VueRouter from 'vue-router'
Vue.use(VueRouter)

// 第2步,创建一个vue-router实例
const router = new VueRouter({
  mode: 'history',
  base: process.env.Base_URL,
  routes: [ {path: '/xxx', name: 'xxx', component: () => import("@/views/xxx.vue")}]
})

// 第3步,将实例挂载到Vue实例上
new Vue({
  router, // 注意key是小写
  store,
  render: h => h(App),
}).$mount('#app')
<!-- 第4步,使用router-link和router-view组件 -->
<router-link :to="{name:'home'}" class="nav">主页</router-link>
<router-view></router-view>

那我们要实现的功能:

  1. 创建一个插件:实现VueRouter类和install方法
  2. 实现两个全局组件:router-view用于显示匹配组件的内容,router-link用于跳转
  3. 监听url变化:监听hashchangepopstate事件
  4. 响应url变化:创建一个响应式的属性current, 当它改变时获取对应组件并显示

实现过程

1. 创建一个VueRouter插件

在这之前,我们要先了解下Vue的一些特性:

  • 通过Vue构造函数的配置项,都会被挂载到Vue实例的$options属性上
  • 使用Vue插件时,会默认将Vue构造函数传入Vue.use = function(Vue, options) {...}
  • 使用Vue.mixin可以全局混入各个组件,从而在其生命周期中可以拿到实例对象

创建的过程,就是通过插件方式,获取到Vue的构造函数,然后通过全局mixinbeforeCreate钩子中获取到router实例,并将其挂载到Vue的原型上

  • Vue的构造函数为什么不用import的方式导入?
    因为如果通过import Vue from 'vue'方式导入的话,在打包时,会将Vue包一起解析,插件过大,应尽可能让插件独立且体积小的前提下,可以使用传入的参数来获取Vue构造函数
  • 为什么不在Vue.use时,将router直接绑定到Vue.prototype上?
    因为在入口文件main.js中可以看到,是先执行Vue.use(VueRouter),才执行new Vue({router}),所以在安装插件时,还并没有获取到router实例,所以要在插件中绑定router实例
// krouter.js
let Vue;

class KVueRouter {
  constructor(options) {
    this.$options = options
  }
}
// 1. 实现插件
// 在使用插件时,会把Vue的构造函数当成参数传进来
KVueRouter.install = function(_Vue) {
  Vue = _Vue; // 保存构造函数,方便在KVueRouter里面使用

  // 使用mixin获取根实例上的router选项
  Vue.mixin({
    beforeCreate() {
      console.log(this);  // 每个vue组件实例对象
      //  确保根实例时,才执行挂载
      if(this.$options.router) {
        Vue.prototype.$router = this.$options.router; // 将根实例上的router设在Vue.prototype.$router上,方便在每个组件中通过this.$router获取到这个router实例

        // 只有Vue根实例上才有router对象,router对象会被挂载在this.$options上
        // new Vue({
        //   router, // 注意key是小写
        //   store,
        //   render: h => h(App),
        // }).$mount('#app')
      }
    }
  })
}
export default KVueRouter;

2. 实现router-link组件

我们要实现下面这样一个组件,特点是传入一个to的属性写入跳转路径,使用render函数,生成a标签,并设置href属性方便跳转,中间的内容是用slot插槽处理的。要注意:
to属性,我们可能直接传字符串(比如:to="/home"),也有可能传入对象(如下面这个例子),所以要做不同的解析
<router-link :to="{name:'home'}" class="nav">主页</router-link>

KVueRouter.install = function(_Vue) {
  // 2. 实现router-link组件
  Vue.component('router-link', {
      props: {
        to: {
          required: true
        }
      },
      // 不能使用template来,因为不能在只有运行时版本时候使用template,而是要使用render
      render(h) {
        // <a href="#/about">关于</a>
        let path = ''
        if(typeof this.to === 'stirng') {
          path = this.to
        }else if(typeof this.to === 'object') {
          path = '/' + this.to.name
        }
        console.log(this.$slots)  // 当前组件的插槽集合,default:[Vnode]为其默认插槽内容
        return h('a', {attrs: { href: '#' + path}}, this.$slots.default)  // 里面的内容使用一个匿名插槽 
      }
    })
}

3. 实现router-view组件

  • 通过监听hashchange事件,获取当前路由地址
  • 通过与routes映射表中的路径比对,如相同,则渲染对应的组件
let Vue;

class KVueRouter {
  constructor(options) {
    this.$options = options
    this.curPath = '/' // 记录当前路由地址
    // 监听url变化
    window.addEventListener('hashchange', this.onHashChange.bind(this)) // 这里注意改变this指向,不然this会默认指向window
    window.addEventListener('load', this.onHashChange.bind(this)) // 在用户刷新时
  }
  // 获取当前的路由地址
  onHashChange() {
    console.log(window.location.hash);  // #/about
    console.log(window.location.hash.slice(1)); // /about
    this.curPath = window.location.hash.slice(1)  // 从第1位截取到最后,即去掉#号
  }

  // 创建一个路由的映射表,避免每次路由变化都要去遍历
  this.routeMap = {}
  options.routes.forEach(route => {
    this.routeMap[route.path] = route.component // '/home': {() => import("@/views/Home.vue")}
  })
}
KVueRouter.install = function(_Vue) {
  Vue.component('router-view', {
    render(h) {
      const {routeMap, curPath} = this.$router; // 获取路由映射表,和当前路由值
      let component = routeMap[curPath] || null
      return h(component)
    }
  })
}

4. 需要创建响应式的curPath属性

使用Vue.util.defineRective方法,来创建响应式的路由地址属性,这样,当路由curPath变化时,才会让依赖这个属性的组件重新渲染;否则只会在初始值时渲染一次,不会随路由变化而重新渲染

class KVueRouter {
  constructor(options) {
    this.$options = options

    // 5. 需要创建响应式的curPath属性
    Vue.util.defineReactive(this, 'curPath', '/')
    // this.curPath = '/' // 记录当前路由地址
  }
}

完整代码:

// kvue-router.js

// 创建一个全局变量,用来保存Vue的构造函数
let Vue;

class KVueRouter {
  constructor(options) {
    this.$options = options

    // 5. 需要创建响应式的curPath属性
    Vue.util.defineReactive(this, 'curPath', '/')
    // this.curPath = '/' // 记录当前路由地址
    // 4. 监听url变化
    window.addEventListener('hashchange', this.onHashChange.bind(this)) // 这里注意改变this指向,不然this会默认指向window
    window.addEventListener('load', this.onHashChange.bind(this)) // 在用户刷新时

    // 创建一个路由的映射表,避免每次路由变化都要去遍历
    this.routeMap = {}
    options.routes.forEach(route => {
      this.routeMap[route.path] = route.component // '/home': {() => import("@/views/Home.vue")}
    })
  }
  
  // 获取当前的路由地址
  onHashChange() {
    console.log(window.location.hash);  // #/about
    console.log(window.location.hash.slice(1)); // /about
    this.curPath = window.location.hash.slice(1)  // 从第1位截取到最后,即去掉#号
  }
}

// 1. 实现插件
// 在使用插件时,会把Vue的构造函数当成参数传进来
KVueRouter.install = function(_Vue) {
  Vue = _Vue; // 保存构造函数,在KVueRouter里面使用

  // 使用mixin获取根实例上的router选项
  Vue.mixin({
    beforeCreate() {
      console.log(this);
      //  确保根实例时,才执行挂载
      if(this.$options.router) {
        Vue.prototype.$router = this.$options.router;
      }
    }
  })


  // 2. 实现router-link组件
  Vue.component('router-link', {
    props: {
      to: {
        required: true
      }
    },
    // 不能使用template来,因为不能在只有运行时版本时候使用template,而是要使用render
    render(h) {
      // <a href="#/about">关于</a>
      let path = ''
      if(typeof this.to === 'stirng') {
        path = this.to
      }else if(typeof this.to === 'object') {
        path = '/' + this.to.name
      }
      console.log(this.$slots)  // 当前组件的插槽集合,default:[Vnode]为其默认插槽内容
      return h('a', {attrs: { href: '#' + path}}, this.$slots.default)  // 里面的内容使用一个匿名插槽 
    }
  })

  // 3. 实现router-view组件
  Vue.component('router-view', {
    render(h) {
      const {routeMap, curPath} = this.$router; // 获取路由映射表,和当前路由值
      let component = routeMap[curPath] || null
      return h(component)
    }
  })  
}

export default KVueRouter;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容