Vue Router自動化路由
在開發vue專案時,需要建立路由時都需要手動到指定目錄檔案配置,如果只是小專案可能還好,但是如果是中大型專案,這個未免會顯得枯燥繁瑣,有沒有一種可以簡化路由配置的方法呢?就像Nuxt.js
服務端會依據 pages
目錄結構自動生成 vue-router 模組的路由配置。接下來將由本人帶大家如何在非服務端渲染下實現路由自動化。
為方便講解以下示例內容基於vuecli4腳手架搭建。 本文功能實現原始碼地址:github.com/zhicaizhu12…。
實現思路
- 路由
component
可以根據目錄結構進行自動化建立。 - 路由元資訊
meta
和其他路由資訊 在需要路由配置的檔案使用自定義塊(custom-blocks)meta
,是否路由按需載入等資訊,如果檔案不包含改自定義塊的檔案則不會自動生成路由配置。在本文中自定義塊為z-route
,在裡面自定義需要的路由資訊:
<z-route>
{
"dynamic": true,"meta": {
"title": "首頁","icon": "el-icon-plus","auth": "homepage",....
}
}
</z-route>
複製程式碼
- 巢狀路由
如果是巢狀路由,可以在需要配置為子路由的檔案的當前目錄定義一個模板檔案,在本文中模板檔案是_layout.vue
,裡面定義巢狀路由的模板,只有有一個router-view
<!-- _layout.vue -->
<template>
<div>
<p>父頁面內容</p>
<router-view></router-view>
</div>
</template>
複製程式碼
- 路由動態配置
如果想實現路由的動態配置,例如
/user/:id?
,可以通過建立_id.vue
或者_id/index.vue
檔案實現,例如。
...
|-- user
|-- _id.vue
...
複製程式碼
- 路由路徑
path
根據指定專案資料夾下建立的檔案目錄結構作為路由的訪問路徑,本文指定的是views
|-- views
|-- _layout.vue
|-- homepage.vue
|-- system
|-- user
|-- index.vue
|-- _id.vue
複製程式碼
根據上述目錄,期望生成的path
如下
{
path: '/'
...
children: [
{
path: '/homepage',...
},{
path: '/user',...
children:[
{
path: ':id?',...
}
]
}
]
}
複製程式碼
基於上述的實現思路,我們需要對vue檔案目錄結構和檔案的內容資訊進行獲取解析並根據解析的資訊自動生成所需的路由配置資訊。所以我們需要用到webpack外掛和vue-template-compiler解析vue檔案的功能,無論是增、刪、修改檔案都可以監聽到並自動更新路由資訊。接下來我們將講解如何使用webpack編寫一個外掛和獲取並生成路由配置檔案。
功能實現
webpack
外掛由以下組成:
- 一個 JavaScript 命名函式。
- 在外掛函式的 prototype 上定義一個
apply
方法。- 指定一個繫結到 webpack 自身的事件鉤子。
- 處理 webpack 內部例項的特定資料。
- 功能完成後呼叫 webpack 提供的回撥。
// 一個 JavaScript 命名函式。
function MyExampleWebpackPlugin() {
};
// 在外掛函式的 prototype 上定義一個 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定一個掛載到 webpack 自身的事件鉤子。
compiler.plugin('webpacksEventHook',function(compilation /* 處理 webpack 內部例項的特定資料。*/,callback) {
console.log("This is an example plugin!!!");
// 功能完成後呼叫 webpack 提供的回撥。
callback();
});
};
複製程式碼
wepack事件鉤子有很多,如果有需要的同學可以到webpack官方文件查閱,本文自動化路由webpack外掛實現程式碼如下:
class AutoRoutingPlugin {
constructor(private options: Options) { }
apply(compiler: Compiler) {
// 更新路由配置資訊
const generate = () => {
const code = generateRoutes(this.options)
const to = this.options.routePath ?path.join(process.cwd(),this.options.routePath) : path.join(__dirname,'./routes.js')
if (
fs.existsSync(to) &&
fs.readFileSync(to,'utf8').trim() === code.trim()
) {
return
}
fs.writeFileSync(to,code)
}
let watcher: any = null
// 設定完初始外掛之後,執行外掛
compiler.hooks.afterPlugins.tap(pluginName,() => {
generate()
})
// 生成資源到 output 目錄之前執行
compiler.hooks.emit.tap(pluginName,() => {
const chokidar = require('chokidar')
watcher = chokidar.watch(path.join(process.cwd(),this.options.pages || 'src/views'),{
persistent: true,}).on('change',() => {
generate()
});
})
// 監聽模式停止執行
compiler.hooks.watchClose.tap(pluginName,() => {
if (watcher) {
watcher.close()
watcher = null
}
})
}
}
複製程式碼
上述程式碼中可以看到我們在外掛初始化完成的時候(afterPlugins
)執行了一次建立或者更新路由配置檔案,因為在首次啟動是自動生成一份路由配置檔案,然後在生成資源到 output 目錄之前監聽需要配置路由的資料夾檔案變化,如果監聽到變化則會更新路由配置檔案,另外generateRoutes
方法會生成路由的配置資訊然後被寫入到指定目錄下的檔案中,下面我們看下generateRoutes
方法到底做了些什麼?
export function generateRoutes({
pages = 'src/views',importPrefix = '@/views/',dynamic = true,// 是否需要按需載入
chunkNamePrefix = '',layout = '_layout.vue',}: GenerateConfig): string {
// 指定檔案不需要生成路由配置
const patterns = ['**/*.vue',`!**/${layout}`]
// 獲取所有layout的檔案路徑
const layoutPaths = fg.sync(`**/${layout}`,{
cwd: pages,onlyFiles: true,})
// 獲取所有需要路由配置的檔案路徑
const pagePaths = fg.sync(patterns,})
// 獲取路由配置資訊
const metaList = resolveRoutePaths(
layoutPaths,pagePaths,importPrefix,layout,(file) => {
return fs.readFileSync(path.join(pages,file),'utf8')
}
)
// 返回需要寫入路由檔案的內容
return createRoutes(metaList,dynamic,chunkNamePrefix)
}
複製程式碼
從上述程式碼中我們可以看到,我們首先需要獲取到模板檔案和需要配置路由的檔案路徑,然後resolveRoutePaths
方法根據這些資訊進一步獲取路由相關資訊。接下來我們看下resolveRoutePaths
方法到底做了什麼?
export function resolveRoutePaths(
layoutPaths: string[],paths: string[],importPrefix: string,layout: string,readFile: (path: string) => string
): PageMeta[] {
const map: NestedMap<string[]> = {}
// 分割模板路徑為單元資訊
const splitedLayouts = layoutPaths.map((p) => p.split('/'))
const hasRootLayout = splitedLayouts.some(item => item.length === 1)
if (hasRootLayout) {
// 判斷是否是根模板檔案,如果存在,則將為模板檔案生成巢狀檔案對映關係
splitedLayouts.forEach((path) => {
let dir = path.slice(0,path.length - 1)
// 判斷是否有自定義塊,如果有才生成相關資訊
dir.unshift(rootPathLayoutName)
setToMap(map,pathToMapPath(dir),path)
})
} else {
將為模板檔案生成巢狀檔案對映關係
splitedLayouts.forEach((path) => {
setToMap(map,pathToMapPath(path.slice(0,path.length - 1)),path)
})
}
const splitted = paths.map((p) => p.split('/'))
splitted.forEach((path) => {
if (hasRouteBlock(path,readFile)) {
// 判斷是否有自定義塊,如果有才生成相關資訊
let dir = path
if (hasRootLayout) {
// 如果有根模板檔案者需要在當前路徑前下插入模板的路徑資訊
dir.unshift(rootPathLayoutName)
}
// 生成巢狀檔案對映關係
setToMap(map,path)
}
})
return pathMapToMeta(map,readFile,layout)
}
// 獲取自定義標籤內容
function getRouteBlock(path: string[],readFile: (path: string) => string) {
const content = readFile(path.join('/'))
// 解析vue檔案下內容
const parsed = parseComponent(content,{
pad: 'space',})
// 獲取自定義塊的內容
return parsed.customBlocks.find(
(b) => b.type === routeBlockName
)
}
// 是否有自定義塊
function hasRouteBlock(path: string[],readFile: (path: string) => string) {
const routeBlock = getRouteBlock(path,readFile)
return routeBlock && tryParseCustomBlock(routeBlock.content,path,routeBlockName)
}
// 將巢狀的對映關係轉換成路由需要的配置資訊
function pathMapToMeta(
map: NestedMap<string[]>,readFile: (path: string) => string,parentDepth: number = 0,): PageMeta[] {
if (map.value) {
const path = map.value
if (path[0] === rootPathLayoutName) {
path.shift()
}
...
const routeBlock = getRouteBlock(path,readFile)
if (routeBlock) {
// 判斷是否有自定義塊,如果有則將轉換為生成的路由資訊
meta.route = tryParseCustomBlock(routeBlock.content,routeBlockName)
}
...
return [meta]
}
...
}
...
複製程式碼
從上述程式碼中沒有把具體的實現細節呈現出來,但是我們大概可以知道整體思路,我們優先會獲取模板檔案路徑資訊,然後使用setToMap
方法根據這個資訊生成一個對映關係,緊接著處理非模板需要配置成路由的檔案,同樣setToMap
方法根據它們的路徑資訊生成一個對映關係,通過getRouteBlock
和tryParseCustomBlock
方法解析每個檔案的自定義塊資訊,最後結合對映關係和自定義塊的資訊生成我們期望的路由配置資訊,具體實現可以到z-auto-route檢視具體實現。
實際專案使用配置
在需要生成路由的 vue
檔案頭部加上z-route
標籤,裡面內容為 JSON
格式
<z-route>
{
"dynamic": false,"meta": {
"title": "根佈局頁面"
}
}
</z-route>
複製程式碼
其中meta
為vue-router
配置的meta
屬性一致,dynamic
為單獨設定該路由是否為按需載入,不設定預設使用全域性配置的dynamic
注意:
- 如果沒有
z-route
標籤則該頁面不會不會生成路由 - 暫時只支援
meta
和dynamic
兩個設定項。 - 如果需要
z-route
標籤高亮,可以設定vs-code
的settings.json
"vetur.grammar.customBlocks": {
"z-route": "json"
}
複製程式碼
執行 vscode
命令
Vetur: Generate grammar from vetur.grammar.customBlocks
複製程式碼
webpack 配置
在 weppack
配置檔案中配置內容,以下為 vue.config.js
的配置資訊
// vue.config.js
const ZAutoRoute = require('z-auto-route/lib/webpack-plugin')
...
configureWebpack: (config) => {
config.plugins = [
...config.plugins,new ZAutoRoute({
pages: 'src/views',// 路由頁面檔案存放地址, 預設為'src/views'
importPrefix: '@/views/',// import引入頁面檔案的字首目錄,預設為'@/views/'
}),]
}
...
複製程式碼
路由檔案配置
// 路由初始化
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from 'z-auto-route'
Vue.use(VueRouter)
// 根據專案額外配置相關資訊,例如根據路由生成選單資訊等
// ...
const router = new VueRouter({
mode: 'history',base: process.env.BASE_URL,routes,})
export default router
複製程式碼
例項專案目錄
|-- views
|-- _layout.vue // 全域性佈局元件
|-- homepage.vue // 首頁
|-- system // 系統管理
|-- _layout.vue // 巢狀路由
|-- role // 角色管理
|-- index.vue
|-- user // 使用者管理
|-- index
|-- _id // 使用者詳情
|-- index.vue
複製程式碼
生成路由結構
import _layout from '@/views/_layout.vue'
function system__layout() {
return import(
/* webpackChunkName: "system-layout" */ '@/views/system/_layout.vue'
)
}
function system_role_index() {
return import(
/* webpackChunkName: "system-role-index" */ '@/views/system/role/index.vue'
)
}
function system_user_index() {
return import(
/* webpackChunkName: "system-user-index" */ '@/views/system/user/index.vue'
)
}
function system_user__id_index() {
return import(
/* webpackChunkName: "system-user-id-index" */ '@/views/system/user/_id/index.vue'
)
}
import homepage from '@/views/homepage.vue'
export default [
{
name: 'layout',path: '/',component: _layout,meta: {
title: '佈局元件',hide: true
},dynamic: false,children: [
{
name: 'system-layout',path: '/system',component: system__layout,meta: {
title: '系統管理'
},sortIndex: 0,children: [
{
name: 'system-role',path: 'role',component: system_role_index,meta: {
title: '角色管理'
}
},{
name: 'system-user',path: 'user',component: system_user_index,meta: {
title: '使用者管理'
}
},{
name: 'system-user-id',path: 'user/:id',component: system_user__id_index,meta: {
title: '使用者詳情',hide: true
}
}
]
},{
name: 'homepage',path: '/homepage',component: homepage,meta: {
title: '首頁'
},sortIndex: -1
}
]
}
]
複製程式碼
專案效果圖
參考原始碼
結語
文中如有錯誤,歡迎在評論區指正,如果本篇文章的內容可以提高同學們在專案中的開發效率,歡迎點贊和關注,原始碼地址:github.com/zhicaizhu12…。