Vue路由自動注入實踐

Qymh發表於2019-04-16

什麼是路由自動注入

路由自動注入概念學習自nuxt,我們不需要在router.js中每次手動輸入程式碼引入模組而是自動根據檔案目錄格式生成router.js

我們把這個功能獨立成一個webpack外掛,並對相關功能進行了完善,而且實現了vue-router的所有核心功能

更詳細使用指南和文件可以檢視我們的github倉庫

舉一個簡單的列子,比如你的目錄長這樣

src
├── views
│   ├── Login
│   │   └── Index.vue
│   └── User
│       ├── Account
│       │   └── Index.vue
│       ├── Home
│       │   └── Index.vue
│       └── Index.vue
複製程式碼

規則很簡單,如果目錄的一層是Index.vue,則目錄名便是當前的路由名字,如果是子資料夾則是第二層路由,之後自動生成的router.js會長成這樣

{
  component: () =>
    import('@/views/Login/Index.vue'),
  name: 'login',
  path: '/login'
},
{
  component: () =>
    import('@/views/User/Index.vue'),
  name: 'user',
  path: '/user'
},
{
  component: () =>
    import('@/views/User/Account/Index.vue'),
  name: 'user-account',
  path: '/user/account'
},
{
  component: () =>
    import('@/views/User/Home/Index.vue'),
  name: 'user-home',
  path: '/user/home'
}
複製程式碼

這裡值得一提的是其實生成的router.js是沒有必要加入到版本控制當中的,因為不論在開發(development)還是生產(production)第一次構建專案都會自動生成,比如你專案用到了giteslint,那麼應該把它放在.gitignore.eslintignore

為什麼使用路由自動注入

  • 方便

不用每次去引用模組,只用建立資料夾,router.js會自動生成

  • 統一路由命名

Vue路由自動注入實踐

如果有完整的code review這個問題是不會存在的,但我們稍微做了一點簡便,只要code review資料夾的命名就好了,最終生成的路由path會以駝峰命名,生成的name會以駝峰命名並且以連字元-連線不同層級的路由

  • 統一路由層級

Vue路由自動注入實踐

如圖片中的列子,我們無法從檔案的命名去判斷路由到底在幾級,而且經常寫的時候,明明是2級或3級路由卻和1級路由在一層路由下,這是很不規範而且與邏輯不符的

對比一下使用自動注入劃分層級後的路由

src/views
├── Index.vue
├── NotFound.vue
├── Withdraw
    <!-- 第一級 -->
│   ├── Index.vue
│   └── Result
│       ├── Description
            <!-- 第三級 -->
│       │   └── Index.vue
        <!-- 第二級 -->
│       └── Index.vue
└── WithdrawHistory
    <!-- 第一級 -->
    └── Index.vue
複製程式碼

可以從目錄結構看出路由的層級

我們再來看看生成的路由,不同層級的路由名字通過連字元-連線,層級很清晰

{
    component: () => import('@/views/Withdraw/Index.vue'),
    name: 'withdraw',
    path: '/withdraw'
},
{
    component: () => import('@/views/Withdraw/Result/Index.vue'),
    name: 'withdraw-result',
    path: '/withdraw/result'
},
{
    component: () => import('@/views/Withdraw/Result/Description/Index.vue'),
    name: 'withdraw-result-description',
    path: '/withdraw/result/description'
},
{
    component: () => import('@/views/WithdrawHistory/Index.vue'),
    name: 'withdrawHistory',
    path: '/withdrawHistory'
},
複製程式碼

為什麼選擇vue-router-invoke-webpack-plugin

  • 完善的單元測試

Vue路由自動注入實踐

  • types支援

Vue路由自動注入實踐

vue-router-invoke-webpack-plugin中獨特的路由劃分思維

當我們的頁面過多的時候,比如專案有60多個甚至70多個單頁面,檔案不可能會放在一個目錄下,一般這種時候,我們會按功能將相似功能的路由放在一個目錄下,我們之前也是這麼做的,其實這麼做也是沒啥問題的,但在路由自動注入下,我們提出了另外一種思路按路由層級劃分

什麼是層級劃分呢,簡單的一句話就是根據頁面所在的相對url地址進行劃分,舉個列子,我們的首頁如下

Vue路由自動注入實踐

首頁的路由為/,我們把首頁當作根路由,那麼可以進入的一級路由分別為提現 提現記錄 分成資料等,點選提現後,我們進入了提現路由/withdraw

Vue路由自動注入實踐

進入提現頁面後,會有兩處可點選,這兩處便是二級頁面,放在一級頁面的子資料夾中,按剛才的說法,路由目錄(擷取部分)便是這樣

src/views
├── Bank
    <!-- 銀行卡管理 -->
│   └── Index.vue
├── DivideData
    <!-- 分成資料 -->
│   └── Index.vue
<!- 首頁 --->
├── Index.vue
<!-- 404路由 -->
├── NotFound.vue
├── Withdraw
│   ├── BankDetails
        <!-- 提現中檢視銀行卡資訊 -->
│   │   └── Index.vue
│   ├── Description
        <!-- 提現說明 -->
│   │   └── Index.vue
    <!-- 提現頁面 -->
│   └── Index.vue
└── WithdrawHistory
     <!--提現記錄 -->
    └── Index.vue
複製程式碼

其實一般這麼分下來,相似功能的是會在一個資料夾下面的,也實現了按功能分路由的思路,而且這種層級劃分是一目瞭然的,很容易可以看出路由的從屬關係

但有時候也會遇到一個麻煩,就是有些頁面可能出現在當前層級下面,也可能出現在另外一個層級下面,按功能分的時候也有這種,就是功能可能存在於兩個功能點之間,這種情況其實可以考慮下在哪個層級的權重重一點或者從使用者的點選習慣考慮,哪個位置進去會多一點就放在哪個層級下面

vue-router-invoke-webpack-plugin中獨特的檔案結構

也許大家會有疑問,為啥非要寫成Index.vue並多加一層資料夾封裝,直接命名vue檔案不好嗎,用過nuxt的同學可能也會感覺到這一點的區別,這也是我們在nuxt的基礎上增加的一個feature,為了更友好的封裝一個單頁面

舉個列子,如果你的專案沒有引用ui庫,很多業務元件需要自己寫,除了常用的元件會放在目錄最外面的components檔案,其餘的對應一個單頁面的業務元件你會放在哪裡呢,這就是我們預留的位置,比如一個目錄結構如下

src/views
├── Audit
│   ├── Index.vue
│   ├── components
│   │   └── AuditItem.vue
│   └── images
│       └── AuditIntro.png
複製程式碼

Audit是我們的審批頁面,其中用到了一個只有當前頁面所用的AuditItem.vue元件,也引用了一個只有當前頁面所用到的圖片AuditIntro.png,獨特的檔案結構就是為了這種需求而生的,當前頁面的元件圖片放在一個資料夾中會更清晰,但值得一提的是,你也需要在外掛中設定ignore去忽略掉不被我們解析的目錄,比如這樣

plugins: [
  new VueRouterInvokeWebpackPlugin({
    dir: 'src/views',
    alias: '@/views',
    language: 'javascript',
    ignore: ['images', 'components', 'template.vue']
  })
];
複製程式碼

那麼 images components template.vue 會被忽略不解析

聊一聊路由許可權控制

關於前端控制路由許可權,前段時間看到過一個文章,感覺實現思路稍微複雜了點,其實有一個比較簡單的思路,就是後端給定當前使用者沒有許可權的路由,然後前端在beforeEach鉤子中去匹配,如果匹配到沒有許可權則直接跳404或者沒有許可權的頁面就行了,如果用vue-router-invoke-webpack-plugin寫會這麼寫

apis.getForbiddenRoute

export default {
  // 請求當前沒有許可權的路由列表
  async getForbiddenRoute() {
    return ['/single/user'];
  }
};
複製程式碼
plugins: [
    new VueRouterInvokePlugin({
      // 觀察的目錄
      dir: 'demos/src',
      // 觀察目錄的別名
      alias: '@/src',
      // 當前語言
      language: 'javascript',
      // 生成router.js的位置
      routerDir: 'demos',
      // 忽略資料夾
      ignore: ['images', 'template.vue', 'components', 'notfound.vue'],
      // 404路由地址
      notFound: '@/src/NotFound.vue',
      // 引用的模組
      modules: [
        {
          name: 'apis',
          package: '@/apis'
        }
      ],
      // 同scrollBehavior
      scrollBehavior: (to, from, savedPosition) => {
        if (savedPosition) {
          return savedPosition;
        } else {
          return { x: 0, y: 0 };
        }
      },
      <!-- 主要是這段程式碼 -->
      /* eslint-disable */
      beforeEach: async (to, from, next) => {
        // 通過繫結在靜態屬性上的_cachedForbiddenRoute判斷是否請求過介面
        if (!Vue._cachedForbiddenRoute) {
          Vue._cachedForbiddenRoute = [];
          await apis.getForbiddenRoute().then(res => {
            Vue._cachedForbiddenRoute = res;
          });
        }
        // 噹噹前頁面的地址存在於禁止訪問的列表中,則直接跳轉到404頁面
        if (Vue._cachedForbiddenRoute.includes(to.path)) {
          next({
            name: 'notFound'
          });
        } else {
          next();
        }
      }
    }),
]
複製程式碼

但話說回來,任何實現思路,前端獲取的介面資料想篡改還是能繞過去的,所以還是得後端再防一層

專案實現思路

專案實現不太複雜,但要照顧到的地方很多

  • 基本路由
  • 動態路由
  • 多層巢狀路由
  • 多層巢狀動態路由
  • meta替代品
  • 檔案不符合規則的友好處理
  • 命名轉換統一
  • node中原生fs模組十分不友好

要考慮的小細節還挺多的,特別是當路由過於複雜的情況

但node的fs的坑點是我沒有想到的,特別是在跨平臺上,所以我們捨棄了使用原生的fs模組,用chokidarfs-extra替代了fs的部分功能

前段時間也在學習vue的ast語法樹,所以學習了下思路去嘗試構建一棵ast,不過方法還是有區別的,vue構建語法樹是通過正則拆分了元素開始標籤 元素屬性 元素字元 元素結束標籤等然後拼接而成的,拼接的過程特別複雜,這個專案會簡單很多,直接通過檔案讀取遞迴遍歷目錄就可以生成一棵ast

Vue路由自動注入實踐

然後通過語法樹去構建字串的router.js,構建的過程還比較麻煩,最後將構建好的字串寫入檔案就大功告成了

專案還需要完善的地方

  • 單元測試

現在的單元測試覆蓋率已經100%了,但我覺得仍然有比較多稍微複雜的情況沒有寫到,之後會不僅看單元測試覆蓋率,而是按想到需要測試得功能點去補充完整

  • 測試環境

專案接入的是circleci,沒法在windows下測試,平常用的開發環境也是mac,所以測試環境方面之後還要去研究研究其他可以支援windows的ci工具,並對不同node版本進行測試

其實現在在windows下也有一個bug,但我發現nuxt也有這個bug,所以感覺可能這不是一個bug或許是一個feature,之後也會去提一個issue去請教一下,也不知道是不是我電腦的問題,簡單說就是fs.watch去監聽檔案目錄的時候(但這裡其實用的是chokidar,不過都一樣)當去改變之前已有的檔案目錄的名字是改不了的,windows下會提示你什麼當前檔案被引用了,需要結束掉程式這個檔名才能被修改

  • 更友好的支援

專案目前支援的是node版本> 8.15.1,僅支援webpack4,之後會支援webpack3和即將到來的webpack5

更多的功能

除了剛才提到的一個簡單路由的列子和設定忽略項,我們還支援了vue-router的其他核心功能,包括動態路由 巢狀路由 全域性路由守衛 meta替代品 等其他功能,相關功能點都寫在了我們開源倉庫的文件中,詳細的用法和注意事項,可以訪問我們的github倉庫,如果覺得專案還不錯的話,可以給我們點一顆小星星,當然如果你在使用中發現了和預期不太一樣的情況或者bug可以隨時給我們提issue

相關文章