技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 的钩子函数:路由导航守卫、KeepAlive 与生命周期

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/48-Vue的钩子函数[路由导航守卫、keep-alive、生命周期钩子]
本文目录15 个章节
  1. 一、先区分三种“钩子”
  2. 二、Vue Router 的导航守卫
  3. 三、全局、路由独享和组件守卫
  4. 四、组件内路由守卫
  5. 五、导航解析流程
  6. 六、如何避免登录跳转死循环
  7. 七、KeepAlive 是什么
  8. 八、KeepAlive 生命周期
  9. 九、组件生命周期对照
  10. 十、父子组件的常见挂载顺序
  11. 十一、接口请求应该放在哪个钩子
  12. 十二、常见错误总结
  13. 十三、总结
  14. 官方参考
  15. 原文出处

Vue 的钩子函数:路由导航守卫、KeepAlive 与生命周期

原文把 Vue Router 的导航守卫、keep-alive 和组件生命周期放在一起讨论,主题仍然很实用。本文保留 Vue Router 3 的 next 写法,补充 Vue Router 4 推荐的返回值写法,并修正原文中“请求一定放 mounted”“afterEach 可以直接改变当前导航”“KeepAlive 等于复用 DOM”等容易误导的表述。

一、先区分三种“钩子”

本文中的钩子来自三个不同系统:

  1. 路由导航守卫:决定一次导航是否允许、取消或重定向;
  2. KeepAlive 生命周期:组件实例被激活或停用时触发;
  3. 组件生命周期:组件创建、挂载、更新和卸载过程中的回调。

它们的触发顺序会受到路由组件是否复用、是否缓存、是否为异步组件、是否存在 Suspense/SSR 等因素影响。因此不要把所有项目都硬套成一条固定的“生命周期顺序”。

二、Vue Router 的导航守卫

导航守卫可以放在:

  • 全局:router.beforeEachrouter.beforeResolverouter.afterEach
  • 单个路由:beforeEnter
  • 组件内:beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
  • Vue 3 Composition API:onBeforeRouteUpdateonBeforeRouteLeave

最常见的场景是登录校验、权限校验、离开页面前确认、页面标题和全局 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()

守卫返回值的含义:

返回值含义
trueundefined允许导航继续
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 中跳转,这会启动一次新的导航,不是修改当前导航本身;若没有排除登录页,仍然可能形成循环。权限判断应放在 beforeEachafterEach 只做导航完成后的副作用。

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>

onBeforeRouteUpdateonBeforeRouteLeave 可以在 setup 中使用,不要求组件必须是直接的 <router-view> 子组件。beforeRouteEnter 没有组件实例可用,应使用路由参数、组合式逻辑或进入后的生命周期处理 DOM;不要在守卫中依赖尚未创建的 this

五、导航解析流程

以 Vue Router 3/4 的常规客户端导航为例,官方流程大致是:

  1. 触发导航;
  2. 调用将离开组件的 beforeRouteLeave
  3. 调用全局 beforeEach
  4. 调用被复用组件的 beforeRouteUpdate
  5. 调用路由记录的 beforeEnter
  6. 解析异步路由组件;
  7. 调用将进入组件的 beforeRouteEnter
  8. 调用全局 beforeResolve
  9. 导航确认;
  10. 调用全局 afterEach
  11. 触发 DOM 更新;
  12. 执行 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.requiresAuthmeta.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 includeexcludemax

<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;
  • 再次从缓存恢复时,不会重新执行 createdmounted
  • max 淘汰、被 exclude 移除或父级卸载时,才会触发真正的 unmount/destroy;
  • activated/deactivated 会沿 KeepAlive 子树触发,嵌套组件不一定与父组件使用者的代码顺序完全相同;
  • SSR 不执行依赖浏览器 DOM 的 mounted、activated 等客户端钩子。

原文把 deactivated 说成 beforeDestroy 的完全替代,这是不准确的:它表示“暂时停用”,并不表示资源一定已经被销毁。轮询、事件订阅等需要在停用时暂停,在 onUnmounted 中再做最终清理。

九、组件生命周期对照

阶段Vue 2.7Vue 3.5
创建前beforeCreateonBeforeMount 之前的 setup 初始化;Options API 仍有 beforeCreate
创建完成createdsetup() / onBeforeMount 前;Options API 仍有 created
挂载前beforeMountonBeforeMount
挂载后mountedonMounted
更新前beforeUpdateonBeforeUpdate
更新后updatedonUpdated
KeepAlive 激活activatedonActivated
KeepAlive 停用deactivatedonDeactivated
销毁前beforeDestroyonBeforeUnmount
销毁后destroyedonUnmounted

Vue 3 的 setup() 执行时机可以理解为组件实例创建过程的一部分,Composition API 代码通常不再把逻辑分散到 created。Vue 3 迁移时要把 beforeDestroy 改为 beforeUnmount,把 destroyed 改为 unmounted

Vue 生命周期示意图(本地化,历史资料;名称以版本表为准)

十、父子组件的常见挂载顺序

在没有异步组件、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 的初始数据:可以在 createdsetup 或路由数据加载阶段请求;
  • 需要浏览器 API:放在客户端 onMounted 或客户端专用逻辑中;
  • Vue 3 SSR/Nuxt:使用框架提供的 useAsyncDatauseFetchserverPrefetch 等数据获取机制;
  • 组件被 KeepAlive 恢复:需要刷新时使用 onActivated,但要避免重复创建请求;
  • 组件卸载或路由变化:配合 AbortController/请求库取消旧请求,避免竞态。

服务端渲染不会执行 mountedupdatedonMounted 等需要 DOM 的钩子。Vue 2 SSR 通常只执行 beforeCreatecreated;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 或用户状态会导致不同请求之间相互污染。

十二、常见错误总结

  1. 把 Vue Router 3 的 next 和 Router 4 的返回值混用;
  2. 在一个守卫的多个分支中调用两次 next
  3. afterEach 里无条件 router.push,造成重复导航或死循环;
  4. 误以为 afterEach 能取消当前导航;
  5. beforeRouteEnter 中访问不存在的 this
  6. 把 KeepAlive 当成永久 DOM 缓存,忘记 activated/deactivated;
  7. 只设置 include 却没有让组件具有匹配的 name
  8. deactivated 当作真正销毁,导致资源在缓存期间丢失;
  9. 认为所有接口都必须在 mounted 请求;
  10. 把异步组件、路由守卫、组件 mounted 和 KeepAlive 激活强行拼成一条固定顺序。

十三、总结

  • Router 守卫决定导航是否继续,组件生命周期描述组件自身,KeepAlive 钩子描述缓存实例的激活和停用;
  • Vue Router 3 使用 next,Vue Router 4 推荐返回 truefalse、路由位置或抛错;
  • 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/异步场景的版本说明。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS