ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue Router 程序化导航完全指南:router.push / replace / go 与 History 操作原理

Vue Router 程序化导航完全指南:router.push / replace / go 与 History 操作原理 Vue Router 程序化导航完全指南router.push / replace / go 与 History 操作原理【免费下载链接】vue-router The official router for Vue 2项目地址: https://gitcode.com/gh_mirrors/vu/vue-router在 Vue 2 项目中router-link提供了声明式的页面跳转能力但在表单提交、登录鉴权、业务回调等场景下我们往往需要在 JavaScript 代码中主动控制路由跳转。本指南基于 vue-router 官方文档中的程序化导航章节系统讲解router.push、router.replace、router.go三大实例方法的完整用法与参数细节并结合仓库源码src/router.js、src/history/等剖析其底层调用链与三种路由模式history / hash / abstract下的实现差异。读完本文你将掌握在 Vue 组件内外进行精确导航、替换历史记录、前进后退以及处理导航回调与异步流程的完整实战能力。一、声明式导航与程序化导航vue-router 提供两种导航方式声明式Declarative在模板中使用router-link :to...渲染锚点标签由组件自身处理点击事件程序化Programmatic调用路由实例的方法push/replace/go在代码中触发导航。两种方式完全等价官方文档给出了如下对照表声明式程序式router-link :to...router.push(...)router-link :to... replacerouter.replace(...)这并非简单的殊途同归而是有源码层面的事实依据在 src/components/link.js 中router-link组件的点击处理函数handler内部正是调用router.push(location, noop)或router.replace(location, noop)取决于是否设置了replace属性const handler e { if (guardEvent(e)) { if (this.replace) { router.replace(location, noop) } else { router.push(location, noop) } } }也就是说点击router-link :to...本质就是执行了一次router.push(...)。两者接受相同的 location 描述字符串路径或描述对象并且router-link的to属性与router.push的第一个参数遵循完全相同的规则。在 Vue 实例中访问路由实例在 Vue 组件内部路由实例通过$router暴露因此组件内可以写this.$router.push(...)。这一注入机制定义在 src/install.js 中Object.defineProperty(Vue.prototype, $router, { get () { return this._routerRoot._router } })Vue.prototype.$router的 getter 返回根实例上的_router所有组件实例都能直接访问。同理this.$route指向当前路由对象二者是配套使用的高频 API。二、router.push向 history 栈压入新记录2.1 方法签名router.push(location, onComplete?, onAbort?)router.push用于跳转到不同的 URL。它会在 history 栈中压入一条新记录因此用户点击浏览器后退按钮时可以回到之前的 URL——这是它区别于replace的核心语义。location参数可以是一个字符串路径也可以是一个描述目标地址的对象// 字符串路径 router.push(home) // 对象形式 router.push({ path: home }) // 命名路由named route router.push({ name: user, params: { userId: 123 } }) // 携带 query结果形如 /register?planprivate router.push({ path: register, query: { plan: private } })2.2 命名路由与 params使用命名路由时params会填充到路由模式的动态段中const userId 123 router.push({ name: user, params: { userId } }) // - /user/123这里的命名路由要求路由配置中为对应记录声明了name例如const router new VueRouter({ routes: [ { path: /user/:id, name: user, component: User } ] })2.3 关键陷阱path 与 params 不可混用官方文档特别强调当提供了path时params会被忽略query不受此限制。因此下面这种写法不会得到期望的 URL// 这不会生效path 存在时 params 被忽略结果仍是 /user router.push({ path: /user, params: { userId } }) // - /user正确的两种替代方案是// 方案一使用 name params router.push({ name: user, params: { userId } }) // - /user/123 // 方案二把参数直接拼进 path router.push({ path: /user/${userId} }) // - /user/123这一行为可以从源码层面得到印证。在 src/util/location.js 的normalizeLocation中只有没有 path 且提供了 params时才会走相对参数合并逻辑if (!next.path next.params current)一旦对象中带path最终只会按path解析params不会参与路径拼接。同样的规则也适用于router-link的to属性。2.4 onComplete / onAbort 回调2.2.0自 2.2.0 起router.push与router.replace可以接收第二个和第三个参数onComplete在导航成功完成所有异步守卫钩子均已解析之后调用onAbort在导航被中止时调用。中止的典型场景包括导航到与当前相同的路由重复导航、当前导航尚未结束时又发起了新的导航。从 src/router.js 的实现可以看到push只是把参数透传给对应的 history 实例push (location: RawLocation, onComplete?: Function, onAbort?: Function) { if (!onComplete !onAbort typeof Promise ! undefined) { return new Promise((resolve, reject) { this.history.push(location, resolve, reject) }) } else { this.history.push(location, onComplete, onAbort) } }2.5 Promise 支持3.1.0从 3.1.0 开始如果同时省略第二个和第三个参数并且运行环境支持 Promiserouter.push/router.replace会返回一个 Promise成功时 resolve 出当前路由对象失败如重复导航、被守卫拦截时 reject 出导航失败信息。例如router.push(/foo) .then(route { /* 导航成功 */ }) .catch(err { /* 导航失败或被中止 */ })单元测试 test/unit/specs/api.spec.js 对回调与 Promise 两种形态都做了覆盖验证包括传入回调时返回值是undefined不返回 Promisepush complete/push abort/replace complete/replace abort等场景下守卫钩子的执行顺序与回调触发情况Promise 形态下.then收到router.currentRoute、.catch收到中止错误。例如测试中通过连续两次push制造前一次导航未完成就被新导航覆盖的场景验证第一次的onAbortspy2被调用而onCompletespy1未被调用——这正是onAbort的典型触发路径。2.6 参数变化时的组件复用问题注意如果目标地址与当前路由相同只有 params 发生变化例如从/users/1跳到/users/2同一个组件实例会被复用组件的生命周期钩子不会重新触发。此时必须使用beforeRouteUpdate或 watch$route来响应变化例如重新拉取用户信息const User { template: ..., beforeRouteUpdate(to, from, next) { // 响应 /users/1 - /users/2 的参数变化 // 别忘了调用 next() next() } }该守卫的详细说明参见 动态路由匹配文档官方文档中给出的 watch 方案如下const User { template: ..., watch: { $route(to, from) { // 响应路由变化…… } } }三、router.replace替换当前 history 记录router.replace(location, onComplete?, onAbort?)router.replace的行为与router.push几乎一致唯一区别是不往 history 栈中新增记录而是替换当前的记录。因此跳转后用户点击浏览器后退不会回到被替换前的那个页面。对应到声明式写法则是给router-link加上replace属性声明式程序式router-link :to... replacerouter.replace(...)在实际业务中replace最常见的应用是登录后跳转登录成功页面本身不应留在历史记录中。仓库示例 examples/auth-flow/components/Login.vue 就是典型用法this.$router.replace(this.$route.query.redirect || /)从源码看replace与push的底层差异体现在 URL 的写入方式上。以 HTML5 History 模式为例src/history/html5.js 中push成功回调里调用pushState(...)而replace调用的是replaceState(...)——与浏览器 History API 一一对应。四、router.go在 history 栈中前进后退router.go(n)router.go接收一个整数参数表示在 history 栈中前进正数或后退负数多少步语义与window.history.go(n)一致// 前进一条记录等价于 history.forward() router.go(1) // 后退一条记录等价于 history.back() router.go(-1) // 前进 3 条记录 router.go(3) // 如果栈中没有足够多的记录则静默失败无任何效果 router.go(-100) router.go(100)在 src/router.js 中还有两个便捷方法back()与forward()内部就是go(-1)与go(1)go (n: number) { this.history.go(n) } back () { this.go(-1) } forward () { this.go(1) }关于越界静默失败的行为不同模式有各自的实现浏览器环境中HTML5History.go与HashHistory.go直接委托给window.history.go(n)越界时浏览器本身不会抛错而在abstract 模式下src/history/abstract.js 用内存数组模拟历史栈go会先检查目标索引是否越界越界直接returngo (n: number) { const targetIndex this.index n if (targetIndex 0 || targetIndex this.stack.length) { return } // ...confirmTransition 到目标路由 }五、History 操作与三种路由模式细心的读者会发现router.push、router.replace、router.go与浏览器原生 API 存在清晰的对应关系vue-router 方法window.history API 对应router.pushwindow.history.pushStaterouter.replacewindow.history.replaceStaterouter.gowindow.history.govue-router 正是有意模仿window.historyAPI设计了这三个方法。因此熟悉 Browser History API 的开发者可以无缝上手 vue-router 的历史操作。官方文档特别强调push、replace、go三个导航方法在全部三种路由模式history、hash、abstract下都保持一致的 API 行为。从仓库源码可以得到具体印证history 模式push/replace通过pushState/replaceState修改 URL并监听popstate事件响应浏览器前进后退hash 模式URL 写入改为pushHash/replaceHash内部根据是否支持pushState选择pushState或直接改window.location.hash事件监听则在支持pushState时用popstate、否则回退到hashchangeabstract 模式不依赖浏览器 URL用内存中的stack数组 index指针模拟完整的历史栈push会截断后续记录再追加新记录push (location, onComplete?, onAbort?) { this.transitionTo(location, route { this.stack this.stack.slice(0, this.index 1).concat(route) this.index onComplete onComplete(route) }, onAbort) }abstract 模式通常用于非浏览器环境如 Node.js 服务端渲染或单元测试src/router.js 中通过!inBrowser自动切换到该模式。三种模式的选择与回退逻辑同样在VueRouter构造函数中默认mode为hashhistory模式在浏览器不支持pushState且未显式关闭fallback时会自动回退为hash非浏览器环境强制使用abstract见 src/router.js。六、源码级原理一次 push 的完整调用链为了更深入地理解程序化导航这里梳理一次router.push(/foo)的完整底层链路入口VueRouter.prototype.pushsrc/router.js判断是否返回 Promise随后调用this.history.push(location, onComplete, onAbort)location 归一化history 的push内部调用transitionTo(location, ...)后者首先执行this.router.match(location, this.current)match内部又通过normalizeLocationsrc/util/location.js把字符串路径或描述对象解析为规范化的Location包含path、query、hash、name、params等字段再交给 matcher 匹配出目标Route守卫流水线confirmTransitionsrc/history/base.js按固定顺序执行导航守卫队列组件内beforeRouteLeave→ 全局beforeEach→ 组件内beforeRouteUpdate→ 路由配置的beforeEnter→ 异步组件解析 → 组件内beforeRouteEnter→ 全局beforeResolve。任何一步通过next(false)或next(Error)中止导航都会走abort分支触发onAbort重复导航检测如果目标路由与当前路由相同isSameRoute判定且匹配记录未变confirmTransition会直接以重复导航失败结束对应 src/util/errors.js 中的NavigationFailureType.duplicated即NavigationDuplicated错误——这也是 Promise 形态下常见的 reject 原因之一成功收尾全部守卫通过后执行onComplete由具体的 history 实现写入 URLpushState/replaceHash等、触发滚动行为处理、调用afterEach钩子并通知所有应用实例更新_route。值得注意的是导航失败重复、中止、取消、重定向都被建模为带有type字段的错误对象见 src/util/errors.js 中的NavigationFailureTyperedirected/aborted/cancelled/duplicated并可通过VueRouter.isNavigationFailure静态方法进行类型判断配合onAbort回调或 Promise 的catch做精细化处理。七、实战建议与最佳实践7.1 组件内导航组件内统一使用this.$routerexport default { methods: { goToUser(id) { this.$router.push({ name: user, params: { userId: id } }) }, submitAndReplace() { this.$router.replace({ path: /result, query: { ok: 1 } }) }, goBack() { this.$router.go(-1) } } }7.2 携带 query 的导航query会序列化到 URL 中如/register?planprivate适合传递无需保存在路径中的临时参数。仓库示例 examples/composables/app.js 展示了用对象形式合并更新 queryrouter.push({ query: { n: 1 (Number(route.query.n) || 0) } })7.3 登录跳转用 replace登录/授权类跳转建议使用replace避免登录成功页残留在历史记录中导致用户按后退又回到登录页参考 examples/auth-flow/components/Login.vue。7.4 配合导航守卫与动态匹配参数变化场景/users/1→/users/2使用beforeRouteUpdate或 watch$route响应详见 动态路由匹配文档更复杂的全局拦截、权限校验逻辑可结合 导航守卫文档 中的beforeEach/beforeResolve/afterEach使用完整的方法签名、返回值等 API 细节可查阅 API 参考。7.5 异步流程中的错误处理在异步函数中优先使用 Promise 形态配合isNavigationFailure区分被守卫中止与其他错误避免把正常的NavigationDuplicated当成异常抛出async function safePush(to) { try { await router.push(to) } catch (err) { if (VueRouter.isNavigationFailure(err)) { // 重复导航、被守卫中止等可接受的失败按需处理 } else { // 真正的异常 throw err } } }结语程序化导航是 vue-router 日常开发中使用频率最高的 API 之一。理解push压栈、replace替换当前记录、go步进历史栈三者的语义差异掌握path与params的混用陷阱、回调与 Promise 两种异步形态并透过 src/router.js 与 src/history/ 的实现看清其在 history / hash / abstract 三种模式下的一致性你就能在任何导航场景下写出准确、健壮的代码。【免费下载链接】vue-router The official router for Vue 2项目地址: https://gitcode.com/gh_mirrors/vu/vue-router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进