[譯] Vue Router 實戰手冊

除了 DOM 操做,事件處理,表單和組件以外,每一個單頁應用程序(SPA)框架若是要用於大型應用程序都須要兩個核心部分:前端

  1. 客戶端路由
  2. 顯式狀態管理(一般是單向的)

幸運的是,Vue 爲路由和狀態管理提供了官方解決方案。這篇文章裏,咱們將要探尋 vue-router,以瞭解路由在諸多場景中的行爲表現,並探索一些編寫優雅代碼的模式。這裏假設你已經對 vue,vue-router 和 SPA 有所深刻了解。vue

咱們將使用下面開啓了 HTML5 路由模式的應用程序做爲示例。android

路由:

  1. 項目中全部用戶的列表 /projects/:projectId/users
  2. 單個用戶的詳細信息視圖 /projects/:projectId/users/:userId
  3. 單個用戶的簡要信息視圖 /projects/:projectId/users/:userId/profile
  4. 建立一個新用戶 /projects/:projectId/users/new

組件樹結構:

從應用程序路由派生出的組件層次結構ios


1. 當前的路由對象是共享且不可變的

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
複製代碼

2. Vue-router 不是狀態路由

理論上來講,路由是分解大型網絡應用程序的第一級抽象。狀態管理更晚一些。shell

有兩種關於分解網絡應用程序的思考方式。一種是把應用程序分解成一系列的頁面(例如,每一個頁面都根據 URL 邊界進行拆分),另外一種是把應用程序理解成已經定義好的一組狀態(可選擇讓每一個狀態都有一個 URL)。

state-router 會把應用程序拆解成一組狀態。url-router 會把應用程序拆解成一組頁面

Vue-router 是 url-router。Vue 沒有官方 state-router。有 Angular 背景的人員立刻會意識到它們的區別。狀態路由器(state-router)相較於 URL 路由器(url-router)方式的區別:

  • 狀態路由器像狀態機同樣工做。
  • 狀態路由器中 URL 是非必要的。
  • 狀態是能夠嵌套的。
  • 一個應用程序被拆分紅一組定義好的狀態集合而不是頁面。從一個狀態轉變爲另外一個狀態時可選擇性的改變 URL。
  • 當從一個狀態轉變成另外一個狀態時能夠傳遞任何複雜的數據。但使用 URL 路由器,在頁面間傳遞數據通常是將它做爲 URL 地址的一部分或查詢參數。
  • 使用狀態路由器,當總體頁面發生刷新的時候已傳遞的數據會丟失(除非你使用了 session 或者 local storage 作了存儲)。使用 URL 路由器,能夠重建狀態,由於大部分傳遞的數據都存在於 URL 中。

3. 路由之間傳遞的隱式數據

即使不是狀態路由器,在轉變過程當中,你仍然能夠把複雜數據從一個路徑傳遞到另外一個上,而不用將數據做爲 URL 的一部分。

當使用 vue-router 從一個路由導航到另外一個路由時,你能夠傳遞隱式數據或狀態。

這在哪裏有用呢?主要是優化的時候。考慮下面的例子:

  1. 咱們有兩個頁面: 詳情頁 —— /users/:userId 簡介頁 —— /users/:userId/profile
  2. 在詳情頁面裏,咱們調起一個 API 請求獲取用戶信息。而且,頁面上有一個連接幫助用戶跳轉到簡介頁面。
  3. 第二個頁面上,咱們須要發起兩個 API 請求 —— 獲取用戶信息和獲取用戶概要。
  4. 這裏的問題是 —— 當我從詳情頁面導航到簡介頁面時作了兩次同樣的 API 請求。最佳的解決方案是當咱們用戶從詳情視圖頁轉變成簡介視圖頁時,把已檢索的用戶數據傳遞給下一個路由。另外,這些已檢索的數據不須要做爲 URL 的一部分(就像狀態路由器同樣,傳遞一個隱式的狀態)。
  5. 若是用戶經過任何其餘方式直接跳轉到簡介頁面,好比整個頁面刷新或者從其餘視圖進入,那麼在 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 對象注入在每一個組件中且是共享不可變的。否則會很難辦。

4. 導航保護阻塞父組件

若是你有嵌套配置,那麼任何子組件上的保護都有可能阻塞父組件的渲染。例如:

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 的一些概念以後,是時候討論關於編寫優雅代碼的模式了。

5. Vue-router 不是基於前綴(trie-based)的路由器

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,
        }
    ]
}];
複製代碼

基於樹結構配置有一些優勢:

  1. 結構清晰。易於維護。
  2. 受權/保護的管理變得容易。基於 CRUD (增刪改查) 的權限執行變得很是簡單。
  3. 比起扁平的路由列表有更可預見的路由。

使用基於樹結構配置的細微差異在於建立中間組件,它們可能只包含一個 router-view 組件。Vue-router 沒有將 RouterView 組件直接暴露給最終開發者。可是一個包裝 router-view 的小技巧能夠極大地幫助減小中間組件:

const RouterViewWrapper = Vue.extend({ 
    template: `<router-view></router-view>`
});

// 如今,能夠在路由配置樹的任何位置
// 使用 RouterViewWrapper 組件。
複製代碼

注意:Trie 是一種搜索樹數據結構的類型(譯者注:前綴樹)。基於前綴的路由是可預見的,而且無論路由的定義順序。在 Nodejs 生態環境裏,存在不少基於前綴或者相似的路由。Hapi.js 和 Fastify.js 使用的是基於前綴的路由。

簡而言之:

樹結構配置優於扁平結構配置。

6. 路由器的依賴注入

當你使用導航保護的時候,你可能在這些保護函數裏須要一些依賴。大多數常見的例子是 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: []
    })
}
複製代碼

7. 單次監聽路由對象

設想你在一個異步組件裏使用路由配置。異步組件是經過懶加載方式引入的。這一般是使用像 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();
        });
    }
}
複製代碼

8. 使用扁平路由混合監聽嵌套組件

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-behaviorlazy-loading 等。

此外,當咱們使用導航保護,預路由數據獲取時,vue-router 設計了關於用戶體驗(UX)的考量。你可使用全局或者組件內保護,但需謹慎地使用它們,所以你應該牢記關注點分離並把路由職責從組件中移除。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索