vue-router 的 router.push 利用底层函数 pushWithRedirect 来实现,pushWithRedirect最后会利用 navigate 来实现真正的导航。
navigate
做了什么?
拆解路由匹配记录→按『离开→全局前置→更新→进入→解析前』的固定顺序执行各类守卫→全程校验并发导航取消。
- 准备阶段:
声明守卫变量,收集路由记录leavingRecords,updatingRecords,enteringRecords。 - 守卫执行顺序:
- 组件离开守卫(beforeRouteLeave),按从子组件到父组件的顺序执行,同时执行路由记录上的
leaveGuards。 - 全局前置守卫(beforeEach) :执行所有注册的全局前置守卫。
- 组件更新守卫(beforeRouteUpdate) :执行复用组件的更新守卫
,同时执行路由记录上的
updateGuards。 - 路由独享守卫(beforeEnter)。
- 组件进入守卫(beforeRouteEnter。
- 全局解析守卫(beforeResolve) 。
- 组件离开守卫(beforeRouteLeave),按从子组件到父组件的顺序执行,同时执行路由记录上的
- 导航校验。
在每轮守卫执行前,都会插入canceledNavigationCheck函数,用于检查导航是否被取消(如并发导航冲突)。- 如果是取消,直接返回错误。
- 其他,继续抛出,给上层捕获。
- 错误处理。
捕获导航过程中的错误,特别是导航取消的情况,进行特殊处理。
/**
* 守卫执行
* @param to 目标路由
* @param from 当前路由
* @returns
*/
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
// 声明守卫队列变量
let guards: Lazy<any>[]
// 拆解路由记录(离开/更新/进入)
const [
leavingRecords,
updatingRecords,
enteringRecords
] = extractChangingRecords(to, from)
// all components here have been resolved once because we are leaving
// 提取组件离开守卫(beforeRouteLeave)
guards = extractComponentsGuards(
leavingRecords.reverse(), // 反转:子组件守卫先执行,父组件后执行
'beforeRouteLeave', // 组件离开守卫
to,
from
)
// leavingRecords is already reversed
for (const record of leavingRecords) {
// leaveGuards 是提前缓存的(在组件挂载 / 路由匹配时注册)
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
null,
to,
from
)
// 将校验函数加入守卫列表
guards.push(canceledNavigationCheck)
// run the queue of per route beforeRouteLeave guards
// 离开守卫(beforeRouteLeave)
// → 全局前置(beforeEach)
// → 更新守卫(beforeRouteUpdate)
// → 路由进入(beforeEnter)
// → 组件进入(beforeRouteEnter)
// → 全局解析前(beforeResolve)
return (
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
// 插入并发导航校验(每轮守卫前必加)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components beforeRouteUpdate
// 提取组件内 beforeRouteUpdate 守卫(复用组件的更新守卫)
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
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(() => {
// check the route beforeEnter
guards = []
for (const record of enteringRecords) {
// do not trigger beforeEnter on reused views
if (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(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
// 清空之前的 enterCallbacks(避免重复执行)
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from,
runWithContext
)
// 插入并发导航校验(每轮守卫前必加)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
)
}
1、路由调整导航流程示例
在 http://localhost:5173/yo/dashboard 页面点击按钮跳转至 http://localhost:5173/yo/user。
导航守卫流程
- 组件内离开守卫
beforeRouteLeave,在导航离开渲染该组件的对应路由时调用。 - 全局前置守卫
router.beforeEeach。当一个导航触发时,全局前置守卫按照创建顺序调用。 - 路由独享守卫
beforeEnter,只在进入路由时触发。 - 组件内进入守卫
beforeRouteEnter,在渲染该组件的对应路由被验证前调用。 - 全局解析守卫
router.beforeResolve(navigate只到这里!)。 - 全局后置守卫
router.afterEach。
2、全局路由守卫配置
// 全局前置守卫
router.beforeEach((to, from) => {
console.log('全局前置守卫', to,from)
return true
})
// 全局解析守卫
router.beforeResolve((to, from) => {
console.log('全局解析守卫', to,from)
return true
})
// 全局后置守卫
router.afterEach((to, from, failure) => {
if(failure) {
console.log('全局后置守卫-failure', failure);
return;
}
console.log('全局后置守卫', to,from)
document.title = to.meta.title ? `Vue3 管理端 | ${to.meta.title}` : `Vue3 管理端`;
})
3、路由独享守卫配置
{
path: '/user',
name: 'user',
component: () => import('@/views/user/UserView.vue'),
meta: {
title: '用户管理',
icon: 'user',
roles: ['admin']
},
children: [ // 嵌套路由
{
path: 'lists',
name: 'user-list',
component: () => import('@/views/user/UserList.vue'),
beforeEnter: (to, from) => {
console.log('user-list独享路由', to,from)
return true
}
},
{
path: ':id',
// name: 'user-detail',
component: () => import('@/views/user/UserDetail.vue'),
beforeEnter: (to, from) => {
console.log('user-detail独享路由', to,from)
return true
}
}
],
beforeEnter: (to, from) => {
console.log('user-view独享路由', to,from)
return true
},
},
4、组件内路由守卫(组合式)
<script setup lang="ts">
defineOptions({
name: "UserDetail",
// 路由进入守卫
beforeRouteEnter(to, from) {
console.log("user-detail-enter", to, from);
return true;
},
beforeRouteUpdate(to, from) {
console.log("user-detail-update", to, from);
return true;
},
// 路由离开守卫
beforeRouteLeave(to, from) {
console.log("user-detail-leave", to, from);
return true;
},
});
</script>
<script lang="ts">
import { defineComponent } from "vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "NotFound",
beforeRouteEnter(to, from) {
console.log("not-found-enter", to, from);
return true;
},
beforeRouteUpdate(to, from) {
console.log("not-found-update", to, from);
return true;
},
beforeRouteLeave(to, from) {
console.log("not-found-leave", to, from);
return true;
},
setup() {
const router = useRouter();
const handleClick = () => {
router.push("/404?from=not-found");
};
return {
handleClick,
};
},
});
<script lang="ts">
import { ref } from "vue";
import { onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";
interface Project {
name: string;
status: string;
rate: string;
icon: string;
iconColor: string;
}
export default {
name: "ProjectList",
setup() {
onBeforeRouteLeave((to, from) => {
console.log("project-list-leave", to, from);
return true;
});
onBeforeRouteUpdate((to, from) => {
console.log("project-list-update", to, from);
return true;
});
const projects = ref<Project[]>([]);
return {
projects,
};
},
};
</script>
extractChangingRecords
/**
* Split the leaving, updating, and entering records.
* @internal
*
* @param to - Location we are navigating to 目标
* @param from - Location we are navigating from 来源
*/
export function extractChangingRecords(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): [
leavingRecords: RouteRecordNormalized[],
updatingRecords: RouteRecordNormalized[],
enteringRecords: RouteRecordNormalized[],
] {
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) {
if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
// 路径相同,记录为更新记录
updatingRecords.push(recordFrom)
// 路径不同,记录为离开记录
else leavingRecords.push(recordFrom)
}
// 目标记录存在
const recordTo = to.matched[i]
if (recordTo) {
// the type doesn't matter because we are comparing per reference
if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
// 路径不同,记录为进入记录
enteringRecords.push(recordTo)
}
}
}
return [leavingRecords, updatingRecords, enteringRecords]
}
extractComponentsGuards
/**
* 从匹配的路由记录里提取组件内路由守卫
* @param matched 路由记录
* @param guardType 守卫类型
* @param to 目标路由
* @param from 来源路由
* @param runWithContext
* @returns
*/
export function extractComponentsGuards(
matched: RouteRecordNormalized[], // 路由记录
guardType: GuardType, // 守卫类型
to: RouteLocationNormalized, // 目标路由
from: RouteLocationNormalizedLoaded, // 来源路由
runWithContext: <T>(fn: () => T) => T = fn => fn()
) {
const guards: Array<() => Promise<void>> = []
for (const record of matched) {
// 开发警告:无组件、无子路由
// if (
// __DEV__ &&
// !record.components &&
// // in the new records, there is no children, only parents
// record.children &&
// !record.children.length
// ) {
// warn(
// `Record with path "${record.path}" is either missing a "component(s)"` +
// ` or "children" property.`
// )
// }
// 遍历路由记录中的组件
for (const name in record.components) {
let rawComponent = record.components[name]
// if (__DEV__) {
// // 警告1:组件不是合法对象/函数
// if (
// !rawComponent ||
// (typeof rawComponent !== 'object' &&
// typeof rawComponent !== 'function')
// ) {
// warn(
// `Component "${name}" in record with path "${record.path}" is not` +
// ` a valid component. Received "${String(rawComponent)}".`
// )
// // throw to ensure we stop here but warn to ensure the message isn't
// // missed by the user
// throw new Error('Invalid route component')
// // 警告2:异步组件写成 import() 而非 () => import()
// } else if ('then' in rawComponent) {
// // warn if user wrote import('/component.vue') instead of () =>
// // import('./component.vue')
// 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.`
// )
// const promise = rawComponent
// rawComponent = () => promise
// // 警告3:误用 defineAsyncComponent 包裹异步组件
// } else if (
// (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'))".`
// )
// }
// }
// TODO: extract the logic relying on instances into an options-api plugin
// skip update and leave guards if the route component is not mounted
// 非 beforeRouteEnter 守卫,且组件未挂载 → 跳过(无实例无法执行)
if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue
// 判断是否为同步组件
if (isRouteComponent(rawComponent)) {
// __vccOpts is added by vue-class-component and contain the regular options
const options: ComponentOptions = (rawComponent as any).__vccOpts || rawComponent
// 获取组件内路由守卫
const guard = options[guardType]
guard &&
guards.push(
// 将守卫函数封装为 Promise 格式
guardToPromiseFn(guard, to, from, record, name, runWithContext)
)
} else {
// start requesting the chunk already
// 执行懒加载函数,开始加载组件
let componentPromise: Promise<
RouteComponent | null | undefined | void
> = (rawComponent as Lazy<RouteComponent>)()
// 开发环境警告:懒加载函数未返回 Promise
// if (__DEV__ && !('catch' in componentPromise)) {
// warn(
// `Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`
// )
// componentPromise = Promise.resolve(componentPromise as RouteComponent)
// }
guards.push(() =>
componentPromise.then(resolved => {
if (!resolved)
throw new Error(
`Couldn't resolve component "${name}" at "${record.path}"`
)
const resolvedComponent = isESModule(resolved)
? resolved.default : resolved
// keep the resolved module for plugins like data loaders
record.mods[name] = resolved
// replace the function with the resolved component
// cannot be null or undefined because we went into the for loop
// 替换组件为已加载组件
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, runWithContext)()
)
})
)
}
}
}
return guards
}
runGuardQueue
function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
// Vue Router 的 beforeEach/beforeEnter/beforeResolve 等守卫被收集为一个数组(guards),需要串行执行
// 只有前一个守卫通过(Promise resolve),才能执行下一个
return guards.reduce(
(promise, guard) => promise.then(() => runWithContext(guard)),
Promise.resolve()
)
}
runWithContext
function runWithContext<T>(fn: () => T): T {
// 获取已安装的第一个 Vue 应用实例
const app: App | undefined = installedApps.values().next().value
// support Vue < 3.3
// 兼容逻辑:优先用 Vue 3.3+ 的 app.runWithContext,否则直接执行函数
return app && typeof app.runWithContext === 'function'
? app.runWithContext(fn)
: fn()
}
triggerAfterEach
执行全局后置守卫。
function triggerAfterEach(
to: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
failure?: NavigationFailure | void
): void {
// navigation is confirmed, call afterGuards
// TODO: wrap with error handlers
afterGuards
.list()
.forEach(guard => runWithContext(() => guard(to, from, failure)))
}
checkCanceledNavigation
function checkCanceledNavigation(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): NavigationFailure | void {
// 全局挂起的路由 ≠ 当前待完成的路由
// 说明有新的导航请求,当前导航应被取消
if (pendingLocation !== to) {
// 创建并返回「导航取消」类型的失败错误
return createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_CANCELLED,
{
from,
to,
}
)
}
}