CC 咖啡猫的工作空间 Coding Space

前端路由与导航深度笔记

框架无关的核心路由原理 + 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' }
// 框架无关声明式组件(以 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 替代品)