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

vue自動化路由

好久不見~ 若羽又開篇Vue的內容了。
年初的時候發布了第一版的ea-router自動化路由庫,歡迎大家安裝使用。[Github地址] [npm地址]

經歷一年的使用。還是發現了不少問題和不足的地方,因此在前段時間抽空整理了所有需求並做了個規劃。並發布了一個版本。下面來看看其中的原理和實現吧。

前言

因為之前都是寫後端邏輯,因此接觸前端後始終不太習慣js的原生語法。更偏向於es6的class寫法,並且從ECMAScript後續的標準來看,官方也是比較推薦class的寫法來更好的組織程式碼,並使其具有更強的表義性。哈哈,當然因為更熟悉後者,所以更偏袒一點。

功能需求

功能規劃圖

功能主要分為兩部分:

  • 路由自動化
  • 服務於庫的裝飾器

路由自動化中,除了原有的自動生成外,還增加了另外兩個在業務中會經常使用到的功能:

  1. 設置預設的Layout
  2. 設置預設的404頁面

目錄中的子目錄關係,用路由中嵌套路由來進行表達,因此需要一個入口進行渲染,這就是Layout存在的一個意義,另外一層則是作為某個模組的通用布局存在。

裝飾器主要用於補充路由相關特性,比如vue-router中的各種特性(命名路由別名params等等),無縫的接入業務中。就像vue-property-decorators庫一樣。

原理

原理圖

為了達成自動化路由的目的,本質就是要將路由對象按照某種特定的規則進行生成即可。那麼參考後端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;

因為使用的是webpackrequire.context函數,但是它有一個缺陷就是掃描出來的並不是目錄原來的層次結構。而是一維的結構,因此我們首先要還原原來的層次結構,並在此基礎上封裝、解析一些必要的資訊。

適配器

適配者模式在這個場景下非常合適,輸入是解析後的目錄對象,而輸出則是變化的。有可能是:

  • vue-router的路由對象routes
  • vue-router-next的路由對象routes
  • 其他路由框架的路由對象
    想要適配其他框架, 則只需要實現對應的適配器並載入即可。

使用

目前有3個api以及5個裝飾器

api:

decorators:

generate api

構造函數中傳入通過require.context指定目錄及過濾規則, 如下實例是指定views目錄下所有.vue文件。

路由生成的api, 調用此方法將生成一個對應 路由適配器 生成的路由對象,目前默認內置的時基於vue 2.xvue-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的內容可能都是下面這樣:

<template>
    <router-view></router-view>
</template>

這種情況下,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()
})
<!-- /src/components/defaultLayout.vue -->
<template>
    <router-view></router-view>
</template>

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()
})
<!-- /src/components/notFound.vue -->
<template>
    <div>
        嘿,頁面走丟啦!    
    </div>
</template>

@RouteName(name: string) decorator

設置路由名稱,在vue-router中對應了命名路由

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中的別名

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

會根據傳入的順序生成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中的路由傳參-布爾模式

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中的路由元資訊

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. 路由跳轉,建議使用命名路由的跳轉方式。去查看相關文檔
  2. 給路由命名,並統一定義路由的名稱,便於管理(如,都定義在/src/domain/views.js中)。
  3. 路由上下文使用Props進行傳參。

計劃

  • 實現 vue-router-next 的適配器
  • 實現路由文件的自動生成(基於模板語法)
  • 添加可設置所有選項配置的裝飾器
  • 開放載入自定義適配器
  • typescript支援
  • 回補單元測試

總結

做這個庫之前,也查找了很多相關資料。並且翻了不少類似庫的源碼進行學習,發現比較常見的做法:

  1. 動態載入,即請求時去import 實現動態載入。但這個只是做了自動尋找路由,對於路由的組織還是沒有比較好的解決。
  2. webpack動態解析路徑,通過正則表達式或者vue單文件組件解析器對文件進行解析,提取內容。這種方式非常接近本文中的方式,但是缺點也比較明顯:不支援變數,如果全部硬編碼到文件里,管理也是一個問題。

最後結合大家的經驗,實現了這個庫。下一步也會考慮開始實現生成路由文件,補充這一塊的空白。

關於自動化路由這部分,將會從分析實現使用以及後續開發都會記錄下來,並且會開源用了此庫的一些個人項目,形成系列文章。這篇就當是起個頭,如有不足,歡迎各位指正。

~另外歡迎大家使用並提出寶貴的意見喲