如何手写一个简单的 vue-router

778 阅读2分钟

前言

在我们使用 spa (单页面应用)开发时,路由是我们开发的基石,那么我们平时不应该只是会使用,我们也应该了解一下它的基本构造以及有时候配置为何会这么书写。

你是否平常会有这些疑惑?

  • 为什么我们在使用 vue-router 会有一个 vue.use 的写法,它到底干了什么?
  • 为什么 new Vue 的时候需要将 rouer 加入到配置项中?

如果你也有相似的疑问,那么本文可以带着你解开这些问题。

友情提示:本文只是想阐述一个简单的 vue-router 如何实现,关于嵌套路由等功能并不会实现。

实现一个 vue-router

在实现一个 vue-router 之前,我们首先需要想一想,一个基本的 vue-router 需要些什么功能

  1. 需将 $router 实例挂载到 Vue 的原型链上
  2. 全局注册 router-link 和 router-view 组件
  3. 实现一个 VueRouter 类
  4. ...

至此一个简单的 vue-router 已完成,若需了解更多功能,请移步源码观看。

vue.use

通常我们在使用 vue-router 的时候,一般我们需要在 main.js 头部引入 vue-router,然后 router 里的 index.js 里执行Vue.use(VueRouter)

Vue.use 会调用当前对象的 install 方法,并将 Vue 类传递至 install 函数中(请注意这一步十分重要)。

为什么说这一步十分重要呢,因为这样我们的插件将不再需要 Vue 依赖,我们在书写一个插件的时候,如果还连带着很多依赖这是不合理的。

挂在到原型链上

在执行 install 函数时,我们可以借助 mixin(混入)进行挂载(当然,这里得判断一下是否为根组件或者是否具有 router)

VueRouter.install = function(_Vue) {
  Vue = _Vue
  // 1.挂载$router属性
  // 全局混入目的:延迟下面逻辑到 router 创建完毕并且附加到选项上时才执行
  Vue.mixin({
    beforeCreate() {
      // 次钩子在每个组件创建实例时都会调用
      // 根实例才有该选项
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    }
 }

这里采用混入的最大原因是要等到 router 创建完毕。

创建 router 组件

因为 vue-router 具有router-link 和 router-view组件,所以我们需要在 install 这里,进行组件挂载。

router-link 这个组件很简单,我们只需要把它当做 a 标签进行设计即可。

router-view 这个组件需要根据路由表进行渲染,我们需要从路由表查到相应的组件(当前这里可以使用 es6 推荐的按需加载)。

注意:这里注册组件为什么没有使用 template 的写法,这里涉及到一个概念,叫做 runtime-only 和 runtime-compiler。runtime-compiler 和 runtime-only 功能差不多,但是多了编辑的功能可以解析 template。而我们平时使用 vue-cli3,一般都是runtime-only,我们在以 .vue 后缀书写代码时,在 webpack 编译是已被转译为相应的 render 函数能读取的格式。runtime-only 比 runtime-compiler 环境更小,并且速度更快(因为少了编译的过程)。

VueRouter.install = function(_Vue) {
  Vue = _Vue
  // 1.挂载$router属性
  // this.$router.push()
  // 全局混入目的:延迟下面逻辑到router创建完毕并且附加到选项上时才执行
  Vue.mixin({
    beforeCreate() {
      // 次钩子在每个组件创建实例时都会调用
      // 根实例才有该选项
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    },
  })

  // 2.注册实现两个组件router-view,router-link
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      // <a href="to">xxx</a>
      // return <a href={'#'+this.to}>{this.$slots.default}</a>
      return h(
        'a',
        {
          attrs: {
            href: '#' + this.to,
          },
        },
        this.$slots.default
      )
    },
  })
  Vue.component('router-view', {
    render(h) {
      // 获取当前路由对应的组件
      let component = null
      const route = this.$router.$options.routes.find((route) => route.path === this.$router.current)
      if (route) {
        component = route.component
      }
      return h(component)
    },
  })
}

实现一个 VueRouter 类

  1. 在 VueRouter 类中,我们首先需要获取到当前的组件名称,这里我们通过 window.location.hash 获取并且截取。
  2. 我们需要新增一个事件,用于监听 hash 路由的更改
  3. 新增响应式数据,这里借助到了 Vue 里面的工具函数(Vue.util.defineReactive)

    特别注意:这里使用 Object.defineProperty 是不可以的,Object.defineProperty 仅仅只是进行数据的劫持;当然使用 $set 也是不可以的,使用 $set 的前提必须是,第一个参数也必须是响应式的。

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

    // 把current作为响应式数据
    // 将来发生变化,router-view的render函数能够再次执行
    const initial = window.location.hash.slice(1) || '/'
    Vue.util.defineReactive(this, 'current', initial)

    // 监听hash变化
    window.addEventListener('hashchange', () => {
      console.log(this.current)
      this.current = window.location.hash.slice(1)
    })
  }
}

完整代码

main.js

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')

router/index.js

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

// 1.VueRouter是一个插件?
// 内部做了什么:
//    1)实现并声明两个组件router-view  router-link
//    2) install: this.$router.push()
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
  },
]

// 2.创建实例
const router = new VueRouter({
  mode: 'hash',
  routes,
})

export default router

vue-router.js

// 保存Vue构造函数,插件中要使用,不导入还能用
let Vue
class VueRouter {
  constructor(options) {
    this.$options = options
    // 把current作为响应式数据
    // 将来发生变化,router-view的render函数能够再次执行
    const initial = window.location.hash.slice(1) || '/'
    Vue.util.defineReactive(this, 'current', initial)

    // 监听hash变化
    window.addEventListener('hashchange', () => {
      console.log(this.current)
      this.current = window.location.hash.slice(1)
    })
  }
}
// 参数1是Vue.use调用时传入的
VueRouter.install = function(_Vue) {
  Vue = _Vue
  // 1.挂载$router属性
  // this.$router.push()
  // 全局混入目的:延迟下面逻辑到router创建完毕并且附加到选项上时才执行
  Vue.mixin({
    beforeCreate() {
      // 次钩子在每个组件创建实例时都会调用
      // 根实例才有该选项
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    },
  })

  // 2.注册实现两个组件router-view,router-link
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      // <a href="to">xxx</a>
      // return <a href={'#'+this.to}>{this.$slots.default}</a>
      return h(
        'a',
        {
          attrs: {
            href: '#' + this.to,
          },
        },
        this.$slots.default
      )
    },
  })
  Vue.component('router-view', {
    render(h) {
      // 获取当前路由对应的组件
      let component = null
      const route = this.$router.$options.routes.find((route) => route.path === this.$router.current)
      if (route) {
        component = route.component
      }
      console.log(this.$router.current, component)

      return h(component)
    },
  })
}

export default VueRouter