🧐长文警告!Vue-Router 源码浅析,自己动手实现一款简易版
前言
相信各位前端开发的小伙伴肯定都用过 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 来挂载路由匹配的路由组件到页面某一位置。
总的来说,使用步骤如下:
项目搭建
首先初始化一个 Vue2 的项目,目录结构如下:
页面的代码结构如下,后面用于测试路由:
plugins/vue-router
中导出一个 VueRouter 构造函数,然后在 router.js
中引入使用,首先会通过 Vue.use 调用一下,接着实例化一个路由实例,关于 Vue.use 的作用下面还会说到,router.js
中具体的代码如下:
在 main.js
中挂载路由:
接下来专心实现 VueRouter 即可!
源码实现
下面是实现 vue-router 的目录结构以及每个文件的作用,只要把这些文件的功能都实现了,那么你就能拥有一款简易版的路由啦!
1. 提供install方法
现在又会出现一个问题,为什么一定要使用 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 文件中:
这个方法里面主要有三个步骤:
- 扁平化用户传入的数据,创建路由映射表:这里调用了
createRouteMap
方法,将 new VueRouter 时的配置项 routes 传入:
递归遍历 routes,如果有父亲,路径前面需要拼接上,处理完成后得到 pathList、pathMap,我们来打印下这两个变量:
其中 pathList 存储的是所有路径,pathMap 存储的是每个路径对应的记录。
- 提供动态添加路由的方法:因为在项目中可能存在需要动态添加路由的情况,尤其是管理后台系统的权限处理,上面提到的 routes 是在 new VueRouter 的时候写死的,所以需要提供这么一个方法
addRoutes
,它内部调用的还是createRouteMap
,只不过现在要多传入两个参数。 - 提供用来匹配的方法:根据传入的路径,找到对应的记录,并且要根据记录产生一个匹配数据
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 类中每个方法具体干了什么:
- createRoute:对于嵌套路由,比如
/about/a
,在我们要渲染 a 页面的时候,肯定也要把他的父组件也给渲染出来,这里就是 about 页面,因此这个方法会返回一个字段matched
,记录当前路径需要渲染的全部页面。上面说到的生成 matcher,也是用到这个方法。 - transitionTo:这是跳转的核心逻辑,通过当前跳转的路径拿到需要匹配的路由,例如:访问路径
/about/a => {path: '/about/a', matched: [{paht: '/about/a', component: xxx, parent: '/about'}, {paht: '/about', component: xxx, parent: undefined}]}
,然后更新当前路由,如果有传入跳转之后的回调 onComplete ,那么就去执行。 - updateRoute:更新路由的方法,History 类中有个字段 current 记录了当前的路由信息,此时要更新该字段,如果有 cb,再执行一下。
- listen:监听的方法,接收一个 cb,当更新路由的时候调用 cb,从而更新 vue 根实例上的 _route 属性
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 事件,当路径发生变化的时候,执行跳转方法。
在路由初始化的时候,使用 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 上面!
转载自:https://juejin.cn/post/7316171289096781858