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

前言

相信各位前端开发的小伙伴肯定都用过 Vue-Router,也熟悉它的一些配置,但是不少人对其内部原理的实现可能不是那么了解,也没阅读过源码,如果你是个初级前端,其实不用怎么深入的去研究,但是如果你想往中高级的方向发展,那么就必须研究其内部原理的实现,只有这样,你才会比别人更加突出。

毕竟现在初级前端太多了,工作个4 5年可能跟刚毕业的大学生的水平差不多,那这样很容易被淘汰。因此,必须卷起来,深入研究一些框架、库的实现,只有阅读源码、理解原理,自己才能够写出高质量的代码,往进阶方向发展。

那么这篇文章与大家一起研究 Vue-Router 的源码,并实现一款简易版本,当然也不可能完全实现 Vue-Router 的一些功能,那样涉及到的东西太多了。作为初学者,能实现个大概就差不多了,在面试中也够了,能够跟面试官有的聊即可,阅读这篇文章需要一定的基础,有些细节不必深究。废话不多说,马上开始!

使用流程

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

js 复制代码
// 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 来挂载路由匹配的路由组件到页面某一位置。

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

项目搭建

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

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

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

main.js 中挂载路由:

接下来专心实现 VueRouter 即可!

源码实现

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

1. 提供install方法

首先,Vue-Router 身上一定要有一个 install 方法,这是为什么呢?因为我们要使用 Vue.use(VueRouter),如果你阅读过 Vue.use 的话,你就会没有疑问。关于其简单的说明如下,如果想要深入的了解 Vue.use 的话,可以参考这篇文章

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

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

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

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

2. VueRouter实例

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

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

3. 生成matcher

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

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

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

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

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

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

4. 实现路由模式

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

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

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 属性

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

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 事件,当路径发生变化的时候,执行跳转方法。

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

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

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

5. 创建全局组件

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

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

结语

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

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

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax