- 原文地址:Vue Router — The Missing Manual
- 原文做者:Harshal Patil
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Sam
- 校對者:Ranjay, shixi-li
除了 DOM 操做,事件處理,表單和組件以外,每一個單頁應用程序(SPA)框架若是要用於大型應用程序都須要兩個核心部分:前端
幸運的是,Vue 爲路由和狀態管理提供了官方解決方案。這篇文章裏,咱們將要探尋 vue-router,以瞭解路由在諸多場景中的行爲表現,並探索一些編寫優雅代碼的模式。這裏假設你已經對 vue,vue-router 和 SPA 有所深刻了解。vue
咱們將使用下面開啓了 HTML5 路由模式的應用程序做爲示例。android
/projects/:projectId/users
/projects/:projectId/users/:userId
/projects/:projectId/users/:userId/profile
/projects/:projectId/users/new
從應用程序路由派生出的組件層次結構ios
Vue-router 在每個組件裏注入當前路由對象。每一個組件裏能夠經過 this.$route
訪問它。但關於這個對象有兩點須要注意的事項。git
路由對象是不可改變的。github
若是你使用 $router.push()
,$router.replace()
或者連接導航到任何路由上,則會建立 $route
對象的新副本。已有的(路由)對象是不會被修改的。因爲它(路由對象)是不可變的,因此你不須要設置 deep 屬性監聽這個 $route
對象:web
Vue.component('app-component', {
watch: {
$route: {
handler() {},
deep: true // <-- 並不須要
}
}
});
複製代碼
路由對象是共享的。正則表達式
不可變性帶來了進一步的優點。路由在全部組件內部共享同一個 $route
對象實例。因此下面這些內容都將生效:vue-router
// 父組件
Vue.component('app-component', {
mounted() { window.obj1 = this.$route; }
});
// 子組件
Vue.component('user-list', {
mounted() { window.obj2 = this.$route; }
});
// 一旦 App 實例化
window.obj1 === window.obj2; // <-- 返回 true
複製代碼
理論上來講,路由是分解大型網絡應用程序的第一級抽象。狀態管理更晚一些。shell
有兩種關於分解網絡應用程序的思考方式。一種是把應用程序分解成一系列的頁面(例如,每一個頁面都根據 URL 邊界進行拆分),另外一種是把應用程序理解成已經定義好的一組狀態(可選擇讓每一個狀態都有一個 URL)。
state-router 會把應用程序拆解成一組狀態。url-router 會把應用程序拆解成一組頁面。
Vue-router 是 url-router。Vue 沒有官方 state-router。有 Angular 背景的人員立刻會意識到它們的區別。狀態路由器(state-router)相較於 URL 路由器(url-router)方式的區別:
即使不是狀態路由器,在轉變過程當中,你仍然能夠把複雜數據從一個路徑傳遞到另外一個上,而不用將數據做爲 URL 的一部分。
當使用 vue-router 從一個路由導航到另外一個路由時,你能夠傳遞隱式數據或狀態。
這在哪裏有用呢?主要是優化的時候。考慮下面的例子:
/users/:userId
簡介頁 —— /users/:userId/profile
created
鉤子函數裏,咱們能夠選擇檢查數據的可用性。// 用戶詳情組件內部
Vue.component('user-details', {
methods: {
onLinkClick() {
this.$router.push({
name: 'profile',
params: {
userId: 123,
userData // 隱式數據/狀態
}
});
}
}
});
// 用戶簡介組件內部
Vue.component('user-profile', {
created() {
// 訪問附帶過來的數據
if (this.$route.params.userData) {
this.userData = this.$route.params.userData;
} else {
// 否則就發起 API 請求獲取用戶數據
this.getUserDetails(this.$route.params.userId)
.then(/* handle response */);
}
}
});
複製代碼
注意:可以這樣處理是由於 $route
對象注入在每一個組件中且是共享不可變的。否則會很難辦。
若是你有嵌套配置,那麼任何子組件上的保護都有可能阻塞父組件的渲染。例如:
const ParentComp = Vue.extend({
template: `<div>
<progress-loader></progress-loader>
<router-view>
</div>`
});
{
path: '/projects/:projectId',
name: 'project',
component: ParentComp,
children: [{
path: 'users',
name: 'list',
component: UserList,
beforeEnter (to, from, next) {
setTimeout(() => next(), 2000);
}
}]
}
複製代碼
若是你直接導航到 /projects/100/users/list
,那麼因爲 beforeEnter
的異步保護,導航會被看成等待中(pending),而且 ParentComp
組件不會被渲染。因此,若是你但願看到進程加載器(progress-loader)
直到保護解除,它應該是不會出現。對於你可能從父組件發起的任何 API 請求也是如此。
在這種狀況下,若是你但願顯示父級組件而不顧子級路由的保護策略,解決方案是改變你組件的層級結構而且經過某種方式更新 進程加載器(progress-loader)
的邏輯。若是你作不到,那麼你能夠像這樣使用雙重傳遞 —— 先導航到父組件而後再到子組件:
goToUserList () {
this.$router.push('/projects/100',
() => this.$router.replace('users'))
}
複製代碼
這個行爲是有道理的。若是父級視圖不等待子級的保護,那麼它可能先渲染一會父級視圖,而後若是保護失敗則導航到其餘地方去。
注意:相比之下,Angular 的路由是徹底相反地。父級組件通常不會等待任何子級保護的觸發。那麼哪一種方案是正確的?都不是。乍看上去,Angular 採起的方法感受天然而有序,但若是開發者不仔細的話它很容易搞砸用戶體驗(UX)。
使用 vue-router,渲染層級彷佛有點尷尬。但卻少有機會破壞用戶體驗(UX)。Vue 隱含地預先強制執行這項決定。同時,不要忘記 vue-router 提供的做用域。你可使用全局級別,路由級別或者組件內級別的保護。你會擁有真正細粒度的控制。
在理解了關於 vue-router 的一些概念以後,是時候討論關於編寫優雅代碼的模式了。
Vue-router 是構建在 path-to-regexp 之上的。Express.js 路由也是如此。URL 匹配是基於正則表達式的。這意味着你能夠像這樣定義你的路由:
const prefix = `/projects/:projectId/users`;
const routes = [
{
path: `${prefix}/list`,
name: 'user-list',
component: UserList,
},
{
path: `${prefix}/:userId`,
name: 'user-details',
component: UserDetails
},
{
// 這裏不會形成問題嗎?
path: `${prefix}/new`,
name: 'user-new',
component: NewUser
}
];
複製代碼
這裏不那麼明顯的問題是路徑 ${prefix}/new
永遠不會被匹配,由於它定義在路由列表的最後。這是基於正則表達式路由的缺陷。不止一個路由會被匹配上(譯者注:路徑 ${prefix}/:userId
會覆蓋匹配路徑 ${prefix}/new
)。固然,這對於小型網絡應用程序不是問題。或者,你能夠像這樣定義一棵路由樹:
const routes = [{
path: '/projects/:projectId/users',
name: 'project',
component: ProjectUserView,
children: [
{
path: '',
name: 'list',
component: UserList,
},
{
path: 'new',
name: 'user-details',
component: NewUser,
},
{
path: ':userId',
name: 'user-new',
component: UserDetails,
}
]
}];
複製代碼
基於樹結構配置有一些優勢:
使用基於樹結構配置的細微差異在於建立中間組件,它們可能只包含一個 router-view
組件。Vue-router 沒有將 RouterView
組件直接暴露給最終開發者。可是一個包裝 router-view
的小技巧能夠極大地幫助減小中間組件:
const RouterViewWrapper = Vue.extend({
template: `<router-view></router-view>`
});
// 如今,能夠在路由配置樹的任何位置
// 使用 RouterViewWrapper 組件。
複製代碼
注意:Trie 是一種搜索樹數據結構的類型(譯者注:前綴樹)。基於前綴的路由是可預見的,而且無論路由的定義順序。在 Nodejs 生態環境裏,存在不少基於前綴或者相似的路由。Hapi.js 和 Fastify.js 使用的是基於前綴的路由。
簡而言之:
樹結構配置優於扁平結構配置。
當你使用導航保護的時候,你可能在這些保護函數裏須要一些依賴。大多數常見的例子是 Vuex/Redux 的 store。這個解決方案過於簡單。比起路由器自己,還有更多關於代碼組織的工做要作。假定你有如下這些文件:
src/
|-- main.js
|-- router.js
|-- store.js
複製代碼
你能夠建立一個在定義導航守護時的存儲(store)注入函數:
// 在你的 store.js 裏,定義存儲注入器
export const store = new Vuex.Store({ /* config */ });
export function storeInjector(fn) {
return (...args) => fn(...args, store);
}
// 在你的 router.js 裏,使用存儲注入器
const routeConfig = {
// 其餘內容
beforeEnter: storeInjector((to, from, next, store) => {})
}
複製代碼
或者,你也能夠將路由建立器封裝到能夠傳遞任何依賴的函數中:
// main.js 文件
import { makeStore } from './store.js';
const store = makeStore();
const router = makeRouter(store);
const app = new Vue({ store, router, template: `<div></div>` });
// router.js 文件
export function makeRouter(store) {
// 使用 store 處理任何事情
return new VueRouter({
routes: []
})
}
複製代碼
設想你在一個異步組件裏使用路由配置。異步組件是經過懶加載方式引入的。這一般是使用像 Webpack 或 Rollup 這樣的工具進行包(bundle)拆分實現的。配置看起來將會是這樣的:
const routes = [{
path: '/projects/:projectId/users',
name: 'user-list',
// 異步組件(Webpack 的代碼拆分)
component: import('../UserList.js'),
}];
複製代碼
在根實例或者父級 AppComponent
組件裏,你可能但願檢索 projectId
用來作一些引導性的 API 調用。典型的代碼是:
Vue.component('app-comp', {
created() {
// 問題:projectId 未定義
console.log(this.$route.params.projectId);
}
}
複製代碼
這裏的問題是 projectId
將是未定義的,由於子組件沒有準備好,路由器尚未完成傳遞。
當你在路由配置裏使用異步組件時,在未建立子組件以前,父組件中將不提供路徑或查詢參數。
這裏的解決方案是在父組件裏監聽 $route
。另外,你必須只監聽它一次,由於它只是一個引導性 API 請求而且不該該再被觸發:
Vue.component('app-comp', {
created() {
const unwatch = this.$watch('$route', () => {
const projectId = this.$route.params.projectId;
// 作剩餘的工做
this.getProjectInfo(projectId);
// 當即解開監聽
unwatch();
});
}
}
複製代碼
const routes = [{
path: '/projects/:projectId',
name: 'project',
component: ProjectView,
beforeEnter(to, from, next) {
next();
},
children: [{
// 仔細觀察
// 嵌套路由以 `/` 開頭
path: '/users',
name: 'list',
component: UserList,
}]
}];
複製代碼
在上面的配置中,子級路由以 /
開頭所以被看成根路徑。因此你可使用 https://example.com/users
而不是 https://example.com/projects/100/users
就能夠訪問 UserList
組件。然而,UserList
組件將被渲染成 ProjectView
組件的子組件。這種路徑被稱爲根相對嵌套路徑。
固然,組件層級,導航保護依然在處理中。你仍然須要嵌套的 <router-view>
組件。惟一改變的事情是 URL 的結構。其餘的都還保持原樣。這意味着 beforeEnter
保護將在 UserList
組件以前執行。
這個技巧是純粹的便利,所以須要謹慎的使用它。從長遠來看,它每每會產生使人困惑的代碼。然而 ——
根相對嵌套路徑在構建 App Shell Model 的 PWA 時很是有用。
Vue 提供的官方路由解決方案是很是靈活的。除去簡單的路由,它還提供了許多功能,如 meta
字段,transition
,高級 scroll-behavior
,lazy-loading
等。
此外,當咱們使用導航保護,預路由數據獲取時,vue-router 設計了關於用戶體驗(UX)的考量。你可使用全局或者組件內保護,但需謹慎地使用它們,所以你應該牢記關注點分離並把路由職責從組件中移除。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。