當無情面試官問 vue-next-router 帶來了哪些變化?

前言

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,讓構建單頁面應用變得易如反掌。

本文基於的源碼版本是 vue-next-router alpha.10,爲了與 2.0 中的 Vue Router 區分,下文將當前 vue-router v3.1.6 稱爲 vue2-routerhtml

本文旨在幫助更多人對新版本 Router 有一個初步的瞭解,若是文中有誤導你們的地方,歡迎留言指正。vue

重大改進

這次 Vue 的重大改進隨之而來帶來了 Vue Router 的一系列改進,現階段(alpha.10)相比 vue2-router 的主要變化,現總結以下:node

1. 構建選項 mode

由原來的 mode: "history" 更改成 history: createWebHistory()react

// 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()(和其餘模式) 的第一個參數做爲 basegit

//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"}github

5. router.matchrouter.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 一個不存在的命名路由時,路由會導航的根路由 "/" 下,而且不會渲染任何內容。vue-router

const router = new VueRouter({
  mode: 'history',
  routes: [{ path: '/', name: 'foo', component: Foo }]
}
this.$router.push({name: 'baz'})

瀏覽器控制檯只會提示以下警告。編程

vue-next-router 中,一樣作法會引起錯誤。api

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 的渲染

刪除 tag prop

在 vue2-couter 中,想要將 <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=2vue-router 默認狀況下,Router 匹配規則是「包含匹配」。也就是說,當前的路徑是 /pages 開頭的,那麼 <router-link to="/pages/*"> 都會被設置 CSS 類名。

在這個示例中,若是使用「精確匹配模式」(exact: true),則精確匹配將匹配 /pages/foo/bar,不會匹配 /pages/foo/bar?page=2 由於它在比較中包括查詢參數 ?page=2,因此當選擇第二頁面時,Bar 就不高亮顯示了。

爲了解決上述問題和其餘邊界狀況,這次改進使得 router-link-active 應用方式更嚴謹,詳情請參見這個 rfc

處理此問題的核心:

// 確認路由 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
}

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 進行第一層合併。再處理上述嵌套路由時,將能夠直接經過 to.meta 獲取信息。

router.beforeEach((to, from, next) => {
  if (to.meta.nested) next('/login')
  else next()
})

更多詳細的介紹請看這個 rfc 吧。

關於 vue-next-router 的改進,這裏就介紹這麼多,想了解更多的話,請異步到 vue-next-router

走入源碼

相比 vue2-routerES6-class 的寫法 vue-next-routerfunction-to-function 的編寫更易讀也更容易維護。

Router 的 install

暴露的 Vue 組件解析入口相對來講更清晰,開發插件時定義的 install 也簡化了許多。

咱們現看下 vue2-next-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 也沒有混入,插件將充分利用 provideinject 對外暴露一個組合函數便可,固然,沒了 this 以後也有很差的地方,看這裏

provideinject 這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。

再來看下 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.&dollar;router - router 實例。
  • this.&dollar;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)!
}

源碼中,useRouteruseRoute 經過 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) // 路由對象信息
}

除了能夠命名導入 useRouteruseRoute 以外,還可暴露出不少函數,以更好的支持 tree-shaking(期待新版本的發佈吧)。

NavigationFailureType
RouterLink
RouterView
createMemoryHistory
createRouter
createWebHashHistory
createWebHistory
onBeforeRouteLeave
onBeforeRouteUpdate
parseQuery
stringifyQuery
useLink
useRoute
useRouter
...

最後

我想,就介紹這麼多吧,上文介紹到的只是改進的一部分,感受還有不少不少東西須要咱們去了解和掌握,新版本給咱們帶來了更靈活的編程,讓咱們共同期待 vue 3.0 到到來吧。

參考:

相關文章
相關標籤/搜索