天天看点

【Vuejs】605- Vue3中 router 带来了哪些变化?

【Vuejs】605- Vue3中 router 带来了哪些变化?

作者: Leiy

前言

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​

​ 的主要变化,总结如下:

1. 构建选项 mode

由原来的 ​

​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(),
  ...
})      

2. 构建选项 base

传给 ​

​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('/'),
  ...
})      

4. 捕获所有路由 ( /* ) 时,现在必须使用带有自定义正则表达式的参数进行定义:/: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"}​

​。

5. 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,
  }
}      

6. 删除 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: []
}]      

7. 如果使用 ,则可能需要等待 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 一样。

8. 在服务端渲染 (SSR) 中,需要使用一个三目运算符手动传递合适的 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
})      

9. 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-rfcs​

​​(​

​active​

​ 就是已经讨论通过并且正在实施的特性)。

  • 0021-router-link-scoped-slot
  • 0022-router-merge-meta-routelocation
  • 0028-router-active-link
  • 0029-router-dynamic-routing
  • 0033-router-navigation-failures - 本文略

router-link-scoped-slot

这个 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 。

router-active-link

这个 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。

router-merge-meta-routelocation

在 ​

​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​

​ 的编写更易读也更容易维护。

Router 的 install

暴露的 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