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篇原创系列汇总

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

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

  • 相关阅读:
    解决SharePoint 文档库itemadded eventhandler导致的上传完成后,编辑页面保持报错的问题,错误信息为“该文档已经被编辑过 the file has been modified by...”
    解决SharePoint 2013 designer workflow 在发布的报错“负载平衡没有设置”The workflow files were saved but cannot be run.
    随机实例,随机值
    Spring4笔记
    struts2笔记(3)
    struts2笔记(2)
    获取文本的编码类型(from logparse)
    FileUtil(from logparser)
    DateUtil(SimpleDateFormat)
    struts2笔记
  • 原文地址:https://www.cnblogs.com/pingan8787/p/13069349.html
Copyright © 2011-2022 走看看