vue-router 源码解析(三)-实现路由守卫

基本使用

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,并返回相关API
	const matcher = createRouterMatcher(options.routes, options)
	// createWebHashHistory创建出的路由对象
	const routerHistory = options.history
	//...
	
    // useCallbacks 具有发布订阅功能的hook
	const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
	const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
	const afterGuards = useCallbacks<NavigationHookAfter>()
	// ...
	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)
		//...
		// 初始化完成后,会首次调用push
		if (
			isBrowser &&
			// 在浏览器环境下,避免多个app中使用时造成重复push
			!started &&
			currentRoute.value === START_LOCATION_NORMALIZED // 等于默认信息,说明是首次导航
		) {
		started = true
		// push执行后调用navigate,一系列路由守卫的执行会变成Promise调用(包括动态路由),当一系列Promise解析完成后,才会调用routerHistory.push|routerHistory.replace改变页面url
		push(routerHistory.location).catch(err => {
		  if (__DEV__) warn('Unexpected error when starting the router:', err)
		})
		}
	 }
	}
	return router
}

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

export function useCallbacks<T>() {
  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<NavigationFailure | void | undefined> {
	// 返回路由匹配结果
	const targetLocation: RouteLocation = (pendingLocation = resolve(to))
	// 当前位置
    const from = currentRoute.value //currentRoute始终指向当前页面路由信息,只有在最终完成导航,页面url改变后finalizeNavigation中再重新被赋值
    const data: HistoryState | undefined = (to as RouteLocationOptions).state
    const force: boolean | undefined = (to as RouteLocationOptions).force
    
    // 是否是重定向
    const replace = (to as RouteLocationOptions).replace === true
    // 重定向再次调用pushWithRedirect
    if (shouldRedirect)
      // 调用pushWithRedirect处理重定向路径
      return pushWithRedirect(
        assign(locationAsObject(shouldRedirect), {
          state:
            typeof shouldRedirect === 'object'
              ? assign({}, data, shouldRedirect.state)
              : data,
          force,
          replace,
        }),
        // keep original redirectedFrom if it exists
        redirectedFrom || targetLocation
      )


    // 如果配置了redirect重定向,返回重定向的路由信息
    const shouldRedirect = handleRedirectRecord(targetLocation)
	
	// 当重定向再次调用时,redirectedFrom会有值,为上次的targetLocation
    const toLocation = targetLocation as RouteLocationNormalized
    toLocation.redirectedFrom = redirectedFrom
	
	// 当守卫阻碍时,给出阻碍的原因
	let failure: NavigationFailure | void | undefined
	
    // 判断是否跳转当前路由记录
    if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
      failure = createRouterError<NavigationFailure>(
        ErrorTypes.NAVIGATION_DUPLICATED,  // 重复跳转
        { to: toLocation, from }
      )
      // trigger scroll to allow scrolling to the same anchor
      handleScroll(
        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 push
        true,
        // This cannot be the first navigation because the initial location
        // cannot be manually navigated to
        false
      )
    }
	// 如果是重复跳转,返回 NAVIGATION_DUPLICATED 重复跳转的错误,结束流程
	return (failure ? Promise.resolve(failure): navigate(toLocation, from)
		//....
}

resolve返回路由记录匹配结果

  • 调用matcher.resolve返回当前路由的匹配记录及其所有上层链路放进matched属性中,当前路由的匹配记录会被放进数组最后一个
  // 路由匹配,并返回解析结果
 function resolve(
   rawLocation: Readonly<RouteLocationRaw>,
   currentLocation?: RouteLocationNormalizedLoaded
 ): RouteLocation & { href: string } {
   currentLocation = assign({}, currentLocation || currentRoute.value)
    if (typeof rawLocation === 'string') {
      // 返回完整的pathname、query、hash、path
      const 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 object
      return 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<MatcherLocationRaw>,
  currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
    let matcher: RouteRecordMatcher | undefined
    let 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 encoding
	    path = location.path
	
	    if (__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 loop
	
	    if (matcher) {
	      params = matcher.parse(path)!
	      name = matcher.record.name
	    }
	    // location is a relative path
	}
    // 根据当前路由匹配结果,获取该路由的所有上层链路,根据parent属性一路向上查找
    const matched: MatcherLocation['matched'] = []
    let parentMatcher: RouteRecordMatcher | undefined = matcher
    while (parentMatcher) {
      // 父路由在数组开头,当前路由记录在末尾
      matched.unshift(parentMatcher.record)
      parentMatcher = parentMatcher.parent
    } 
    return {
      name,
      path,
      params,
      matched,
      // 合并链路上的所有meta
      meta: mergeMetaFields(matched),
    }
}

navigate 开始守卫

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

路由守卫前置方法

抽取路由记录

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

  • 存放要去到的路由记录中,新记录中不存在的旧记录
  • 存放要去到的路由记录中,新记录中存在的旧记录
  • 存放要去到的路由记录中,旧记录中没有的新记录
 function navigate(
   to: RouteLocationNormalized,
   from: RouteLocationNormalizedLoaded
 ): Promise<any> {
   let guards: Lazy<any>[]
   // 抽取 旧路由->新路由 中三种记录:旧记录离开、旧记录更新、新记录
   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)
      // 如果要去到的路由记录中没有之前的记录,则把之前的记录放进 leavingRecords
      else leavingRecords.push(recordFrom)
    }
    const recordTo = to.matched[i]
    if (recordTo) {
      // the type doesn't matter because we are comparing per reference
      // 如果要去到的路由的是新记录,则把新记录放进 enteringRecords
      if (!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<void> {
	// 保存 beforeRouteEnter 中的next回调,等到组件挂载后调用
	const enterCallbackArray =
		record &&
		(record.enterCallbacks[name!] = record.enterCallbacks[name!] || [])
	return () =>
		new Promise((resolve, reject) => {
		// 统一将守卫返回结果变成Promise
		const guardReturn = guard.call(
		  record && record.instances[name!],
		  to,
		  from,
		next  
		)
		let guardCall = Promise.resolve(guardReturn)
		
		// 守卫方法参数小于3个,即守卫内不调用next时(通过return的方式),在.then中自动调用next
		// 如果不加参数<3,那么then中的回调会调用两次
		if (guard.length < 3) guardCall = guardCall.then(next)
		guardCall.catch(err => reject(err))
		
		const next = ()=>{
		 // ...
		}
	}
      
}
  • next 方法
    处理next|return各种返回值,以及保存beforeRouteEnter中的next回调,等待路由组件挂载后调用(才能访问到组件实例)
 const next: NavigationGuardNext = (
   // 参数路由守卫中next或return返回值
   valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
 ) => {
   // 拒绝导航,即return false, 产生一个 NavigationFailure,类型为NAVIGATION_ABORTED
   if (valid === false) {
     reject(
       createRouterError<NavigationFailure>(
         ErrorTypes.NAVIGATION_ABORTED,
         {
           from,
           to,
         }
       )
     )
   } else if (valid instanceof Error) { // 自定义错误类型
     reject(valid)
   } else if (isRouteLocation(valid)) { // 重定向到新的地址也会产生一个 NavigationFailure,类型为NAVIGATION_GUARD_REDIRECT
     reject(
       createRouterError<NavigationRedirectError>(
         ErrorTypes.NAVIGATION_GUARD_REDIRECT,
         {
           from: to,
           to: valid,
         }
       )
     )
   } else { // 不阻断导航时会进入这里(比如返回 true 等)
     // 导航返回了一个函数的情况,比如 beforeRouteEnter 中访问this,当调用时路由组件还没创建完毕,所以需要保存next中回调
     /**
      *   next(vm => { access to component public instance via `vm`})
      */
     if (
       enterCallbackArray &&
       record!.enterCallbacks[name!] === enterCallbackArray &&
       typeof valid === 'function'
     ) {
       enterCallbackArray.push(valid) // 保存next中回调,待到要导航的路由组件创建完毕后调用
     }
     resolve()
   }
 }

extractComponentsGuards 从组件中抽取守卫

  • 组件还未挂载时,跳过update 和 leave守卫
  • 会处理vue-class-component类型组件、一般组件和动态组件
  • 动态组件的处理时会将组件的then回调存入执行队列中,
export function extractComponentsGuards(
  matched: RouteRecordNormalized[],  // 给定要抽取的路由记录
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {

	const guards: Array<() => Promise<void>> = []
	
	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 = rawComponent
			          rawComponent = () => promise
				 }else if ( 
				    // 使用了 defineAsyncComponent() 的检查
			          (rawComponent as any).__asyncLoader &&
			          // warn only once per component
			          !(rawComponent as any).__warnedDefineAsync
			        ) {
			          ; (rawComponent as any).__warnedDefineAsync = true
			          warn(
			            `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
			
			// 抽取组件中guard...
		}

	}
}
  • 从vue-component-class、普通组件中抽取guard

直接从组件options中获取

export function extractComponentsGuards(
  matched: RouteRecordNormalized[],  // 给定要抽取的路由记录
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
	// ...
    if (isRouteComponent(rawComponent)) {
       // 兼容 vue-class-component 格式编写的的类组件和一般组件
       const options: ComponentOptions =
         (rawComponent as any).__vccOpts || rawComponent
       // 获取到路由组件中的守卫
       const guard = options[guardType]
       // 将守卫返回结果变成Promise,并处理守卫返回值
       guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
     } 
}
  • 从动态组件中抽取guard

通过.then获取动态路由的请求结果后,替换record记录中的动态路由,之后的处理和普通路由组件一致

export function extractComponentsGuards(
  matched: RouteRecordNormalized[],  // 给定要抽取的路由记录
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
   // ...
   
   // start requesting the chunk already
   let componentPromise: Promise<
     RouteComponent | null | undefined | void
   > = (rawComponent as Lazy<RouteComponent>)()
   
   // 判断路由组件是否返回Promise
   guards.push(() =>
     componentPromise.then(resolved => {
       // 拿到动态组件请求结果
       if (!resolved)
         return Promise.reject(
           new Error(
             `Couldn't resolve component "${name}" at "${record.path}"`
           )
         )
       // 判断当前环境(node取default)
       const resolvedComponent = isESModule(resolved)
         ? resolved.default
         : resolved
       // 将动态路由 import 完成后,替换记录中的对应components路由
       record.components![name] = resolvedComponent
       // __vccOpts is added by vue-class-component and contain the regular options
       // 当动态路由处理完成后,接下来的守卫处理方式和普通路由组件一样
       const options: ComponentOptions =
         (resolvedComponent as any).__vccOpts || resolvedComponent
       const guard = options[guardType]
       return guard && guardToPromiseFn(guard, to, from, record, name)()
     })
   )
}

checkCanceledNavigationAndReject 守卫之后地址校验

  • 会在每一个类型的守卫收集完毕后添加进guard队列中,用于校验经过守卫后地址是否改变,如果改变返回一个 类型为 NAVIGATION_CANCELLED 的 NavigationFailure
function checkCanceledNavigationAndReject(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): Promise<void> {
  const error = checkCanceledNavigation(to, from)
  return error ? Promise.reject(error) : Promise.resolve()
}

function checkCanceledNavigation(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): NavigationFailure | void {
  // 如果要导航的地址发生了变化,产生一个NavigationFailure
  if (pendingLocation !== to) {
    return createRouterError<NavigationFailure>(
      ErrorTypes.NAVIGATION_CANCELLED,
      {
        from,
        to,
      }
    )
  }
}

runGuardQueue 执行Promise队列,返回一个Promise结果

  • 通过reduce实现Promise的串行,等到上一个Promise执行完毕后,再执行一下个
function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
  return guards.reduce(
    (promise, guard) => promise.then(() => guard()),
    Promise.resolve()
  )
}

在这里插入图片描述

beforeRouteLeave 守卫收集

  • 拿到所有离开记录的守卫
  • 将守卫返回值变成Promise返回存入数组中返回,兼容动态路由和普通路由方式
function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
  let guards: Lazy<any>[]
  // 抽取 旧路由->新路由 中三种记录:旧记录离开、旧记录更新、新记录
  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 包装的 NavigationFailure,根据实际要去到的路由记录和之前记录的要去到的路由记录是否是同一个
  // 如果不是,返回一个 NAVIGATION_CANCELLED
  const 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
  ).value

  if (!activeRecord) {
    __DEV__ &&
      warn(
        'No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of <router-view>. 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
  ).value

  registerGuard(activeRecord, 'updateGuards', updateGuard)
}

守卫阶段性执行

  • 通过runGuardQueue执行之前收集的守卫队列
  • 在每一个.then中处理对应阶段守卫的执行结果

执行 beforeRouteLeave 守卫

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards) // 执行 beforeRouteLeave 守卫
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		}
		.then(){
		 // 执行 beforeResolve 守卫
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

执行全局 beforeEach 守卫

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards) // guards是上面收集到的beforeRouteLeave
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		  guards = []
          // 这里的守卫通过 router.beforeEach 进行添加的
          for (const guard of beforeGuards.list()) {
            guards.push(guardToPromiseFn(guard, to, from))
          }
          // 每次检查导航是否被取消
          guards.push(canceledNavigationCheck)

          return runGuardQueue(guards)
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		}
		.then(){
		 // 执行 beforeResolve 守卫
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

执行 beforeRouteUpdate 守卫

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards)
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		  guards = extractComponentsGuards( // 组件中抽取beforeRouteUpdate
            updatingRecords,
            'beforeRouteUpdate',
            to,
            from
          )
          // 在setup中使用的 onBeforeRouteUpdate 路由守卫会被放进 updateGuards 中
          for (const record of updatingRecords) {
            record.updateGuards.forEach(guard => {
              guards.push(guardToPromiseFn(guard, to, from))
            })
          }
          guards.push(canceledNavigationCheck)

          // run the queue of per route beforeEnter guards
          return runGuardQueue(guards)
		 
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		}
		.then(){
		 // 执行 beforeResolve 守卫
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

执行全局 beforeEnter 守卫

  • 这里需要注意如果之前导航的路由记录中已经出发过对应记录的beforeEnter,那么不再执行
function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards)
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
          guards = []
          for (const record of to.matched) {
            // 如果之前已经触发过了该路由记录的beforeEnter,不再触发
            if (record.beforeEnter && !from.matched.includes(record)) {
              // 兼容 beforeEnter 的两种格式
              if (isArray(record.beforeEnter)) {
                for (const beforeEnter of record.beforeEnter)
                  guards.push(guardToPromiseFn(beforeEnter, to, from))
              } else {
                guards.push(guardToPromiseFn(record.beforeEnter, to, from))
              }
            }
          }
          guards.push(canceledNavigationCheck)

          // run the queue of per route beforeEnter guards
          return runGuardQueue(guards)
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		}
		.then(){
		 // 执行 beforeResolve 守卫
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

执行 beforeRouteEnter 守卫

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards)
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		 
		  // 清除之前可能存在的 enterCallbacks
          // enterCallbacks 会在 extractComponentsGuards 中的 guardToPromiseFn 进行处理,用来保存传入next的函数
          to.matched.forEach(record => (record.enterCallbacks = {}))

          // check in-component beforeRouteEnter
          guards = extractComponentsGuards(
            enteringRecords,
            'beforeRouteEnter',
            to,
            from
          )
          guards.push(canceledNavigationCheck)


          // run the queue of per route beforeEnter guards
          return runGuardQueue(guards)
		}
		.then(){
		 // 执行 beforeResolve 守卫
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

执行全局 beforeResolve 守卫

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
	
	return runGuardQueue(guards)
		.then(){
		 // 如果 beforeRouteLeave 全部成功执行,执行 beforeEach 全局守卫
		}
		.then(){
		 // 如果 beforeEach 全部成功执行,执行 beforeRouteUpdate 守卫
		}
		.then(){
		 // 执行全局 beforeEnter 守卫
		}
		.then(){
		 // 执行 beforeRouteEnter 守卫
		}
		.then(){
		 // 执行 beforeResolve 守卫
		  
		  guards = []
		  //通过router.beforeResolve添加
          for (const guard of beforeResolveGuards.list()) {
            guards.push(guardToPromiseFn(guard, to, from))
          }
          guards.push(canceledNavigationCheck)

          return runGuardQueue(guards)
		}
        .catch(err =>
          // 捕获中途任意一个导致导航取消的 NavigationFailure
          isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
            ? err
            : Promise.reject(err)
        )
}

处理守卫中阻断

  • 守卫过程中任何阻挡产生的failure都会被catch捕捉,如果是重定向的原因,那么重新导航,其他原因直接返回failure
function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
	// ...
	
	return (failure ? Promise.resolve(failure) 
		: navigate(toLocation, from))
	      .catch((error: NavigationFailure | NavigationRedirectError) =>
	        isNavigationFailure(error)
	          ? // navigation redirects still mark the router as ready
	          isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
	            ? error
	            : markAsReady(error) // also returns the error
	          : // reject any unknown error
	          triggerError(error, toLocation, from)
	      )
		  .then((failure: NavigationFailure | NavigationRedirectError | void)=>{
		  	if (failure) {
				if ( // 如果failure是因为重定向
				  isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
				) {
				    // 重新导航到重定向记录
					return pushWithRedirect(
					            // keep options
					            assign(
					              {
					                // preserve an existing replacement but allow the redirect to override it
					                replace,
					              },
					              locationAsObject(failure.to), // 跳转守卫中返回的路径
					              {
					                state:
					                  typeof failure.to === 'object'
					                    ? assign({}, data, failure.to.state)
					                    : data,
					                force,
					              }
					            ),
					            // preserve the original redirectedFrom if any
					            redirectedFrom || toLocation
					          )
					}
		   } else{ 
		   		// 没有阻断情况
		   
		   }
		    // 导航最后触发 afterEach 守卫,从这里可以看出,路由变化后才会触发afterEach
	        triggerAfterEach(
	          toLocation as RouteLocationNormalizedLoaded,
	          from,
	          failure
	        )
	        return failure
		}) // 无阻断情况
}

无阻碍完成守卫,更改页面URL

  • 调用 finalizeNavigation 更改页面URL,初次导航设置popstate监听
  • 调用全局 afterEach 守卫
function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
	// ...
	
	return (failure ? Promise.resolve(failure) 
		: navigate(toLocation, from))
	      .catch((error: NavigationFailure | NavigationRedirectError) =>
			// 处理failure
	      )
		  .then((failure: NavigationFailure | NavigationRedirectError | void)=>{
		  	if (failure) {
		  		// 守卫阻断
		  	}
		   } else{ 
			    // 没有阻断情况
				failure = finalizeNavigation(
					toLocation as RouteLocationNormalizedLoaded,
					from,
					true,
					replace,
					data
				)
	        }
		   }
		    // 导航最后触发 afterEach 守卫,从这里可以看出,路由变化后才会触发afterEach
	        triggerAfterEach(
	          toLocation as RouteLocationNormalizedLoaded,
	          from,
	          failure
	        )
	        return failure
		}) // 无阻断情况
}

finalizeNavigation 完成导航设置监听

  • 路由初始化中的导航使用replace,避免初始化留下两次当前页面的路由记录
  • 调用markAsReady往popstate事件中添加回调
function finalizeNavigation(
  toLocation: RouteLocationNormalizedLoaded,
  from: RouteLocationNormalizedLoaded,
  isPush: boolean,
  replace?: boolean,
  data?: HistoryState
): NavigationFailure | void {
	// 最终确定导航地址是否改变
	const error = checkCanceledNavigation(toLocation, from)
    if (error) return error
	
	// from为默认值,判断为初始导航
    const isFirstNavigation = from === START_LOCATION_NORMALIZED
    const state = !isBrowser ? {} : history.state

    if (isPush) {
      // 第一次导航使用replace或者指定replace
      if (replace || isFirstNavigation)
        routerHistory.replace(
          toLocation.fullPath,
          assign(
            {
              scroll: isFirstNavigation && state && state.scroll,
            },
            data
          )
        )
      else routerHistory.push(toLocation.fullPath, data)
    }

    // currentRoute 始终指向当前页面录音信息
    currentRoute.value = toLocation
    handleScroll(toLocation, from, isPush, isFirstNavigation)
    // 可以进行popstate监听
    markAsReady()

}

markAsReady 添加回调,处理后续浏览器前进后退等事件

function markAsReady<E = any>(err?: E): E | void {
    if (!ready) {
      // still not ready if an error happened
      ready = !err
      // 往popstate中添加回调
      setupListeners()
      readyHandlers
        .list()
        .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
      readyHandlers.reset()
    }
    return err
}

setupListeners 监听浏览器前进后退等事件

  • 只会在路由初始化时添加,通过routerHistory.listen添加进popstate中
  • 剩余流程和pushWithRedirect差不多
function setupListeners() {
    // 只有首次有效
	if (removeHistoryListener) return
	
	removeHistoryListener = routerHistory.listen((to, _from, info) => {
		if (!router.listening) return
		// 返回匹配的路由信息
		const toLocation = resolve(to) as RouteLocationNormalized
		// 是否设置了replace属性
		const shouldRedirect = handleRedirectRecord(toLocation)
		if (shouldRedirect) {
		  pushWithRedirect(
		    assign(shouldRedirect, { replace: true }),
		    toLocation
		  ).catch(noop)
		  return
		}
		
		// 更新缓存信息
		pendingLocation = toLocation
        const from = currentRoute.value
        // 最后调用navigate完成导航
        navigate(toLocation, from)
         // 处理守卫过程中抛出的redirect failure
         .catch((error: NavigationFailure | NavigationRedirectError) => {
          if (
            isNavigationFailure(
              error,
              ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
            )
          ) {
            return error
          }
          if (
            isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
          ) {
            pushWithRedirect(
              (error as NavigationRedirectError).to,
              toLocation

            )
              .then(failure => {
                if (
                  isNavigationFailure(
                    failure,
                    ErrorTypes.NAVIGATION_ABORTED |
                    ErrorTypes.NAVIGATION_DUPLICATED
                  ) &&
                  !info.delta &&
                  info.type === NavigationType.pop
                ) {
                  routerHistory.go(-1, false)
                }
              })
              .catch(noop)
            // avoid the then branch
            return Promise.reject()
          }
          // do not restore history on unknown direction
          if (info.delta) {
            routerHistory.go(-info.delta, false)
          }
          // unrecognized error, transfer to the global handler
          return triggerError(error, toLocation, from)
        })
         .then((failure: NavigationFailure | void) => {
          failure =
            failure || // 如果有其它错误不走finalizeNavigation
            finalizeNavigation( // 更改页面URL
              // after navigation, all matched components are resolved
              toLocation as RouteLocationNormalizedLoaded,
              from,
              false
            )
		  // 处理其它错误
          // revert the navigation
          if (failure) {
            if (
              info.delta &&
              // a new navigation has been triggered, so we do not want to revert, that will change the current history
              // entry while a different route is displayed
              !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED)
            ) {
              routerHistory.go(-info.delta, false)
            } else if (
              info.type === NavigationType.pop &&
              isNavigationFailure(
                failure,
                ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
              )
            ) {
              // manual change in hash history #916
              // it's like a push but lacks the information of the direction
              routerHistory.go(-1, false)
            }
          }
          // 触发 afterEach 守卫
          triggerAfterEach(
            toLocation as RouteLocationNormalizedLoaded,
            from,
            failure
          )
        })
        .catch(noop)
	}
}
  • popstate的加载时机在第一篇文章创建路由对象中有介绍,在调用createWebHistory等模式时创建
  const popStateHandler: PopStateListener = ({
    state,
  }: {
    state: StateEntry | null
  }) => {
    // 返回去掉base路径后的window.loaction的url
    const to = createCurrentLocation(base, location)
    const from: HistoryLocation = currentLocation.value
    const fromState: StateEntry = historyState.value
    let delta = 0
    // 更新最新路由信息
    if (state) {
      currentLocation.value = to
      historyState.value = state

      // ignore the popstate and reset the pauseState
      if (pauseState && pauseState === from) {
        pauseState = null
        return
      }
      delta = fromState ? state.position - fromState.position : 0
    } else {
      replace(to)
    }
    // 路由记录改变后触发监听,在setupListeners中会添加回调,对于history路由浏览器前进后退时触发导航
    listeners.forEach(listener => {
      listener(currentLocation.value, from, {
        delta,
        type: NavigationType.pop,  // 监听popstate
        direction: delta
          ? delta > 0
            ? NavigationDirection.forward
            : NavigationDirection.back
          : NavigationDirection.unknown,
      })
    })
  }

window.addEventListener('popstate', popStateHandler)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值