1、前言

本篇文章循序渐进带大家实现VueRouter的实现原理,阅读前需要对vue的使用有基本的认识,学习过class了解其基本概念。

实现注意点:

  • 如何注册插件
  • 实现router-view和router-link两个组件
  • 如何根据当前路由显示对应组件
  • 路由切换时如何更新组件
  • 嵌套路由如何实现

带着这些问题下面我们就开始一步一步的实现

最终代码链接

github 链接

2、准备测试数据

我们可以使用VueCli搭建一个VueRouter的项目。这里简单的说一下命令

  1. # 如果你使用 yarn
  2. yarn global add @vue/cll
  3. # 如果你使用 npm
  4. npm install -g @vue/cli
  5. # 创建 Vue2 项目
  6. vue create vue-router-study

安装完后,直接启动该服务

  1. cd vue-router-study
  2. yarn serve

接着我们可以先使用官方的 vue-router 先跑一个测试例子

  1. yarn add vue-router

编写文件 router/index.js

  1. import Vue from 'vue'
  2. import VueRouter from 'vue-router'
  3. Vue.use(VueRouter)
  4. export default new VueRouter({
  5. routes: [
  6. {
  7. path: '/',
  8. component: () => import('../components/HelloWorld.vue'),
  9. },
  10. {
  11. path: '/a',
  12. component: () => import('../components/A.vue'),
  13. children: [{ path: '/a/b', component: () => import('../components/B.vue') }],
  14. },
  15. ],
  16. })

编辑 main.jsrouter 添加到 Vue 选项中

  1. import router from './router'
  2. // ...
  3. new Vue({
  4. router,
  5. render: h => h(App),
  6. }).$mount('#app')

App.vue 中显示我们的数据

  1. <template>
  2. <div id="app">
  3. <div>
  4. <router-link to="/">首页</router-link>
  5. </div>
  6. <div>
  7. <router-link to="/a">a页面</router-link>
  8. </div>
  9. <div>
  10. <router-link to="/a/b">b页面</router-link>
  11. </div>
  12. <router-view></router-view>
  13. </div>
  14. </template>
  15. // ...

创建两个组件 A.vueB.vue

components/A.vue

  1. <template>
  2. <div>
  3. 我是A组件
  4. <div>
  5. <router-view></router-view>
  6. </div>
  7. </div>
  8. </template>
  9. <script>
  10. export default {
  11. name: "A"
  12. };
  13. </script>

componets/B.vue

  1. <template>
  2. <div>
  3. 我是B页面
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: "B"
  9. };
  10. </script>

现在回到页面看看效果

01.gif

下面我就开始实现自己的 vue-router 插件

3、实现插件注册

我们使用 VueRouter 的时候是通过 use 进行注册,说明 VueRouter 是一个插件。需要实现一个install方法

创建一个新文件实现我们自己的 VueRouter

创建一个 VueRouter 类,以及编写一个 install 方法,并定义一个变量保存 Vue

src/avue-router.js

  1. let Vue
  2. class VueRouter {}
  3. VueRouter.install = function(_Vue) {
  4. Vue = _Vue
  5. Vue.mixin({
  6. beforeCreate() {
  7. if (this.$options.router) Vue.prototype.$router = this.$options.router
  8. },
  9. })
  10. }
  11. export default VueRouter

在 router/index.js 使用我们自己的 avue-router.js

  1. // import VueRouter from 'vue-router'
  2. import VueRouter from '../avue-router'

回到页面,看看是否正常显示。如果显示成功了,证明插件成功注册

在 Vue.use(VueRouter) 时,Vue会自动调用 install 方案。
使用mixin,将我们在Vue选项中的router实例,挂载到原型上
我们就可以在Vue实例中,通过 this.$router 获得实例数据

4、实现 router-link 组件

通过 Vue 挂载全局组件,并将 props 拼接到 href 中,将默认插槽的值填充进去

  1. VueRouter.install = function(_Vue) {
  2. // ...
  3. Vue.component('router-link', {
  4. props: {
  5. to: {
  6. type: String,
  7. require: true,
  8. },
  9. },
  10. render(h) {
  11. return h(
  12. 'a',
  13. {
  14. attrs: {
  15. href: '#' + this.to,
  16. },
  17. },
  18. this.$slots.default
  19. )
  20. },
  21. })
  22. }

现在回到页面,router-link 已经正常显示。

5、实现 router-view 组件

需要声明一个响应式的变量 current 保存当前的 hash 路径。router-view 组件根据这个路径匹配 routes 表中对应的组件,显示出来。并监听 hashchange 在路径更新的时候,更新 current 的路径。

  1. // ...
  2. class VueRouter {
  3. constructor(options) {
  4. // 保存实例时候配置项
  5. this.$options = options
  6. Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
  7. addEventListener('hashchange', this.onHashChange.bind(this))
  8. addEventListener('load', this.onHashChange.bind(this))
  9. }
  10. onHashChange() {
  11. this.current = window.location.hash.slice(1) || '/'
  12. }
  13. }
  14. VueRouter.install = function(_Vue) {
  15. // ...
  16. Vue.component('router-view', {
  17. render(h) {
  18. let component = null
  19. const route = this.$router.$options.routes.filter(route => route.path === this.$router.current)[0]
  20. if (route) component = route.component
  21. return h(component)
  22. },
  23. })
  24. }

因为此刻我们没有实现嵌套路由,所以需要先把 A.vue 中的 router-view 注释掉,否则会造成死循环

components/A.vue

  1. <template>
  2. <div>
  3. 我是A组件
  4. <div>
  5. <!-- <router-view></router-view> -->
  6. </div>
  7. </div>
  8. </template>
  9. <script>
  10. export default {
  11. name: "A"
  12. };
  13. </script>

现在回到页面,我们发现可以使用 router-link 切换页面了。

5.1 实现嵌套路由

我们参考一下官方的写法

02.jpg

可以看出,他给每个 router-view 组件定义了个 depth 的变量确定它的深度,并且有个 matched 数组,记录当前路径的对应路由数组。

比如我们现在的 hash 地址为 /a/bmatched 应该为

  1. let matched = [
  2. {
  3. path: '/a',
  4. component: () => import('../components/A.vue'),
  5. children: [{ path: '/a/b', component: () => import('../components/B.vue') }],
  6. },
  7. {
  8. path: '/a/b',
  9. component: () => import('../components/B.vue'),
  10. },
  11. ]

现在我们在 VueRouter 类中实现 matched 方法。通过递归 route 表。收集当前路径的所有 route 数组。

  1. class VueRouter {
  2. // ...
  3. match (routes) {
  4. routes = routes || this.$options.routes
  5. for (const route of routes) {
  6. // 如果为根目录,push 一个 route 进去后,直接返回
  7. if (route.path === '/' && this.current === '/') {
  8. this.matched.push(route)
  9. return
  10. }
  11. // 如果不为根目录,则判断当前 current 所包含的所有 route,并收集起来
  12. if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
  13. this.matched.push(route)
  14. if (route.children) this.match(route.children)
  15. }
  16. }
  17. }
  18. }

接着我们要改写 route-view 组件。在每个 route-view 组件中添加 routerView 的属性,以此判断是否为 router-view 组件。从当前实例出发,向上循环,计算出当前组件,是在第几层。保存该值到 depth 变量中。然后再根据 matched 表选择对应层数的 route 获取该表的 component 显示即可。

  1. VueRouter.install = function (_Vue) {
  2. // ...
  3. Vue.component('router-view', {
  4. render (h) {
  5. this.$vnode.data.routerView = true
  6. let depth = 0
  7. let parent = this.$parent
  8. while (parent) {
  9. const vnodeData = parent.$vnode && parent.$vnode.data
  10. if (vnodeData && vnodeData.routerView) {
  11. depth++
  12. }
  13. parent = parent.$parent
  14. }
  15. let component = null
  16. const route = this.$router.matched[depth]
  17. if (route) component = route.component
  18. return h(component)
  19. }
  20. })
  21. }

现在我们可以解开 components/A.vuerouter-view 中的注释。显示正常~

End 最终效果

03.jpg