编程式导航
除了使用
<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.99router.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) | RouteLocationRaw | Promise | 导航并添加历史记录 |
replace(location) | RouteLocationRaw | Promise | 导航并替换历史记录 |
go(delta) | number | void | 前进/后退指定步数 |
forward() | - | void | 前进一步 |
back() | - | void | 后退一步 |
beforeEach(guard) | NavigationGuard | () => void | 注册全局前置守卫 |
beforeResolve(guard) | NavigationGuard | () => void | 注册全局解析守卫 |
afterEach(hook) | NavigationHook | () => void | 注册全局后置钩子 |
addRoute(route) | RouteRecord | () => void | 动态添加路由 |
removeRoute(name) | string | symbol | void | 动态移除路由 |
hasRoute(name) | string | symbol | boolean | 检查路由是否存在 |
getRoutes() | - | RouteRecord[] | 获取所有路由 |
resolve(location) | RouteLocationRaw | RouteLocation | 解析路由地址 |
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 导航核心的简化实现,帮助你理解 push → resolve → finalizeNavigation 的完整链路。
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
}
}NavigationFailure 生成机制
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,
}
}下一步
- 路由守卫 - 学习路由守卫