Vue Router 组件注入机制详解:$router、$route 与组件内导航守卫 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.jsVue.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()动态增删路由新增 API2.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.pathstring当前路由的路径恒为绝对路径如/foo/bar$route.paramsObject动态段与星号段的 key/value 对无参数时为空对象$route.queryObject查询字符串解析出的 key/value 对如/foo?user1时$route.query.user 1$route.hashstring当前路由的 hash含#无 hash 时为空字符串$route.fullPathstring包含查询与 hash 的完整解析 URL$route.matchedArrayRouteRecord当前路由所有嵌套匹配段对应的路由记录数组父在前、子在后$route.namestring当前路由名称若有命名见 docs-gitbook/ja/essentials/named-routes.md$route.redirectedFromstring若存在重定向则为重定向来源的名称见 docs-gitbook/ja/essentials/redirect-and-alias.md3.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 为路由组件提供了三个可选的导航守卫选项官方文档列出的即这三者beforeRouteEnterbeforeRouteUpdate2.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之间切换。此时可以访问thisbeforeRouteUpdate (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) )待异步组件解析完成后才会提取beforeRouteEnterextractEnterGuards并与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),仅供参考