zoukankan      html  css  js  c++  java
  • 【Vuejs】605- Vue3中 router 带来了哪些变化?

    作者: 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 的主要变化,总结如下:

    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. 如果使用 <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 一样。

    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-rfcsactive 就是已经讨论通过并且正在实施的特性)。

    • 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=2vue2-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

    原创系列推荐

    1. JavaScript 重温系列(22篇全)

    2. ECMAScript 重温系列(10篇全)

    3. JavaScript设计模式 重温系列(9篇全)

    4. 正则 / 框架 / 算法等 重温系列(16篇全)

    5. Webpack4 入门(上)|| Webpack4 入门(下)

    6. MobX 入门(上) ||  MobX 入门(下)

    7. 59篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点这,与大家一起分享本文吧~

  • 相关阅读:
    编写程序计算所输日期是当年的第几天
    如何使提取的星期为中文
    Recordset.State 属性
    Dependency Walker Frequently Asked Questions
    setlocale
    StringBuilder 拼接sql语句比较快
    用sql 语句给字段添加描述
    委托事件
    将一个tabel加到另一个table
    winform的tab跳到下一个
  • 原文地址:https://www.cnblogs.com/pingan8787/p/13069349.html
Copyright © 2011-2022 走看看