03|编程式导航与失败结果
第六章概览 / 03。目标:把
router.push()当作异步导航事务的入口,等待其结果后再提交 UI 后续动作。
三类 history 操作
const router = useRouter()
await router.push({ name: 'order', params: { id: '42' } }) // 新增记录
await router.replace({ name: 'login' }) // 替换当前记录
router.go(-1) // 在现有栈移动
router.back()
router.forward()2
3
4
5
6
7
push 类似新增浏览器 history entry,用户按返回可回到旧页。replace 不增加 entry,适合登录回跳后的临时页、规范化无效 query 等“不应回到”的位置。go/back/forward 的结果取决于已有栈;它们不返回可等待的导航 Promise。
await router.push() 的分支
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
const failure = await router.push({ name: 'settings' })
if (!failure) {
closeMenu() // 已到达目标,重定向也已完成
} else if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
showMessage('导航被守卫阻止')
} else if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
// 在本次完成前出现了更新的导航,通常无需打扰用户
} else if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// 已在目标位置;按产品需要当作无操作
}2
3
4
5
6
7
8
9
10
11
12
13
成功(包括最终完成的重定向)通常 resolve 为 falsy;守卫返回 false、被更新导航抢先或重复导航会 resolve 为 NavigationFailure。守卫抛错、懒加载 import 拒绝等意外错误会 reject,因此关键流程还要 try/catch/finally。
async function saveAndLeave() {
saving.value = true
try {
await saveDraft()
const failure = await router.push({ name: 'documents' })
if (failure) return { ok: false, reason: 'navigation' as const }
return { ok: true as const }
} catch (error) {
report(error)
return { ok: false, reason: 'exception' as const }
} finally {
saving.value = false
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
只有导航确认成功才关闭依赖当前页面的 modal/store 状态;否则 leave guard 阻止时,页面仍需要这些状态。
实验:正在等待不等于已经到达
下面程序使用已安装的 Vue Router,在 Node 中执行 node navigation-race.mjs。它没有浏览器窗口,只验证导航确认与当前位置;beforeEach 在这里只是一个可暂停的前置检查,完整守卫职责在第07篇展开。
import assert from 'node:assert/strict'
import {
createRouter, createMemoryHistory,
isNavigationFailure, NavigationFailureType
} from 'vue-router'
const Empty = { render: () => null }
const router = createRouter({
history: createMemoryHistory(),
routes: ['/', '/slow', '/fast', '/blocked'].map(path => ({ path, component: Empty }))
})
let releaseSlow
let markEntered
const entered = new Promise(resolve => { markEntered = resolve })
const gate = new Promise(resolve => { releaseSlow = resolve })
const trace = []
router.beforeEach(async to => {
if (to.path === '/blocked') return false
if (to.path === '/slow') {
markEntered()
await gate
trace.push('slow-check-finished')
}
})
await router.push('/')
const slow = router.push('/slow')
await entered // 确认检查已开始,不依赖猜测延迟
assert.equal(router.currentRoute.value.path, '/')
assert.equal(await router.push('/fast'), undefined)
assert.equal(router.currentRoute.value.path, '/fast')
releaseSlow()
assert.equal(isNavigationFailure(await slow, NavigationFailureType.cancelled), true)
assert.deepEqual(trace, ['slow-check-finished'])
assert.equal(router.currentRoute.value.path, '/fast')
const blocked = await router.push('/blocked')
assert.equal(isNavigationFailure(blocked, NavigationFailureType.aborted), true)
const duplicate = await router.push('/fast')
assert.equal(isNavigationFailure(duplicate, NavigationFailureType.duplicated), true)
console.log('navigation decisions passed')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
时间线上有三个位置:已确认的 /、待定的 /slow、后来确认的 /fast。新导航确认后,slow的检查仍能运行完,说明取消导航没有替你取消任意外部工作;Router只阻止旧目标成为当前页面。若旧检查里直接改store,写入仍可能发生,所以外部副作用也要使用第五章的会话/请求身份保护。
将最后一次 /fast 改成 /fast?page=2,它就不再是同一个规范位置;query也是位置的一部分。再将blocked分支的 return false 改为 throw new Error('check failed'),调用方必须catch拒绝,不能继续只检查failure对象。两种失败都让页面留在原处,但它们代表用户决策与意外故障,反馈策略不同。
位置对象比字符串拼接可靠
await router.push({
name: 'search',
query: { q: keyword, page: '1' },
hash: '#results',
})2
3
4
5
命名位置由 Router 编码 params 并生成 href。若同时给 path 和 params,params 会被忽略;要么使用 name+params,要么自己把参数编码进 path。
在执行前可用 router.resolve(location) 预览 href,但 resolve 不运行守卫,也不保证懒加载组件能成功下载。
全局观察结果
router.afterEach((to, from, failure) => {
if (failure) recordNavigationFailure(to, from, failure)
else recordPageView(to.fullPath)
})
router.onError((error, to) => {
reportNavigationException(error, to)
})2
3
4
5
6
7
8
afterEach 不能改变导航,只适合埋点、标题和失败记录;第三个参数区分失败。抛出的错误走 onError/Promise rejection,不应与“用户取消”混成错误告警。
失败路径
- 调用
router.push()后立即清表单:守卫取消时用户数据已经丢失。 - 到处 catch 并吞掉异常:chunk 加载失败变成无提示白屏。
- 登录页每次都
push:返回按钮又回到受保护页,再次重定向;登录重定向通常用 replace 或明确回跳策略。 - 用
window.history.replaceState({}, ...)覆盖 Router state:会丢失 Router 保存的滚动和导航信息;需要自定义 state 时保留现有history.state,优先用 Router API。 - 把 duplicated 当严重错误:很多按钮重复点击只需要无操作,但应由产品合同决定。
练习:提交后导航
实现 submitAndOpen(id):先调用 save(),成功后 push 到命名路由 detail;导航成功才清草稿。aborted 时保留草稿并返回 blocked,cancelled/duplicated 分别返回明确结果,异常返回 error;loading 必须在所有分支恢复为 false。
变化题:登录成功从 /login?redirect=/orders/42 回跳时使用 replace,并拒绝跨 origin 的 redirect 字符串。
参考答案:第 03 题。
本页结论
push/replace 决定 history,守卫决定是否提交,Promise 告诉调用方结果。先等待导航,再清理当前页面状态。