由于之前的搭建的单页面架构自动路由这块,配置meta信息,需要单独写配置文件,而且只配置支持二级路由,三级路由需要手写配置生成。动态路由也是需要配置id。对使用者不太友好。现在就想使用webpack 插件的形式,自动生成路由文件,meta信息在每个vue页面里面去配置,支持多级路由,来优化路由这块的代码。(网上也有一些自动路由插件,但不太合适接入到之前写的单页面架构中,于是就自己动手写了一个)
整体思路
1、写一个webpack插件,在代码运行时生成routes.js文件
2、代码运行时传入需要生成文件的目录,以及输出routes.js的目录路径
3、根据目录路径(system/user/index.vue)生成 嵌套树
system:{
value:[],
children:[]
}
4、根据嵌套树在递归遍历配置路由信息生成json数据
5、nodejs读取目录下vue文件里面的meta信息整合到路由Json中
[ { name: 'system',
path: 'system',
component: '@/views/autoRouter/system/_layout.vue',
index: 1,
meta:
'{\n title: \'系统管理\',\n icon: \'form\',\n permissionArray: [1, 2, 3],\n sortIndex: 1,\n newTime: \'2022-05-20\'\n }',
children: [ [Object], [Object], [Object] ] } ]
6、在将路由json数据递归匹配可生成routes.js 的字符串数据
7、使用nodejs 在输出的目录下写入routes.js 文件
代码实现
const fg = require('fast-glob') // 读取文件目录
const prettier = require('prettier') // 处理文件格式
1、定义一个自动路由插件类,在这个类里面定义一个方法实现目录路径输入,routes.js文件输出数据。在写入文件中
// 定义一个自动路由插件类
const generate = () => {
const code = generateRoutes(this.options)
let to
// 处理路由文件生成目录 默认与插件文件并级 如果配置routePath,那生成文件就是此路径下
if (this.options.routePath) {
to = path.join(process.cwd(), this.options.routePath)
} else {
to = path.join(__dirname, './routes.js')
}
// 以同步的方法检测目录是否存在。 如果目录存在 返回true,如果目录不存在 返回false 语法
// 读取文件信息 如果没有改变直接返回
if (fs.existsSync(to) &&
fs.readFileSync(to, 'utf8').trim() === code.trim()) {
return
}
fs.writeFileSync(to, code)
}
2、将目录生成嵌套树
/**
* 根据目录去生成路由
* @param pages 解析路由文件目录
* @param importPrefix import导入前缀目录
* @param layout 想要生成嵌套路由,目录下需要layout文件,用于配置父节点的meta信息 另个作用是当路由大于二级路由时,_layout需要有<router-view/>,不然页面不会显示
* @param common 过滤文件路径路径
* @returns {*}
*/
function generateRoutes({ pages = 'src/views', importPrefix = '@/views/', layout = '_layout.vue', common = 'common' }) {
// 指定文件不需要生成路由配置
const patterns = ['**/*.vue', `!**/${layout}`, `!**/${common}/*.vue`]
// 获取所有layout的文件路径
const layoutPaths = fg.sync(`**/${layout}`, {
cwd: pages,
onlyFiles: true
})
// 获取所有需要路由配置的文件路径
const paths = fg.sync(patterns, {
cwd: pages,
onlyFiles: true
})
const pathsArr = paths.map((p) => p.split('/'))
const layoutPathsArr = layoutPaths.map((p) => p.split('/'))
// 生成嵌套目录
const map = {}
layoutPathsArr.forEach((path) => {
const dir = path.slice(0, path.length - 1)
dir.unshift('rootPathLayoutName')
setToMap(map, pathToMapPath(path.slice(0, path.length - 1)), path)
})
pathsArr.forEach((path) => {
let dir = path
if (path.indexOf('index.vue') > -1) {
dir = path.slice(0, path.length - 1)
}
setToMap(map, pathToMapPath(dir), path)
})
// 将目录匹配生成路由json
const routerStr = pathMapToMeta({
children: map.children,
routers: [],
pages: pages,
importPrefix: importPrefix
})
// 将json转换文件字符串
return createRoutes(routerStr)
}
/**
* 将目录生成嵌套树 将每个目录生成key:{value:[],childer:[]}
* @param map {}
* @param paths 不带.vue的路径
* @param value 所有解析处理的层级目录数组
*/
function setToMap(map, paths, value) {
const target = paths.reduce((item, key) => {
if (!item.children) {
item.children = new Map()
}
let child = item.children.get(key)
if (!child) {
child = {}
item.children.set(key, child)
}
return child
}, map)
target.value = value
}
3、递归处理将嵌套树生成路由json数据,并读取vue页面中的meta信息
注意:想要生成嵌套路由,目录下需要layout文件,用于配置父节点的meta信息 另个作用是当路由大于二级路由时,_layout需要有,不然页面不会显示
/**
*
* @param children 嵌套目录
* @param routers 路由
* @param pages 需要自动生成文件的目录
* @param importPrefix import 引入页面文件的前缀目录
* @returns {*[]}
*/
function pathMapToMeta({ children, routers = [], pages, importPrefix }) {
Array.from(children.keys()).forEach((row) => {
const item = children.get(row)
// 配置参数
const router = {
name: row,
path: (row.indexOf('_id') > -1 ? (row.indexOf('_id') === 0 ? row.replace('_', ':') : row.replace('_', '/:')) : row),
component: '',
index: 1,
meta: `{ title: "${row}", icon: "form"}`,
children: []
}
// 如果value有值,说明可以根据文件路径取meta信息
if (item.value) {
router.component = path.join(importPrefix, item.value.join('/'))
const file = fs.readFileSync(path.join(pages, item.value.join('/')), 'utf8')
const metaArr = file.match(/\meta: {[^\{]+\}/g) || file.match(/\meta:{[^\{]+\}/g)
if (metaArr) {
const metaStr = metaArr[0]
// 匹配meta信息
let meta = ''
if (metaStr.indexOf('meta') > -1) {
meta = metaStr.substring(metaStr.indexOf('{'), metaStr.indexOf('}') + 1)
}
router.meta = meta
// 匹配排序
if (metaStr.indexOf('sortIndex') > -1) {
const sortIndexAll = metaStr.substring(metaStr.indexOf('sortIndex'), metaStr.indexOf('}'))
const sortIndex = sortIndexAll.substring(sortIndexAll.indexOf(':') + 1, (sortIndexAll.indexOf(',') > -1 ? sortIndexAll.indexOf(',') : sortIndexAll.length - 1))
router.index = Number(sortIndex)
}
}
}
// 如果有children 需要遍历循环匹配
if (item.children) {
router.children = pathMapToMeta({
children: item.children,
routers: router.children,
pages: pages,
importPrefix: importPrefix
})
}
routers.push(router)
})
return routers
}
4、将路由json数据匹配生成routes.js 文件字符串数据
/**
* 将路由json格式化成字符串
* @param routers 路由json
* @returns {*}
*/
function createRoutes(routers) {
const code = routers.map(createRoute)
return prettier.format(`import Layout from '@/layout'\nexport default [\n
${code}
\n]`, {
parser: 'babel',
semi: false,
singleQuote: true,
trailingComma: 'none' // 处理最后一行不加,的问题
})
}
/**
* 一级路由转换 第一级的时候需要把component换成Layout 如果没有子节点,则需要将父节点复制一份成为子节点,component指向文件路径
* @param map
* @param children
* @returns {string}
*/
function createRoute(map, children = {}) {
if (map.children && map.children.length !== 0) {
children = map.children.map(createRouteZJ)
return `\n{
path:'/${map.path}',
name:'${map.name}',
meta:${map.meta},
index:${map.index},
alwaysShow: false,
component: Layout,
children:[${children}]
}`
} else {
// 如果只有一级目录,需要单独处理name 不然会报警告name相同
children = `\n{
path:'${map.path}',
name:'${map.name}',
meta:${map.meta},
index:${map.index},
alwaysShow: false,
component:() => import('${map.component}')
}`
return `\n{
path:'/${map.path}',
name:'${map.name}p',
meta:${map.meta},
index:${map.index},
alwaysShow: false,
component: Layout,
children:[${children}]
}`
}
}
/**
* 二级及以上路由转换
* @param map json里面的children 子节点里面的path不需要'/'
* @param children
* @returns {string}
*/
function createRouteZJ(map, children = {}) {
if (map.children) {
children = map.children.map(createRouteZJ)
}
return `\n{
path:'${map.path}',
name:'${map.name}',
meta:${map.meta},
index:${map.index},
component:() => import('${map.component}'),
children:[${children}]
}`
}
使用方式
1、导入
npm install ff-auto-router
2、修改vue.config.js
const autoRouter = require('ff-auto-router/lib/router-webpack-plugin')
configureWebpack(config) {
config.plugins = [
...config.plugins,
// eslint-disable-next-line new-cap
new autoRouter({
pages: 'src/views/autoRouter',
importPrefix: '@/views/autoRouter',
routePath: 'src/router/routes.js'
})
]
}
pages 需要自动生成文件的目录
importPrefix import 引入页面文件的前缀目录
routePath 路由生成的文件目录,如果设置了则会在当前项目指定目录生成路由文件,否则可以从ff-auto-route
导入 ff-auto-router/lib/routes
其中meta
为vue-router
配置的meta
属性一致
写在每个组件的export default {}中
meta: {
title: '系统管理',
icon: 'form',
permissionArray: [1, 2, 3],
sortIndex: 1,
newTime: '2022-05-20'
}
3、修改src/router/index.js 文件,
这里的配置根据项目实际情况写。这样是基本写法可使用
import routes from './routes'
const createRouter = () => new Router({
mode: 'history', // require service support
base: process.env.BASE_URL,
scrollBehavior: () => ({ y: 0 }),
routes
})
展示效果
{
path: '/system',
name: 'system',
meta: {
title: '系统管理',
icon: 'form',
permissionArray: [1, 2, 3],
sortIndex: 1,
newTime: '2022-05-20'
},
index: 1,
alwaysShow: false,
component: Layout,
children: [
{
path: 'menu',
name: 'menu',
meta: {
title: '菜单管理',
icon: 'form',
permissionArray: [1, 2, 3],
sortIndex: 2,
newTime: '2022-05-20'
},
index: 2,
component: () => import('@/views/autoRouter/system/menu.vue'),
children: []
},
{
path: 'user',
name: 'user',
meta: {
title: '用户管理',
icon: 'form',
permissionArray: [1, 2, 3],
sortIndex: 1,
newTime: '2022-05-20'
},
index: 1,
component: () => import('@/views/autoRouter/system/user.vue'),
children: []
},
{
path: 'role/:id',
name: 'role_id',
meta: {
title: '角色管理',
icon: 'form',
permissionArray: [1, 2, 3],
sortIndex: 3,
newTime: '2022-05-20'
},
index: 3,
component: () => import('@/views/autoRouter/system/role_id/index.vue'),
children: []
}
]
}
代码可自行npm 下载查看
https://www.npmjs.com/package/ff-auto-router