Vue Router 组件注入机制详解:$router、$route 与组件内导航守卫
【免费下载链接】vue-router🚦 The official router for Vue 2项目地址: https://gitcode.com/gh_mirrors/vu/vue-router
导读
本文以 vue-router 官方文档的「コンポーネント注入(组件注入)」章节为主体,深入剖析 Vue 2 应用中路由能力向组件树渗透的完整机制:$router与$route两个注入属性的来源与用法,以及beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave三个组件内导航守卫的触发时机与边界。读完本文,你将理解这些 API 的底层实现原理(涉及 src/install.js、src/history/base.js 等核心文件),并能在实际项目中正确运用它们完成权限控制、离开确认、动态参数响应等实战场景。
一、注入机制的起点:把 router 选项交给根实例
在 Vue 2 中使用 vue-router,第一步是把路由器实例作为router选项传给根 Vue 实例:
import Vue from 'vue' import VueRouter from 'vue-router' import App from './App.vue' Vue.use(VueRouter) const router = new VueRouter({ routes: [/* ... */] }) new Vue({ router, // <- 关键:根实例的 router 选项 render: h => h(App) }).$mount('#app')一旦根实例携带router选项创建,vue-router 便会向所有子组件注入两个属性:$router和$route。这正是官方文档「コンポーネント注入」章节的核心结论:注入不是逐组件手动完成的,而是通过全局 mixin 与原型扩展自动进行的。
从源码看,注入逻辑集中在 src/install.js:
Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } })这段代码揭示了三个关键点:
- **根实例(携带
router选项)**在beforeCreate阶段把自己记为_routerRoot,并调用_router.init(this)启动路由;其余组件则沿着$parent链向上找到最近的_routerRoot。 $router与$route被定义为Vue.prototype上的 getter,因此任意深度、任意位置的组件都能通过this.$router/this.$route访问——这正是"注入到所有子组件"的实现方式。_route通过Vue.util.defineReactive定义为响应式属性,当路由变化时(src/router.js 中history.listen会为每个 app 更新app._route),依赖$route的组件会自动重渲染。
二、$router:全局路由器实例
2.1 是什么
$router是路由器实例本身,即new VueRouter({...})创建出来的那个对象。它在所有组件中都是同一个引用,因此你可以在任何组件内调用路由器的方法来控制导航、注册全局钩子。
2.2 常用成员
$router的完整 API 由 src/router.js 与官方 API 文档(docs-gitbook/ja/api/router-instance.md)定义,常用成员包括:
| 成员 | 说明 |
|---|---|
$router.push(location) | 导航到新 URL,向历史栈添加一条记录 |
$router.replace(location) | 导航到新 URL,替换当前历史记录 |
$router.go(n)/back()/forward() | 按步数前进/后退历史 |
$router.beforeEach(fn) | 注册全局前置守卫 |
$router.beforeResolve(fn) | 注册全局解析守卫(2.5.0+) |
$router.afterEach(fn) | 注册全局后置钩子 |
$router.onReady(cb, errorCb) | 首次导航完成时回调 |
$router.onError(cb) | 导航出错时回调 |
$router.currentRoute | 当前路由对象的只读 getter |
$router.addRoute(...)/getRoutes() | 动态增删路由(新增 API) |
2.3 多应用(多实例)场景下的行为
从源码和单元测试(test/unit/specs/api.spec.js)可以看到,同一个路由器可以被多个 Vue 根实例共享:router.init(app)会把每个 app 压入this.apps数组,因此$router在所有 app 中返回的都是同一个路由器引用,router.app指向最近初始化的主 app。这在"同一路由配置驱动多个应用挂载点"的场景下非常有用。
三、$route:当前激活路由对象
3.1 是什么
$route是当前激活的路由对象(Route Object),它描述"当前 URL 解析后的状态以及匹配到的路由记录"。官方文档(docs-gitbook/ja/api/route-object.md)明确指出:
该属性是只读的且不可变更,但可以被 watch。
这意味着你不能直接修改$route.path来改变导航——路由状态的唯一合法来源是路由器内部维护的history.current;每次成功的导航都会产生一个新的路由对象(Route Object 具有不可变性)。
3.2 常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
$route.path | string | 当前路由的路径,恒为绝对路径,如"/foo/bar" |
$route.params | Object | 动态段与星号段的 key/value 对,无参数时为空对象 |
$route.query | Object | 查询字符串解析出的 key/value 对,如/foo?user=1时$route.query.user === '1' |
$route.hash | string | 当前路由的 hash(含#),无 hash 时为空字符串 |
$route.fullPath | string | 包含查询与 hash 的完整解析 URL |
$route.matched | Array<RouteRecord> | 当前路由所有嵌套匹配段对应的路由记录数组(父在前、子在后) |
$route.name | string | 当前路由名称(若有命名,见 docs-gitbook/ja/essentials/named-routes.md) |
$route.redirectedFrom | string | 若存在重定向,则为重定向来源的名称(见 docs-gitbook/ja/essentials/redirect-and-alias.md) |
3.3 响应式原理
$route之所以"可 watch",是因为它在 src/install.js 中通过Vue.util.defineReactive(this, '_route', ...)被定义成了根实例上的响应式属性,而Vue.prototype.$route的 getter 返回this._routerRoot._route。于是:
watch: { '$route' (to, from) { // 参数或查询变化时触发 } }这样的 watcher 是完全可行的,官方也建议:当仅参数或查询变化(组件被复用、不触发 enter/leave 守卫)时,用$route的 watcher 或beforeRouteUpdate来响应变化(详见 docs-gitbook/ja/essentials/dynamic-matching.md)。
四、组件内导航守卫:三个可选钩子
组件注入的另一个重要部分是:vue-router 为路由组件提供了三个可选的导航守卫选项(官方文档列出的即这三者):
beforeRouteEnterbeforeRouteUpdate(2.2 版本新增)beforeRouteLeave
这三个钩子与全局守卫、路由级守卫一起,构成 vue-router 的"组件内守卫"层级。完整说明见官方导航守卫章节 docs-gitbook/ja/advanced/navigation-guards.md。
4.1 beforeRouteEnter:进入前
在确认导航之前调用,此时组件实例尚未创建,因此this不可用:
const Foo = { template: `...`, beforeRouteEnter (to, from, next) { // 此组件对应的路由被确认之前调用 // 注意:此时组件实例还没创建,无法访问 this! } }如果需要访问组件实例,官方推荐给next传入回调,回调会在导航确认后执行并把组件实例作为参数传入:
beforeRouteEnter (to, from, next) { next(vm => { // 通过 vm 访问组件实例(如设置 data、调用方法) vm.title = 'Hello' }) }该机制在 src/history/base.js 的bindEnterGuard中实现:beforeRouteEnter中传给next的函数回调会被收集到match.enteredCbs[key]数组,待导航确认、组件实例化后再通过handleRouteEntered依次调用(参见 src/components/view.js 中 init 钩子对handleRouteEntered(route)的调用)。
4.2 beforeRouteUpdate:复用更新(2.2+)
当路由变化但组件实例被复用时调用——典型场景是动态路由/foo/:id在/foo/1与/foo/2之间切换。此时可以访问this:
beforeRouteUpdate (to, from, next) { // 组件实例被复用时触发,this 可用 this.name = to.params.name next() }从源码看,该钩子由 src/history/base.js 的extractUpdateHooks从updated路由记录中提取,updated记录由resolveQueue计算得出(新旧 matched 数组中相同的头部段)。
4.3 beforeRouteLeave:离开前
在导航即将离开当前组件对应路由时调用,this可用。最经典的用途是防止未保存内容丢失:
beforeRouteLeave (to, from, next) { const answer = window.confirm('确定要离开吗?你有未保存的更改!') if (answer) { next() } else { next(false) // 取消本次导航 } }next(false)会中止导航,且若浏览器 URL 已被修改(手动输入或后退按钮),会重置回from路由的 URL。实现上,src/history/base.js 的extractLeaveGuards从deactivated记录中提取该钩子,并在队列执行时以reverse: true(由外到内)的顺序调用。
4.4 三个钩子的合并策略
在 src/install.js 中,这三个钩子被显式注册进 Vue 的选项合并策略:
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created这意味着它们与created钩子采用相同的合并规则——来自 mixin、父组件选项与组件自身的同名守卫会被合并为数组依次执行,而不是相互覆盖。这也是组件内守卫能与全局 mixin 中的守卫共存的原因。
五、组件守卫在完整导航流程中的位置
官方文档给出了完整的导航解析流程(见 docs-gitbook/ja/advanced/navigation-guards.md),组件内守卫分布在其中的关键节点:
- 导航被触发
- 在被失活的组件里调用
beforeRouteLeave守卫 - 调用全局
beforeEach守卫 - 在复用的组件里调用
beforeRouteUpdate守卫(2.2+) - 调用路由配置里的
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter守卫 - 调用全局
beforeResolve守卫(2.5+) - 导航被确认
- 调用全局
afterEach钩子 - 触发 DOM 更新
- 用创建好的实例调用
beforeRouteEnter中传给next的回调
在 src/history/base.js 中,前七步对应的守卫队列构建如下:
const queue = [].concat( // 组件内 leave 守卫(失活组件) extractLeaveGuards(deactivated), // 全局 before 钩子 this.router.beforeHooks, // 组件内 update 钩子(复用组件) extractUpdateHooks(updated), // 路由配置内的 beforeEnter activated.map(m => m.beforeEnter), // 异步组件 resolveAsyncComponents(activated) )待异步组件解析完成后,才会提取beforeRouteEnter(extractEnterGuards)并与resolveHooks拼接成第二段队列继续执行。这与文档描述的"异步组件解析后才调用beforeRouteEnter"完全一致。
六、实战综合示例
结合注入属性与组件内守卫,一个典型的"编辑页离开确认 + 进入时初始化"的组件如下:
const Editor = { template: `...`, data () { return { dirty: false } }, computed: { articleId () { return this.$route.params.id // 注入属性:读取动态参数 } }, watch: { '$route' (to, from) { // 参数变化但组件被复用时的兜底响应 this.loadArticle(to.params.id) } }, beforeRouteEnter (to, from, next) { // 组件未创建:先加载数据,再在回调中写入实例 fetchArticle(to.params.id).then(article => { next(vm => { vm.article = article }) }) }, beforeRouteUpdate (to, from, next) { // 同组件复用、路由变化:直接 this 访问 this.loadArticle(to.params.id) next() }, beforeRouteLeave (to, from, next) { // 未保存修改时阻止离开 if (this.dirty && !window.confirm('放弃未保存的修改?')) { next(false) } else { next() } } }单元测试 test/unit/specs/api.spec.js 验证了beforeRouteEnter与全局beforeEach的异步执行顺序(先全局 before,再组件 enter),可作为理解守卫时序的参考。
七、注意事项与最佳实践
- 参数/查询变化不触发 enter/leave 守卫:
/foo/1→/foo/2这类切换只触发beforeRouteUpdate,或通过watch: { '$route' }响应;不要依赖beforeRouteEnter/beforeRouteLeave处理参数变化。 beforeRouteEnter中不能访问this:需要实例状态时务必使用next(vm => {...});该回调只支持在beforeRouteEnter中使用,beforeRouteUpdate与beforeRouteLeave因this已可用而不支持传回调。- 务必调用
next:守卫不调用next会导致导航一直处于 pending 状态,后续钩子永远无法执行。 - 守卫的合并:三个组件内守卫与
created采用相同合并策略,mixin 与组件自身的同名守卫会合并为数组按序执行。 - 与 keep-alive 的配合:配合
<keep-alive>使用RouterView时,路由组件实例的注册/注销由 src/components/view.js 中的registerRouteInstance与 init/prepatch 钩子维护,beforeRouteEnter回调仍会通过handleRouteEntered在实例就绪后补调。 - 版本适用性:本文描述的注入机制与三个守卫钩子针对 Vue 2 版本的 vue-router(本仓库即
🚦 The official router for Vue 2)。仓库 src/composables/guards.js 中还提供了面向 Vue 3 Composition API 的onBeforeRouteUpdate/onBeforeRouteLeave组合式守卫实现,供跨版本参考。
【免费下载链接】vue-router🚦 The official router for Vue 2项目地址: https://gitcode.com/gh_mirrors/vu/vue-router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考