前言
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-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