likes
comments
collection
share

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

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

前言

相信各位前端开发的小伙伴肯定都用过 Vue-Router,也熟悉它的一些配置,但是不少人对其内部原理的实现可能不是那么了解,也没阅读过源码,那么这篇文章与大家一起研究 Vue-Router 的源码,并实现一款简易版本。

当然也不可能完全实现 Vue-Router 的一些功能,那样涉及到的东西太多了。作为初学者,能实现个大概就差不多了,在面试中也够了,能够跟面试官有的聊即可,阅读这篇文章需要一定的基础,有些细节不必深究。

使用流程

先来看下在 Vue2 项目中是如何使用路由的,下面是官网提供的一个起步例子:

// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

Vue-router 的基本思路:根据路由记录生成 VueRouter 实例,并传入 Vue 的 app 实例的 router 属性上,然后使用 router-view 来挂载路由匹配的路由组件到页面某一位置。

总的来说,使用步骤如下:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

项目搭建

首先初始化一个 Vue2 的项目,目录结构如下:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

页面的代码结构如下,后面用于测试路由:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

plugins/vue-router 中导出一个 VueRouter 构造函数,然后在 router.js 中引入使用,首先会通过 Vue.use 调用一下,接着实例化一个路由实例,关于 Vue.use 的作用下面还会说到,router.js 中具体的代码如下:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

main.js 中挂载路由:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

接下来专心实现 VueRouter 即可!

源码实现

下面是实现 vue-router 的目录结构以及每个文件的作用,只要把这些文件的功能都实现了,那么你就能拥有一款简易版的路由啦!

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

1. 提供install方法

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

现在又会出现一个问题,为什么一定要使用 Vue.use 呢?不用行不行?当然是不行了,因为 vue-router 上的一些属性、方法需要挂载到 Vue 实例中,调用 Vue.use 后,install 方法会接受一个参数 Vue,这样就能够在 Vue 实例上挂载任何东西了,Vue.use 就有点像是 vue 和 vue-router 之间的桥梁。

下面看下 vue-router 中的 install 方法是如何实现的:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

这里面的核心就是 Vue.mixin 全局混入,每个组件都会执行里面的代码,然后每个组件都会新增一个 _routerRoot 属性,这里还调用了路由实例上的 init 方法初始化路由。

下面来看下 VueRouter 实例是怎样实现的。

2. VueRouter实例

代码在 src/plugins/vue-router/router.js 中:VueRouter 是一个类,可以通过 new VueRouter 得到一个实例,在 VueRouter 身上挂载 install 方法,然后再进行一些初始化的操作。

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

constructor 中得到一个匹配器对象,里面具有两个方法,同时初始化了哈希路由模式,init 中主要的操作是:根据当前路径,显示对应的组件;里面用到路由模式的一些方法,待会讲到路由模式时,再回来梳理下逻辑。

3. 生成matcher

代码在 create-matcher.js 文件中:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

这个方法里面主要有三个步骤:

  1. 扁平化用户传入的数据,创建路由映射表:这里调用了 createRouteMap 方法,将 new VueRouter 时的配置项 routes 传入:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

递归遍历 routes,如果有父亲,路径前面需要拼接上,处理完成后得到 pathList、pathMap,我们来打印下这两个变量:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

其中 pathList 存储的是所有路径,pathMap 存储的是每个路径对应的记录。

  1. 提供动态添加路由的方法:因为在项目中可能存在需要动态添加路由的情况,尤其是管理后台系统的权限处理,上面提到的 routes 是在 new VueRouter 的时候写死的,所以需要提供这么一个方法 addRoutes,它内部调用的还是 createRouteMap,只不过现在要多传入两个参数。
  2. 提供用来匹配的方法:根据传入的路径,找到对应的记录,并且要根据记录产生一个匹配数据

4. 实现路由模式

我们知道路由是有几种模式的,那么我这里只实现 hash 模式,同时也实现了路由模式的一些公共内容。

路由模式的公共功能: 定义一个 History 类,代码所在位置:history/base.js

export default class History {
  constructor(router) {
    this.router = router
    this.current = createRoute(null, {
      path: '/',
    })
  }
  // 跳转的核心逻辑
  transitionTo(location, onComplete) {
    // route就是当前路径需要匹配哪些路由
    // 例如:访问路径 /about/a   =>   {path: '/about/a', matched: [{paht: '/about/a', component: xxx, parent: '/about'}, {paht: '/about', component: xxx, parent: undefined}]}
    let route = this.router.match(location)
    if (
      this.current.path === location &&
      route.matched.length === this.current.matched.length
    ) {
      return
    }
    this.updateRoute(route)
    onComplete && onComplete()
  }
  // 更新路由
  updateRoute(route) {
    this.current = route
    this.cb && this.cb(route) // 监听路径的变化
  }
  listen(cb) {
    this.cb = cb
  }
}

export function createRoute(record, location) {
  let res = []
  // record  =>  {paht: '/about/a', component: xxx, parent: '/about'}
  if (record) {
    while (record) {
      res.unshift(record)
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res,
  }
}

分析下 History 类中每个方法具体干了什么:

  1. createRoute:对于嵌套路由,比如 /about/a,在我们要渲染 a 页面的时候,肯定也要把他的父组件也给渲染出来,这里就是 about 页面,因此这个方法会返回一个字段 matched,记录当前路径需要渲染的全部页面。上面说到的生成 matcher,也是用到这个方法。
  2. transitionTo:这是跳转的核心逻辑,通过当前跳转的路径拿到需要匹配的路由,例如:访问路径 /about/a => {path: '/about/a', matched: [{paht: '/about/a', component: xxx, parent: '/about'}, {paht: '/about', component: xxx, parent: undefined}]},然后更新当前路由,如果有传入跳转之后的回调 onComplete ,那么就去执行。
  3. updateRoute:更新路由的方法,History 类中有个字段 current 记录了当前的路由信息,此时要更新该字段,如果有 cb,再执行一下。
  4. listen:监听的方法,接收一个 cb,当更新路由的时候调用 cb,从而更新 vue 根实例上的 _route 属性

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

hash模式: 定义一个 HashHistory 类,继承自 History 类,代码所在位置:history/hash.js

import History from './base'

function getHash() {
  return window.location.hash.slice(1)
}

export default class HashHistory extends History {
  constructor(router) {
    // 调用父类的constructor
    super(router)
  }
  // 获取当前路径的hash值
  getCurrentLocation() {
    return getHash()
  }
  // 监听路径的变化
  setupListener() {
    window.addEventListener('hashchange', () => {
      this.transitionTo(getHash())
    })
  }
}

HashHistory 类做的事很简单,获取当前路径的 hash 值,监听 hashchange 事件,当路径发生变化的时候,执行跳转方法。

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

在路由初始化的时候,使用 hash 模式的类,将当前路由实例作为参数传入

现在回到 VueRouter 类中的初始化方法中:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

理解了路由模式的具体实现后,应该就能看懂这段代码干了些什么:首先拿到初始页面的路径,执行跳转逻辑,更新完路由后,执行 setupHashLister,也就是监听页面的 hash 值的变化。

5. 创建全局组件

vue-Router 中提供了两个全局组件 router-link,router-view,这里我只实现了 router-view 组件,采用的是函数式组件的写法,使用 render 函数渲染路由对应的组件:

🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版

总结下具体的逻辑:找到当前路径对应的组件进行渲染。首先是拿到 route 属性,以及 matched,上面提到过,当访问 /about/a 是会匹配到多个记录,这些记录存储在 matched 中,循环判断当前组件有多少个父组件,也就是计算其深度,然后在 matched 中找到对应的记录,拿到 component 属性进行渲染即可。

结语

到这里就差不多结束啦,简单实现了一款 Vue-Router,对其内部的一些原理有了一定的了解,其实 Vue-Router 的源码里面还有更多内容以及细节的,由于本人的水平有限,掌握得还不够多,这里不再展开阐述,有兴趣的小伙伴可以去阅读完整版的源码,如果有讲错的地方,希望各位小伙伴指出,大家多多交流,共同进步!

这篇文章的源码放在了 github 上面!

转载自:https://juejin.cn/post/7316171289096781858
评论
请登录