Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,讓構建單頁面應用變得易如反掌。
本文基於的源碼版本是 vue-next-router alpha.10,爲了與 2.0 中的 Vue Router 區分,下文將當前 vue-router v3.1.6 稱爲 vue2-router
。html
本文旨在幫助更多人對新版本 Router 有一個初步的瞭解,若是文中有誤導你們的地方,歡迎留言指正。vue
這次 Vue
的重大改進隨之而來帶來了 Vue Router 的一系列改進,現階段(alpha.10
)相比 vue2-router
的主要變化,現總結以下:node
由原來的 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(), ... })
傳給 createWebHistory()
(和其餘模式) 的第一個參數做爲 base
。git
//vue2-router const router = new VueRouter({ base: __dirname, }) // vue-next-router import { createRouter, createWebHistory } from 'vue-next-router' const router = createRouter({ history: createWebHistory('/'), ... })
// 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
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
一個不存在的命名路由時,路由會導航的根路由 "/"
下,而且不會渲染任何內容。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
就是已經討論經過而且正在實施的特性。
這個 rfc 主要提議及改進以下:
tag
prop - 使用做用域插槽代替event
prop - 使用做用域插槽代替scoped-slot
APIclick
事件分配給內部錨點custom
prop 以徹底自定義 router-link
的渲染在 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 吧。
這個 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
。vue-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 }
在 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-router
的 ES6-class
的寫法 vue-next-router
的 function-to-function
的編寫更易讀也更容易維護。
暴露的 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
也沒有混入,插件將充分利用 provide
和 inject
對外暴露一個組合函數便可,固然,沒了 this
以後也有很差的地方,看這裏。
provide
和inject
這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。
再來看下 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
實例,下面這些屬性成員會被注入到每一個子組件。
可是 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 到到來吧。
參考: