1. 程式人生 > >Vue自動化路由(基於Vue-Router)開篇

Vue自動化路由(基於Vue-Router)開篇

# vue自動化路由 好久不見~ 若羽又開篇Vue的內容了。 年初的時候釋出了第一版的`ea-router`自動化路由庫,歡迎大家安裝使用。[[Github地址]](https://github.com/WhileKing/ea-router) [[npm地址]](https://www.npmjs.com/package/ea-router) 經歷一年的使用。還是發現了不少問題和不足的地方,因此在前段時間抽空整理了所有需求並做了個規劃。併發布了一個版本。下面來看看其中的原理和實現吧。 ## 前言 因為之前都是寫後端邏輯,因此接觸前端後始終不太習慣js的原生語法。更偏向於es6的`class`寫法,並且從**ECMAScript**後續的標準來看,官方也是比較推薦`class`的寫法來更好的組織程式碼,並使其具有更強的**表義性**。哈哈,當然因為更熟悉後者,所以更偏袒一點。 ## 功能需求 ![功能規劃圖](https://ask.qcloudimg.com/http-save/yehe-1428089/lmhz5q6l17.png) 功能主要分為兩部分: - 路由自動化 - 服務於庫的裝飾器 路由自動化中,除了原有的**自動生成**外,還增加了另外兩個在業務中會經常使用到的功能: 1. 設定預設的Layout 2. 設定預設的404頁面 目錄中的子目錄關係,用路由中**巢狀路由**來進行表達,因此需要一個入口進行**渲染**,這就是`Layout`存在的一個意義,另外一層則是作為某個模組的通用佈局存在。 **裝飾器**主要用於補充路由相關特性,比如`vue-router`中的各種特性(**命名路由**,**別名**,**params**等等),無縫的接入業務中。就像`vue-property-decorators`庫一樣。 ## 原理 ![原理圖](https://ask.qcloudimg.com/http-save/yehe-1428089/30u8lvuk78.png) 為了達成自動化路由的目的,本質就是要將路由物件按照某種特定的規則進行生成即可。那麼參考後端`MVC中的路由`以及其他前端路由框架,將需要路由的頁面按照目錄的層次結構進行組織,然後對目錄進行解析是比較通用並容易實現的。 1. 掃描目錄檔案 2. 還原目錄結構 3. 轉換為目錄物件 4. 載入介面卡(預設為`vue-router`的介面卡) 5. 介面卡將目錄物件轉換為`routes` 6. 使用`routes` ### 目錄物件 將實際的目錄結構對映成物件,下面看一個例子: 目錄結構如下: ``` views |-- About.vue |-- Home.vue |-- Layout.vue |-- user |-- Add.vue ``` `router/index.js`程式碼如下: ``` // /src/router/index.js import Vue from "vue"; import VueRouter from "vue-router"; import AutoRouteGenerator from "ea-router"; import defaultLayout from "../components/defaultLayout"; import notFoundPage from "../components/notFound"; Vue.use(VueRouter); let generator = new AutoRouteGenerator( require.context("../views", true, /\.vue$/) ); generator.setDefaultLayout(defaultLayout); generator.setNotFoundPage(notFoundPage); const routes = generator.generate(); const router = new VueRouter({ routes }); export default router; ``` 對應`vue-router`,自動生成的路由物件會是如下形式(裡面的物件是自動生成的,匯出語句不是喔,只是為了演示): ``` const routes = [ { path: "/", component: () => import("src/views/layout.vue"), children: [ { path: "home/:id/:name", component: () => import("src/views/home.vue"), props: true }, { path: "about", component: () => import("src/views/about.vue") }, { path: "user", component: () => import("src/components/defaultLayout.vue"), children: [ { path: "add", component: () => import("src/views/user/add.vue") } ] } ] }, { path: "*", component: () => import("src/components/notFound.vue") } ]; export default routes; ``` 因為使用的是`webpack`的`require.context`函式,但是它有一個缺陷就是掃描出來的並不是目錄原來的層次結構。而是一維的結構,因此我們首先要還原原來的層次結構,並在此基礎上封裝、解析一些必要的資訊。 ### 介面卡 **適配者模式**在這個場景下非常合適,**輸入**是解析後的目錄物件,而**輸出**則是變化的。有可能是: - `vue-router`的路由物件`routes` - `vue-router-next`的路由物件`routes` - 其他路由框架的路由物件 想要適配其他框架, 則只需要實現對應的介面卡並載入即可。 ## 使用 目前有3個api以及5個裝飾器 api: - [`generate`](#generate) - [`setDefaultLayout`](#setDefaultLayout) - [`setNotFoundPage`](#setNotFoundPage) decorators: - [`@RouteName`](#RouteName) - [`@Alias`](#Alias) - [`@Context`](#Context) - [`@EnableProps`](#EnableProps) - [`@Meta`](#Meta) ### `generate` api 建構函式中傳入通過`require.context`指定目錄及過濾規則, 如下例項是指定`views`目錄下所有`.vue`檔案。 路由生成的api, 呼叫此方法將生成一個對應 **路由介面卡** 生成的路由物件,目前預設內建的時基於`vue 2.x`的`vue-router`。 ``` // src/router/index.js import Vue from 'vue' import Router from 'vue-router' import RouteGenerator from "ea-router"; Vue.use(Router) let generator = new RouteGenerator(require.context('./views', true, /\.vue$/)) export default new Router({ routes: generator.generate() }) ``` 那麼在 `main.js` 中,我們不用改動原有的程式碼即可直接使用: ``` // src/main.js import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app') ``` ### `setDefaultLayout` api 指定預設的`Layout.vue`,因為在大多數情況下,`Layout.vue`的內容可能都是下面這樣: ``` ``` 這種情況下,`Layout.vue`的目的僅僅是作為子路由的入口。那麼我們可以直接利用`setDefaultLayout`來設定預設的`Layout`。 規則如下: - 噹噹前目錄中沒有`Layout.vue`時,會嘗試使用設定的預設`Layout`。 - 當沒有`Layout.vue`並且沒有設定預設`Layout`時,將會丟擲異常。 例項: ``` // src/router.js import Vue from 'vue' import Router from 'vue-router' import RouteGenerator from "ea-router"; import DefaultLayout from './components/defaultLayout.vue'; Vue.use(Router) let generator = new RouteGenerator(require.context('./views', true, /\.vue$/)) generator.setDefaultLayout(DefaultLayout); export default new Router({ routes: generator.generate() }) ``` ```
``` ### `setNotFoundPage` api 設定路由匹配失敗時顯示的頁面。 例項: ``` // src/router.js import Vue from 'vue' import Router from 'vue-router' import RouteGenerator from "ea-router"; import NotFoundPage from './components/notFound.vue'; Vue.use(Router) let generator = new RouteGenerator(require.context('./views', true, /\.vue$/)) generator.setNotFoundPage(NotFoundPage); export default new Router({ routes: generator.generate() }) ``` ```
``` ### `@RouteName(name: string)` decorator 設定路由名稱,在`vue-router`中對應了[**命名路由**](https://router.vuejs.org/zh/guide/essentials/named-routes.html) ``` import { Vue, Component } from 'vue-property-decorator'; import { RouteName } from 'ea-router'; @RouteName('YourComponentRouteName') @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory', name: 'YourComponentRouteName', component: YourComponent, } ] }) ``` **Note that:** path的生成規則是相對路徑噢(根目錄是建構函式中傳入的目錄,示例中也就是`src/views`) ### `@Alias(alias: string)` decorator 設定路由別名,對應`vue-router`中的[**別名**](https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#%252525E5%25252588%252525AB%252525E5%25252590%2525258D) ``` import { Vue, Component } from 'vue-property-decorator'; import { Alias } from 'ea-router'; @Alias('YourComponentAlias') @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory', alias: 'YourComponentAlias', component: YourComponent, } ] }) ``` ### `@Context(params: string[])` decorator 設定路由上下文,對應了`vue-router`中的[**$routes.params**](https://router.vuejs.org/zh/api/?#%252525E8%252525B7%252525AF%252525E7%25252594%252525B1%252525E5%252525AF%252525B9%252525E8%252525B1%252525A1%252525E5%252525B1%2525259E%252525E6%25252580%252525A7) 會根據傳入的順序生成`path`。 ``` import { Vue, Component } from 'vue-property-decorator'; import { Context } from 'ea-router'; @Context('id', 'type') @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory/:id/:type', component: YourComponent, } ] }) ``` **Note that:** 如果同時使用 `@Alias` 和 `@Context`, 上下文的引數會自動新增在`alias`後面, 就像下面的例子: ``` import { Vue, Component } from 'vue-property-decorator'; import { Context, Alias } from 'ea-router'; @Alias('YourComponentAlias') @Context('id', 'type') @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory/:id/:type', alias:'YourComponentAlias/:id/:type', component: YourComponent, } ] }) ``` ### `@EnableProps()` decorator 開啟路由引數的`Boolean`模式, 對應了`vue-router`中的[**路由傳參-布林模式**](https://router.vuejs.org/zh/guide/essentials/passing-props.html#%252525E5%252525B8%25252583%252525E5%252525B0%25252594%252525E6%252525A8%252525A1%252525E5%252525BC%2525258F) ``` import { Vue, Component } from 'vue-property-decorator'; import { EnableProps } from 'ea-router'; @EnableProps() @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory', props: true, component: YourComponent, } ] }) ``` **Note that:** 一般搭配 `@Context` 使用。 ### `@Meta(meta: object)` decorator 設定路由元資訊,對應`vue-router`中的[**路由元資訊**](https://router.vuejs.org/zh/guide/advanced/meta.html) ``` import { Vue, Component } from 'vue-property-decorator'; import { Meta } from 'ea-router'; @Meta({ title: 'Component Title', requireAuthorize: true }) @Component export default class YourComponent extends Vue { } ``` 等價於 ``` const router = new VueRouter({ routes: [ { path: 'path/to/component/on/directory', component: YourComponent, meta: { title: 'Component Title', requireAuthorize: true }, } ] }) ``` ## 建議 在開發過程中,使用`Class`形式的寫法是最為推薦的,表義性和組織性會更強一些。配合`vue-property-decorators` 食用更佳喔。好了,接下來說正經的: 1. 路由跳轉,建議使用命名路由的跳轉方式。[去檢視相關文件](https://router.vuejs.org/zh/guide/essentials/named-routes.html) 2. 給路由命名,並統一定義路由的名稱,便於管理(如,都定義在`/src/domain/views.js`中)。 3. 路由上下文使用`Props`進行傳參。 ## 計劃 - 實現 `vue-router-next` 的介面卡 - 實現路由檔案的自動生成(基於模板語法) - 新增可設定所有選項配置的裝飾器 - 開放載入自定義介面卡 - typescript支援 - 回補單元測試 ## 總結 做這個庫之前,也查找了很多相關資料。並且翻了不少類似庫的原始碼進行學習,發現比較常見的做法: 1. 動態載入,即請求時去`import` 實現動態**載入**。但這個只是做了自動**尋找**路由,對於路由的組織還是沒有比較好的解決。 2. `webpack`動態解析路徑,通過**正則表示式**或者**vue單檔案元件解析器**對檔案進行解析,提取內容。這種方式非常接近本文中的方式,但是缺點也比較明顯:**不支援變數**,如果全部硬編碼到檔案裡,管理也是一個問題。 最後結合大家的經驗,實現了這個庫。下一步也會考慮開始實現生成路由檔案,補充這一塊的空白。 關於自動化路由這部分,將會從**分析**、**實現**、**使用**以及後續開發都會記錄下來,並且會開源用了此庫的一些個人專案,形成系列文章。這篇就當是起個頭,如有不足,歡迎各位指正。 ~另外歡迎大家使用並提出寶貴的