{T}

编程式导航

除了使用 <router-link> 声明式导航,还可以通过 JavaScript API 进行编程式导航。

概述

编程式导航适用于需要根据条件、事件或异步操作结果进行导航的场景。

code
┌────────────────────────────────────────────────────────────────┐
│                    导航方式对比                                 │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  声明式导航 (router-link)          编程式导航 (API)            │
│  ┌─────────────────────┐          ┌─────────────────────┐     │
│  │ <router-link to="/">│          │ router.push('/')    │     │
│  │   首页               │          │ router.replace('/') │     │
│  │ </router-link>       │          │ router.go(-1)       │     │
│  └─────────────────────┘          └─────────────────────┘     │
│                                                                │
│  适用场景:                         适用场景:                    │
│  - 导航链接                        - 按钮点击                   │
│  - 菜单项                          - 表单提交后跳转             │
│  - 固定跳转                        - 条件判断后跳转             │
│                                   - 登录成功后跳转             │
│                                                                │
└────────────────────────────────────────────────────────────────┘

基本用法

useRouter 组合式函数

Vue SFC
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

// 导航方法
function goHome() {
  router.push('/')
}

function goBack() {
  router.back()
}
</script>

选项式 API

Vue SFC
<script>
export default {
  methods: {
    goHome() {
      this.$router.push('/')
    },
    goBack() {
      this.$router.back()
    }
  }
}
</script>

router.push()

导航到指定路由,会向 history 栈添加新记录。

参数形式

js
import { useRouter } from 'vue-router'

const router = useRouter()

// 1. 字符串路径
router.push('/users/1')

// 2. 路径对象
router.push({ path: '/users/1' })

// 3. 命名路由 + 参数
router.push({ 
  name: 'User', 
  params: { id: 1 } 
})

// 4. 带查询参数
router.push({ 
  path: '/search', 
  query: { q: 'vue', page: 1 } 
})

// 5. 带 hash
router.push({ 
  path: '/about', 
  hash: '#team' 
})

// 6. 组合使用
router.push({ 
  name: 'User', 
  params: { id: 1 },
  query: { tab: 'profile' },
  hash: '#avatar'
})
// 结果: /users/1?tab=profile#avatar

完整参数

ts
interface RouteLocationRaw {
  path?: string
  name?: string | symbol
  params?: Record<string, string | string[]>
  query?: Record<string, string | number | (string | number)[]>
  hash?: string
  state?: Record<string, any>  // History State
  replace?: boolean            // 是否替换当前记录
  force?: boolean              // 强制导航(跳过重复检查)
}

带 State 参数

js
// 传递状态数据(不会出现在 URL 中)
router.push({
  path: '/result',
  state: {
    from: 'payment',
    amount: 99.99
  }
})

// 在目标组件中获取
const route = useRoute()
console.log(history.state.from)  // 'payment'
console.log(history.state.amount) // 99.99

router.replace()

替换当前路由,不会向 history 栈添加新记录。

js
// 直接替换
router.replace('/home')

// 使用 push + replace 选项
router.push({ path: '/home', replace: true })

// 使用场景:登录后不希望回退到登录页
async function handleLogin() {
  await login(credentials)
  router.replace('/dashboard')  // 替换,无法回退到登录页
}

router.go()

在历史记录中前进或后退指定步数。

js
// 前进 1 步
router.go(1)

// 后退 1 步
router.go(-1)

// 后退 2 步
router.go(-2)

// 前进 3 步
router.go(3)

// 如果步数超出范围,静默失败
router.go(-100)  // 不会报错

简写方法

js
// 前进
router.forward()  // 等同于 router.go(1)

// 后退
router.back()     // 等同于 router.go(-1)

导航结果处理

Promise 返回值

router.push()router.replace() 返回 Promise:

Vue SFC
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

async function navigate() {
  try {
    // 等待导航完成
    await router.push('/dashboard')
    console.log('导航成功')
  } catch (error) {
    // 导航失败
    console.log('导航失败:', error)
  }
}
</script>

导航失败类型

js
import { isNavigationFailure, NavigationFailureType } from 'vue-router'

async function navigate() {
  try {
    await router.push('/protected')
  } catch (error) {
    if (isNavigationFailure(error, NavigationFailureType.aborted)) {
      console.log('导航被守卫中止')
    } else if (isNavigationFailure(error, NavigationFailureType.duplicated)) {
      console.log('导航到相同位置')
    } else if (isNavigationFailure(error, NavigationFailureType.cancelled)) {
      console.log('导航被新的导航覆盖')
    }
  }
}

导航失败类型说明

类型说明常见原因
aborted导航被守卫中止beforeEach 返回 false
duplicated重复导航导航到当前页面
cancelled导航被取消新导航覆盖旧导航

检测导航失败

js
import { isNavigationFailure } from 'vue-router'

router.push('/admin').catch(failure => {
  if (isNavigationFailure(failure)) {
    console.log('导航失败原因:', failure.type)
    console.log('目标路由:', failure.to)
    console.log('来源路由:', failure.from)
  }
})

实际应用场景

1. 登录后跳转

Vue SFC
<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()
const credentials = ref({ username: '', password: '' })

async function handleLogin() {
  try {
    await login(credentials.value)
    
    // 获取重定向地址
    const redirect = route.query.redirect || '/dashboard'
    
    // 替换历史记录,防止回退到登录页
    router.replace(redirect)
  } catch (error) {
    console.error('登录失败:', error)
  }
}
</script>

2. 表单提交后跳转

Vue SFC
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const form = ref({ title: '', content: '' })

async function handleSubmit() {
  try {
    const post = await createPost(form.value)
    
    // 跳转到详情页
    router.push({ 
      name: 'PostDetail', 
      params: { id: post.id } 
    })
  } catch (error) {
    console.error('创建失败:', error)
  }
}
</script>

3. 条件导航

Vue SFC
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = useRouter()
const userStore = useUserStore()

function navigateToDashboard() {
  const target = computed(() => {
    // 根据用户角色跳转不同页面
    if (userStore.isAdmin) return '/admin/dashboard'
    if (userStore.isEditor) return '/editor/dashboard'
    return '/dashboard'
  })
  
  router.push(target.value)
}
</script>

4. 带确认的导航

Vue SFC
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const hasChanges = ref(false)

async function navigateAway() {
  if (hasChanges.value) {
    const confirmed = confirm('有未保存的更改,确定离开吗?')
    if (!confirmed) return
  }
  
  router.push('/other-page')
}
</script>

5. 多步导航

Vue SFC
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

async function multiStepNavigation() {
  // 第一步:导航到确认页
  await router.push({ 
    name: 'Confirm', 
    query: { action: 'delete' }
  })
  
  // 后续导航根据用户操作执行
}

function confirmAction() {
  // 替换确认页,直接跳转结果页
  router.replace({ 
    name: 'Result', 
    params: { status: 'success' }
  })
}
</script>

常见模式

1. 返回上一页并刷新

js
// 方式1:返回后利用守卫刷新数据
router.back()

// 方式2:使用 state 传递刷新标志
router.push({
  path: '/list',
  state: { shouldRefresh: true }
})

2. 外部链接

js
// 不使用 Vue Router
function openExternal(url) {
  window.open(url, '_blank')
}

// 或者使用 location
function gotoExternal(url) {
  window.location.href = url
}

3. 新标签页打开

Vue SFC
<template>
  <!-- 声明式 -->
  <router-link to="/detail" target="_blank">新窗口打开</router-link>
  
  <!-- 编程式 -->
  <button @click="openInNewTab">新窗口打开</button>
</template>

<script setup>
function openInNewTab() {
  const routeData = router.resolve({ name: 'Detail', params: { id: 1 } })
  window.open(routeData.href, '_blank')
}
</script>

4. 下载文件

js
function downloadFile(fileId) {
  // 构建下载链接
  const routeData = router.resolve({
    path: '/api/download',
    query: { id: fileId }
  })
  
  // 直接触发下载
  window.location.href = routeData.href
}

API 速查表

router 实例方法

方法参数返回值说明
push(location)RouteLocationRawPromise导航并添加历史记录
replace(location)RouteLocationRawPromise导航并替换历史记录
go(delta)numbervoid前进/后退指定步数
forward()-void前进一步
back()-void后退一步
beforeEach(guard)NavigationGuard() => void注册全局前置守卫
beforeResolve(guard)NavigationGuard() => void注册全局解析守卫
afterEach(hook)NavigationHook() => void注册全局后置钩子
addRoute(route)RouteRecord() => void动态添加路由
removeRoute(name)string | symbolvoid动态移除路由
hasRoute(name)string | symbolboolean检查路由是否存在
getRoutes()-RouteRecord[]获取所有路由
resolve(location)RouteLocationRawRouteLocation解析路由地址
currentRoute-RouteLocation当前路由对象(响应式)

RouteLocationRaw 类型

ts
type RouteLocationRaw = string | {
  path?: string
  name?: string | symbol
  params?: Record<string, string | string[]>
  query?: Record<string, string | number | (string | number)[]>
  hash?: string
  state?: Record<string, any>
  replace?: boolean
  force?: boolean
}

常见问题

1. 重复导航警告

问题:导航到当前页面时控制台警告

js
// Vue Router 4 默认允许重复导航
// 如需禁止,可在全局守卫中处理

router.beforeEach((to, from) => {
  if (to.path === from.path) {
    return false  // 阻止重复导航
  }
})

2. params 丢失

问题:使用 path 时 params 被忽略

js
// 错误:params 会被忽略
router.push({ 
  path: '/user', 
  params: { id: 1 }  // 无效!
})

// 正确方式1:使用命名路由
router.push({ 
  name: 'User', 
  params: { id: 1 } 
})

// 正确方式2:在路径中包含参数
router.push({ 
  path: `/user/${id}` 
})

3. 导航未完成就执行后续代码

js
// 错误:导航是异步的
router.push('/home')
console.log('已经跳转')  // 此时可能还未跳转

// 正确:等待导航完成
await router.push('/home')
console.log('已经跳转')

最佳实践

1. 封装导航逻辑

js
// utils/navigation.js
import { useRouter } from 'vue-router'

export function useNavigation() {
  const router = useRouter()
  
  function goToUser(id) {
    return router.push({ name: 'User', params: { id } })
  }
  
  function goToLogin(redirect) {
    return router.push({
      path: '/login',
      query: { redirect }
    })
  }
  
  function goBack() {
    if (window.history.length > 1) {
      router.back()
    } else {
      router.push('/')
    }
  }
  
  return { goToUser, goToLogin, goBack }
}

2. 使用组合式函数

Vue SFC
<script setup>
import { useNavigation } from '@/composables/useNavigation'

const { goToUser, goToLogin, goBack } = useNavigation()

// 使用
function handleUserClick(id) {
  goToUser(id)
}
</script>

3. 类型安全的导航

ts
// types/router.ts
import type { RouteLocationRaw } from 'vue-router'

export type AppRouteName = 
  | 'Home' 
  | 'User' 
  | 'Settings'

export interface TypedRouteLocation extends Omit<RouteLocationRaw, 'name'> {
  name: AppRouteName
}

// 使用
function navigate(route: TypedRouteLocation) {
  router.push(route)
}

下一步


重定向与别名

重定向和别名提供了灵活的路由配置方式,用于处理 URL 变更、兼容旧路径等场景。

概述

code
┌────────────────────────────────────────────────────────────────┐
│                    重定向 vs 别名                               │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  重定向 (redirect)                   别名 (alias)               │
│  ┌─────────────────────┐          ┌─────────────────────┐     │
│  │ /home → redirect → /│          │ /users/:id          │     │
│  │                     │          │ alias: /u/:id       │     │
│  │ URL 会变更为目标     │          │                     │     │
│  │ /home → /           │          │ URL 保持不变         │     │
│  │                     │          │ /u/123 仍显示 /u/123 │     │
│  └─────────────────────┘          └─────────────────────┘     │
│                                                                │
│  特点:                              特点:                       │
│  - 改变 URL                        - URL 不变                  │
│  - 添加历史记录                     - 无历史记录                │
│  - 用户能看到 URL 变化              - 多个 URL 指向同一组件     │
│                                                                │
└────────────────────────────────────────────────────────────────┘

重定向

基本用法

js
const routes = [
  // 字符串重定向
  { path: '/home', redirect: '/' },
  
  // 命名路由重定向
  { path: '/users', redirect: { name: 'UserList' } },
  
  // 对象形式
  { 
    path: '/old-path', 
    redirect: { path: '/new-path' } 
  }
]

动态重定向

js
const routes = [
  {
    path: '/search/:keyword',
    redirect: to => {
      // to: 目标路由对象
      // 返回重定向目标
      return { 
        path: '/results', 
        query: { q: to.params.keyword } 
      }
    }
  },
  
  // 示例:/search/vue → /results?q=vue
]

带参数重定向

js
const routes = [
  {
    path: '/user/:id',
    redirect: to => {
      // 保留参数
      return { 
        name: 'UserDetail', 
        params: { id: to.params.id } 
      }
    }
  },
  
  // 示例:/user/123 → /users/123/detail
]

相对重定向

js
const routes = [
  {
    path: '/users/:id',
    component: User,
    children: [
      // 相对路径重定向
      { path: '', redirect: 'profile' },
      { path: 'profile', component: UserProfile },
      { path: 'posts', component: UserPosts }
    ]
  }
  
  // /users/123 → /users/123/profile
]

导航守卫中的重定向

js
// 全局守卫重定向
router.beforeEach((to, from) => {
  if (to.path === '/old-dashboard') {
    return '/dashboard'  // 重定向
  }
  
  if (!isAuthenticated() && to.meta.requiresAuth) {
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
})

别名

基本用法

js
const routes = [
  {
    path: '/users/:id',
    component: User,
    alias: '/u/:id'
  },
  
  // /users/123 和 /u/123 都显示 User 组件
  // URL 不会改变
]

多个别名

js
const routes = [
  {
    path: '/users/:id',
    component: User,
    alias: ['/u/:id', '/user/:id', '/profile/:id']
  }
  
  // 所有路径都能访问同一组件
]

根路径别名

js
const routes = [
  {
    path: '/home',
    component: Home,
    alias: '/'  // 首页别名
  }
  
  // / 和 /home 都能访问 Home 组件
]

嵌套路由别名

js
const routes = [
  {
    path: '/users/:id',
    component: User,
    children: [
      {
        path: 'profile',
        component: UserProfile,
        alias: [
          '@/:id',    // /@/123 → 实际访问 /users/123/profile
          'info'      // /users/123/info
        ]
      }
    ]
  }
]

实际应用场景

1. 版本迁移

js
const routes = [
  // 旧 API 路径重定向到新路径
  { path: '/api/v1/users', redirect: '/api/v2/users' },
  { path: '/api/v1/posts', redirect: '/api/v2/articles' },
  
  // 保留兼容性别名
  {
    path: '/api/v2/articles',
    component: Articles,
    alias: '/api/v2/posts'  // 旧名称仍然可用
  }
]

2. 简化 URL

js
const routes = [
  {
    path: '/dashboard/reports/monthly-sales',
    component: MonthlySales,
    alias: '/reports/sales'  // 简短别名
  },
  
  {
    path: '/settings/account/profile',
    component: Profile,
    alias: '/profile'  // 直接访问
  }
]

3. 多语言路由

js
const routes = [
  {
    path: '/en/products',
    component: Products,
    alias: ['/zh/products', '/ja/products']
  },
  
  // 或使用动态重定向
  {
    path: '/:lang/products',
    redirect: to => {
      return `/en/products`  // 统一重定向
    }
  }
]

4. SEO 友好路径

js
const routes = [
  {
    path: '/products/:id',
    component: ProductDetail,
    alias: '/p/:id'  // 短链接
  },
  
  // 带产品名称的 SEO 友好路径
  {
    path: '/products/:id/:name?',
    component: ProductDetail,
    alias: '/p/:id/:name?'
  }
  
  // /products/123 和 /products/123/iphone-15 都有效
]

5. 登录后跳转

js
const routes = [
  {
    path: '/login',
    redirect: to => {
      const redirect = to.query.redirect || '/dashboard'
      return redirect
    }
  }
]

// 配合守卫使用
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
})

重定向与别名的区别

特性重定向 (redirect)别名 (alias)
URL 变化改变为目标 URL保持不变
历史记录添加一条记录
组件匹配匹配目标路由匹配当前路由
导航守卫触发两次导航触发一次导航
适用场景旧路径迁移、权限跳转简化路径、多路径访问

导航流程对比

code
重定向流程:
/old-path → 触发导航 → 守卫检查 → 重定向到 /new-path → 触发新导航 → 渲染组件
                      ↑
                 URL 变为 /new-path

别名流程:
/alias-path → 触发导航 → 守卫检查 → 渲染组件
                           ↑
                     URL 保持 /alias-path

高级用法

带条件的重定向

js
const routes = [
  {
    path: '/admin',
    redirect: to => {
      // 根据用户角色重定向
      const userRole = getUserRole()
      
      if (userRole === 'super') {
        return '/admin/super'
      } else if (userRole === 'moderator') {
        return '/admin/moderator'
      }
      return '/admin/dashboard'
    }
  }
]

异步重定向

js
const routes = [
  {
    path: '/go/:code',
    redirect: async to => {
      // 异步获取真实路径
      const realPath = await resolveShortLink(to.params.code)
      return realPath
    }
  }
]

正则别名

js
const routes = [
  {
    path: '/user-:id(\\d+)',
    component: User,
    alias: [
      '/u/:id(\\d+)',
      '/profile/:id(\\d+)'
    ]
  }
]

完整示例

路由配置

js
// router/index.js
const routes = [
  // 首页重定向
  { path: '/', redirect: '/dashboard' },
  { path: '/home', redirect: '/dashboard' },
  
  // 旧路径迁移
  { path: '/article/:id', redirect: '/articles/:id' },
  { path: '/post/:id', redirect: '/articles/:id' },
  
  // 带别名的路由
  {
    path: '/articles/:id',
    name: 'ArticleDetail',
    component: () => import('@/views/ArticleDetail.vue'),
    alias: ['/a/:id', '/blog/:id'],
    meta: { title: '文章详情' }
  },
  
  // 用户相关
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    alias: ['/u/:id', '/profile/:id'],
    redirect: to => {
      // 如果只有 ID,重定向到完整路径
      if (!to.query.tab) {
        return { path: to.path, query: { tab: 'overview' } }
      }
    }
  },
  
  // 管理后台
  {
    path: '/admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    redirect: '/admin/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'AdminDashboard',
        component: () => import('@/views/admin/Dashboard.vue'),
        alias: ['', 'home']  // /admin 和 /admin/home 都能访问
      }
    ]
  }
]

重定向服务

js
// services/redirectService.js
const redirects = new Map([
  ['/old-page', '/new-page'],
  ['/legacy/docs', '/documentation'],
  ['/support', '/help']
])

export function setupRedirects(router) {
  router.beforeEach((to) => {
    const redirect = redirects.get(to.path)
    if (redirect) {
      return redirect
    }
  })
}

// 使用
// router/index.js
import { setupRedirects } from '@/services/redirectService'

const router = createRouter({ ... })
setupRedirects(router)

常见问题

1. 重定向循环

问题:A 重定向到 B,B 又重定向到 A

js
// 错误示例
const routes = [
  { path: '/a', redirect: '/b' },
  { path: '/b', redirect: '/a' }  // 循环重定向
]

// 解决方案:检查避免循环
router.beforeEach((to, from) => {
  if (to.redirectedFrom === '/a' && to.path === '/b') {
    return false  // 阻止循环
  }
})

2. 别名参数不匹配

js
// 错误:别名参数数量不匹配
{
  path: '/users/:id/posts/:postId',
  component: UserPost,
  alias: '/u/:id'  // 缺少 postId 参数
}

// 正确:参数必须匹配
{
  path: '/users/:id/posts/:postId',
  component: UserPost,
  alias: '/u/:id/p/:postId'
}

3. 重定向守卫执行顺序

js
// 重定向会触发两次导航
// 第一次:/old-path → 守卫执行 → 重定向
// 第二次:/new-path → 守卫执行 → 渲染

router.beforeEach((to, from) => {
  console.log('导航到:', to.path)
  console.log('重定向来源:', to.redirectedFrom)  // 第二次导航时有值
})

最佳实践

1. 优先使用命名路由重定向

js
// 推荐:使用 name,URL 变更无需修改
{ path: '/old', redirect: { name: 'NewPage' } }

// 不推荐:硬编码路径
{ path: '/old', redirect: '/new-path' }

2. 重定向与别名结合使用

js
const routes = [
  // 旧路径重定向
  { path: '/old-path', redirect: '/new-path' },
  
  // 新路径带别名(兼容短链接)
  {
    path: '/new-path',
    component: NewPage,
    alias: '/n'  // 短链接
  }
]

3. 文档化重定向规则

js
// 迁移配置文件
export const ROUTE_MIGRATIONS = [
  {
    from: '/old-users',
    to: '/users',
    reason: 'API v2 迁移',
    since: '2024-01-01'
  },
  {
    from: '/article',
    to: '/articles',
    reason: 'RESTful 规范',
    since: '2024-02-01'
  }
]

// 生成重定向规则
export const redirectRoutes = ROUTE_MIGRATIONS.map(m => ({
  path: m.from,
  redirect: m.to,
  meta: { migration: m }
}))

以下为深度补充内容,涵盖源码分析、性能优化和生产级实践。

router.push 导航流程源码解析

导航流程概览

Vue Router 4 的导航流程是一个多阶段的异步管道。理解其内部机制有助于排查导航时序问题和编写更健壮的路由代码。

code
用户调用 router.push('/target')
         │
         ▼
   ┌─────────────────┐
   │ 1. resolve()    │  将 RouteLocationRaw 解析为 RouteLocationNormalized
   │    参数规范化     │
   └────────┬────────┘
            │
            ▼
   ┌─────────────────┐
   │ 2. triggerError │  检查是否需要触发错误处理(如重复导航)
   │    错误检测       │
   └────────┬────────┘
            │
            ▼
   ┌─────────────────────────────────────────────┐
   │ 3. finalizeNavigation()                     │
   │    ├── extractChangeRecords() 提取变更记录    │
   │    ├── runGuardQueue() 执行守卫队列           │
   │    │   ├── beforeEach 全局前置守卫             │
   │    │   ├── beforeEnter 路由独享守卫            │
   │    │   └── beforeRouteEnter 组件内守卫         │
   │    ├── beforeResolve 全局解析守卫              │
   │    └── 更新 currentRoute                     │
   └────────┬────────────────────────────────────┘
            │
            ▼
   ┌─────────────────┐
   │ 4. afterEach     │  触发全局后置钩子
   │    后置钩子       │
   └─────────────────┘

简化源码实现

以下代码展示了 Vue Router 4 导航核心的简化实现,帮助你理解 pushresolvefinalizeNavigation 的完整链路。

ts
// 简化版 Vue Router 4 导航核心实现
import type {
  RouteLocationNormalized,
  RouteLocationRaw,
  NavigationGuard,
  NavigationFailure,
} from 'vue-router'

// NavigationFailure 类型枚举
enum NavigationFailureType {
  aborted = 4,
  cancelled = 8,
  duplicated = 16,
}

// 创建 NavigationFailure 的工厂函数
function createNavigationFailure(
  type: NavigationFailureType,
  from: RouteLocationNormalized,
  to: RouteLocationNormalized
): NavigationFailure {
  const error = new Error(
    `Navigating from ${from.fullPath} to ${to.fullPath} was ${NavigationFailureType[type]}`
  ) as NavigationFailure
  error.type = type
  error.from = from
  error.to = to
  return error
}

// 判断是否为 NavigationFailure
function isNavigationFailure(
  error: unknown,
  type?: NavigationFailureType
): error is NavigationFailure {
  return (
    error instanceof Error &&
    '__navigationFailure' in error &&
    (type == null || (error as NavigationFailure).type === type)
  )
}

// 标记错误为 NavigationFailure
function markAsNavigationFailure(
  error: Error,
  type: NavigationFailureType
): NavigationFailure {
  ;(error as any).__navigationFailure = true
  ;(error as any).type = type
  return error as NavigationFailure
}

// 简化版 Router 类
class SimplifiedRouter {
  private currentRoute: RouteLocationNormalized
  private beforeGuards: NavigationGuard[] = []
  private beforeResolveGuards: NavigationGuard[] = []
  private afterHooks: Array<(to: RouteLocationNormalized, from: RouteLocationNormalized) => void> = []

  // 当前正在进行的导航 Promise(用于取消旧导航)
  private pendingNavigation: Promise<void | NavigationFailure> | null = null

  // ─── 入口:push ───
  async push(to: RouteLocationRaw): Promise<void | NavigationFailure> {
    // 1. 解析目标位置
    const targetLocation = this.resolve(to)

    // 2. 检测重复导航
    if (
      targetLocation.fullPath === this.currentRoute.fullPath &&
      !targetLocation.matched.length
    ) {
      // 无匹配路由的重复导航,直接返回
      return
    }

    // 3. 如果已有进行中的导航,取消旧导航
    if (this.pendingNavigation) {
      // 旧导航会被 reject,触发 cancelled 错误
      this.triggerCancellation()
    }

    // 4. 开始新导航
    const navigationPromise = this.finalizeNavigation(
      targetLocation,
      this.currentRoute
    )

    this.pendingNavigation = navigationPromise

    try {
      const result = await navigationPromise
      return result
    } catch (error) {
      // 导航被取消或中止时抛出
      if (isNavigationFailure(error)) {
        return error
      }
      throw error
    } finally {
      this.pendingNavigation = null
    }
  }

  // ─── 解析路由位置 ───
  resolve(to: RouteLocationRaw): RouteLocationNormalized {
    if (typeof to === 'string') {
      // 字符串路径:解析 path、query、hash
      return this.resolveStringPath(to)
    }

    // 对象形式
    if (to.name) {
      // 命名路由:根据 name 查找路由记录,填充 params
      return this.resolveNamedRoute(to)
    }

    // path 形式
    return this.resolvePath(to)
  }

  private resolveStringPath(raw: string): RouteLocationNormalized {
    // 解析 URL 各组成部分
    const [pathPart, hashPart] = raw.split('#')
    const [path, queryString] = pathPart.split('?')

    const query: Record<string, string> = {}
    if (queryString) {
      new URLSearchParams(queryString).forEach((value, key) => {
        query[key] = value
      })
    }

    return this.matchRoute(path, query, hashPart || '')
  }

  private resolveNamedRoute(to: RouteLocationRaw & { name: string | symbol }): RouteLocationNormalized {
    // 根据 name 查找路由记录
    const record = this.findRouteByName(to.name)
    if (!record) {
      throw new Error(`No route named "${String(to.name)}"`)
    }

    // 用 params 填充路径
    const filledPath = this.fillParams(record.path, to.params || {})

    return this.matchRoute(filledPath, to.query || {}, to.hash || '')
  }

  private resolvePath(to: RouteLocationRaw & { path: string }): RouteLocationNormalized {
    return this.matchRoute(to.path, to.query || {}, to.hash || '')
  }

  private matchRoute(
    path: string,
    query: Record<string, any>,
    hash: string
  ): RouteLocationNormalized {
    // 遍历路由表,匹配路径
    // 实际实现涉及路径排名、正则匹配、嵌套路由等
    // 此处为简化示意
    const matched = this.findMatchingRecords(path)

    return {
      fullPath: this.buildFullPath(path, query, hash),
      path,
      query,
      hash,
      params: this.extractParams(path, matched),
      matched,
      meta: matched[matched.length - 1]?.meta || {},
      redirectedFrom: undefined,
      name: matched[matched.length - 1]?.name,
    } as RouteLocationNormalized
  }

  // ─── 核心:finalizeNavigation ───
  private async finalizeNavigation(
    to: RouteLocationNormalized,
    from: RouteLocationNormalized
  ): Promise<void | NavigationFailure> {
    // 检查是否已被取消
    if (this.isCancelled()) {
      throw createNavigationFailure(
        NavigationFailureType.cancelled,
        from,
        to
      )
    }

    // 阶段 1:执行全局 beforeEach 守卫
    const beforeResult = await this.runGuardQueue(
      this.beforeGuards,
      to,
      from
    )

    if (beforeResult === false) {
      throw createNavigationFailure(
        NavigationFailureType.aborted,
        from,
        to
      )
    }

    // 守卫可能返回新的导航目标(重定向)
    if (typeof beforeResult === 'object') {
      return this.push(beforeResult as RouteLocationRaw)
    }

    // 阶段 2:执行路由独享 beforeEnter 守卫
    for (const record of to.matched) {
      if (record.beforeEnter) {
        const enterResult = await record.beforeEnter(to, from)
        if (enterResult === false) {
          throw createNavigationFailure(
            NavigationFailureType.aborted,
            from,
            to
          )
        }
        if (typeof enterResult === 'object') {
          return this.push(enterResult as RouteLocationRaw)
        }
      }
    }

    // 阶段 3:执行组件内 beforeRouteEnter 守卫
    // (实际实现中通过 extractComponentsGuards 提取)

    // 阶段 4:执行全局 beforeResolve 守卫
    const resolveResult = await this.runGuardQueue(
      this.beforeResolveGuards,
      to,
      from
    )

    if (resolveResult === false) {
      throw createNavigationFailure(
        NavigationFailureType.aborted,
        from,
        to
      )
    }

    // 阶段 5:确认导航——更新 currentRoute
    this.currentRoute = to

    // 阶段 6:触发 afterEach 钩子
    for (const hook of this.afterHooks) {
      hook(to, from)
    }
  }

  // ─── 守卫队列执行器 ───
  private async runGuardQueue(
    guards: NavigationGuard[],
    to: RouteLocationNormalized,
    from: RouteLocationNormalized
  ): Promise<boolean | RouteLocationRaw | void> {
    for (const guard of guards) {
      // 每次迭代检查是否被取消
      if (this.isCancelled()) {
        return false
      }

      const result = await guard(to, from)

      // 返回 false 或 Error 表示中止
      if (result === false || result instanceof Error) {
        return false
      }

      // 返回路由对象表示重定向
      if (typeof result === 'string' || (typeof result === 'object' && result !== null)) {
        return result as RouteLocationRaw
      }
    }
  }

  // ─── 取消机制 ───
  private cancelId = 0

  private triggerCancellation(): void {
    this.cancelId++
  }

  private isCancelled(): boolean {
    // 在真实实现中,通过 AbortController 或 Promise.race 实现
    return false
  }

  // ─── 辅助方法 ───
  private findRouteByName(name: string | symbol): any {
    // 从路由表中查找
    return null
  }

  private findMatchingRecords(path: string): any[] {
    return []
  }

  private fillParams(path: string, params: Record<string, string | string[]>): string {
    return path.replace(/:(\w+)/g, (_, key) => {
      const value = params[key]
      return Array.isArray(value) ? value[0] : String(value)
    })
  }

  private extractParams(path: string, matched: any[]): Record<string, string | string[]> {
    return {}
  }

  private buildFullPath(path: string, query: Record<string, any>, hash: string): string {
    const queryString = Object.keys(query).length
      ? '?' + new URLSearchParams(query).toString()
      : ''
    const hashString = hash ? '#' + hash : ''
    return path + queryString + hashString
  }
}
ts
// Vue Router 4 源码中 NavigationFailure 的生成逻辑

// 1. duplicated:导航到与当前完全相同的路由
function checkDuplicated(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): NavigationFailure | undefined {
  if (to.fullPath === from.fullPath) {
    return createRouterError(
      ErrorTypes.NAVIGATION_DUPLICATED,
      { to, from }
    )
  }
}

// 2. aborted:守卫返回 false 或调用 next(false)
function checkAborted(
  guardResult: any
): NavigationFailure | undefined {
  if (guardResult === false) {
    return createRouterError(
      ErrorTypes.NAVIGATION_ABORTED,
      { to, from }
    )
  }
}

// 3. cancelled:新导航触发时,旧导航被取消
function createCancelledError(
  from: RouteLocationNormalized,
  to: RouteLocationNormalized
): NavigationFailure {
  return createRouterError(
    ErrorTypes.NAVIGATION_CANCELLED,
    { to, from }
  )
}

Promise 返回值的实际行为

ts
// 为什么 router.push 返回 Promise 但有时不等待也能工作?

// 原因:Vue Router 在内部已经启动了导航流程,
// Promise 只是让你能感知导航完成或失败的时机。

// 场景 1:不关心结果——不 await 也没问题
function handleClick() {
  router.push('/dashboard')
  // 导航已启动,组件会正常切换
  // 即使不 await,导航也会完成
}

// 场景 2:需要等待导航完成——必须 await
async function handleLogin() {
  await loginApi()
  await router.push('/dashboard')
  // 此时导航已完成,可以安全地执行后续操作
  fetchUserData() // 组件已挂载,数据获取正常
}

// 场景 3:需要捕获导航失败——必须 await + try/catch
async function handleProtectedNavigation() {
  try {
    await router.push('/admin')
    // 导航成功
  } catch (error) {
    if (isNavigationFailure(error, NavigationFailureType.aborted)) {
      // 权限不足,守卫拦截了导航
      showPermissionDenied()
    }
  }
}

// 场景 4:快速连续导航——旧导航自动取消
async function rapidNavigation() {
  // 不 await 时,两次 push 几乎同时触发
  router.push('/page-a') // 启动导航 A
  router.push('/page-b') // 导航 A 被取消,启动导航 B

  // 如果 await 第一次 push:
  await router.push('/page-a') // 等待导航 A 完成
  router.push('/page-b')       // 然后才启动导航 B
}

重定向与别名的内部实现

redirect 选项的编译时处理

Vue Router 在创建路由时,会对 redirect 选项进行预处理,将其包装为导航守卫。

ts
// Vue Router 4 源码中 redirect 的处理逻辑(简化)

interface RouteRecordRedirect {
  redirect: RouteLocationRaw | ((to: RouteLocationNormalized) => RouteLocationRaw)
}

function normalizeRedirect(
  record: RouteRecordNormalized,
  redirect: RouteRecordRedirect['redirect']
): NavigationGuard {
  if (typeof redirect === 'function') {
    // 函数式重定向:包装为守卫
    const redirectGuard: NavigationGuard = async (to) => {
      const target = await redirect(to)
      return target
    }
    return redirectGuard
  }

  // 静态重定向:直接返回目标
  const redirectGuard: NavigationGuard = () => {
    return redirect as RouteLocationRaw
  }
  return redirectGuard
}

// 在路由匹配阶段,redirect 被注入为 beforeEnter 守卫
function applyRedirect(
  record: RouteRecordNormalized
): void {
  if (record.redirect) {
    const redirectGuard = normalizeRedirect(record, record.redirect)

    // redirect 作为最高优先级的 beforeEnter 守卫
    if (!record.beforeEnter) {
      record.beforeEnter = redirectGuard
    } else {
      const originalGuard = record.beforeEnter
      record.beforeEnter = async (to, from) => {
        const redirectResult = await redirectGuard(to, from)
        // 如果 redirect 返回了路由对象,直接使用
        if (redirectResult && typeof redirectResult !== 'boolean') {
          return redirectResult
        }
        // 否则继续执行原始守卫
        return originalGuard(to, from)
      }
    }
  }
}

alias 的路由记录展开机制

ts
// alias 的处理:为每个别名创建额外的路由记录

interface RouteRecordRaw {
  path: string
  alias?: string | string[]
  // ...
}

function normalizeAlias(
  record: RouteRecordRaw
): RouteRecordRaw[] {
  if (!record.alias) return [record]

  const aliases = Array.isArray(record.alias)
    ? record.alias
    : [record.alias]

  // 为每个别名生成独立的路由记录
  const aliasRecords = aliases.map((alias) => {
    // 别名路径需要规范化
    const normalizedAlias = normalizeAliasPath(alias, record.path)

    return {
      ...record,
      path: normalizedAlias,
      alias: undefined, // 防止递归展开
      // 标记为别名记录,指向原始路由
      meta: {
        ...record.meta,
        __aliasOf: record.path,
      },
    }
  })

  return [record, ...aliasRecords]
}

function normalizeAliasPath(alias: string, originalPath: string): string {
  // 处理绝对路径别名
  if (alias.startsWith('/')) {
    return alias
  }

  // 处理相对路径别名(相对于父路由)
  return joinPath(originalPath, alias)
}

// 别名记录在路由表中独立存在,但共享同一个路由 name
// 当通过别名访问时:
// - route.path 保持别名路径不变
// - route.name 仍为原始路由的 name
// - route.matched 包含原始路由记录

重定向导致的守卫二次执行问题

ts
// 重定向会触发两次完整的导航流程

// 示例:守卫中的重定向检测
let navigationCount = 0

router.beforeEach((to, from) => {
  navigationCount++
  console.log(`第 ${navigationCount} 次守卫执行`)
  console.log('目标:', to.fullPath)
  console.log('来源:', from.fullPath)

  // to.redirectedFrom 仅在第二次导航时有值
  if (to.redirectedFrom) {
    console.log('这是重定向后的导航,原始路径:', to.redirectedFrom.fullPath)
  }
})

// 访问 /old-path(配置了 redirect: '/new-path'):
// 输出:
// 第 1 次守卫执行
// 目标: /old-path
// 来源: /current-page
// 第 2 次守卫执行
// 目标: /new-path
// 来源: /old-path
// 这是重定向后的导航,原始路径: /old-path

// 生产级解决方案:避免守卫逻辑重复执行
function createRedirectAwareGuard(
  guard: NavigationGuard
): NavigationGuard {
  return (to, from) => {
    // 如果是重定向触发的二次导航,跳过某些逻辑
    if (to.redirectedFrom) {
      // 权限检查等逻辑可能已经在第一次导航中执行过
      // 这里可以只执行必要的逻辑
      return true
    }
    return guard(to, from)
  }
}

// 使用
router.beforeEach(
  createRedirectAwareGuard(async (to, from) => {
    // 仅首次导航时执行
    if (to.meta.requiresAuth && !isAuthenticated()) {
      return { path: '/login', query: { redirect: to.fullPath } }
    }
  })
)

别名路由的参数继承规则

ts
// 别名路径中的参数必须与原始路径参数一一对应

// 参数继承验证
function validateAliasParams(
  originalPath: string,
  aliasPath: string
): void {
  const originalParams = extractParamNames(originalPath)
  const aliasParams = extractParamNames(aliasPath)

  // 检查参数名称是否匹配
  const missingParams = originalParams.filter(
    (p) => !aliasParams.includes(p)
  )
  const extraParams = aliasParams.filter(
    (p) => !originalParams.includes(p)
  )

  if (missingParams.length > 0) {
    console.warn(
      `别名 "${aliasPath}" 缺少参数: ${missingParams.join(', ')}`
    )
  }

  if (extraParams.length > 0) {
    console.warn(
      `别名 "${aliasPath}" 包含额外参数: ${extraParams.join(', ')}。` +
      `这些参数在原始路径 "${originalPath}" 中不存在。`
    )
  }
}

function extractParamNames(path: string): string[] {
  const regex = /:(\w+)/g
  const params: string[] = []
  let match: RegExpExecArray | null
  while ((match = regex.exec(path)) !== null) {
    params.push(match[1])
  }
  return params
}

// 参数继承示例
// 原始路径: /users/:userId/posts/:postId
// 别名路径: /u/:userId/p/:postId
// 访问 /u/42/p/100 时:
// - params.userId = '42'  (从别名路径提取)
// - params.postId = '100' (从别名路径提取)
// - route.name 仍为原始路由的 name
// - route.path 保持为 /u/42/p/100

// 可选参数的别名处理
// 原始路径: /products/:id/:name?
// 别名路径: /p/:id/:name?
// 访问 /p/123 时 params.name 为 undefined,正常匹配

生产级导航模式

多页面表单向导(Wizard)

ts
// composables/useWizard.ts
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute, type RouteLocationRaw } from 'vue-router'

interface WizardStep {
  name: string
  path: string
  component: () => Promise<any>
  meta?: {
    title: string
    description?: string
    skipAllowed?: boolean
  }
}

interface WizardConfig {
  id: string
  steps: WizardStep[]
  basePath: string
  completionRoute: RouteLocationRaw
}

export function useWizard(config: WizardConfig) {
  const router = useRouter()
  const route = useRoute()

  const currentStepIndex = ref(0)
  const stepData = ref<Record<string, any>>({})
  const isSubmitting = ref(false)

  // 从当前 URL 推断步骤索引
  const syncStepFromRoute = () => {
    const stepName = route.path.replace(config.basePath + '/', '')
    const idx = config.steps.findIndex((s) => s.path === stepName)
    if (idx !== -1) {
      currentStepIndex.value = idx
    }
  }

  syncStepFromRoute()
  watch(() => route.path, syncStepFromRoute)

  const currentStep = computed(() => config.steps[currentStepIndex.value])
  const isFirstStep = computed(() => currentStepIndex.value === 0)
  const isLastStep = computed(() => currentStepIndex.value === config.steps.length - 1)
  const totalSteps = computed(() => config.steps.length)
  const progress = computed(() =>
    ((currentStepIndex.value + 1) / config.steps.length) * 100
  )

  // 导航到指定步骤
  async function goToStep(index: number): Promise<void> {
    if (index < 0 || index >= config.steps.length) return

    const step = config.steps[index]
    try {
      await router.push(`${config.basePath}/${step.path}`)
      currentStepIndex.value = index
    } catch (error) {
      console.error('向导步骤导航失败:', error)
    }
  }

  async function nextStep(): Promise<void> {
    if (isLastStep.value) {
      await complete()
      return
    }
    await goToStep(currentStepIndex.value + 1)
  }

  async function previousStep(): Promise<void> {
    if (isFirstStep.value) return
    await goToStep(currentStepIndex.value - 1)
  }

  // 保存当前步骤数据
  function saveStepData(data: Record<string, any>): void {
    stepData.value = {
      ...stepData.value,
      [currentStep.value.name]: data,
    }
  }

  // 完成向导
  async function complete(): Promise<void> {
    if (isSubmitting.value) return
    isSubmitting.value = true

    try {
      // 提交所有步骤数据
      await submitWizardData(config.id, stepData.value)

      // 使用 replace 防止回退到向导
      await router.replace(config.completionRoute)
    } catch (error) {
      console.error('向导提交失败:', error)
    } finally {
      isSubmitting.value = false
    }
  }

  // 取消向导
  async function cancel(): Promise<void> {
    // 回退到进入向导前的页面
    if (window.history.length > config.steps.length + 1) {
      router.go(-(currentStepIndex.value + 1))
    } else {
      await router.replace('/')
    }
  }

  return {
    currentStep,
    currentStepIndex,
    isFirstStep,
    isLastStep,
    totalSteps,
    progress,
    stepData,
    isSubmitting,
    goToStep,
    nextStep,
    previousStep,
    saveStepData,
    complete,
    cancel,
  }
}

// 模拟 API
async function submitWizardData(
  wizardId: string,
  data: Record<string, any>
): Promise<void> {
  await fetch(`/api/wizards/${wizardId}/submit`, {
    method: 'POST',
    body: JSON.stringify(data),
  })
}

// ─── 使用示例 ───
// 路由配置
const wizardRoutes = [
  {
    path: '/wizard/checkout',
    component: () => import('@/views/CheckoutWizard.vue'),
    children: [
      { path: 'cart', component: () => import('@/views/wizard/CartStep.vue') },
      { path: 'shipping', component: () => import('@/views/wizard/ShippingStep.vue') },
      { path: 'payment', component: () => import('@/views/wizard/PaymentStep.vue') },
      { path: 'confirm', component: () => import('@/views/wizard/ConfirmStep.vue') },
    ],
  },
]

// 组件中使用
// <script setup lang="ts">
// const wizard = useWizard({
//   id: 'checkout',
//   basePath: '/wizard/checkout',
//   steps: [
//     { name: 'cart', path: 'cart', component: () => import('...') },
//     { name: 'shipping', path: 'shipping', component: () => import('...') },
//     { name: 'payment', path: 'payment', component: () => import('...') },
//     { name: 'confirm', path: 'confirm', component: () => import('...') },
//   ],
//   completionRoute: { name: 'OrderSuccess' },
// })
// </script>

带确认的导航拦截模式

ts
// composables/useNavigationGuard.ts
import { ref, onBeforeUnmount } from 'vue'
import { onBeforeRouteLeave, useRouter, type RouteLocationRaw } from 'vue-router'

interface NavigationConfirmOptions {
  message?: string
  title?: string
  confirmText?: string
  cancelText?: string
}

export function useNavigationGuard(options: NavigationConfirmOptions = {}) {
  const {
    message = '有未保存的更改,确定要离开吗?',
    title = '确认离开',
    confirmText = '确定',
    cancelText = '取消',
  } = options

  const isDirty = ref(false)
  const isConfirming = ref(false)
  const pendingNavigation = ref<RouteLocationRaw | null>(null)
  const router = useRouter()

  // 注册组件内路由守卫
  onBeforeRouteLeave((to, from) => {
    if (!isDirty.value || isConfirming.value) return true

    // 阻止导航,显示确认对话框
    pendingNavigation.value = to
    showConfirmDialog()
    return false
  })

  // 浏览器级别的离开拦截
  function setupBeforeUnload() {
    const handler = (event: BeforeUnloadEvent) => {
      if (isDirty.value) {
        event.preventDefault()
        event.returnValue = message
        return message
      }
    }
    window.addEventListener('beforeunload', handler)
    return () => window.removeEventListener('beforeunload', handler)
  }

  const cleanupBeforeUnload = setupBeforeUnload()
  onBeforeUnmount(cleanupBeforeUnload)

  // 确认离开
  async function confirmLeave(): Promise<void> {
    if (!pendingNavigation.value) return

    isConfirming.value = true
    isDirty.value = false // 先清除 dirty 标记,避免守卫再次拦截

    try {
      await router.push(pendingNavigation.value)
    } catch (error) {
      console.error('导航失败:', error)
      isDirty.value = true // 恢复 dirty 标记
    } finally {
      isConfirming.value = false
      pendingNavigation.value = null
    }
  }

  // 取消离开
  function cancelLeave(): void {
    pendingNavigation.value = null
  }

  // 标记为已保存
  function markClean(): void {
    isDirty.value = false
  }

  // 标记为有更改
  function markDirty(): void {
    isDirty.value = true
  }

  // 自定义确认对话框(可替换为 UI 库的 Modal)
  function showConfirmDialog(): void {
    const confirmed = window.confirm(message)
    if (confirmed) {
      confirmLeave()
    } else {
      cancelLeave()
    }
  }

  return {
    isDirty,
    isConfirming,
    pendingNavigation,
    confirmLeave,
    cancelLeave,
    markClean,
    markDirty,
  }
}

// ─── 使用示例 ───
// <script setup lang="ts">
// const form = reactive({ name: '', email: '' })
// const { isDirty, markDirty, markClean } = useNavigationGuard({
//   message: '表单内容尚未保存,确定要离开吗?',
// })
//
// watch(form, () => markDirty(), { deep: true })
//
// async function handleSave() {
//   await saveForm(form)
//   markClean()
// }
// </script>

导航历史栈管理

ts
// composables/useNavigationHistory.ts
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'

interface HistoryEntry {
  path: string
  fullPath: string
  name?: string | symbol | null
  timestamp: number
}

export function useNavigationHistory(maxEntries = 50) {
  const router = useRouter()
  const route = useRoute()

  const history = ref<HistoryEntry[]>([])
  const canGoBack = ref(false)
  const canGoForward = ref(false)

  // 记录导航历史
  function recordEntry(): void {
    const entry: HistoryEntry = {
      path: route.path,
      fullPath: route.fullPath,
      name: route.name,
      timestamp: Date.now(),
    }

    // 避免重复记录相同路径
    const lastEntry = history.value[history.value.length - 1]
    if (lastEntry && lastEntry.fullPath === entry.fullPath) return

    history.value.push(entry)

    // 限制历史记录数量
    if (history.value.length > maxEntries) {
      history.value = history.value.slice(-maxEntries)
    }

    updateNavigationState()
  }

  function updateNavigationState(): void {
    canGoBack.value = history.value.length > 1
    // forward 状态依赖于浏览器 history API
    canGoForward.value = false // 需要额外逻辑追踪
  }

  // 安全后退:防止无限回退
  function safeGoBack(fallbackPath = '/'): void {
    if (history.value.length > 1) {
      router.back()
    } else {
      router.replace(fallbackPath)
    }
  }

  // 回退到指定路径(在历史栈中查找)
  function goBackTo(targetPath: string): boolean {
    const targetIndex = history.value.findLastIndex(
      (entry) => entry.path === targetPath
    )

    if (targetIndex === -1) return false

    const stepsBack = history.value.length - 1 - targetIndex
    if (stepsBack > 0) {
      router.go(-stepsBack)
      return true
    }

    return false
  }

  // 获取上一个路径
  function getPreviousPath(): string | null {
    if (history.value.length < 2) return null
    return history.value[history.value.length - 2].path
  }

  // 判断是否从某个路径导航而来
  function cameFrom(path: string): boolean {
    return getPreviousPath() === path
  }

  // 清理历史记录(如退出登录时)
  function clearHistory(): void {
    history.value = []
  }

  // 监听路由变化
  onMounted(() => {
    recordEntry()
    router.afterEach(() => {
      recordEntry()
    })
  })

  return {
    history,
    canGoBack,
    canGoForward,
    safeGoBack,
    goBackTo,
    getPreviousPath,
    cameFrom,
    clearHistory,
  }
}

新窗口/新Tab 打开的 URL 构造

ts
// utils/openInNewTab.ts
import type { Router, RouteLocationRaw } from 'vue-router'

interface OpenInNewTabOptions {
  target?: '_blank' | '_self' | '_parent' | '_top'
  features?: string
  focus?: boolean
}

/**
 * 在新窗口或新 Tab 中打开 Vue Router 路由
 * 使用 router.resolve 生成完整 URL,避免手动拼接
 */
export function openInNewTab(
  router: Router,
  location: RouteLocationRaw,
  options: OpenInNewTabOptions = {}
): Window | null {
  const { target = '_blank', features = 'noopener,noreferrer', focus = true } = options

  // 使用 router.resolve 生成完整 URL
  const resolved = router.resolve(location)

  const newWindow = window.open(resolved.href, target, features)

  if (newWindow && focus) {
    newWindow.focus()
  }

  return newWindow
}

/**
 * 生成可分享的完整 URL
 * 适用于复制链接、生成二维码等场景
 */
export function getShareableUrl(
  router: Router,
  location: RouteLocationRaw
): string {
  const resolved = router.resolve(location)
  return window.location.origin + resolved.href
}

/**
 * 在新窗口中打开路由,并传递上下文数据
 * 通过 history.state 传递数据,避免 URL 过长
 */
export function openWithContext(
  router: Router,
  location: RouteLocationRaw,
  context: Record<string, any>
): Window | null {
  // 将上下文数据编码到 state 中
  const locationWithState: RouteLocationRaw = {
    ...(typeof location === 'string' ? { path: location } : location),
    state: {
      ...((typeof location === 'object' && location.state) || {}),
      __context: context,
    },
  }

  return openInNewTab(router, locationWithState)
}

// ─── 使用示例 ───
// import { useRouter } from 'vue-router'
// import { openInNewTab, getShareableUrl } from '@/utils/openInNewTab'
//
// const router = useRouter()
//
// // 在新 Tab 中打开用户详情
// function viewUserInNewTab(userId: number) {
//   openInNewTab(router, {
//     name: 'UserDetail',
//     params: { id: userId },
//     query: { tab: 'profile' },
//   })
// }
//
// // 复制分享链接
// async function copyShareLink(postId: number) {
//   const url = getShareableUrl(router, {
//     name: 'PostDetail',
//     params: { id: postId },
//   })
//   await navigator.clipboard.writeText(url)
// }

router.resolve 深入应用

解析路由但不导航的实战场景

router.resolve 返回完整的路由位置信息而不触发导航,这在许多场景中非常有用。

ts
import { useRouter, type RouteLocationRaw } from 'vue-router'

const router = useRouter()

// router.resolve 返回的类型
interface ResolvedRoute {
  href: string           // 完整路径,如 /users/42?tab=profile#bio
  path: string           // 路径部分,如 /users/42
  query: Record<string, any>  // 查询参数
  params: Record<string, any> // 路径参数
  hash: string           // hash 部分
  fullPath: string       // 等同于 href
  matched: RouteRecordNormalized[]  // 匹配的路由记录
  meta: Record<string, any>        // 合并后的 meta
  name: string | symbol | null | undefined  // 路由名称
}

// 场景 1:预验证路由是否存在
function validateRoute(location: RouteLocationRaw): boolean {
  try {
    const resolved = router.resolve(location)
    return resolved.matched.length > 0
  } catch {
    return false
  }
}

// 场景 2:获取路由的完整信息用于面包屑
function getBreadcrumbForRoute(
  location: RouteLocationRaw
): Array<{ title: string; path: string }> {
  const resolved = router.resolve(location)

  return resolved.matched
    .filter((record) => record.meta?.title)
    .map((record) => ({
      title: record.meta.title as string,
      path: record.path,
    }))
}

// 场景 3:批量预生成静态路径(SSG/预渲染)
function generateStaticPaths(
  routeName: string,
  paramsList: Array<Record<string, string>>
): string[] {
  return paramsList.map((params) => {
    const resolved = router.resolve({ name: routeName, params })
    return resolved.href
  })
}

// 示例:生成所有博客文章的静态路径
const postPaths = generateStaticPaths('PostDetail', [
  { id: '1' },
  { id: '2' },
  { id: '3' },
])
// ['/posts/1', '/posts/2', '/posts/3']

生成可分享的 URL

ts
// composables/useShareableLink.ts
import { useRouter, type RouteLocationRaw } from 'vue-router'
import { computed, ref } from 'vue'

export function useShareableLink(location: RouteLocationRaw) {
  const router = useRouter()
  const isCopied = ref(false)

  const resolved = computed(() => router.resolve(location))

  // 完整 URL(含域名)
  const fullUrl = computed(() => {
    return window.location.origin + resolved.value.href
  })

  // 相对 URL
  const relativeUrl = computed(() => resolved.value.href)

  // 复制到剪贴板
  async function copyToClipboard(): Promise<boolean> {
    try {
      await navigator.clipboard.writeText(fullUrl.value)
      isCopied.value = true
      setTimeout(() => {
        isCopied.value = false
      }, 2000)
      return true
    } catch {
      // 降级方案
      const textarea = document.createElement('textarea')
      textarea.value = fullUrl.value
      textarea.style.position = 'fixed'
      textarea.style.opacity = '0'
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand('copy')
      document.body.removeChild(textarea)
      isCopied.value = true
      setTimeout(() => {
        isCopied.value = false
      }, 2000)
      return true
    }
  }

  // 生成二维码数据
  const qrData = computed(() => fullUrl.value)

  // 分享 API
  async function share(title?: string, text?: string): Promise<boolean> {
    if (!navigator.share) return false

    try {
      await navigator.share({
        title: title || document.title,
        text: text || '',
        url: fullUrl.value,
      })
      return true
    } catch {
      return false
    }
  }

  return {
    fullUrl,
    relativeUrl,
    isCopied,
    copyToClipboard,
    qrData,
    share,
    resolved,
  }
}

// ─── 使用示例 ───
// <script setup lang="ts">
// const { fullUrl, copyToClipboard, isCopied } = useShareableLink({
//   name: 'ProductDetail',
//   params: { id: product.id },
// })
// </script>

在 Worker/Service Worker 中使用路由解析

ts
// workers/routeResolver.worker.ts
// 在 Web Worker 中无法直接使用 Vue Router 实例,
// 但可以复用路由配置进行路径匹配

interface WorkerRouteConfig {
  name: string
  path: string
  children?: WorkerRouteConfig[]
}

// 路由配置(与主线程共享)
const routeConfig: WorkerRouteConfig[] = [
  {
    name: 'Home',
    path: '/',
  },
  {
    name: 'UserDetail',
    path: '/users/:id',
  },
  {
    name: 'PostDetail',
    path: '/posts/:id',
  },
]

// 路径匹配器(Worker 版本)
class WorkerRouteMatcher {
  private routes: Array<{
    config: WorkerRouteConfig
    regex: RegExp
    paramNames: string[]
  }> = []

  constructor(configs: WorkerRouteConfig[]) {
    this.compileRoutes(configs)
  }

  private compileRoutes(configs: WorkerRouteConfig[], prefix = ''): void {
    for (const config of configs) {
      const fullPath = prefix + config.path

      // 将路径转换为正则
      const paramNames: string[] = []
      const regexStr = fullPath
        .replace(/:(\w+)/g, (_, name) => {
          paramNames.push(name)
          return '([^/]+)'
        })
        .replace(/\//g, '\\/')

      const regex = new RegExp(`^${regexStr}$`)

      this.routes.push({ config, regex, paramNames })

      // 递归处理子路由
      if (config.children) {
        this.compileRoutes(config.children, fullPath + '/')
      }
    }
  }

  match(path: string): {
    name: string
    params: Record<string, string>
  } | null {
    for (const { config, regex, paramNames } of this.routes) {
      const match = path.match(regex)
      if (match) {
        const params: Record<string, string> = {}
        paramNames.forEach((name, index) => {
          params[name] = match[index + 1]
        })
        return { name: config.name, params }
      }
    }
    return null
  }
}

const matcher = new WorkerRouteMatcher(routeConfig)

// Worker 消息处理
self.onmessage = (event: MessageEvent<{ type: string; path: string }>) => {
  const { type, path } = event.data

  if (type === 'match') {
    const result = matcher.match(path)
    self.postMessage({ type: 'matchResult', result })
  }

  if (type === 'prefetch') {
    const result = matcher.match(path)
    if (result) {
      // 在 Worker 中预取路由组件所需的数据
      prefetchRouteData(result.name, result.params)
    }
  }
}

async function prefetchRouteData(
  routeName: string,
  params: Record<string, string>
): Promise<void> {
  // 根据路由名称预取对应 API 数据
  const prefetchMap: Record<string, (params: Record<string, string>) => Promise<void>> = {
    UserDetail: async (p) => {
      await fetch(`/api/users/${p.id}`)
    },
    PostDetail: async (p) => {
      await fetch(`/api/posts/${p.id}`)
    },
  }

  const prefetcher = prefetchMap[routeName]
  if (prefetcher) {
    try {
      await prefetcher(params)
      self.postMessage({ type: 'prefetchComplete', routeName })
    } catch (error) {
      self.postMessage({ type: 'prefetchError', routeName, error })
    }
  }
}

// ─── Service Worker 中的路由感知缓存策略 ───
// service-worker.ts
const CACHE_STRATEGIES: Record<string, 'network-first' | 'cache-first'> = {
  Home: 'network-first',
  UserDetail: 'cache-first',
  PostDetail: 'network-first',
}

self.addEventListener('fetch', (event: FetchEvent) => {
  const url = new URL(event.request.url)
  const matched = matcher.match(url.pathname)

  if (matched) {
    const strategy = CACHE_STRATEGIES[matched.name] || 'network-first'

    if (strategy === 'cache-first') {
      event.respondWith(cacheFirstStrategy(event.request))
    } else {
      event.respondWith(networkFirstStrategy(event.request))
    }
  }
})

async function cacheFirstStrategy(request: Request): Promise<Response> {
  const cache = await caches.open('v1')
  const cached = await cache.match(request)
  if (cached) return cached

  const response = await fetch(request)
  cache.put(request, response.clone())
  return response
}

async function networkFirstStrategy(request: Request): Promise<Response> {
  const cache = await caches.open('v1')
  try {
    const response = await fetch(request)
    cache.put(request, response.clone())
    return response
  } catch {
    const cached = await cache.match(request)
    if (cached) return cached
    throw new Error('Network unavailable and no cache found')
  }
}

导航性能优化

高频导航操作的防抖和去重

ts
// composables/useDebouncedNavigation.ts
import { ref } from 'vue'
import { useRouter, type RouteLocationRaw } from 'vue-router'

interface DebouncedNavigateOptions {
  delay?: number
  leading?: boolean
  trailing?: boolean
}

export function useDebouncedNavigation(options: DebouncedNavigateOptions = {}) {
  const { delay = 300, leading = true, trailing = true } = options

  const router = useRouter()
  const isNavigating = ref(false)
  let timer: ReturnType<typeof setTimeout> | null = null
  let lastCallTime = 0
  let pendingLocation: RouteLocationRaw | null = null

  // 防抖导航
  async function debouncedPush(
    location: RouteLocationRaw
  ): Promise<void> {
    const now = Date.now()
    const elapsed = now - lastCallTime

    // leading edge:立即执行
    if (leading && elapsed > delay) {
      lastCallTime = now
      await executeNavigation(location)
      return
    }

    // trailing edge:延迟执行
    pendingLocation = location

    if (timer) clearTimeout(timer)

    return new Promise((resolve, reject) => {
      timer = setTimeout(async () => {
        if (trailing && pendingLocation) {
          lastCallTime = Date.now()
          try {
            await executeNavigation(pendingLocation)
            resolve()
          } catch (error) {
            reject(error)
          }
        }
        pendingLocation = null
        timer = null
      }, delay - elapsed)
    })
  }

  async function executeNavigation(location: RouteLocationRaw): Promise<void> {
    if (isNavigating.value) return
    isNavigating.value = true
    try {
      await router.push(location)
    } finally {
      isNavigating.value = false
    }
  }

  // 去重导航:相同目标不重复导航
  function createDeduplicatedPush() {
    let lastResolvedPath = ''

    return async function deduplicatedPush(
      location: RouteLocationRaw
    ): Promise<void> {
      const resolved = router.resolve(location)

      if (resolved.fullPath === lastResolvedPath) {
        return // 跳过重复导航
      }

      lastResolvedPath = resolved.fullPath
      await router.push(location)
    }
  }

  // 清理
  function cleanup(): void {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
    pendingLocation = null
  }

  return {
    debouncedPush,
    createDeduplicatedPush,
    isNavigating,
    cleanup,
  }
}

// ─── 使用示例 ───
// 搜索框输入防抖导航
// const { debouncedPush } = useDebouncedNavigation({ delay: 500 })
//
// watch(searchQuery, (value) => {
//   debouncedPush({ query: { q: value } })
// })

router.replace vs router.push 的选择策略

ts
// utils/navigationStrategy.ts
import type { Router, RouteLocationRaw } from 'vue-router'

/**
 * 导航策略选择指南
 *
 * router.push:
 * - 向 history 栈添加新条目
 * - 用户点击后退按钮时回到上一页
 * - 适用场景:正常页面跳转、列表到详情、菜单导航
 *
 * router.replace:
 * - 替换当前 history 条目
 * - 用户点击后退按钮时跳过当前页
 * - 适用场景:登录后跳转、表单提交后跳转、错误页跳转、筛选参数更新
 */

interface SmartNavigateOptions {
  /** 是否替换当前历史记录 */
  replace?: boolean
  /** 如果目标与当前路径相同,是否跳过 */
  skipIfSame?: boolean
}

export function createSmartNavigator(router: Router) {
  /**
   * 智能导航:根据场景自动选择 push 或 replace
   */
  async function smartNavigate(
    location: RouteLocationRaw,
    options: SmartNavigateOptions = {}
  ): Promise<void> {
    const { replace = false, skipIfSame = true } = options

    const resolved = router.resolve(location)

    // 跳过相同路径的导航
    if (skipIfSame && resolved.fullPath === router.currentRoute.value.fullPath) {
      return
    }

    if (replace) {
      await router.replace(location)
    } else {
      await router.push(location)
    }
  }

  /**
   * 场景化导航方法
   */
  return {
    smartNavigate,

    // 登录后跳转(必须 replace,防止回退到登录页)
    async navigateAfterLogin(location: RouteLocationRaw): Promise<void> {
      await router.replace(location)
    },

    // 表单提交后跳转(replace,防止重复提交)
    async navigateAfterSubmit(location: RouteLocationRaw): Promise<void> {
      await router.replace(location)
    },

    // 筛选/排序参数更新(replace,避免产生大量历史记录)
    async navigateWithFilters(query: Record<string, any>): Promise<void> {
      await router.replace({
        query: {
          ...router.currentRoute.value.query,
          ...query,
        },
      })
    },

    // 正常页面跳转(push,保留浏览历史)
    async navigateToPage(location: RouteLocationRaw): Promise<void> {
      await router.push(location)
    },

    // Tab 切换(replace,避免每个 Tab 都产生历史记录)
    async navigateToTab(tabName: string): Promise<void> {
      await router.replace({
        query: {
          ...router.currentRoute.value.query,
          tab: tabName,
        },
      })
    },

    // 错误页跳转(replace,不保留错误页在历史中)
    async navigateToError(
      errorCode: number,
      message?: string
    ): Promise<void> {
      await router.replace({
        name: 'Error',
        query: {
          code: String(errorCode),
          ...(message ? { message } : {}),
        },
      })
    },
  }
}

// ─── 性能对比 ───
// push 和 replace 在性能上的差异主要在于浏览器 History API:
//
// pushState:
// - 创建新的 history 条目
// - 内存占用略高(history.length 增加)
// - 不影响现有条目
//
// replaceState:
// - 修改当前 history 条目
// - 内存占用不变
// - 适用于频繁更新的参数(筛选、分页等)
//
// 推荐策略:
// - 用户主动跳转 → push
// - 参数/状态更新 → replace
// - 登录/提交后 → replace
// - 错误/重定向 → replace

// ─── 使用示例 ───
// const { smartNavigate, navigateAfterLogin, navigateWithFilters } =
//   createSmartNavigator(router)
//
// // 登录成功
// await navigateAfterLogin({ name: 'Dashboard' })
//
// // 更新筛选条件
// await navigateWithFilters({ page: '2', sort: 'date' })
//
// // 正常跳转
// await smartNavigate({ name: 'UserDetail', params: { id: '42' } })

导航触发的组件渲染次数控制

ts
// composables/useNavigationOptimizer.ts
import { watch, onBeforeUnmount, type Ref } from 'vue'
import { useRoute, useRouter, type RouteLocationRaw } from 'vue-router'

/**
 * 导航渲染优化器
 *
 * 问题:快速连续导航时,每次导航都会触发组件销毁和重建,
 * 导致不必要的渲染开销。
 *
 * 解决方案:
 * 1. 批量导航:收集多次导航请求,只执行最后一次
 * 2. 导航合并:将连续的 push 合并为一次
 * 3. 条件跳过:避免导航到相同路由
 */
export function useNavigationOptimizer() {
  const router = useRouter()
  const route = useRoute()

  // ─── 方案 1:微任务批量导航 ───
  let pendingNavigation: RouteLocationRaw | null = null
  let batchScheduled = false

  function batchedPush(location: RouteLocationRaw): void {
    pendingNavigation = location

    if (!batchScheduled) {
      batchScheduled = true
      // 使用微任务,在当前事件循环结束时执行
      Promise.resolve().then(async () => {
        batchScheduled = false
        if (pendingNavigation) {
          const target = pendingNavigation
          pendingNavigation = null
          await router.push(target)
        }
      })
    }
  }

  // ─── 方案 2:导航请求去重 ───
  const navigationCache = new Map<string, Promise<void | NavigationFailure>>()

  async function deduplicatedPush(
    location: RouteLocationRaw
  ): Promise<void | NavigationFailure> {
    const resolved = router.resolve(location)
    const key = resolved.fullPath

    // 如果已有相同目标的进行中导航,复用 Promise
    const existing = navigationCache.get(key)
    if (existing) {
      return existing
    }

    const promise = router.push(location).finally(() => {
      navigationCache.delete(key)
    })

    navigationCache.set(key, promise)
    return promise
  }

  // ─── 方案 3:条件导航(避免不必要的渲染) ───
  function shouldNavigate(location: RouteLocationRaw): boolean {
    const resolved = router.resolve(location)

    // 相同路径不导航
    if (resolved.fullPath === route.fullPath) {
      return false
    }

    // 相同路由名 + 相同参数不导航
    if (
      resolved.name &&
      resolved.name === route.name &&
      JSON.stringify(resolved.params) === JSON.stringify(route.params)
    ) {
      // 但 query 或 hash 不同时需要导航
      if (
        JSON.stringify(resolved.query) === JSON.stringify(route.query) &&
        resolved.hash === route.hash
      ) {
        return false
      }
    }

    return true
  }

  async function optimizedPush(
    location: RouteLocationRaw
  ): Promise<void> {
    if (!shouldNavigate(location)) return
    await router.push(location)
  }

  // ─── 方案 4:导航节流(限制导航频率) ───
  function createThrottledNavigator(intervalMs = 200) {
    let lastNavigationTime = 0
    let pendingLocation: RouteLocationRaw | null = null
    let throttleTimer: ReturnType<typeof setTimeout> | null = null

    return async function throttledPush(
      location: RouteLocationRaw
    ): Promise<void> {
      const now = Date.now()
      const elapsed = now - lastNavigationTime

      if (elapsed >= intervalMs) {
        lastNavigationTime = now
        await router.push(location)
      } else {
        // 保存最新的导航请求
        pendingLocation = location

        if (!throttleTimer) {
          throttleTimer = setTimeout(async () => {
            throttleTimer = null
            lastNavigationTime = Date.now()
            if (pendingLocation) {
              const target = pendingLocation
              pendingLocation = null
              await router.push(target)
            }
          }, intervalMs - elapsed)
        }
      }
    }
  }

  // 清理
  onBeforeUnmount(() => {
    navigationCache.clear()
  })

  return {
    batchedPush,
    deduplicatedPush,
    optimizedPush,
    shouldNavigate,
    createThrottledNavigator,
  }
}

// ─── 方案 5:使用 keep-alive 减少重复渲染 ───
// 在路由配置层面配合 keep-alive 使用
//
// <template>
//   <router-view v-slot="{ Component, route }">
//     <keep-alive :include="cachedRoutes">
//       <component :is="Component" :key="route.fullPath" />
//     </keep-alive>
//   </router-view>
// </template>
//
// <script setup lang="ts">
// const cachedRoutes = ['UserList', 'ProductList']
// </script>

// ─── 导航性能监控 ───
export function useNavigationPerformance() {
  const router = useRouter()
  const navigationTimings: Array<{
    from: string
    to: string
    duration: number
    timestamp: number
  }> = []

  let navigationStart = 0

  router.beforeEach(() => {
    navigationStart = performance.now()
  })

  router.afterEach((to, from) => {
    const duration = performance.now() - navigationStart
    navigationTimings.push({
      from: from.fullPath,
      to: to.fullPath,
      duration: Math.round(duration),
      timestamp: Date.now(),
    })

    // 保留最近 100 条记录
    if (navigationTimings.length > 100) {
      navigationTimings.shift()
    }

    // 慢导航告警
    if (duration > 500) {
      console.warn(
        `慢导航告警: ${from.fullPath} → ${to.fullPath} 耗时 ${duration.toFixed(0)}ms`
      )
    }
  })

  function getAverageNavigationTime(): number {
    if (navigationTimings.length === 0) return 0
    const sum = navigationTimings.reduce((acc, t) => acc + t.duration, 0)
    return Math.round(sum / navigationTimings.length)
  }

  function getSlowNavigations(threshold = 500): typeof navigationTimings {
    return navigationTimings.filter((t) => t.duration > threshold)
  }

  return {
    navigationTimings,
    getAverageNavigationTime,
    getSlowNavigations,
  }
}

下一步