前端路由与导航深度笔记
框架无关的核心路由原理 + Vue Router / React Router 实践,涵盖手写 Router 实现、导航守卫、性能优化及常见方案。
1. 前端路由核心原理
1.1 为什么需要前端路由
SPA(单页面应用)所有页面切换都在一个 HTML 页面内完成,不触发整页刷新。前端路由的本质:JavaScript 接管 URL 变化,匹配路由表,动态渲染对应视图组件。
传统 MPA SPA (前端路由)
┌─────────┐ ┌──────────────┐
│ 点击链接 │ ───→ 请求 /about │ 点击链接 │ ───→ JS 阻止默认跳转
│ 服务端渲染│ ───→ 返回新 HTML │ pushState() │ ───→ 更新 URL 不刷新
│ 整页刷新 │ │ 匹配路由表 │ ───→ 渲染对应组件
└─────────┘ │ XHR/fetch │ ───→ 按需获取数据
└──────────────┘
1.2 Hash 路由
利用 URL 中 # 及其后面部分不会发送到服务端的特性(始终返回 index.html)。
// 核心实现:hashchange 事件
window.addEventListener('hashchange', () => {
const hash = window.location.hash.slice(1) || '/'
matchRoute(hash)
})
// 手动跳转
const push = (path) => { window.location.hash = path }
const replace = (path) => {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
优点:实现简单、兼容 IE8+、服务端无需特殊配置。
缺点:URL 带 # 不美观、不支持 SEO、锚点冲突(hash 原本用于页面内锚点定位)。
1.3 History 路由
基于 History API(pushState / replaceState)+ popstate 事件。
// pushState 改变 URL(不触发页面刷新)
history.pushState({ page: 'about' }, '', '/about')
history.replaceState({ page: 'home' }, '', '/')
// popstate 监听浏览器前进/后退
window.addEventListener('popstate', (e) => {
// e.state 即为 pushState 传入的 state 对象
matchRoute(location.pathname)
})
// 拦截链接点击
document.addEventListener('click', (e) => {
const a = e.target.closest('a')
if (!a || !a.href) return
const url = new URL(a.href)
if (url.origin !== location.origin) return // 外部链接正常跳转
e.preventDefault()
history.pushState({}, '', url.pathname)
matchRoute(url.pathname)
})
服务端必须配置 fallback:所有路径返回 index.html,否则刷新 404。
Nginx 配置:
location / {
try_files $uri $uri/ /index.html
}
1.4 Hash vs History 对比
| 维度 | Hash 路由 | History 路由 |
|---|---|---|
| URL 美观 | 带 #,不美观 |
干净、标准 URL |
| SEO | 不支持(搜索引擎忽略 # 后内容) |
良好(前提是 SSR/SSG 配合) |
| 服务端要求 | 无(始终返回 index.html) |
需配置 fallback(否则 404) |
| 兼容性 | IE8+ | IE10+ |
| 部署复杂度 | 低 | 中(需服务端配合) |
| 锚点冲突 | 有(与页面内锚点共用 #) |
无 |
| state 传递 | 不支持(仅存储路径) | 支持复杂对象(history.state) |
| 刷新行为 | 始终加载 index.html |
服务端不配合则 404 |
1.5 内存路由(Memory History)
非浏览器环境(React Native、小程序、测试环境)模拟路由。
class MemoryHistory {
constructor(initialEntry = '/') {
this._entries = [initialEntry]
this._index = 0
}
get current() { return this._entries[this._index] }
get length() { return this._entries.length }
get canGoBack() { return this._index > 0 }
get canGoForward() { return this._index < this._entries.length - 1 }
push(path) {
this._entries = this._entries.slice(0, this._index + 1)
this._entries.push(path)
this._index++
this._onChange?.(path)
}
replace(path) {
this._entries[this._index] = path
this._onChange?.(path)
}
go(n) {
this._index = Math.max(0, Math.min(this._entries.length - 1, this._index + n))
this._onChange?.(this._entries[this._index])
}
back() { this.go(-1) }
forward() { this.go(1) }
listen(cb) { this._onChange = cb }
}
2. 手写 Router 实现
2.1 HashRouter 核心
class HashRouter {
constructor(routes) {
this.routes = routes
this.current = null
window.addEventListener('hashchange', () => this._resolve())
window.addEventListener('load', () => this._resolve())
}
_resolve() {
const hash = window.location.hash.slice(1) || '/'
const route = this._match(hash)
this.current = route?.component || NotFoundComponent
this._render()
}
_match(path) {
for (const route of this.routes) {
const match = this._matchPath(route.path, path)
if (match) return { ...route, params: match.params }
}
return null
}
_matchPath(pattern, path) {
// 将 /user/:id 转为正则 /^\/user\/([^/]+)$/
const keys = []
const regexStr = pattern.replace(/:([^/]+)/g, (_, key) => {
keys.push(key)
return '([^/]+)'
})
const match = path.match(new RegExp(`^${regexStr}$`))
if (!match) return null
const params = {}
keys.forEach((key, i) => { params[key] = match[i + 1] })
return { params }
}
_render() {
document.querySelector('#app').innerHTML = this.current
}
push(path) { window.location.hash = path }
replace(path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
}
2.2 HistoryRouter 核心
class HistoryRouter {
constructor(routes) {
this.routes = routes
this.current = null
document.addEventListener('click', (e) => {
const a = e.target.closest('a[data-router]')
if (!a) return
e.preventDefault()
this.push(a.pathname)
})
window.addEventListener('popstate', () => this._resolve())
this._resolve()
}
push(path) {
history.pushState(null, '', path)
this._resolve()
}
replace(path) {
history.replaceState(null, '', path)
this._resolve()
}
_resolve() {
const path = location.pathname
const route = this._match(path)
this.current = route?.component || NotFoundComponent
document.querySelector('#app').innerHTML = this.current
}
_match(path) {
for (const route of this.routes) {
const matched = this._matchSegments(route, path.split('/').filter(Boolean))
if (matched) return matched
}
return null
}
_matchSegments(route, segments, parentParams = {}) {
const pattern = route.path.split('/').filter(Boolean)
if (segments.length < pattern.length) return null
const params = { ...parentParams }
for (let i = 0; i < pattern.length; i++) {
if (pattern[i].startsWith(':')) {
params[pattern[i].slice(1)] = segments[i]
} else if (pattern[i] !== segments[i]) {
return null
}
}
if (route.children && segments.length > pattern.length) {
const rest = segments.slice(pattern.length)
for (const child of route.children) {
const childMatch = this._matchSegments(child, rest, params)
if (childMatch) return {
...childMatch,
parent: route
}
}
}
return segments.length === pattern.length
? { ...route, params }
: null
}
}
2.3 路由匹配算法:参数化路径转正则
function compilePath(path) {
const paramNames = []
let regexpSource = '^'
const parts = path.split('/')
for (const part of parts) {
if (part === '') continue
regexpSource += '/'
if (part === '*') {
regexpSource += '.*'
} else if (part.startsWith(':')) {
const optional = part.endsWith('?')
const name = optional ? part.slice(1, -1) : part.slice(1)
paramNames.push(name)
regexpSource += optional ? '([^/]*)' : '([^/]+)'
} else {
regexpSource += part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
}
return { regexp: new RegExp(regexpSource + '$'), paramNames }
}
function matchPath(pathname, pattern) {
const { regexp, paramNames } = compilePath(pattern)
const match = pathname.match(regexp)
if (!match) return null
const params = {}
paramNames.forEach((name, i) => { params[name] = match[i + 1] || '' })
return { params, path: pattern, url: match[0] }
}
// 用法
matchPath('/user/42/posts/hello-world', '/user/:id/posts/:postId')
// → { params: { id: '42', postId: 'hello-world' }, path: '/user/:id/posts/:postId', url: '/user/42/posts/hello-world' }
2.4 RouterLink / RouterView 组件
// 框架无关声明式组件(以 React 风格展示)
function RouterLink({ to, children, ...props }) {
const handleClick = (e) => {
e.preventDefault()
window.__router.push(to)
}
return <a href={to} onClick={handleClick} {...props}>{children}</a>
}
function RouterView() {
const [component, setComponent] = useState(null)
useEffect(() => {
const unsubscribe = window.__router.subscribe((route) => {
setComponent(() => route.component)
})
return unsubscribe
}, [])
return component ? <component /> : null
}
3. 路由设计模式
3.1 路由配置设计
| 方式 | 代表框架 | 特点 |
|---|---|---|
| 集中式配置 | Vue Router、React Router v6 data router | 单一配置对象,类型推导好,动态路由方便 |
| 约定式路由(文件系统) | Next.js、Nuxt.js、Remix | 文件路径即路由,零配置,但动态路径不够灵活 |
| 装饰器式配置 | Angular 早期版本、NestJS | 装饰器在组件上声明路由,已逐渐被淘汰 |
集中式配置示例:
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/user/:id', name: 'user', component: User, props: true },
{ path: '/about', name: 'about', meta: { title: '关于我们' } },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
]
约定式路由(文件系统):
pages/
index.vue → /
about.vue → /about
blog/
index.vue → /blog
[id].vue → /blog/:id
[...slug].vue → /blog/* (catch-all)
(auth)/
login.vue → /login (路由分组,不影响路径)
3.2 嵌套路由
父子路由通过 <router-view> / <Outlet> 嵌套:
const routes = [
{
path: '/dashboard',
component: DashboardLayout, // 包含 <Outlet /> / <router-view>
children: [
{ path: '', component: DashboardHome }, // /dashboard
{ path: 'settings', component: DashboardSettings }, // /dashboard/settings
{ path: 'users', component: DashboardUsers } // /dashboard/users
]
}
]
相对路径 vs 绝对路径:
| 框架 | 子路由不加 / |
子路由加 / |
|---|---|---|
| Vue Router | settings → /dashboard/settings(拼接父路径) |
/settings → /settings(根路径) |
| React Router | settings → /dashboard/settings(继承父路径) |
/settings → /settings(根路径) |
3.3 动态路由
// 路由定义
{ path: '/user/:id/posts/:postId?', component: UserPost }
// :id 必选参数,:postId? 可选参数
// 获取参数
// Vue: route.params.id / useRoute().params
// React: useParams() → { id, postId }
// 跳转
// Vue: router.push({ name: 'user', params: { id: '42' } })
// React: navigate('/user/42')
3.4 命名路由
通过 name 跳转,避免硬编码 path(解耦):
// 定义
{ path: '/user/:id', name: 'user-profile', component: UserProfile }
{ path: '/admin/user/:id', name: 'admin-user-profile', component: UserProfile }
// 跳转(框架无关风格)
navigate({ name: 'user-profile', params: { id: 42 }, query: { tab: 'posts' } })
// 好处:路径变化时只需改路由配置,不用改所有跳转代码
3.5 路由元信息(meta)
{ path: '/admin', meta: {
requiresAuth: true,
roles: ['admin'],
title: '管理后台',
cache: true,
breadcrumb: ['首页', '管理后台']
},
component: AdminLayout,
children: [
{ path: 'users', meta: { title: '用户管理', breadcrumb: ['首页', '管理后台', '用户管理'] } }
]
}
// 在导航守卫中使用
beforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return { path: '/login', query: { redirect: to.fullPath } }
}
if (to.meta.roles && !to.meta.roles.includes(user.role)) {
return '/403'
}
document.title = to.meta.title || 'Default Title'
})
3.6 查询参数与状态保持
| 方式 | URL | 刷新不丢失 | 适合场景 |
|---|---|---|---|
| query | /search?q=vue&page=2 |
是 | 搜索条件、筛选、分页(可分享) |
| params | /user/42 |
是 | 资源 ID(RESTful) |
| state (history.state) | 不出现 | 否 | 复杂对象(弹窗来源等) |
| store (Pinia/Zustand) | 不出现 | 否(默认) | 全局业务状态 |
页面状态保持:
<router-view v-slot="{ Component }">
<keep-alive :include="cacheList">
<component :is="Component" />
</keep-alive>
</router-view>
4. 导航守卫与鉴权
4.1 守卫类型体系
| 类型 | 触发时机 | 终止方式 | 框架 |
|---|---|---|---|
全局前置 beforeEach |
导航触发前 | return false / redirect | Vue / React |
全局解析 beforeResolve |
所有组件内守卫执行后 | return false | Vue Router |
全局后置 afterEach |
导航确认后 | 无法终止 | Vue / React |
路由级 beforeEnter |
进入该路由前 | return false | Vue / React |
组件内 beforeRouteEnter |
组件渲染前(不能访问 this) |
next(false) | Vue |
组件内 beforeRouteUpdate |
路由变化但组件被复用 | next(false) | Vue |
组件内 beforeRouteLeave |
离开当前组件前 | next(false) | Vue |
React Blocker useBlocker |
离开当前路由前 | blocker.proceed() / reset() | React Router v6 |
4.2 鉴权流程
用户访问 /dashboard
↓
beforeEach 触发
↓
有 token? ──是──→ 访问白名单路由? ──是──→ 正常渲染
│ │
│ └──否──→ 有权限? ──是──→ 正常渲染
│ │
│ └──否──→ /403
│
└──否──→ 目标路由在白名单? ──是──→ 正常渲染(/login)
│
└──否──→ 重定向 /login?redirect=/dashboard
↓
登录成功
↓
跳回 redirect 参数指定的 URL
// 完整鉴权实现(Vue Router 风格)
const WHITE_LIST = ['/login', '/register', '/404', '/403']
router.beforeEach(async (to, from) => {
const token = localStorage.getItem('token')
if (!token) {
if (WHITE_LIST.includes(to.path)) return true
return { path: '/login', query: { redirect: to.fullPath } }
}
if (to.path === '/login') return '/'
if (!userStore.info) {
try {
await userStore.fetchUserInfo()
} catch {
localStorage.removeItem('token')
return '/login'
}
}
if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
return '/403'
}
return true
})
4.3 权限路由(动态添加)
根据用户角色动态挂载路由表:
const asyncRoutes = {
admin: [
{ path: '/admin', component: AdminLayout, children: [
{ path: 'users', component: UserManage },
{ path: 'settings', component: SystemSettings }
]}
],
editor: [
{ path: '/editor', component: EditorLayout, children: [
{ path: 'posts', component: PostManage }
]}
]
}
function setupDynamicRoutes(router, role) {
const routes = asyncRoutes[role] || []
routes.forEach(route => router.addRoute(route))
}
// 退出登录时重置
function resetRouter(router) {
router.getRoutes()
.filter(r => r.meta?.dynamic)
.forEach(r => router.removeRoute(r.name))
}
4.4 路由拦截场景
表单未保存离开确认:
// Vue 组件内
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const ok = window.confirm('有未保存的修改,确定离开?')
if (!ok) return next(false)
}
next()
}
// React Router v6 — useBlocker
function useUnsavedChangesBlocker(hasUnsavedChanges) {
const blocker = useBlocker(hasUnsavedChanges)
useEffect(() => {
if (blocker.state === 'blocked') {
const ok = window.confirm('有未保存的修改,确定离开?')
if (ok) blocker.proceed()
else blocker.reset()
}
}, [blocker])
}
数据预加载失败重定向:
const route = {
path: '/user/:id',
loader: async ({ params }) => {
const user = await fetch(`/api/users/${params.id}`)
if (!user.ok) throw new Response('Not Found', { status: 404 })
return user.json()
},
errorElement: <NotFound />
}
5. 路由懒加载与性能
5.1 代码分割
// Vue 3 — 动态 import
const routes = [
{ path: '/about', component: () => import('@/views/About.vue') },
{ path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
children: [
{ path: 'profile', component: () => import('@/views/Profile.vue') }
]
}
]
// React — React.lazy + Suspense
import { lazy, Suspense } from 'react'
const About = lazy(() => import('./About'))
// 构建时自动生成独立 chunk
// dist/assets/About.[hash].js
5.2 预加载策略
// webpackPrefetch — 空闲时加载
const About = () => import(/* webpackPrefetch: true */ './About.vue')
// webpackPreload — 与当前页面并行加载
const Dashboard = () => import(/* webpackPreload: true */ './Dashboard.vue')
// 鼠标悬停菜单时预加载
function prefetchRoute(routePath) {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = routePath
document.head.appendChild(link)
}
5.3 路由级 Suspense
// React Router v6 + Suspense
<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
} errorElement={<ErrorBoundary />} />
</Routes>
// Vue 3 + defineAsyncComponent
const Dashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: DashboardSkeleton,
errorComponent: DashboardError,
delay: 200,
timeout: 10000
})
5.4 路由过渡动画
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" />
</transition>
</router-view>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-left-enter-active, .slide-left-leave-active { transition: transform 0.3s; }
.slide-left-enter-from { transform: translateX(30px); opacity: 0; }
.slide-left-leave-to { transform: translateX(-30px); opacity: 0; }
</style>
6. 各框架路由实践
6.1 Vue Router 4
// 创建路由实例
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(), // createWebHashHistory() 用于 hash 模式
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/user/:id',
name: 'user',
component: () => import('./User.vue'),
props: true,
meta: { title: '用户详情', transition: 'slide-left' }
}
],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash, behavior: 'smooth' }
return { top: 0, behavior: 'smooth' }
}
})
Vue Router 4 组合式 API:
import { useRouter, useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { watch } from 'vue'
export default {
setup() {
const router = useRouter()
const route = useRoute()
const goToUser = (id) => {
router.push({ name: 'user', params: { id } })
}
watch(() => route.params.id, (newId) => {
fetchUser(newId)
})
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return window.confirm('确定离开?')
}
})
onBeforeRouteUpdate(async (to, from) => {
// 响应参数变化(相同组件不同参数)
})
}
}
命名视图:
<router-view name="sidebar" />
<router-view />
<!-- 路由配置 -->
{
path: '/dashboard',
components: {
default: DashboardMain,
sidebar: DashboardSidebar
}
}
6.2 React Router v6
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <RootErrorBoundary />,
children: [
{ index: true, element: <Home /> },
{
path: 'dashboard',
element: <DashboardLayout />,
loader: ({ request }) => fetch('/api/dashboard'),
errorElement: <DashboardError />,
children: [
{ index: true, element: <DashboardHome /> },
{
path: 'users/:userId',
element: <UserDetail />,
loader: ({ params }) => fetch(`/api/users/${params.userId}`),
lazy: () => import('./UserDetail')
}
]
},
{ path: 'login', element: <Login /> },
{ path: '*', element: <NotFound /> }
]
}
])
function App() {
return <RouterProvider router={router} />
}
React Router v6 Hooks:
import { useNavigate, useParams, useSearchParams, useLocation, useLoaderData } from 'react-router-dom'
function UserProfile() {
const navigate = useNavigate()
const { userId } = useParams()
const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation()
const user = useLoaderData()
const page = parseInt(searchParams.get('page') || '1')
return (
<div>
<button onClick={() => navigate(-1)}>返回</button>
<button onClick={() => navigate('/dashboard')}>去首页</button>
<button onClick={() => setSearchParams({ page: '2' })}>下一页</button>
<button onClick={() => navigate('/confirm', { state: { from: 'profile', userId } })}>
去确认
</button>
</div>
)
}
loader + action 数据流程:
const route = {
path: '/posts/:id/edit',
element: <EditPost />,
loader: async ({ params }) => {
const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())
return post
},
action: async ({ request, params }) => {
const formData = await request.formData()
const title = formData.get('title')
await fetch(`/api/posts/${params.id}`, {
method: 'PUT',
body: JSON.stringify({ title })
})
return redirect(`/posts/${params.id}`)
}
}
function EditPost() {
const post = useLoaderData()
const fetcher = useFetcher()
return (
<fetcher.Form method="post">
<input name="title" defaultValue={post.title} />
<button type="submit">保存</button>
</fetcher.Form>
)
}
6.3 框架路由对比
| 概念 | Vue Router 4 | React Router v6 | Angular Router |
|---|---|---|---|
| 路由配置 | routes 数组 |
createBrowserRouter data router |
Routes 数组 + forRoot |
| 路由模式 | createWebHistory / createWebHashHistory |
createBrowserRouter / createHashRouter |
useHash: true / PathLocationStrategy |
| 动态路径 | :id / :id? / :pathMatch(.*)* |
:id / :id? / * |
:id / ** (wildcard) |
| 嵌套渲染 | <router-view> |
<Outlet> |
<router-outlet> |
| 导航指令 | <router-link> |
<Link> / <NavLink> |
routerLink directive |
| 编程导航 | router.push / router.replace |
navigate / navigate(to, { replace: true }) |
router.navigate / router.navigateByUrl |
| 获取参数 | useRoute().params / useRoute().query |
useParams / useSearchParams |
ActivatedRoute.params / queryParams |
| 导航守卫 | beforeEach / beforeResolve / beforeEnter |
loader 拦截 / useBlocker |
canActivate / canDeactivate guards |
| 路由元信息 | meta 字段 |
handle + useMatches |
data 字段 |
| 代码分割 | () => import() |
lazy: () => import() / React.lazy |
loadComponent |
| 错误边界 | 组件内 try-catch | errorElement(路由级) |
errorHandler |
| 滚动恢复 | scrollBehavior |
ScrollRestoration |
scrollPositionRestoration |
| 数据预加载 | beforeEnter + store |
loader(内置) |
resolve guards |
| 表单处理 | 手动处理 | action + Form / useFetcher |
手动处理 |
7. 通用路由问题与方案
7.1 路由传参方式对比
| 方式 | 出现 URL | 刷新丢失 | 容量 | 适合场景 |
|---|---|---|---|---|
| query | 是 ?key=val |
否 | URL 长度限制 (~2KB) | 搜索条件、分页、可分享链接 |
| params | 是 /user/42 |
否 | 路径段 | RESTful 资源 ID |
| state | 否 (history.state) |
是 | 大(结构化对象) | 弹窗来源、临时状态 |
| store | 否 | 是(默认) | 无限制 | 业务数据、全局状态 |
// 场景选择
// ✅ query: 搜索条件 → 用户刷新后保留,可复制分享
navigate('/search?q=vue&sort=latest&page=2')
// ✅ params: 资源详情 → RESTful 路径清晰
navigate('/user/42/posts/hello-world')
// ✅ state: 来源追踪 → 不暴露 URL
navigate('/checkout', { state: { from: 'cart', couponId: 'XMAS2024' } })
// ✅ store: 全局业务数据 → 不依赖路由
userStore.setCurrentUser(user)
navigate('/profile')
7.2 多级路由与面包屑
根据 matched 路由数组逐级生成面包屑:
const routes = [
{
path: '/dashboard',
meta: { breadcrumb: [{ label: '首页', path: '/' }, { label: '仪表盘' }] },
children: [
{
path: 'users',
meta: { breadcrumb: [{ label: '首页', path: '/' }, { label: '仪表盘', path: '/dashboard' }, { label: '用户管理' }] },
children: [
{
path: ':id',
meta: { breadcrumb: [{ label: '首页', path: '/' }, { label: '仪表盘', path: '/dashboard' }, { label: '用户管理', path: '/dashboard/users' }, { label: '用户详情' }] }
}
]
}
]
}
]
// 通用面包屑生成器
function useBreadcrumbs(route) {
return route.matched
.filter(r => r.meta?.breadcrumb)
.flatMap(r => r.meta.breadcrumb)
}
7.3 标签页路由(多标签缓存)
类似浏览器标签页,已打开的页面组件状态保持,关闭后清除缓存:
const cacheList = ref([])
router.beforeEach((to) => {
if (!cacheList.value.includes(to.name)) {
cacheList.value.push(to.name)
}
})
function closeTab(tabName) {
const idx = cacheList.value.indexOf(tabName)
if (idx > -1) cacheList.value.splice(idx, 1)
if (router.currentRoute.value.name === tabName) {
router.push({ name: cacheList.value[0] || 'home' })
}
}
7.4 微前端路由协作
主应用 (Base: /app1)
├── 子应用 A (activeRule: /app1/a)
│ └── 内部路由: /page1, /page2 (实际 URL: /app1/a/page1)
└── 子应用 B (activeRule: /app1/b)
└── 内部路由: /home, /settings (实际 URL: /app1/b/home)
// 主应用 — qiankun 路由分配
const apps = [
{
name: 'appA',
entry: '//localhost:3001',
container: '#sub-app-container',
activeRule: '/app1/a'
},
{
name: 'appB',
entry: '//localhost:3002',
container: '#sub-app-container',
activeRule: '/app1/b'
}
]
// 子应用使用 basename
const router = createRouter({
history: createWebHistory('/app1/a'),
routes: [ /* 子应用内部路由 */ ]
})
7.5 路由过渡动画方向判断
根据路由 meta 中的深度判断前进/后退方向:
const routes = [
{ path: '/', meta: { depth: 0 } },
{ path: '/list', meta: { depth: 1 } },
{ path: '/detail', meta: { depth: 2 } },
]
router.beforeEach((to, from) => {
const toDepth = to.meta.depth ?? 0
const fromDepth = from.meta.depth ?? 0
to.meta.transition = toDepth >= fromDepth ? 'slide-left' : 'slide-right'
})
参考:Vue Router 4 官方文档、React Router v6 官方文档、wouter(2KB 轻量 React Router 替代品)