赞
踩
作者: Leiy
https://segmentfault.com/a/1190000022582928
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。
本文基于的源码版本是 vue-next-router alpha.10,为了与 Vue 2.0 中的 Vue Router 区分,下文将 vue-router v3.1.6 称为 vue2-router
。
本文旨在帮助更多人对新版本 Router 有一个初步的了解,如果文中有误导大家的地方,欢迎留言指正。
此次 Vue
的重大改进随之而来带来了 Vue Router 的一系列改进,现阶段(alpha.10
)相比 vue2-router
的主要变化,总结如下:
由原来的 mode: "history"
更改为 history: createWebHistory()
。(设置其他 mode
也是同样的方式)。
- // vue2-router
- const router = new VueRouter({
- mode: history ,
- ...
- })
-
- // vue-next-router
- import { createRouter, createWebHistory } from vue-next-router
- const router = createRouter({
- history: createWebHistory(),
- ...
- })
传给 createWebHistory()
(和其他模式) 的第一个参数作为 base
。
- //vue2-router
- const router = new VueRouter({
- base: __dirname,
- })
-
- // vue-next-router
- import { createRouter, createWebHistory } from vue-next-router
- const router = createRouter({
- history: createWebHistory( / ),
- ...
- })
/*
) 时,现在必须使用带有自定义正则表达式的参数进行定义:/:catchAll(.*
)。- // vue2-router
- const router = new VueRouter({
- mode: history ,
- routes: [
- { path: /user/:a* },
- ],
- })
-
-
- // vue-next-router
- const router = createRouter({
- history: createWebHistory(),
- routes: [
- { path: /user/:a:catchAll(.*) , component: component },
- ],
- })

当路由为 /user/a/b
时,捕获到的 params
为 {"a": "a", "catchAll": "/b"}
。
router.match
与 router.resolve
合并在一起为 router.resolve
,但签名略有不同。- // vue2-router
- ...
- resolve ( to: RawLocation, current?: Route, append?: boolean) {
- ...
- return {
- location,
- route,
- href,
- normalizedTo: location,
- resolved: route
- }
- }
-
- // vue-next-router
- function resolve(
- rawLocation: Readonly<RouteLocationRaw>,
- currentLocation?: Readonly<RouteLocationNormalizedLoaded>
- ): RouteLocation & { href: string } {
- ...
- let matchedRoute = matcher.resolve(matcherLocation, currentLocation)
- ...
- return {
- fullPath,
- hash,
- query: normalizeQuery(rawLocation.query),
- ...matchedRoute,
- redirectedFrom: undefined,
- href: routerHistory.base + fullPath,
- }
- }

router.getMatchedComponents
,可以从 router.currentRoute.value.matched
中获取。
router.getMatchedComponents
返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。
- [{
- aliasOf: undefined
- beforeEnter: undefined
- children: []
- components: {default: {…}, other: {…}}
- instances: {default: null, other: Proxy}
- leaveGuards: []
- meta: {}
- name: undefined
- path: "/"
- props: ƒ (to)
- updateGuards: []
- }]
<transition>
,则可能需要等待 router
准备就绪才能挂载应用程序。- app.use(router)
- // Note: on Server Side, you need to manually push the initial location
- router.isReady().then(() => app.mount( #app ))
一般情况下,正常挂载也是可以使用 <transition>
的,但是现在导航都是异步的,如果在路由初始化时有路由守卫,则在 resolve
之前会出现一个初始渲染的过渡,就像给 <transiton>
提供一个 appear 一样。
mode
。- let history = isServer ? createMemoryHistory() : createWebHistory()
- let router = createRouter({ routes, history })
- // on server only
- router.push(req.url) // request url
- router.isReady().then(() => {
- // resolve the request
- })
push
或者 resolve
一个不存在的命名路由时,将会引发错误,而不是导航到根路由 "/"
并且不显示任何内容。在 vue2-router
中,当 push
一个不存在的命名路由时,路由会导航到根路由 "/"
下,并且不会渲染任何内容。
- const router = new VueRouter({
- mode: history ,
- routes: [{ path: / , name: foo , component: Foo }]
- }
- this.$router.push({name: baz })
浏览器控制台只会提示如下警告,并且 url
会跳转到根路由 /
下。
在 vue-next-router
中,同样做法会引发错误。
- const router = createRouter({
- history: routerHistory(),
- routes: [{ path: / , name: foo , component: Foo }]
- })
- ...
- import { useRouter } from vue-next-router
- ...
- const router = userRouter()
- router.push({name: baz })) // 这段代码会报错
以下内容的改进来自 active-rfcs
(active
就是已经讨论通过并且正在实施的特性)。
0021-router-link-scoped-slot
0022-router-merge-meta-routelocation
0028-router-active-link
0029-router-dynamic-routing
0033-router-navigation-failures - 本文略
这个 rfc 主要提议及改进如下:
删除 tag
prop - 使用作用域插槽代替
删除 event
prop - 使用作用域插槽代替
增加 scoped-slot
API
停止自动将 click
事件分配给内部锚点
添加 custom
prop 以完全支持自定义的 router-link
渲染
在 vue2-router 中,想要将 <roter-link>
渲染成某种标签,例如 <button>
,需要这么做:
- <router-link to="/" tag="button">按钮</router-link>
- !-- 渲染结果 -->
- <button>按钮</button>
根据此次 rfc
,以后可能需要这样做:
- <router-link to="/" custom v-slot="{ navigate, isActive, isExactActive }">
- <button role="link" @click="navigate" :class="{ active: isActive, exact-active : isExactActive }">
- 按钮
- </button>
- <router-link>
- !-- 渲染结果 -->
- <button role="link">按钮</button>
更多详细的介绍请看这个 rfc 。
这个 rfc 改进的缘由是 gayhub
上名为 zamakkat 的大哥提出来的,他的 issues 主要内容是,有一个嵌套组件,像这样:
- Foo (links to /pages/foo)
- |-- Bar (links to /pages/foo/bar)
需求:需要突出显示当前选中的页面(并且只能突出显示一项)。
如果用户打开 /pages/foo
,则仅 Foo
高亮显示。
如果用户打开 /pages/foo/bar
,则仅 Bar
应高亮显示。
但是,Bar
页面也有分页,选择第二页时,会导航到 /pages/foo/bar?page=2
。vue2-router
默认情况下,路由匹配规则是「包含匹配」。也就是说,当前的路径是 /pages
开头的,那么 <router-link to="/pages/*">
都会被设置 CSS
类名。
在这个示例中,如果使用「精确匹配模式」(exact: true
),则精确匹配将匹配 /pages/foo/bar
,不会匹配 /pages/foo/bar?page=2
因为它在比较中包括查询参数 ?page=2
,所以当选择第二页面时,Bar
就不高亮显示了。
所以无论是「精确匹配」还是「包含匹配」都不能满足此需求。
为了解决上述问题和其他边界情况,此次改进使得 router-link-active
应用方式更严谨,处理此问题的核心:
- // 确认路由 isActive 的行为
- function includesParams(
- outer: RouteLocation[ params ],
- inner: RouteLocation[ params ]
- ): boolean {
- for (let key in inner) {
- let innerValue = inner[key]
- let outerValue = outer[key]
- if (typeof innerValue === string ) {
- if (innerValue !== outerValue) return false
- } else {
- if (
- !Array.isArray(outerValue) ||
- outerValue.length !== innerValue.length ||
- innerValue.some((value, i) => value !== outerValue[i])
- )
- return false
- }
- }
- return true
- }

详情请参见这个 rfc。
在 vue2-router
中,在处理嵌套路由时,meta
仅包含匹配位置的 route meta
信息。 看个栗子:
- {
- path: /parent ,
- meta: { nested: true },
- children: [
- { path: foo , meta: { nested: true } },
- { path: bar }
- ]
- }
在导航到 /parent/bar
时,只会显示当前路由对应的 meta
信息为 {}
,不会显示父级的 meta
信息。
meta: {}
所以在这种情况下,需要通过 to.matched.some()
检查 meta
字段是否存在,而进行下一步逻辑。
- router.beforeEach((to, from, next) => {
- if (to.matched.some(record => record.meta.nested))
- next( /login )
- else next()
- })
因此为了避免使用额外的 to.matched.some
, 这个 rfc
提议,将父子路由中的 meta
进行第一层合并(同 Object.assing()
)。如果再遇到上述嵌套路由时,将可以直接通过 to.meta
获取信息。
- router.beforeEach((to, from, next) => {
- if (to.meta.nested) next( /login )
- else next()
- })
更多详细介绍请看这个 rfc。
router-dynamic-routing
这个 rfc 的主要内容是,允许给 Router 添加和删除(单个)路由规则。
router.addRoute(route: RouteRecord)
- 添加路由规则
router.removeRoute(name: string | symbol)
- 删除路由规则
router.hasRoute(name: string | symbol): boolean
- 检查路由是否存在
router.getRoutes(): RouteRecord[]
- 获取当前路由规则的列表
相比 vue2-router
删除了动态添加多个路由规则的 router.addRoutes API。
在 Vue 2.0 中,给路由动态添加多个路由规则时,需要这么做:
- router.addRoutes(
- [
- { path: /d , component: Home },
- { path: /b , component: Home }
- ]
- )
而在 Vue 3.0 中,需要使用 router.addRoute()
单个添加记录,并且还可以使用更丰富的 API:
- router.addRoute({
- path: /new-route ,
- name: NewRoute ,
- component: NewRoute
- })
-
- // 给现有路由添加子路由
- router.addRoute( ParentRoute , {
- path: new-route ,
- name: NewRoute ,
- component: NewRoute
- })
- // 根据路由名称删除路由
- router.removeRoute( NewRoute )
-
- // 获得路由的所有记录
- const routeRecords = router.getRoutes()

关于 RfCS
上提出的改进,这里就介绍这么多,想了解更多的话,请移步到 active-rfcs。
相比 vue2-router
的 ES6-class
的写法 vue-next-router
的 function-to-function
的编写更易读也更容易维护。
暴露的 Vue 组件解析入口相对来说更清晰,开发插件时定义的 install
也简化了许多。
我们先看下 vue2-router
源码中 install
方法的定义:
- import View from ./components/view
- import Link from ./components/link
- export let _Vue
- export function install (Vue) {
- // 当 install 方法被同一个插件多次调用,插件将只会被安装一次。
- if (install.installed && _Vue === Vue) return
- install.installed = true
- _Vue = Vue
- const isDef = v => v !== undefined
- const registerInstance = (vm, callVal) => {
- let i = vm.$options._parentVnode
- if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
- i(vm, callVal)
- }
- }
- // 将 router 全局注册混入,影响注册之后所有创建的每个 Vue 实例
- 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
- }
- // 注册实例,将 this 传入
- registerInstance(this, this)
- },
- destroyed () {
- registerInstance(this)
- }
- })
- // 将 $router 绑定的 vue 原型对象上
- Object.defineProperty(Vue.prototype, $router , {
- get () { return this._routerRoot._router }
- })
-
- // 将 $route 手动绑定到 vue 原型对象上
- Object.defineProperty(Vue.prototype, $route , {
- get () { return this._routerRoot._route }
- })
- // 注册全局组件 RouterView、RouterLink
- Vue.component( RouterView , View)
- Vue.component( RouterLink , Link)
-
- const strats = Vue.config.optionMergeStrategies
- // use the same hook merging strategy for route hooks
- strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
- }

我们可以看到,在 2.0
中,Router
提供的 install()
方法中更触碰底层,需要用到选项的私有方法 _parentVnode()
,还会用的 Vue.mixin()
进行全局混入,之后会手动将 $router
、$route
绑定到 Vue 的原型对象上。
- VueRouter.install = install
- VueRouter.version = __VERSION__
-
- // 以 src 方法导入
- if (inBrowser && window.Vue) {
- window.Vue.use(VueRouter)
- }
做了这么多事情之后,然后会在定义 VueRouter 类的文件中,将 install()
方法绑定到 VueRouter 的静态属性 install
上,以符合插件的标准。
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
我们可以看到,在 2.0
中开发一个插件需要做的事情很多,install
要处理很多事情,这对不了解 Vue 的童鞋,会变得很困难。
说了这么多,那么 vue-next-router
中暴露的 install
是什么样的呢?applyRouterPlugin()
方法就是处理 install()
全部逻辑的地方,请看源码:
- import { App, ComputedRef, reactive, computed } from vue
- import { Router } from ./router
- import { RouterLink } from ./RouterLink
- import { RouterView } from ./RouterView
-
- export function applyRouterPlugin(app: App, router: Router) {
- // 全局注册组件 RouterLink、RouterView
- app.component( RouterLink , RouterLink)
- app.component( RouterView , RouterView)
- //省略部分代码
- // 注入 Router 实例,源码其他地方会用到
- app.provide(routerKey, router)
- app.provide(routeLocationKey, reactive(reactiveRoute))
- }
基于 3.0
使用 composition API
时,没有 this
也没有混入,插件将充分利用 provide
和 inject
对外暴露一个组合函数即可,当然,没了 this
之后也有不好的地方,看这里。
provide
和inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
再来看下 vue-next-router
中 install()
是什么样的:
- export function createRouter(options: RouterOptions): Router {
- // 省略大部分代码
- const router: Router = {
- currentRoute,
- addRoute,
- removeRoute,
- hasRoute,
- history: routerHistory,
- ...
- // install
- install(app: App) {
- applyRouterPlugin(app, this)
- },
- }
- return router
- }

很简单,在 vue-next-router
提供的 install()
方法中调用 applyRouterPlugin
将 Vue 和 Router 作为参数传入。
最后在应用程序中使用 Router 时,只需要导入 createRouter
然后显示调用 use()
方法,传入 Vue,就可以在程序中正常使用了。
- import { createRouter, createWebHistory } from vue-next-router
- const router = createRouter({
- history: createWebHistory(),
- strict: true,
- routes: [
- { path: /home , redirect: / }
- })
-
- const app = createApp(App)
- app.use(router)
$router
、$route
我们知道在 vue2-router 中,通过在 Vue 根实例的 router
配置传入 router
实例,下面这些属性成员会被注入到每个子组件。
this.$router - router 实例。
this.$route - 当前激活的路由信息对象。
但是 3.0 中,没有 this
,也就不存在在 this.$router | $route
这样的属性,那么在 3.0 中应该如何使用这些属性呢?
我们首先看下源码暴露的 api
的地方:
- // useApi.ts
- import { inject } from vue
- import { routerKey, routeLocationKey } from ./injectionSymbols
- import { Router } from ./router
- import { RouteLocationNormalizedLoaded } from ./types
-
- // 导出 useRouter
- export function useRouter(): Router {
- // 注入 router Router (key 与 上文的 provide 对应)
- return inject(routerKey)!
- }
- // 导入 useRoute
- export function useRoute(): RouteLocationNormalizedLoaded {
- // 注入 路由对象信息 (key 与 上文的 provide 对应)
- return inject(routeLocationKey)!
- }

源码中,useRouter
、 useRoute
通过 inject
注入对象实例,并以单个函数的方式暴露出去。
在应用程序中只需要通过命名导入的方式导入即可使用。
- import { useRoute, useRouter } from vue-next-router
- ...
- setup() {
- const route = useRoute()
- const router = useRouter()
- ...
- // router -> this.$router
- // route > this.$route
- router.push( /foo )
- console.log(route) // 路由对象信息
- }
除了可以命名导入 useRouter
、 useRoute
之外,还可暴露出很多函数,以更好的支持 tree-shaking
(期待新版本的发布吧)。
- NavigationFailureType
- RouterLink
- RouterView
- createMemoryHistory
- createRouter
- createWebHashHistory
- createWebHistory
- onBeforeRouteLeave
- onBeforeRouteUpdate
- parseQuery
- stringifyQuery
- useLink
- useRoute
- useRouter
- ...
我想,就介绍这么多吧,上文介绍到的只是改进的一部分,感觉还有很多很多东西需要我们去了解和掌握,新版本给我们带来了更灵活的编程,让我们共同期待 vue 3.0 到到来吧。
参考:
vue-router - https://router.vuejs.org/
vue - https://cn.vuejs.org
vue-next-router - https://github.com/vuejs/vue-...
rfcs - https://github.com/vuejs/rfcs
- ◆ ◆ ◆ ◆ ◆
- 长按关注小生
你的在看我当成喜欢
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。