【Vue 路由系列 03】动态路由与权限控制:addRoute 与 404 问题
"先跳到 404,带 query 参数记录原路径,再 replace 回来。" —— 这是我在模拟面试中给出的方案。
这是一个能工作的 Hack,但不是标准做法。面试官想听到的答案是
next({ ...to, replace: true })。在进入addRoute这个高级话题之前,我们需要先把 Vue Router 的完整路由体系梳理清楚——嵌套路由怎么配?编程式导航有哪些 API?命名路由和路径路由怎么选?动态参数匹配的通配符语法是什么?搞懂这些基础,再看动态路由就会豁然开朗。
零、Vue Router 路由体系全貌(基础铺垫)
在上一篇中,我们讲了守卫系统("谁能进来")。这一节我们讲路由体系本身("路怎么走")。很多同学直接跳到 addRoute 却连基础路由配置都没搞透,面试时自然答不完整。
0.1 基础路由配置
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
// 最简单的静态路由
{ path: '/', component: Home },
{ path: '/about', component: About },
]
})
这是最基础的写法——每个路径对应一个组件。但实际项目中,只有这种配置远远不够。
0.2 嵌套路由(Children)—— ⚠️ 面试高频
嵌套路由是 Vue Router 最核心的概念之一,也是企业级项目布局的基础:
const routes = [
{
path: '/',
component: Layout, // 父组件(通常是整体布局框架)
children: [
// 子路由的 path 不要加前导斜杠!
// 最终路径是 /dashboard(父路径 + 子路径拼接)
{
path: '', // 空字符串表示默认子路由
name: 'home',
component: Dashboard // 访问 / 时渲染在 Layout 的 <router-view> 中
},
{
path: 'user', // 最终路径:/user
name: 'user-list',
component: UserList,
},
{
path: 'user/:id', // 最终路径:/user/123
name: 'user-detail',
component: UserDetail
},
{
path: 'settings', // 最终路径:/settings
name: 'settings',
component: Settings,
// 子路由还可以继续嵌套!
children: [
{
path: 'profile', // 最终路径:/settings/profile
component: ProfileSettings
},
{
path: 'security', // 最终路径:/settings/security
component: SecuritySettings
}
]
}
]
}
]
Layout 组件的结构:
<!-- Layout.vue -->
<template>
<div class="layout">
<aside class="sidebar">侧边菜单</aside>
<main class="content">
<!-- 这里是关键!子路由渲染的位置 -->
<router-view />
<!--
访问 / → router-view 渲染 Dashboard
访问 /user → router-view 渲染 UserList
访问 /user/123 → router-view 渲染 UserDetail
访问 /settings → router-view 渲染 Settings
-->
</main>
</div>
</template>
⚠️ 嵌套 <router-view> 的坑:
<!-- 如果 Settings 也有自己的子路由,Settings 内部也需要 <router-view> -->
<!-- Settings.vue -->
<template>
<div>
<h1>设置页</h1>
<nav>
<router-link to="/settings/profile">个人资料</router-link>
<router-link to="/settings/security">安全设置</router-link>
</nav>
<!-- Settings 的子路由在这里渲染 -->
<router-view />
<!--
访问 /settings/profile → 这里渲染 ProfileSettings
访问 /settings/security → 这里渲染 SecuritySettings
-->
</div>
</template>
记忆法:N 层嵌套路由 = N 个 <router-view>,每个组件负责自己那一层的视图出口。
0.3 动态路径参数
基本用法
const routes = [
// :id 是动态参数,匹配 /user/123、/user/abc 等
{ path: '/user/:id', component: UserDetail, name: 'user-detail' },
// 多个参数
{ path: '/post/:postId/comment/:commentId', component: CommentDetail },
// 可选参数(用 ? 标记)
{ path: '/category/:categoryId?', component: CategoryPage },
]
在组件内获取参数:
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
// 获取路径参数
console.log(route.params.id) // /user/123 → "123"
console.log(route.params.postId) // /post/42/comment/7 → "42"
// 获取 query 参数(URL 中 ? 后面的部分)
// URL: /search?keyword=vue&page=2
console.log(route.query.keyword) // "vue"
console.log(route.query.page) // "2"
</script>
⚠️ 参数变化但组件复用的陷阱:
// 从 /user/1 导航到 /user/2
// 默认情况下,Vue Router 会**复用同一个组件实例**
// (因为路由配置没变,只是参数变了)
// 这意味着:
// ❌ created() 和 mounted() 不会重新执行!
// ❌ setup() 不会重新执行!
// 解决方案一:监听路由变化
import { watch } from 'vue'
watch(() => route.params.id, (newId) => {
fetchUserData(newId)
}, { immediate: true })
// 解决方案二:用 onBeforeRouteUpdate(第02篇学过的)
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to) => {
fetchUserData(to.params.id)
})
通配符语法(Vue Router 4)
| 模式 | 匹配示例 | 不匹配 |
|---|---|---|
/user/:id | /user/123, /user/abc | /user, /user/12/34 |
/user/:id* | /user/123, /user/12/34, /user | — |
/user/:pathMatch(.*)* | /user/123, /user/a/b/c | /user |
/:pathMatch(.*)* | 所有路径(用于 404) | — |
/:pathMatch(.*) | /abc, /a/b 但不含尾部斜杠后空段 | — |
const routes = [
// 始终放在最后的 404 兜底路由
{
path: '/:pathMatch(.*)*', // Vue Router 4 推荐写法
name: 'not-found',
component: NotFound
}
]
⚠️ Vue Router 3 用的是
path: '*',Vue Router 4 改为path: '/:pathMatch(.*)*'。面试时提到这个区别能体现你对版本差异的了解。
0.4 编程式导航(Programmatic Navigation)
除了用 <router-link> 点击跳转,你还可以在 JS 代码中主动导航:
import { useRouter } from 'vue-router'
const router = useRouter()
// ===== 字符串路径 =====
router.push('/users') // → /users
router.push({ path: '/user/123' }) // → /user/123
// ===== 命名路由 + params(推荐!)=====
router.push({ name: 'user-detail', params: { id: '123' } })
// 如果路由配置是 /user/:id,则导航到 /user/123
// ===== 带 query 参数 =====
router.push({ path: '/search', query: { keyword: 'vue', page: 1 } })
// → /search?keyword=vue&page=1
// ===== 替换当前记录(不留后退历史)=====
router.replace('/login')
// 等同于
router.push({ path: '/login', replace: true })
// ===== 前进/后退 =====
router.go(1) // 前进一步(等同于 history.forward())
router.go(-1) // 后退一步(等同于 history.back())
router.go(-3) // 后退三步
push vs replace vs go 对比:
| 方法 | 历史栈变化 | 适用场景 |
|---|---|---|
router.push() | 新增一条记录 | 正常页面跳转 |
router.replace() | 替换当前记录 | 登录后替换登录页、重定向 |
router.go(n) | 移动 n 步 | 前进/后退操作 |
0.5 命名路由 vs 路径路由
const routes = [
// 路径路由
{ path: '/user/:id', component: UserDetail },
// 命名路由(多了 name 属性)
{ path: '/user/:id', name: 'user-detail', component: UserDetail },
]
为什么推荐使用命名路由?
// ❌ 路径硬编码——如果路径变了,所有地方都要改
router.push('/user/' + userId)
// <router-link to="/user/123">用户详情</router-link>
// ✅ 命名路由——路径变了只需改一处(路由配置表)
router.push({ name: 'user-detail', params: { id: userId } })
// <router-link :to="{ name: 'user-detail', params: { id: user.id } }">用户详情</router-link>
面试加分项:
当被问到"为什么用命名路由",可以回答:(1)解耦——组件不需要知道目标的具体路径,只通过名字引用;(2)可维护性——路径变更只改路由配置一处;(3)params 自动编码——命名路由配合
params会自动进行 URL 编码,而字符串拼接容易出错。
0.6 路由懒加载(提前铺垫,下一篇细讲)
// ❌ 静态导入——打包到一个文件里
import UserList from '@/views/UserList.vue'
{ path: '/user', component: UserList }
// ✅ 动态导入(懒加载)——按需加载,单独打包
{ path: '/user', component: () => import('@/views/UserList.vue') }
这里只需要知道:() => import() 返回一个 Promise 函数,Vue Router 在导航到该路由时才真正执行 import 加载组件代码。完整的懒加载原理和打包优化策略将在下一篇(04篇)深入展开。
0.7 路由元信息(meta)
{
path: '/admin/settings',
component: AdminSettings,
meta: {
requiresAuth: true, // 是否需要登录
requiredPermissions: ['system:settings:view'], // 所需权限
title: '系统设置', // 页面标题
keepAlive: false, // 是否缓存(配合 keep-alive)
breadcrumb: ['首页', '系统管理', '系统设置'] // 面包屑
}
}
// 在守卫中读取
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isLogin()) {
return { name: 'login' }
}
if (to.meta.requiredPermissions) {
checkPermission(to.meta.requiredPermissions)
}
})
一、什么是动态路由
1.1 静态路由 vs 动态路由
静态路由:在项目启动时就确定好所有路由配置:
// router/index.js
const routes = [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/about', component: About },
{ path: '/404', component: NotFound },
]
动态路由:根据用户权限、后端返回数据等运行时条件,在应用启动后动态添加路由:
// 用户登录后,根据后端返回的菜单列表动态添加路由
const menuList = await api.getUserMenu()
menuList.forEach(menu => {
router.addRoute({
path: menu.path,
component: () => import(`@/views/${menu.component}.vue`),
meta: { title: menu.title, requiresAuth: true }
})
})
1.2 为什么需要动态路由
| 场景 | 说明 |
|---|---|
| RBAC 权限系统 | 不同角色看到不同的菜单和页面(管理员看后台、普通用户看首页) |
| 多租户 SaaS | 每个租户有不同的功能模块 |
| 后端驱动路由 | 菜单结构由后端数据库管理,前端不硬编码 |
现在,结合前面 0.X 节的基础知识,你应该理解了:动态路由本质上就是运行时调用
addRoute往路由表里塞新规则,而那些新规则的写法(children、name、meta、component 懒加载)和静态路由完全一样。
二、addRoute API 详解
2.0 ⚠️ 版本迁移:从 Vue Router 3 到 4
如果你维护过老项目或被问到版本差异,这个知识点很重要:
| Vue Router 3 (Vue 2) | Vue Router 4 (Vue 3) | |
|---|---|---|
| 创建实例 | new Router({ ... }) | createRouter({ ... }) |
| 历史模式 | mode: 'history' | history: createWebHistory() |
| 动态添加路由 | router.addRoutes(routes) ❌ 已移除 | router.addRoute(route) |
| 添加方式 | 批量传入数组 | 逐个添加(支持嵌套路由) |
| 通配符语法 | path: '*' | path: '/:pathMatch(.*)*' |
📌 关键变化:
addRoutes()在 Vue Router 4 中已被完全移除。原因:新 API 支持更细粒度的控制——可以单独添加顶级路由,也可以通过router.addRoute('parentName', route)添加子路由到指定父路由下。如果需要批量添加,只需用forEach循环调用addRoute()即可。参考:官方迁移指南
2.1 基本用法
const router = createRouter({
history: createWebHistory(),
// 初始只有基础路由(登录页、错误页等)
routes: [
{
path: '/login',
name: 'login',
component: Login,
meta: { public: true }
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFound
}
]
})
// 登录成功后动态添加业务路由
async function initRoutes() {
const { data } = await getUserRoutes() // 从后端获取
data.routes.forEach(route => {
router.addRoute(route) // 动态添加
})
console.log('当前路由表:', router.getRoutes())
}
2.2 addRoute vs addRoute(parentName, route) —— 嵌套路由的关键
这是很多人忽略的重要细节:
// 方式一:添加顶级路由
router.addRoute({
path: '/dashboard',
component: Dashboard
})
// 方式二:添加子路由到已有的父路由下
router.addRoute('layout', {
path: 'settings', // 注意:没有前导斜杠!
component: Settings,
children: [...]
})
// 效果等同于:
// {
// path: '/layout',
// component: Layout,
// children: [
// { path: 'settings', component: Settings } // 最终路径是 /layout/settings
// ]
// }
2.3 removeRoute —— 清理不再需要的路由
// 通过名称移除
router.removeRoute('admin')
// addRoute 返回的函数可以用来移除(更优雅)
const removeSettings = router.addRoute('layout', {
path: 'settings',
component: Settings
})
// 需要时调用
removeSettings() // 移除 settings 路由
三、⚠️ 刷新 404 问题 —— 核心考点
3.1 问题复现
这是我在面试中回答得最差的问题之一。让我们完整还原这个场景:
// 初始路由配置
const staticRoutes = [
{ path: '/login', component: Login },
{ path: '/:pathMatch(.*)*', component: NotFound } // 通配符 404
]
// 用户流程:
// 1. 访问 /login → 输入账号密码 → 登录成功
// 2. 后端返回菜单 → 调用 addRoute 添加 /dashboard, /users 等路由
// 3. 导航到 /dashboard → 正常显示 ✓
// 4. 用户按 F5 刷新……
// 5. 💀 404 页面出现了!
为什么?
F5 刷新之前的状态:
┌─────────────────────────────┐
│ 路由表 (内存中): │
│ - /login │
│ - /dashboard ← addRoute加的│
│ - /users ← addRoute加的│
│ - /:pathMatch(.*)* (404) │
└─────────────────────────────┘
✅ 能匹配 /dashboard
F5 刷新之后:
┌─────────────────────────────┐
│ JS 执行上下文被完全销毁! │
│ 内存清空!Store 清空! │
│ │
│ 路由表回到初始状态: │
│ - /login │
│ - /:pathMatch(.*)* (404) │
│ │
│ /dashboard 不存在了! │
│ 只能被通配符捕获 → 404 💀 │
└─────────────────────────────┘
根因:addRoute 添加的路由只存在于内存中,F5 刷新后一切重来。
3.2 我面试时的方案 vs 标准解法
❌ 我的方案(Hack)
"先跳到 404,带 query 参数记录原路径,再 replace 回来"
问题:
- 用户会闪一下 404 页面(体验极差)
- 触发了不必要的组件生命周期
- 历史记录可能混乱
- 本质上是在用错误的方式绕过问题
✅ 标准解法(Vue Router 官方推荐)
核心思路:在 beforeEach 中拦截 → 拉取路由 → 重新导航
// ✅ 正确写法
router.beforeEach(async (to) => {
// 白名单直接放行
if (isPublicPage(to)) return true
// 检查是否已经初始化过动态路由
if (!isRouteInitialized()) {
// 1. 从后端获取路由配置
const routeConfig = await fetchUserRoutes()
// 2. 动态添加所有业务路由
routeConfig.forEach(route => {
router.addRoute(route)
})
// 3. ⭐⭐⭐ 关键一步:重新触发导航 ⭐⭐⭐
// 写法A(本文采用):返回路由对象 + replace
return { ...to, replace: true }
// 写法B([官方文档](https://router.vuejs.org/guide/advanced/dynamic-routing.html)推荐):返回完整路径字符串
// return to.fullPath ← 等效!更简洁
// 核心原理:addRoute() 只注册路由到路由表,**不会触发重新导航**。
// 所以必须手动触发一次导航,让 Router 用最新的路由表重新匹配当前路径。
// 在守卫中,return 一个新位置 = 触发重定向(等价于 router.replace(to.fullPath))
}
return true
})
时间线推演:
用户在 /dashboard 页面按 F5
↓
浏览器请求 /dashboard → 服务端返回 index.html(fallback 配置)
↓
SPA 启动,创建 Router,初始路由表中没有 /dashboard
↓
beforeEach 触发,to = { path: '/dashboard' }
↓
isRouteInitialized() === false(还没初始化)
↓
fetchUserRoutes() → 获取路由配置
↓
router.addRoute(...) × N → 路由表现在有 /dashboard 了
↓
return { ...to, replace: true }
→ 中断当前导航,用新路由表重新导航到 /dashboard
↓
这次 /dashboard 能匹配到了 ✓
→ 渲染 Dashboard 组件
3.3 关于 404 通配符的一个关键细节
这是另一个容易踩坑的地方——404 通配符应该在什么时候注册?
// ❌ 错误:把 404 写在静态路由里
const staticRoutes = [
{ path: '/login', component: Login },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound } // ❌ 太早了!
]
// 问题:addRoute 添加的路由都在通配符后面,
// 但通配符优先级最高,会先捕获所有路径!
// ✅ 正确:最后才注册 404
const staticRoutes = [
{ path: '/login', component: Login },
// 注意:初始路由表里不放 404 通配符!
]
async function initDynamicRoutes() {
// 1. 添加所有业务路由
const routes = await fetchUserRoutes()
routes.forEach(r => router.addRoute(r))
// 2. ⭐ 所有动态路由添加完毕后,最后挂载 404
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFound
})
}
为什么顺序很重要?
Vue Router 匹配路由的规则是:按照路由配置数组中的定义顺序依次匹配,先定义的路由优先级更高(官方文档)。
💡 官方原文:"When defining routes with dynamic segments, the order of the routes matters: the first route that matches the URL is used."
如果你先把 404 通配符注册了,那后面的任何路由都永远不会被匹配到——因为通配符会"吞掉"所有路径。这也是为什么官方建议将通配符路由放在最后。
四、完整的动态路由实现模板
综合以上要点,一份生产级别的动态路由方案:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 创建路由实例(初始只包含基础路由)
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
meta: { public: true }
},
{
path: '/404',
name: '404',
component: () => import('@/views/NotFound.vue'),
meta: { public: true }
}
// ⚠️ 这里不放 404 通配符!等动态路由加载完再加
]
})
// ========== 状态标记 ==========
let isInitialized = false
let initPromise = null
// ========== 核心初始化函数 ==========
async function initializeRoutes() {
if (isInitialized) return true
// 防止并发重复初始化
if (initPromise) return initPromise
initPromise = (async () => {
try {
const token = localStorage.getItem('token')
if (!token) {
isInitialized = true
return false // 未登录,不需要加载动态路由
}
// 1. 获取用户菜单/路由数据
const { data: menuList } = await api.getUserMenus()
if (!menuList || menuList.length === 0) {
isInitialized = true
return true
}
// 2. 将后端数据转换为 Vue Router 格式
const dynamicRoutes = transformMenuToRoutes(menuList)
// 3. 逐个添加路由
dynamicRoutes.forEach(route => {
router.addRoute(route)
})
// 4. 最后挂载 404 通配符(必须在所有动态路由之后!)
router.addRoute({
path: '/:pathMatch(.*)*',
redirect: '/404'
})
isInitialized = true
return true
} catch (error) {
console.error('路由初始化失败:', error)
throw error
} finally {
initPromise = null
}
})()
return initPromise
}
// ========== 全局前置守卫 ==========
router.beforeEach(async (to) => {
// 公开页面直接放行
if (to.meta.public) return true
// 初始化动态路由
const initialized = await initializeRoutes()
// 如果未登录且访问非公开页面
if (!initialized && to.name !== 'login') {
return {
name: 'login',
query: { redirect: to.fullPath }
}
}
// 如果路由刚刚完成初始化,需要重新导航
// (因为 addRoute 之后需要让 Router 用最新的路由表重新匹配)
if (initialized && !isInitialized) {
// isInitialized 还没被设为 true 说明是首次初始化完成
return { ...to, replace: true }
}
return true
})
export default router
// ========== 工具函数:将后端菜单转为路由配置 ==========
function transformMenuToRoutes(menus) {
return menus.map(menu => ({
path: menu.path,
name: menu.name,
component: loadComponent(menu.component),
meta: {
title: menu.title,
icon: menu.icon,
permission: menu.permission
},
// 递归处理子菜单
children: menu.children ? transformMenuToRoutes(menu.children) : undefined
}))
}
// 动态导入组件
function loadComponent(componentPath) {
// 使用 Vite/Webpack 的动态导入
// componentPath 可能是 "system/User" → import("@/views/system/User.vue")
return () => import(`@/views/${componentPath}.vue`)
}
五、动态路由 + 权限粒度控制
5.1 三级权限模型
第一层:路由级别(能否进入这个页面)
↓ 手段:beforeEach 白名单 + Token 校验
第二层:按钮级别(页面内的操作按钮显隐)
↓ 手段:自定义指令 v-permission
第三层:接口级别(API 请求鉴权)
↓ 手法:请求拦截器携带 Token + 后端校验
5.2 按钮级权限的自定义指令
// directives/permission.js
import { useUserStore } from '@/stores/user'
/**
* 用法:
* <button v-permission="['user:create']">新增用户</button>
* <button v-permission="['user:delete']">删除用户</button>
*/
export default {
mounted(el, binding) {
const { value: requiredPermissions } = binding
const userStore = useUserStore()
const userPermissions = userStore.permissions // 当前用户的权限码列表
// 检查用户是否拥有所需权限
const hasPermission = requiredPermissions.some(
perm => userPermissions.includes(perm)
)
if (!hasPermission) {
// 没有权限 → 从 DOM 中移除该元素
el.parentNode?.removeChild(el)
}
}
}
// main.js 注册
app.directive('permission', permissionDirective)
5.3 路由元信息配合权限
// 在路由配置中使用 meta 存储权限要求
{
path: '/admin/settings',
component: AdminSettings,
meta: {
requiresAuth: true,
requiredPermissions: ['system:settings:view'],
title: '系统设置'
}
}
// beforeEach 中检查
router.beforeEach((to) => {
if (to.meta.requiredPermissions) {
const hasPermission = checkPermission(to.meta.requiredPermissions)
if (!hasPermission) {
// 无权访问 → 403 页面
return { name: 'forbidden' }
}
}
})
六、常见问题排查清单
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| F5 刷新后 404 | addRoute 的路由只在内存中 | 在 beforeEach 里重新拉取路由 + return { ...to, replace: true } |
| 动态路由添加后还是 404 | 404 通配符注册太早 | 把 /:pathMatch(.*)* 放到最后 |
| 新增的路由无法匹配 | 忘记 replace: true 重新导航 | addRoute 后必须重新触发导航 |
| 路由闪烁(白屏) | 异步初始化阻塞渲染 | 配合 loading 状态或骨架屏使用 |
| 多次请求路由数据 | 并发导航导致重复初始化 | 使用单例 Promise(类似第02篇的 authCheckPromise) |
七、本篇小结
| 概念 | 一句话记忆 |
|---|---|
| 嵌套路由 | 父组件 + children 配置 + <router-view> 渲染出口;N层嵌套=N个router-view |
| 动态参数 | :id 捕获路径片段;参数变但组件复用时用 watch 或 onBeforeRouteUpdate |
| 通配符 | Vue Router 4 用 /:pathMatch(.*)* 兜底 404(Vue Router 3 是 *) |
| 编程式导航 | push(新增记录) / replace(替换) / go(n)(前进后退) |
| 命名路由 | 推荐!解耦路径、改一处生效、params 自动编码 |
| meta | 路由的"注释字段",存权限/标题/缓存等自定义信息,守卫中通过 to.meta 读取 |
| 动态路由 | 运行时通过 addRoute() 添加路由,适用于 RBAC 权限系统 |
| F5 刷新 404 根因 | addRoute 的路由存在内存中,刷新后被清除 |
| ❌ Hack 方案 | 先跳 404 再 replace 回来 → 会闪 404 页面,体验差 |
| ✅ 标准解法 | beforeEach 中拦截 → fetchUserRoutes → addRoute → return { ...to, replace: true } |
| 404 通配符时机 | 必须放在所有 addRoute 之后,否则通配符会吞掉所有路径 |
replace: true | 让浏览器用新路由表重新匹配当前 URL,不留多余历史记录 |
| 并发去重 | 单例 Promise:let initPromise = null,避免快速导航时重复请求 |
| 三级权限 | 路由级(beforeEach) → 按钮级(v-permission 指令) → 接口级(请求拦截器) |
下一篇预告:搞懂了路由怎么配(静态/动态)和怎么守(权限控制),下一篇我们关注性能——路由懒加载与打包优化。() => import() 背后发生了什么?Webpack 和 Vite 分别怎么分组?HTTP/2 时代还需要路由分组吗?这些直接影响你的首屏加载速度和用户体验。
👉 Vue 路由系列 04:路由懒加载与打包优化 —— 懒加载原理 + Webpack/Vite 分组策略 + HTTP/2 思考
🔗 回顾前篇:Vue 路由系列 02:导航系统与路由守卫 —— 守卫执行顺序 + 死循环 + 数据预取
参考来源:Gemini 3.1 Pro 模拟面试记录(路由与单页应用章节)、Vue Router 4 官方文档