Vue 的钩子函数:路由导航守卫、KeepAlive 与生命周期
原文把 Vue Router 的导航守卫、
keep-alive和组件生命周期放在一起讨论,主题仍然很实用。本文保留 Vue Router 3 的next写法,补充 Vue Router 4 推荐的返回值写法,并修正原文中“请求一定放 mounted”“afterEach 可以直接改变当前导航”“KeepAlive 等于复用 DOM”等容易误导的表述。
一、先区分三种“钩子”
本文中的钩子来自三个不同系统:
- 路由导航守卫:决定一次导航是否允许、取消或重定向;
- KeepAlive 生命周期:组件实例被激活或停用时触发;
- 组件生命周期:组件创建、挂载、更新和卸载过程中的回调。
它们的触发顺序会受到路由组件是否复用、是否缓存、是否为异步组件、是否存在 Suspense/SSR 等因素影响。因此不要把所有项目都硬套成一条固定的“生命周期顺序”。
二、Vue Router 的导航守卫
导航守卫可以放在:
- 全局:
router.beforeEach、router.beforeResolve、router.afterEach; - 单个路由:
beforeEnter; - 组件内:
beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave; - Vue 3 Composition API:
onBeforeRouteUpdate、onBeforeRouteLeave。
最常见的场景是登录校验、权限校验、离开页面前确认、页面标题和全局 loading。
2.1 Vue Router 4:推荐返回值
Vue Router 4(Vue 3)推荐直接从守卫返回结果:
// router/index.ts:导出工厂,避免 SSR 并发请求共享 router
import {
createMemoryHistory,
createRouter,
createWebHistory,
} from 'vue-router'
const routes = [
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true },
},
]
export function createAppRouter(isLoggedIn: boolean) {
const router = createRouter({
// SSR 每个请求使用独立 memory history;浏览器 SPA 使用 web history
history: typeof window === 'undefined'
? createMemoryHistory()
: createWebHistory(),
routes,
})
router.beforeEach(to => {
if (to.meta.requiresAuth && !isLoggedIn) {
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
return true
})
router.afterEach((to, from, failure) => {
if (typeof document !== 'undefined') {
document.title = String(to.meta.title || '应用')
}
if (failure) {
console.warn('导航没有成功完成', failure)
}
})
return router
}
普通 SPA 入口可以在客户端读取 token 后创建一次:
const router = createAppRouter(
Boolean(window.localStorage.getItem('token')),
)
SSR 则在每个请求中从 cookie/session 得到 isLoggedIn,调用同一个工厂创建独立 router,并执行 router.push(url) 和 await router.isReady()。
守卫返回值的含义:
| 返回值 | 含义 |
|---|---|
true、undefined | 允许导航继续 |
false | 取消当前导航,URL 保持原状 |
| 路由位置对象 | 取消当前导航并开始一次新的导航 |
| 抛出 Error | 取消导航并进入错误处理流程 |
异步守卫可以直接返回 Promise:
router.beforeResolve(async to => {
if (to.meta.requiresData) {
await loadPageData(to.params.id)
}
})
2.2 Vue Router 3:next 历史写法
Vue Router 3(Vue 2)使用 next:
router.beforeEach((to, from, next) => {
const isLoggedIn = Boolean(store.state.token)
if (to.matched.some(record => record.meta.requiresAuth) && !isLoggedIn) {
next({
name: 'login',
query: { redirect: to.fullPath },
})
return
}
next()
})
next 的常见参数:
next():放行;next(false):取消导航;next('/login')或next({ name: 'login' }):重定向;next(error):导航失败并交给router.onError。
每条执行路径必须调用一次 next,不能先调用 next() 又继续调用 next('/login')。
Vue Router 4 仍然兼容部分 next 形式,但官方更推荐返回值写法。不要把 Vue Router 3 的 next 示例和 Router 4 的 return 混在同一个守卫里。
三、全局、路由独享和组件守卫
3.1 全局前置守卫 beforeEach
它在每次导航时调用,适合做全局身份和权限校验:
router.beforeEach(to => {
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
})
全局守卫不是只在“首次进入页面”时执行;参数、query 或 hash 变化是否触发导航,取决于具体导航行为,但每一次成功或失败的导航都应按 Router 规则处理。
3.2 路由独享守卫 beforeEnter
只配置在某个路由记录上:
const routes = [
{
path: '/admin',
component: () => import('./views/AdminView.vue'),
meta: { requiresAuth: true },
beforeEnter: to => {
return authStore.canAccessAdmin
? true
: { name: 'forbidden' }
},
},
]
当从 /admin/users 跳到 /admin/settings,如果父级路由记录没有离开、只是子路由参数变化,父级 beforeEnter 不一定重新调用。需要在复用组件中响应参数变化时使用 beforeRouteUpdate 或监听 route。
3.3 beforeResolve
beforeResolve 是全局解析守卫,位于异步路由组件解析和组件内进入守卫之后、导航确认之前。它适合在确认“所有组件和数据都准备好”后执行的逻辑,例如:
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch {
return false
}
}
})
3.4 afterEach
afterEach 在导航确认后调用,适合:
- 页面统计;
- 设置标题;
- 关闭全局进度条;
- 记录导航日志。
它没有 next,返回值不会取消或重定向当前导航:
router.afterEach((to, from) => {
analytics.trackPageView(to.fullPath)
})
原文通过 router.push() 在 afterEach 中跳转,这会启动一次新的导航,不是修改当前导航本身;若没有排除登录页,仍然可能形成循环。权限判断应放在 beforeEach,afterEach 只做导航完成后的副作用。
3.5 router.onError
router.onError(error => {
console.error('路由加载或守卫异常', error)
})
它处理导航过程中抛出的错误,例如异步组件加载失败或守卫抛错。取消、重复导航和重定向是导航结果,不应都当成程序异常;Router 4 可以在 afterEach 的第三个参数中接收导航 failure。
四、组件内路由守卫
Vue Router 3 Options API
export default {
beforeRouteEnter(to, from, next) {
// 此时组件实例尚未创建,不能访问 this
next(vm => {
// 组件实例创建并完成导航后,可以访问 vm
vm.focusFirstInput()
})
},
beforeRouteUpdate(to, from, next) {
// 当前组件被复用,例如 /users/1 -> /users/2
this.loadUser(to.params.id)
.then(() => next())
.catch(() => next(false))
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedDraft) {
const leave = window.confirm('草稿尚未保存,确定离开吗?')
next(leave)
} else {
next()
}
},
}
Vue Router 4 Composition API
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedDraft = ref(false)
async function loadUser(id: string) {
// 替换为实际的数据请求
console.log('load user', id)
}
onBeforeRouteUpdate(async to => {
await loadUser(String(to.params.id))
})
onBeforeRouteLeave(() => {
if (hasUnsavedDraft.value) {
return window.confirm('草稿尚未保存,确定离开吗?')
}
})
</script>
onBeforeRouteUpdate 和 onBeforeRouteLeave 可以在 setup 中使用,不要求组件必须是直接的 <router-view> 子组件。beforeRouteEnter 没有组件实例可用,应使用路由参数、组合式逻辑或进入后的生命周期处理 DOM;不要在守卫中依赖尚未创建的 this。
五、导航解析流程
以 Vue Router 3/4 的常规客户端导航为例,官方流程大致是:
- 触发导航;
- 调用将离开组件的
beforeRouteLeave; - 调用全局
beforeEach; - 调用被复用组件的
beforeRouteUpdate; - 调用路由记录的
beforeEnter; - 解析异步路由组件;
- 调用将进入组件的
beforeRouteEnter; - 调用全局
beforeResolve; - 导航确认;
- 调用全局
afterEach; - 触发 DOM 更新;
- 执行
beforeRouteEnter传给next的回调(Vue Router 3 的历史写法)。
如果某一步返回 false、重定向或抛错,后续步骤不会按成功导航继续。异步组件、过渡动画、KeepAlive 和 Suspense 会让“DOM 更新完成”的具体时机有所变化。
六、如何避免登录跳转死循环
错误示例:
router.beforeEach(to => {
if (!authStore.isLoggedIn) {
return { name: 'login' }
}
})
进入 login 后仍然满足未登录条件,于是再次跳回 login。应该排除公共页面:
router.beforeEach(to => {
const publicPages = new Set(['login', 'about'])
if (
!authStore.isLoggedIn &&
!publicPages.has(String(to.name))
) {
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
})
更复杂的权限系统可以通过 meta.requiresAuth、meta.roles 和统一的权限函数处理,不要在多个守卫里重复 push。
七、KeepAlive 是什么
<KeepAlive> 是 Vue 内置的抽象组件,用于缓存动态切换的组件实例:
<KeepAlive>
<component :is="currentView" />
</KeepAlive>
它缓存的是组件实例及其渲染状态,不是简单地把所有 DOM 永久留在页面上。组件被切换出去时会被 deactivated,重新切回来时会被 activated;实例被淘汰或父级卸载时才会真正 unmount/destroy。
7.1 Vue 2 缓存路由
<keep-alive>
<router-view />
</keep-alive>
Vue 2 Router 常见的按名称缓存方式:
<keep-alive :include="cachedNames">
<router-view />
</keep-alive>
7.2 Vue Router 4 的推荐写法
Vue Router 4 推荐使用 router-view 插槽,以便 KeepAlive 包裹真正的路由组件:
<router-view v-slot="{ Component }">
<KeepAlive :include="cachedNames">
<component :is="Component" />
</KeepAlive>
</router-view>
配合过渡动画时还可以组合 Transition,但应注意组件层级和 key 的稳定性。
还要区分两种“复用”:路由记录被复用时,参数变化仍可能触发 beforeRouteUpdate;KeepAlive 的 deactivated 只表示组件实例暂时停用。KeepAlive 不会自动跳过路由守卫。
7.3 include、exclude 和 max
<KeepAlive
:include="['HomeView', 'UserView']"
:exclude="['LoginView']"
:max="10"
>
<component :is="Component" />
</KeepAlive>
include:只有名称匹配的组件进入缓存;exclude:匹配的组件不进入缓存,实际使用中相当于排除优先;max:限制缓存实例数量,超过后会淘汰最久未使用的实例;- 字符串、正则、数组都可以作为 include/exclude 的值;正则和数组通常需要
v-bind。
名称匹配不是 URL 匹配。Vue 2 主要检查组件自身的 name,没有时还会尝试局部注册名;Vue 3 主要依赖组件名称。Vue 3 的 SFC 通常可以从文件名推断名称,但需要稳定缓存时建议显式声明:
<script setup lang="ts">
defineOptions({ name: 'UserView' })
</script>
或使用 Options API:
export default {
name: 'UserView',
}
八、KeepAlive 生命周期
<script setup lang="ts">
import { onActivated, onDeactivated, onUnmounted } from 'vue'
onActivated(() => {
// 第一次挂载后,以及之后每次从缓存恢复时调用
refreshWhenVisible()
})
onDeactivated(() => {
// 被缓存停用时调用
pausePolling()
})
onUnmounted(() => {
// 实例真正被卸载时调用
disposeResources()
})
</script>
Vue 2 对应:
export default {
activated() {
this.refreshWhenVisible()
},
deactivated() {
this.pausePolling()
},
beforeDestroy() {
this.disposeResources()
},
destroyed() {
// Vue 2 真正销毁后
},
}
注意:
- 组件第一次进入 KeepAlive 时通常仍会经历创建、挂载,再触发 activated;
- 再次从缓存恢复时,不会重新执行
created、mounted; - 被
max淘汰、被exclude移除或父级卸载时,才会触发真正的 unmount/destroy; - activated/deactivated 会沿 KeepAlive 子树触发,嵌套组件不一定与父组件使用者的代码顺序完全相同;
- SSR 不执行依赖浏览器 DOM 的 mounted、activated 等客户端钩子。
原文把 deactivated 说成 beforeDestroy 的完全替代,这是不准确的:它表示“暂时停用”,并不表示资源一定已经被销毁。轮询、事件订阅等需要在停用时暂停,在 onUnmounted 中再做最终清理。
九、组件生命周期对照
| 阶段 | Vue 2.7 | Vue 3.5 |
|---|---|---|
| 创建前 | beforeCreate | onBeforeMount 之前的 setup 初始化;Options API 仍有 beforeCreate |
| 创建完成 | created | setup() / onBeforeMount 前;Options API 仍有 created |
| 挂载前 | beforeMount | onBeforeMount |
| 挂载后 | mounted | onMounted |
| 更新前 | beforeUpdate | onBeforeUpdate |
| 更新后 | updated | onUpdated |
| KeepAlive 激活 | activated | onActivated |
| KeepAlive 停用 | deactivated | onDeactivated |
| 销毁前 | beforeDestroy | onBeforeUnmount |
| 销毁后 | destroyed | onUnmounted |
Vue 3 的 setup() 执行时机可以理解为组件实例创建过程的一部分,Composition API 代码通常不再把逻辑分散到 created。Vue 3 迁移时要把 beforeDestroy 改为 beforeUnmount,把 destroyed 改为 unmounted。

十、父子组件的常见挂载顺序
在没有异步组件、Suspense 和特殊渲染器介入的普通首次挂载中,宏观顺序通常是:
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted
更新通常是:
父 beforeUpdate
子 beforeUpdate
子 updated
父 updated
销毁/卸载通常是:
父 beforeDestroy / beforeUnmount
子 beforeDestroy / beforeUnmount
子 destroyed / unmounted
父 destroyed / unmounted
这些是普通父子树的宏观顺序。KeepAlive、异步组件、Teleport、Suspense 和组件是否被复用都会改变局部行为,写业务逻辑时应使用明确的组件钩子,而不要依赖所有边界场景的全局顺序。
十一、接口请求应该放在哪个钩子
“请求一定放 mounted”是错误的经验法则。选择取决于请求是否依赖 DOM 和渲染环境:
- 不依赖 DOM 的初始数据:可以在
created、setup或路由数据加载阶段请求; - 需要浏览器 API:放在客户端
onMounted或客户端专用逻辑中; - Vue 3 SSR/Nuxt:使用框架提供的
useAsyncData、useFetch、serverPrefetch等数据获取机制; - 组件被 KeepAlive 恢复:需要刷新时使用
onActivated,但要避免重复创建请求; - 组件卸载或路由变化:配合 AbortController/请求库取消旧请求,避免竞态。
服务端渲染不会执行 mounted、updated、onMounted 等需要 DOM 的钩子。Vue 2 SSR 通常只执行 beforeCreate 和 created;Vue 3 SSR 会执行 setup 和适合 SSR 的逻辑,但不会执行客户端挂载钩子。
SSR 中还必须避免跨请求共享 Router 实例:
// Vue Router 4 SSR:每个请求创建自己的 router
const router = createRouter({
history: createMemoryHistory(),
routes,
})
await router.push(requestUrl)
await router.isReady()
Vue 2 SSR 通常使用 abstract history,并在服务端等待 router.onReady()。共享 Router、store 或用户状态会导致不同请求之间相互污染。
十二、常见错误总结
- 把 Vue Router 3 的
next和 Router 4 的返回值混用; - 在一个守卫的多个分支中调用两次
next; - 在
afterEach里无条件router.push,造成重复导航或死循环; - 误以为
afterEach能取消当前导航; - 在
beforeRouteEnter中访问不存在的this; - 把 KeepAlive 当成永久 DOM 缓存,忘记 activated/deactivated;
- 只设置
include却没有让组件具有匹配的name; - 把
deactivated当作真正销毁,导致资源在缓存期间丢失; - 认为所有接口都必须在 mounted 请求;
- 把异步组件、路由守卫、组件 mounted 和 KeepAlive 激活强行拼成一条固定顺序。
十三、总结
- Router 守卫决定导航是否继续,组件生命周期描述组件自身,KeepAlive 钩子描述缓存实例的激活和停用;
- Vue Router 3 使用
next,Vue Router 4 推荐返回true、false、路由位置或抛错; beforeEach适合全局权限,beforeResolve适合导航确认前的异步准备,afterEach只做完成后的副作用;beforeRouteEnter中没有组件实例,beforeRouteUpdate用于复用组件的参数变化,beforeRouteLeave用于离开确认;- KeepAlive 缓存组件实例,支持 include/exclude/max 和 LRU 淘汰;
- 缓存组件重新出现时使用 activated/onActivated,暂时离开时使用 deactivated/onDeactivated,真正销毁仍使用 unmount/destroy 钩子;
- Vue 2 的
beforeDestroy/destroyed在 Vue 3 中改为beforeUnmount/unmounted; - 请求放在何处要看是否需要 DOM、是否 SSR、是否 KeepAlive,以及是否需要取消过期请求。
官方参考
Vue Router
Vue
原文出处
作者:OBKoro1
来源:稀土掘金
本文保留原文路由守卫、KeepAlive、生命周期和流程图内容,并补充 Vue Router 4、Vue 3.5 和 SSR/异步场景的版本说明。