vue-router 源码解析(三)-实现路由守卫
创始人
2024-05-24 21:11:32
0

文章目录

  • 基本使用
  • 导语
  • 初始化路由守卫
  • useCallbacks 发布订阅模式管理路由守卫
  • push 开始导航
  • resolve返回路由记录匹配结果
  • navigate 开始守卫
  • 抽取路由记录
  • guardToPromiseFn 用Promise包装守卫方法
  • extractComponentsGuards 从组件中抽取守卫
  • beforeRouteLeave 守卫收集
  • composition 守卫收集
    • onBeforeRouteLeave
    • onBeforeRouteUpdate

基本使用

Navigation triggered.
Call beforeRouteLeave guards in deactivated components.
Call global beforeEach guards.
Call beforeRouteUpdate guards in reused components.
Call beforeEnter in route configs.
Resolve async route components.
Call beforeRouteEnter in activated components.
Call global beforeResolve guards.
Navigation is confirmed.
Call global afterEach hooks.
DOM updates triggered.
Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

导语

  • 在上一文中,解释了如何由routes配置转换出一条条matcher,而本文将开启导航部分,当开发者调用push方法后,路由守卫是如何被收集以及触发的(包括动态路由)
    在这里插入图片描述

初始化路由守卫

const router = VueRouter.createRouter({history: VueRouter.createWebHashHistory(), // 创建对应的路由对象routes,
})
export function createRouter(options: RouterOptions): Router {// routes转换成matcher,并返回相关APIconst matcher = createRouterMatcher(options.routes, options)// createWebHashHistory创建出的路由对象const routerHistory = options.history//...// useCallbacks 具有发布订阅功能的hookconst beforeGuards = useCallbacks>()const beforeResolveGuards = useCallbacks>()const afterGuards = useCallbacks()// ...const router: Router = {//...push,replace,go,back: () => go(-1),forward: () => go(1),// 因为useCallbacks是发布订阅模式,所以调用router创建路由守卫时会收集对应回调beforeEach: beforeGuards.add,beforeResolve: beforeResolveGuards.add,afterEach: afterGuards.add,onError: errorHandlers.add,install(app: App) {// 创建 RouterLink、RouterView 这俩全局组件app.component('RouterLink', RouterLink)app.component('RouterView', RouterView)//...// 初始化完成后,会首次调用pushif (isBrowser &&// 在浏览器环境下,避免多个app中使用时造成重复push!started &¤tRoute.value === START_LOCATION_NORMALIZED // 等于默认信息,说明是首次导航) {started = true// push执行后调用navigate,一系列路由守卫的执行会变成Promise调用(包括动态路由),当一系列Promise解析完成后,才会调用routerHistory.push|routerHistory.replace改变页面urlpush(routerHistory.location).catch(err => {if (__DEV__) warn('Unexpected error when starting the router:', err)})}}}return router
}

useCallbacks 发布订阅模式管理路由守卫

export function useCallbacks() {let handlers: T[] = []function add(handler: T): () => void {handlers.push(handler)return () => {const i = handlers.indexOf(handler)if (i > -1) handlers.splice(i, 1)}}function reset() {handlers = []}return {add, // 收集list: () => handlers,reset,}
}

push 开始导航

  function push(to: RouteLocationRaw) {return pushWithRedirect(to)}

pushWithRedirect

  • 会兼容push和redirect两种调用
  • 总体流程为:resolve匹配要跳转的记录->判断重定向(是则重新调用pushWithRedirect,设置replace属性为true)->判断是否跳转相同路由->开始导航->触发路由守卫->完成导航
  • 对于重复跳转的路由,返回 NAVIGATION_DUPLICATED 类型的错误即可,不再走后续流程
function pushWithRedirect(to: RouteLocationRaw | RouteLocation,redirectedFrom?: RouteLocation // 重定向路由信息位置
): Promise {// 返回路由匹配结果const targetLocation: RouteLocation = (pendingLocation = resolve(to))// 当前位置const from = currentRoute.value //currentRoute始终指向当前页面路由信息,只有在最终完成导航,页面url改变后finalizeNavigation中再重新被赋值const data: HistoryState | undefined = (to as RouteLocationOptions).stateconst force: boolean | undefined = (to as RouteLocationOptions).force// 是否是重定向const replace = (to as RouteLocationOptions).replace === true// 重定向再次调用pushWithRedirectif (shouldRedirect)// 调用pushWithRedirect处理重定向路径return pushWithRedirect(assign(locationAsObject(shouldRedirect), {state:typeof shouldRedirect === 'object'? assign({}, data, shouldRedirect.state): data,force,replace,}),// keep original redirectedFrom if it existsredirectedFrom || targetLocation)// 如果配置了redirect重定向,返回重定向的路由信息const shouldRedirect = handleRedirectRecord(targetLocation)// 当重定向再次调用时,redirectedFrom会有值,为上次的targetLocationconst toLocation = targetLocation as RouteLocationNormalizedtoLocation.redirectedFrom = redirectedFrom// 当守卫阻碍时,给出阻碍的原因let failure: NavigationFailure | void | undefined// 判断是否跳转当前路由记录if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {failure = createRouterError(ErrorTypes.NAVIGATION_DUPLICATED,  // 重复跳转{ to: toLocation, from })// trigger scroll to allow scrolling to the same anchorhandleScroll(from,from,// this is a push, the only way for it to be triggered from a// history.listen is with a redirect, which makes it become a pushtrue,// This cannot be the first navigation because the initial location// cannot be manually navigated tofalse)}// 如果是重复跳转,返回 NAVIGATION_DUPLICATED 重复跳转的错误,结束流程return (failure ? Promise.resolve(failure): navigate(toLocation, from)//....
}

resolve返回路由记录匹配结果

  • 调用matcher.resolve返回当前路由的匹配记录及其所有上层链路放进matched属性中,当前路由的匹配记录会被放进数组最后一个
  // 路由匹配,并返回解析结果function resolve(rawLocation: Readonly,currentLocation?: RouteLocationNormalizedLoaded): RouteLocation & { href: string } {currentLocation = assign({}, currentLocation || currentRoute.value)if (typeof rawLocation === 'string') {// 返回完整的pathname、query、hash、pathconst locationNormalized = parseURL(parseQuery,rawLocation,currentLocation.path)// 返回路由记录的匹配结果const matchedRoute = matcher.resolve({ path: locationNormalized.path },currentLocation)const href = routerHistory.createHref(locationNormalized.fullPath)// locationNormalized is always a new objectreturn assign(locationNormalized, matchedRoute, {params: decodeParams(matchedRoute.params),hash: decode(locationNormalized.hash),redirectedFrom: undefined,href,})}// 处理rawLocation为对象的情况// ...}

matcher.resolve

  • 通过之前addRoute方法创建好的一条条matcher去匹配path,返回匹配的matcher及其上层链路所有matcher
function resolve(location: Readonly,currentLocation: Readonly
): MatcherLocation {let matcher: RouteRecordMatcher | undefinedlet params: PathParams = {}let path: MatcherLocation['path']let name: MatcherLocation['name']// ... 其他根据name匹配等情况// 根据path进行匹配的情况if ('path' in location) {// no need to resolve the path with the matcher as it was provided// this also allows the user to control the encodingpath = location.pathif (__DEV__ && !path.startsWith('/')) {warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`)}// 通过每个matcher的正则匹配对应的记录matcher = matchers.find(m => m.re.test(path))// matcher should have a value after the loopif (matcher) {params = matcher.parse(path)!name = matcher.record.name}// location is a relative path}// 根据当前路由匹配结果,获取该路由的所有上层链路,根据parent属性一路向上查找const matched: MatcherLocation['matched'] = []let parentMatcher: RouteRecordMatcher | undefined = matcherwhile (parentMatcher) {// 父路由在数组开头,当前路由记录在末尾matched.unshift(parentMatcher.record)parentMatcher = parentMatcher.parent} return {name,path,params,matched,// 合并链路上的所有metameta: mergeMetaFields(matched),}
}

navigate 开始守卫

function pushWithRedirect(to: RouteLocationRaw | RouteLocation,redirectedFrom?: RouteLocation): Promise {// ...return (failure ? Promise.resolve(failure) : navigate(toLocation, from))// 处理守卫中重定向.catch((error: NavigationFailure | NavigationRedirectError) =>isNavigationFailure(error)? // navigation redirects still mark the router as readyisNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)? error // 返回给下一个then: markAsReady(error) // also returns the error: // reject any unknown errortriggerError(error, toLocation, from))// .then((failure: NavigationFailure | NavigationRedirectError | void) => {if (failure) {// 处理在守卫中进行的重定向,由catch返回if (isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)) {return pushWithRedirect( // 重定向assign({// preserve an existing replacement but allow the redirect to override itreplace,},locationAsObject(failure.to), // 跳转守卫中返回的路径{state:typeof failure.to === 'object'? assign({}, data, failure.to.state): data,force,}),// preserve the original redirectedFrom if anyredirectedFrom || toLocation)}} else {// 整个守卫过程没有被阻断// finalizeNavigation 会调用 routerHistory.push 或 routerHistory.replace 更改路由记录failure = finalizeNavigation(toLocation as RouteLocationNormalizedLoaded,from,true,replace,data)}// 导航最后触发 afterEach 守卫,从这里可以看出,路由变化后(页面地址更新)才会触发afterEachtriggerAfterEach(toLocation as RouteLocationNormalizedLoaded,from,failure)return failure})
}

抽取路由记录

根据路由记录中的matched会抽取三种类型路由记录

  • 存放要去到的路由记录中,新记录中不存在的旧记录
  • 存放要去到的路由记录中,新记录中存在的旧记录
  • 存放要去到的路由记录中,旧记录中没有的新记录
 function navigate(to: RouteLocationNormalized,from: RouteLocationNormalizedLoaded): Promise {let guards: Lazy[]// 抽取 旧路由->新路由 中三种记录:旧记录离开、旧记录更新、新记录const [leavingRecords, updatingRecords, enteringRecords] =extractChangingRecords(to, from)// ...
}

extractChangingRecords

  • 抽离出三种记录,后续根据记录等类型触发相应的路由守卫,这里也说明了为什么resolve中要去拿到上层路由记录,导航时上层链路中的守卫都需要触发
// 抽取 旧路由->新路由 中三种记录:旧记录离开、旧记录更新、新记录
function extractChangingRecords(to: RouteLocationNormalized,from: RouteLocationNormalizedLoaded
) {const leavingRecords: RouteRecordNormalized[] = []  // 存放要去到的路由记录中,新记录中不存在的旧记录const updatingRecords: RouteRecordNormalized[] = [] // 存放要去到的路由记录中,新记录中存在的旧记录const enteringRecords: RouteRecordNormalized[] = [] // 存放要去到的路由记录中,旧记录中没有的新记录const len = Math.max(from.matched.length, to.matched.length)for (let i = 0; i < len; i++) {// 拿到要离开路由的记录const recordFrom = from.matched[i]if (recordFrom) {// 查找要去到路由记录中,是否已有路由记录// 如果有则把之前的路由记录放进 updatingRecords 只需更新if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))updatingRecords.push(recordFrom)// 如果要去到的路由记录中没有之前的记录,则把之前的记录放进 leavingRecordselse leavingRecords.push(recordFrom)}const recordTo = to.matched[i]if (recordTo) {// the type doesn't matter because we are comparing per reference// 如果要去到的路由的是新记录,则把新记录放进 enteringRecordsif (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {enteringRecords.push(recordTo)}}}return [leavingRecords, updatingRecords, enteringRecords]
}

guardToPromiseFn 用Promise包装守卫方法

  • 当守卫中开发者没有调用next,会自动调用next
export function guardToPromiseFn(guard: NavigationGuard,  // 守卫回调方法to: RouteLocationNormalized,from: RouteLocationNormalizedLoaded,record?: RouteRecordNormalized,// 记录name?: string  // name为注册路由时的名称
): () => Promise {}

extractComponentsGuards 从组件中抽取守卫

export function extractComponentsGuards(matched: RouteRecordNormalized[],  // 给定要抽取的路由记录guardType: GuardType,to: RouteLocationNormalized,from: RouteLocationNormalizedLoaded
) {const guards: Array<() => Promise> = []for (const record of matched) {for (const name in record.components) { // 取出路由记录中对应的组件		let rawComponent = record.components[name]if (__DEV__) { // dev 环境下的一些动态组件检查if ('then' in rawComponent) { warn(`Component "${name}" in record with path "${record.path}" is a ` +`Promise instead of a function that returns a Promise. Did you ` +`write "import('./MyPage.vue')" instead of ` +`"() => import('./MyPage.vue')" ? This will break in ` +`production if not fixed.`)// 动态路由使用 () => import('./MyPage.vue') 而非 import('./MyPage.vue')const promise = rawComponentrawComponent = () => promise}else if ( // 使用了 defineAsyncComponent() 的检查(rawComponent as any).__asyncLoader &&// warn only once per component!(rawComponent as any).__warnedDefineAsync) {; (rawComponent as any).__warnedDefineAsync = truewarn(`Component "${name}" in record with path "${record.path}" is defined ` +`using "defineAsyncComponent()". ` +`Write "() => import('./MyPage.vue')" instead of ` +`"defineAsyncComponent(() => import('./MyPage.vue'))".`)}}// 当路由组件挂载后,会在router-view组件中,将组件根据name存入对应record的instances中// 当路由记录的instances中没有组件时,说明组件还没挂载或者被卸载了,跳过update 和 leave相关守卫if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue}}
}

beforeRouteLeave 守卫收集

  • 拿到所有离开记录的守卫
  • 将守卫返回值变成Promise返回存入数组中返回,兼容动态路由和普通路由方式
function navigate(to: RouteLocationNormalized,from: RouteLocationNormalizedLoaded
): Promise {let guards: Lazy[]// 抽取 旧路由->新路由 中三种记录:旧记录离开、旧记录更新、新记录const [leavingRecords, updatingRecords, enteringRecords] =extractChangingRecords(to, from)// 拿到所有离开记录的守卫// 抽取组件中 beforeRouteLeave 守卫,将守卫返回值变成Promise返回存入数组中返回,兼容动态路由和普通路由方式guards = extractComponentsGuards(leavingRecords.reverse(),'beforeRouteLeave',to,from)// 对于setup中使用的onBeforeRouteLeave路由守卫,会被收集进 leaveGuards 中,和选项式进行合并for (const record of leavingRecords) {record.leaveGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))})}// 如果导航被取消,产生一个 Promise 包装的 NavigationFailureconst canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null,to,from)guards.push(canceledNavigationCheck)// 之后执行守卫
}

composition 守卫收集

registerGuard

  • 收集composition守卫的方法,之前在序列化记录的过程中,会专门创建 leaveGuards和updateGuards 两个Set属性来存放对应守卫
function registerGuard(record: RouteRecordNormalized,name: 'leaveGuards' | 'updateGuards',guard: NavigationGuard
) {const removeFromList = () => {record[name].delete(guard)}onUnmounted(removeFromList)onDeactivated(removeFromList)onActivated(() => {record[name].add(guard)})record[name].add(guard) // 将守卫添加进记录的leaveGuards|updateGuards属性中
}

onBeforeRouteLeave

  • 在setup中调用onBeforeRouteLeave时,会根据router-view组件中匹配到的路由记录,将守卫注入到leaveGuards属性中,进行导航时从记录上的该属性取出即可
export function onBeforeRouteLeave(leaveGuard: NavigationGuard) { // leaveGuard即为传递的路由回调// matchedRouteKey在router-view组件中provide,返回当前匹配的路由记录const activeRecord: RouteRecordNormalized | undefined = inject(matchedRouteKey,// to avoid warning{} as any).valueif (!activeRecord) {__DEV__ &&warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of . Maybe you called it inside of App.vue?')return}registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}

onBeforeRouteUpdate

  • 同上
export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {// 在router-view组件中provide,const activeRecord: RouteRecordNormalized | undefined = inject(matchedRouteKey,// to avoid warning{} as any).valueregisterGuard(activeRecord, 'updateGuards', updateGuard)
}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...