在開(kāi)發(fā)vue項(xiàng)目時(shí)四啰,需要?jiǎng)?chuàng)建路由時(shí)都需要手動(dòng)到指定目錄文件配置歇攻,如果只是小項(xiàng)目可能還好译隘,但是如果是中大型項(xiàng)目亲桥,這個(gè)未免會(huì)顯得枯燥繁瑣,有沒(méi)有一種可以簡(jiǎn)化路由配置的方法呢固耘?就像Nuxt.js
服務(wù)端會(huì)依據(jù) pages
目錄結(jié)構(gòu)自動(dòng)生成 vue-router 模塊的路由配置题篷。接下來(lái)將由本人帶大家如何在非服務(wù)端渲染下實(shí)現(xiàn)路由自動(dòng)化。
為方便講解以下示例內(nèi)容基于vuecli4腳手架搭建厅目。
本文功能實(shí)現(xiàn)源碼地址:https://github.com/zhicaizhu123/z-auto-route番枚。
實(shí)現(xiàn)思路
- 路由
component
可以根據(jù)目錄結(jié)構(gòu)進(jìn)行自動(dòng)化創(chuàng)建法严。 - 路由元信息
meta
和其他路由信息
在需要路由配置的文件使用自定義塊(custom-blocks)包含自定義的路由配置信息,例如meta
葫笼,是否路由按需加載等信息深啤,如果文件不包含改自定義塊的文件則不會(huì)自動(dòng)生成路由配置。在本文中自定義塊為z-route
渔欢,在里面自定義需要的路由信息:
<z-route>
{
"dynamic": true,
"meta": {
"title": "首頁(yè)",
"icon": "el-icon-plus",
"auth": "homepage",
....
}
}
</z-route>
- 嵌套路由
如果是嵌套路由墓塌,可以在需要配置為子路由的文件的當(dāng)前目錄定義一個(gè)模板文件,在本文中模板文件是_layout.vue
奥额,里面定義嵌套路由的模板苫幢,只有有一個(gè)router-view
標(biāo)簽,如:
<!-- _layout.vue -->
<template>
<div>
<p>父頁(yè)面內(nèi)容</p>
<router-view></router-view>
</div>
</template>
- 路由動(dòng)態(tài)配置
如果想實(shí)現(xiàn)路由的動(dòng)態(tài)配置垫挨,例如/user/:id?
韩肝,可以通過(guò)創(chuàng)建_id.vue
或者_id/index.vue
文件實(shí)現(xiàn),例如九榔。
...
|-- user
|-- _id.vue
...
- 路由路徑
path
根據(jù)指定項(xiàng)目文件夾下創(chuàng)建的文件目錄結(jié)構(gòu)作為路由的訪問(wèn)路徑哀峻,本文指定的是views
文件夾,例如文件目錄如下哲泊。
|-- views
|-- _layout.vue
|-- homepage.vue
|-- system
|-- user
|-- index.vue
|-- _id.vue
根據(jù)上述目錄剩蟀,期望生成的path
如下
{
path: '/'
...
children: [
{
path: '/homepage',
...
},
{
path: '/user',
...
children:[
{
path: ':id?',
...
}
]
}
]
}
基于上述的實(shí)現(xiàn)思路,我們需要對(duì)vue文件目錄結(jié)構(gòu)和文件的內(nèi)容信息進(jìn)行獲取解析并根據(jù)解析的信息自動(dòng)生成所需的路由配置信息切威。所以我們需要用到webpack插件和vue-template-compiler解析vue文件的功能育特,無(wú)論是增、刪先朦、修改文件都可以監(jiān)聽(tīng)到并自動(dòng)更新路由信息缰冤。接下來(lái)我們將講解如何使用webpack編寫(xiě)一個(gè)插件和獲取并生成路由配置文件。
功能實(shí)現(xiàn)
webpack
插件由以下組成:
- 一個(gè) JavaScript 命名函數(shù)喳魏。
- 在插件函數(shù)的 prototype 上定義一個(gè)
apply
方法棉浸。- 指定一個(gè)綁定到 webpack 自身的事件鉤子。
- 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)刺彩。
- 功能完成后調(diào)用 webpack 提供的回調(diào)迷郑。
// 一個(gè) JavaScript 命名函數(shù)。
function MyExampleWebpackPlugin() {
};
// 在插件函數(shù)的 prototype 上定義一個(gè) `apply` 方法创倔。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定一個(gè)掛載到 webpack 自身的事件鉤子三热。
compiler.plugin('webpacksEventHook', function(compilation /* 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)。*/, callback) {
console.log("This is an example plugin!!!");
// 功能完成后調(diào)用 webpack 提供的回調(diào)三幻。
callback();
});
};
wepack事件鉤子有很多就漾,如果有需要的同學(xué)可以到webpack官方文檔查閱,本文自動(dòng)化路由webpack插件實(shí)現(xiàn)代碼如下:
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
// 設(shè)置完初始插件之后抑堡,執(zhí)行插件
compiler.hooks.afterPlugins.tap(pluginName, () => {
generate()
})
// 生成資源到 output 目錄之前執(zhí)行
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()
});
})
// 監(jiān)聽(tīng)模式停止執(zhí)行
compiler.hooks.watchClose.tap(pluginName, () => {
if (watcher) {
watcher.close()
watcher = null
}
})
}
}
上述代碼中可以看到我們?cè)诓寮跏蓟瓿傻臅r(shí)候(afterPlugins
)執(zhí)行了一次創(chuàng)建或者更新路由配置文件摆出,因?yàn)樵谑状螁?dòng)是自動(dòng)生成一份路由配置文件,然后在生成資源到 output 目錄之前監(jiān)聽(tīng)需要配置路由的文件夾文件變化首妖,如果監(jiān)聽(tīng)到變化則會(huì)更新路由配置文件偎漫,另外generateRoutes
方法會(huì)生成路由的配置信息然后被寫(xiě)入到指定目錄下的文件中,下面我們看下generateRoutes
方法到底做了些什么有缆?
export function generateRoutes({
pages = 'src/views',
importPrefix = '@/views/',
dynamic = true, // 是否需要按需加載
chunkNamePrefix = '',
layout = '_layout.vue',
}: GenerateConfig): string {
// 指定文件不需要生成路由配置
const patterns = ['**/*.vue', `!**/${layout}`]
// 獲取所有l(wèi)ayout的文件路徑
const layoutPaths = fg.sync(`**/${layout}`, {
cwd: pages,
onlyFiles: true,
})
// 獲取所有需要路由配置的文件路徑
const pagePaths = fg.sync(patterns, {
cwd: pages,
onlyFiles: true,
})
// 獲取路由配置信息
const metaList = resolveRoutePaths(
layoutPaths,
pagePaths,
importPrefix,
layout,
(file) => {
return fs.readFileSync(path.join(pages, file), 'utf8')
}
)
// 返回需要寫(xiě)入路由文件的內(nèi)容
return createRoutes(metaList, dynamic, chunkNamePrefix)
}
從上述代碼中我們可以看到象踊,我們首先需要獲取到模板文件和需要配置路由的文件路徑,然后resolveRoutePaths
方法根據(jù)這些信息進(jìn)一步獲取路由相關(guān)信息棚壁。接下來(lái)我們看下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) {
// 判斷是否是根模板文件,如果存在袖外,則將為模板文件生成嵌套文件映射關(guān)系
splitedLayouts.forEach((path) => {
let dir = path.slice(0, path.length - 1)
// 判斷是否有自定義塊史隆,如果有才生成相關(guān)信息
dir.unshift(rootPathLayoutName)
setToMap(map, pathToMapPath(dir), path)
})
} else {
將為模板文件生成嵌套文件映射關(guān)系
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)) {
// 判斷是否有自定義塊,如果有才生成相關(guān)信息
let dir = path
if (hasRootLayout) {
// 如果有根模板文件者需要在當(dāng)前路徑前下插入模板的路徑信息
dir.unshift(rootPathLayoutName)
}
// 生成嵌套文件映射關(guān)系
setToMap(map, pathToMapPath(dir), path)
}
})
return pathMapToMeta(map, importPrefix, readFile, 0, layout)
}
// 獲取自定義標(biāo)簽內(nèi)容
function getRouteBlock(path: string[], readFile: (path: string) => string) {
const content = readFile(path.join('/'))
// 解析vue文件下內(nèi)容
const parsed = parseComponent(content, {
pad: 'space',
})
// 獲取自定義塊的內(nèi)容
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)
}
// 將嵌套的映射關(guān)系轉(zhuǎn)換成路由需要的配置信息
function pathMapToMeta(
map: NestedMap<string[]>,
importPrefix: string,
readFile: (path: string) => string,
parentDepth: number = 0,
layout: string,
): PageMeta[] {
if (map.value) {
const path = map.value
if (path[0] === rootPathLayoutName) {
path.shift()
}
...
const routeBlock = getRouteBlock(path, readFile)
if (routeBlock) {
// 判斷是否有自定義塊曼验,如果有則將轉(zhuǎn)換為生成的路由信息
meta.route = tryParseCustomBlock(routeBlock.content, path, routeBlockName)
}
...
return [meta]
}
...
}
...
從上述代碼中沒(méi)有把具體的實(shí)現(xiàn)細(xì)節(jié)呈現(xiàn)出來(lái)泌射,但是我們大概可以知道整體思路,我們優(yōu)先會(huì)獲取模板文件路徑信息鬓照,然后使用setToMap
方法根據(jù)這個(gè)信息生成一個(gè)映射關(guān)系熔酷,緊接著處理非模板需要配置成路由的文件,同樣setToMap
方法根據(jù)它們的路徑信息生成一個(gè)映射關(guān)系豺裆,通過(guò)getRouteBlock
和tryParseCustomBlock
方法解析每個(gè)文件的自定義塊信息拒秘,最后結(jié)合映射關(guān)系和自定義塊的信息生成我們期望的路由配置信息,具體實(shí)現(xiàn)可以到z-auto-route查看具體實(shí)現(xiàn)留储。
實(shí)際項(xiàng)目使用配置
在需要生成路由的 vue
文件頭部加上z-route
標(biāo)簽,里面內(nèi)容為 JSON
格式
<z-route>
{
"dynamic": false,
"meta": {
"title": "根布局頁(yè)面"
}
}
</z-route>
其中meta
為vue-router
配置的meta
屬性一致咙轩,dynamic
為單獨(dú)設(shè)置該路由是否為按需加載获讳,不設(shè)置默認(rèn)使用全局配置的dynamic
注意:
- 如果沒(méi)有
z-route
標(biāo)簽則該頁(yè)面不會(huì)不會(huì)生成路由 - 暫時(shí)只支持
meta
和dynamic
兩個(gè)設(shè)置項(xiàng)。 - 如果需要
z-route
標(biāo)簽高亮活喊,可以設(shè)置vs-code
的settings.json
"vetur.grammar.customBlocks": {
"z-route": "json"
}
執(zhí)行 vscode
命令
Vetur: Generate grammar from vetur.grammar.customBlocks
webpack 配置
在 weppack
配置文件中配置內(nèi)容丐膝,以下為 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', // 路由頁(yè)面文件存放地址, 默認(rèn)為'src/views'
importPrefix: '@/views/', // import引入頁(yè)面文件的前綴目錄钾菊,默認(rèn)為'@/views/'
}),
]
}
...
路由文件配置
// 路由初始化
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from 'z-auto-route'
Vue.use(VueRouter)
// 根據(jù)項(xiàng)目額外配置相關(guān)信息帅矗,例如根據(jù)路由生成菜單信息等
// ...
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
})
export default router
實(shí)例項(xiàng)目目錄
|-- views
|-- _layout.vue // 全局布局組件
|-- homepage.vue // 首頁(yè)
|-- system // 系統(tǒng)管理
|-- _layout.vue // 嵌套路由
|-- role // 角色管理
|-- index.vue
|-- user // 用戶管理
|-- index
|-- _id // 用戶詳情
|-- index.vue
生成路由結(jié)構(gòu)
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: '系統(tǒng)管理'
},
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: '首頁(yè)'
},
dynamic: false,
sortIndex: -1
}
]
}
]
項(xiàng)目效果圖
參考源碼
結(jié)語(yǔ)
文中如有錯(cuò)誤,歡迎在評(píng)論區(qū)指正煞烫,如果本篇文章的內(nèi)容可以提高同學(xué)們?cè)陧?xiàng)目中的開(kāi)發(fā)效率浑此,歡迎點(diǎn)贊和關(guān)注,源碼地址:https://github.com/zhicaizhu123/z-auto-route滞详。