Vue自動化路由(基於Vue-Router)開篇
阿新 • • 發佈:2021-01-08
# 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單檔案元件解析器**對檔案進行解析,提取內容。這種方式非常接近本文中的方式,但是缺點也比較明顯:**不支援變數**,如果全部硬編碼到檔案裡,管理也是一個問題。
最後結合大家的經驗,實現了這個庫。下一步也會考慮開始實現生成路由檔案,補充這一塊的空白。
關於自動化路由這部分,將會從**分析**、**實現**、**使用**以及後續開發都會記錄下來,並且會開源用了此庫的一些個人專案,形成系列文章。這篇就當是起個頭,如有不足,歡迎各位指正。
~另外歡迎大家使用並提出寶貴的