vue-router-next for vue3 源码分析(附.脑图)
文章目录
慎入😢
vue-router-next 源码分析流程图,此文重点在图,附带一些总结性的文字分 析内容(图一般比较大,只保证自己能看懂系列~~~~),学习过程中一些零碎的笔记。
vue-router-next
脑图:
简要分析:
vue-router 实现从使用上来说有三个部分:
路由注册初始化,以
VueRouter.createRoute({history, routes})
为执行入口创建匹配器 matcher 路由的一些匹配、添加、查找啊什么的操作都是有这个 matcher 来实现的
1 2 3 4 5 6 7 8 9 10 11
export interface RouterMatcher { addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void; removeRoute: { (matcher: RouteRecordMatcher): void; (name: RouteRecordName): void; }; getRoutes: () => RouteRecordMatcher[]; getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined; resolve; }
而上面的接口操作的无非就是两个路由仓库:
1 2 3 4 5
// 这个无论有没有名字的路由记录都会被存储到这个数组中 const matchers: RouteRecordMatcher[] = []; // 这个存储的是带 name 字段的路由 <name, record> 结构 // 方便直接可以通过 map.get(name) 就可以去到路由记录,减少数组查找消耗 const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>();
初始化路由守卫存储器,其实就是个包含
{list,add,result}
的一个对象1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 进入之前的的回调列表 const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>(); // 解析路由之前的回调列表 const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>(); // 进入之后的回调列表 const afterGuards = useCallbacks<NavigationHookAfter>(); export function useCallbacks<T>() { let handlers: T[] = []; function add(handler: T): () => void {} function reset() { handlers = []; } return { add, list: () => handlers, reset, }; }
currentRoute 重要变量,是个 shallow ref 响应式类型的值,与
<router-view/>
当前显示的路由息息相关,或者说就是它,因为RouterView
组件中有间接的监听这个值。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
// RouterView.ts // 就是这里的 matchedRouteRef watch( () => [viewRef.value, matchedRouteRef.value, props.name] as const, ([instance, to, name], [oldInstance, from, oldName]) => { // ... } ); // RouterView.ts > 为什么说是间接呢?看下面 matchedRouteRef 的由来 const injectedRoute = inject(routerViewLocationKey)!; const routeToDisplay = computed(() => props.route || injectedRoute.value); const depth = inject(viewDepthKey, 0); const matchedRouteRef = computed<RouteLocationMatched | undefined>( () => routeToDisplay.value.matched[depth] ); // router.ts > routerViewLocationKey ??? 不记得了吗? router.install... 啊 app.provide(routerKey, router); app.provide(routeLocationKey, reactive(reactiveRoute)); app.provide(routerViewLocationKey, currentRoute); // 看到没,关联上了吧!!! // app.provide -> currentRoute -> // injectedRoute -> routeToDisplay -> // routeToDisplay.value.matched[depth] /* 并且注意看 ~RouterView~ 组件中 setup最后返回的值是个函数,这个函数中有对 routeToDisplay, matchedRouteRef进行引用也就是在执行的时候会触发 track 操 作将它收集到这写值的依赖列表中,只要这些值发生变更就会 trigger这个 setup 执行,来更新 ~<router-view/>~ ,*/
初始化 router 实例
包含一些 api :
路由的增删改查主要来源 matcher:
{addRoute, currentRoute, removeRoute, hasRoute, getRoutes, resolv}
路由的跳转行为:
{push, replace, go, back: () => go(-1), forward: () => go(1)}
, 这里的 push, replace 函数最终调用的都是finalizeNavigation()
而 这里面主要有两个关键地方,一是routerHistory.push/replace
, 二是更新了currentRoute.value
而正是这个更新会触发<router-view/>
组件的更新。go 是直接使用了routerHistory.go(delta)
接口可以看到,不管是 push/replace 还是 go 最后都是使用了 history 的 api 。
路由插件的安装函数
install(app/* vue app */)
, 这里需要注意它做了几件事情:注册 RouterLink
,RouterView
两个 vue 组件定义了全局属性 $route
指向currentRoute
provide routerKey -> router 当前 router 实例 provide routeLocationKey -> reactiveRoute location 相关信息 provide routerViewLocationKey -> currentRoute 当前路由记录 重写 vue 组件的 unmount 函数,执行路由的清理工作,比如:移除事件监听,重置路由属性等
<router-view/>
组件的实现原理,通过<router-link to/>
或router.push/replace/go
api 触发路由跳转动作实现history 的实现原理(结合 Ref + history hash/H5api),这个对用户是不可见的
vue-router 简要图:
TODO 守卫函数完整执行流程
HTML5 history api
api | 描述 |
---|---|
pushState(state, title, url) | 向历史记录中增加一条记录 |
replaceState(state, title, url) | 替换当前记录,不新增记录 |
back() | 返回上一条记录,等价于 go(-1) |
go(n) | 跳转到第 n 条记录 |
forward() | 等价于 go(1) |
onpopstate | 事件,当且执行 history.back() 或 history.forward() 或 history.go(n) 的时候触发 |
state | 记录当前页面的状态信息,在执行 pushState 或 replaceState 之前为 null ,之后为第一个传入的参数,可以在 onpopstate 回调中通过 event.state 取到该信息 |
changeLocation() 测试。。。,
点击下面的按钮,注意观察 location 变化和 history.length 长度变化!针对
onpopstate
只有在执行实际跳转动作的时候才会触发,什么是实际跳转动作?比如:浏览器的后台前进按钮,或者直接手动调用
history.back()
,history.go(n)
,history.forward()
方法触发。
然后 vue-router 中是如何使用 history 实现路由功能的?
createWebHistory
H5 的 history api 封装,返回的结构: RouterHistory
包含以下成员
成员名 | 描述 | - |
---|---|---|
base | 站基地址,会添加到每个 url 前面 | 如: a.com/sub 那么 base 是 /sub |
loation | 当前 history location | 非原生的 location, 封装之后的: {value: location} |
state | 当前的 history state | 非原生 history state ,初始值是这个,但后续的值需要函数手动管理 |
push(to,data?) | 对应 pushState 操作 | 不会触发 popstate |
replace(to,data?) | 对应 replaceState 操作 | 不会触发 popstate |
go(delta, triggerListeners?) | 调用 history.go(delta) | 会触发 popstate 事件 |
listen(callback) | 用户调用添加的监听函数 | popstate 触发期间执行 |
createHref(location) | 构建 href 地址 | - |
destory() | 注销 listen() 注册的事件 | - |
Router 中的
back()
和forward()
分别是调用这里的go(-1)
和go(1)
实现。
|
|
base = normalizeBase(base)
解析网站基路径
!base
? 无自定义地址首先取
<base href="http://ip:port/path/to" />
的 href, 取出/path/to
部分作为 base\: 有自定义的时候,加上开头
/
和去掉尾部/
,如:path/to
变成/path/to
, 或/path/to/
变成/path/to
const historyNavigation = useHistoryStateNavigation(base)
将
window.location
和window.history
进行封装,返回{location, state, push, replace}
对象,所以这里重点就是这个函数。const historyListeners = useHistoryListeners(...)
history 变更监听器。
go(delta, triggerListeners)
函数在调用
history.go(delta)
之前检测是否暂停 history listeners组装
routerHistory
合并
{ location: '', base, go, createHref }
和 historyNavigation, historyListeners在 routerHistory 上定义两个 getter 属性
location
&state
返回 routerHistory 这个将来会被
createRouter({ history })
用到
useHistoryStateNavigation(base: string)
解构 window.history, window.location 组装 {location, state, push, replace}
结
构返回。
|
|
解析 location { pathname, search, hash } 返回不带域名的的 path
如:
http://ip:port/ui/#/a/b/?limit=10&page=1
-> base:/ui/#
->/a/b
http://ip:port/ui/a/b/?limit=10&page=1
-> base:/ui
->/a/b
http://ip:port/a/ui/b/?limit=10&page=1
-> base:/ui
->/a/ui/b
结构:
{value: url}
historyState = { value: history.state }
如果
historyState.value
为空,需要进行初始化 ->changeLocation()
changeLocation(to, state, replace)
函数replace(to, data?)
函数push(to, data?)
函数最后返回结构
{location: currentLocation, state: historyState, push, replace}
createCurrentLocation(base: string,location: Location)
对 location { pathname, search, hash }
加工返回新的 url
|
|
函数作用: base 中含有 #
时,直接从 location.hash 中解析出 path。
比如:
base=/ui/#/
url=https://ip:port/ui/#/base/industry/grouping?limit=10&page=1&tradeId=19×=1614652347338
最后解析出来的
path=/base/industry/grouping?limit=10&page=1&tradeId=19×=1614652347338
如果 base 不含 #
直接取出 path 中去掉 base 部分的 url,如:
base=/ui/
-> url=http://ip:port/ui/path/to...
得到 /path/to
如果 base 在 url pathname 的中间,直接返回 pathname 因为这种情况非 base 情况
http://ip:port/path/ui/to
直接返回 /path/ui/to
changeLocation(to,state,replace)
|
|
去掉 base hash 部分将 to
路由组合成 url 调用 history.replace|pushState(state,
title, url)
改变
url,同时修改 historyState.value 值。
replace(to, data?)
|
|
push(to, data?)
|
|
useHistoryListeners()
|
|
popStateHandler({ state })
因为 history.state 保存了执行跳转是 pushState/replaceState 传入的第一个参数值, 所以可以通过 to/from 上的 state 进行对比得到跳转的方向是 forward 还是 back。
但是 history.state 是实时的,执行完 push/replace 就会发生改变,这里怎么处理这个 问题呢,能让 to&from 状态得以保存?
答. 因为使用 historyState = { value: history.state } 做了个中介, 虽然 history.state 实时变化,但是这个 historyState 是不会的,手动用它来管理 to & from 的前后状态。
|
|
pauseListeners()
|
|
listen(callback)
纯粹的 add 操作,更新 listeners[]
和对应的移除函数列表 teardowns[]
|
|
beforeUnloadListener()
整个页面执行卸载之前的事件,发生在 unload
之前。
|
|
destroy() 注销事件
|
|
createWebHashHistory
/vue-router-next/src/history/hash.ts
从源码可以看出,该函数是基于 createWebHistory(base)
完成的,也就是说这个也是基
于 history api 完成,只不过在这个基础上对 hash 值进行了情况分析和检测,做了进一
步优化处理。
参数 base,可以函数调用时提供,如果存在
<base href/>
标签会优先取这个标签的 href 值解析出 base 值。
如,函数注释,有以下几种可能情况(如: base=https://example.com/folder
)
createWebHashHistory()
无参数createWebHashHistory('/folder/')
匹配
/folder
成功,结果: https://example.com/folder/#createWebHashHistory('/folder/#/app')
中间有
#
符号的:匹配
/folder
成功,结果: https://example.com/folder/#/appcreateWebHashHistory('other-folder')匹配失败,会直接替换,结果: https://example.com/other-folder/#
不推荐这种,因为它会改变根路径。
无主机的地址,比如本地文件访问: ///usr/etc/folder/index.html
createWebHashHistory('/iAmIgnored')
结果: ///usr/etc/folder/index.html#
提供的 base 会被忽略。
|
|
更多请查看 createWebHistory 。
TODO createMemoryHistory
通过一个队列来管理路由。
|
|