仿nuxt.js,自动构建路由,释放你的双手?!
前言
写过nuxt.js的同学,肯定也对nuxt的路由规则有一定的了解,在pages目录下创建文件,即可以自动构建路由,本文来带领大家实现一下在vue里怎么去自动构建路由。我这里使用的是Vue-cli 3.X版本进行初始化。cli 2.X版本也是一样只不过初始化出来的文件目录不一。
做前准备
了解require.context:
它是webpack里一个重要的API,它可以通过一个目录进行搜索,useSubdirectories指定是否搜索子目录,以及与文件匹配的正则表达式。很好理解,它可以对指定目录进行搜索,所以我们可以用它来自动构建路由,也可以用它来构建组建。
- directory:说明需要检索的目录
- useSubdirectories:是否检索子目录
- regExp: 匹配文件的正则表达式
了解nuxt.js路由规则
这里就简单讲一下,不做详诉。想要深入了解:nuxt.js官网地址:https://zh.nuxtjs.org
- 基础路由:文件为index.vue,则path在其对应的路由为空字符。即:有pages/user/index.vue文件,则路由path为:’/user’。 而路由的name由文件目录组成,多级以-拼接,如:pages/user/index.vue文件,name为:user-index
- 嵌套路由:如果想要使用嵌套路由,则在根目录下需要创建同名的文件夹和.vue文件。如跟层级下有pages/user/_id.vue与pages/user.vue。就构成嵌套路由。
- 动态路由:文件以下划线_开头即为动态路由,如有文件pages/user/_id.vue,那么路由path为’user/:id’
开始
一:初始文件目录
利用vue create XXX 创建项目,选配置的时候需要勾选router。初始化之后,我们把默认的views文件夹更名为pages(对应nuxt.js的路由目录),在router文件夹下创建routes.js,在routes里初始化routes导出给router/index.js里引入
# router/index/js
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes' // 引入routes
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
# router/routes/js
const routes = []
export default routes
此时的文件目录如下:(about.vue、home.vue为cli创建后默认带有的文件)
二:在router/routes.js开始写自动构建的逻辑。
1:使用require.context
根据之前我们说过的三个参数,以及我们的目录结构写出以下代码
# router/routes.js
let files = require.context('../pages', true, /\.vue$/) // 根据目录结构去搜索文件
let filesKey = files.keys() // 获取整个目录结构
console.log(files)
console.log(filesKey)
const routes = []
export default routes
我们来看看打印出了什么,files是一个方法,具体什么做用这里先不说,而fileskey我们可以看到打印出来的就是一个文件地址的数组。有了地址便可以根据规则来构建routes
2:但上面仅仅只是基础路由,我们创建动态路由、嵌套路由与动态嵌套路由,为了逻辑的区分,我们删除原有的pages里的文件,再新增文件,目录结构如下:
# pages/index.vue
<template>
<div class="index">
这个是默认的index,路由为:/。基础路由
</div>
</template>
<script>
export default {
name: 'index'
}
</script>
# pages/group.vue
<template>
<div class="group">
group页面 嵌套路由带有route-view,有children属性
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'group'
}
</script>
# pages/group/user.vue
<template>
<div class="user">
group/user页面 嵌套路由带有route-view,有children属性
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'user'
}
</script>
# pages/group/user/_id.vue
<template>
<div class="id">
group/user/:id页面,动态路由
<div>userid为:{{$route.params.id}}</div>
</div>
</template>
<script>
export default {
name: 'id'
}
</script>
现在我们再看一下打印出来的fileskey是什么?
看到现在的fileskey,我们可能会发现,基础路由与动态路由都很简单,只要按照规则replace就好了。但是嵌套路由就很麻烦。根据我们之前打印出的fileskey,还是会有很多疑问:
- 由打印出来的数据,怎么区分最后到底有几个路由对象?(路由会有嵌套关系)
- 我们怎么知道某个路由有没有children?
- 嵌套的深度从何得知?最大的深度呢?
- 我们怎么知道某个路由是被另一个路由嵌套的?
3:分析问题,给予解答。(解答只是思路,具体的代码逻辑在后面)
- 由打印出来的数据,怎么区分最后到底有几个路由对象?(路由会有嵌套关系)
答:我们可以根据一级目录的文件名开始分类,建一个map对象,对象里的key就是一级目录文件的名称,而value就是一个数组,把所有是此目录下的文件全部收集到数组中。(一级.vue文件对象的value数组中就只有自己)
- 我们怎么知道某个路由有没有children?
答:当某个名称叫name的文件满足能在它所对应的数组中查找出来有:name.vue(有.vue文件), name/XXX(有name文件夹),那么久必定有children属性
- 嵌套的深度从何得知?最大的深度呢?
map对象里的数组存的是文件的路径,而其中我们可以用正则提出不必要的相对路径与.vue,如获得:group/user/_id。按字符‘/’切割出来数组的长度就是深度了。对数组中的每个对象切割出来的length进行比较,最大的即为最大深度。
- 我们怎么知道某个路由是被另一个路由嵌套的?
答:循环从1开始到最大深度结束,一层一层的搭建route,而某层的route(如3层)想要知道自己被谁嵌套,就要把自己的的所有父层级获取到(比如当前层级为:./group/user/_id.vue,那么所有的父层级为group、user),去route中一层一层的找出来。
4:创建getRouteItemName方法
/**
* 获取路由name
* @param {*} file type:string (文件完整的目录)
*/
const getRouteItemName = (file) => {
let match = file.match(/\/(.+?)\.vue$/)[1] // 去除相对路径与.vue
let res = match.replace(/_/ig, '').replace(/\//ig, '-') // 把下划线去除, 改变/为-拼接
return res
}
5:创建getRouteItemPath方法,这个替换/index.vue又替换index.vue是因为一级下是有‘/’,而嵌套路由传过来的参数就没有‘/’了
/**
* 获取路由path
* @param {*} file String (目录,一级路由则为完整目录,多级为自身目录名称)
*/
const getRouteItemPath = (file) => {
return file.replace('/index.vue', '').replace('index.vue', '').replace('vue', '').replace(/_/g, ':').replace(/\./g, '')
}
6:创建registerComponent方法。
/**
* 注册组建
* @param {*} componentConfig (即为调用files方法得出的componentConfig)
*/
const registerComponent = (componentConfig) => Vue.component(componentConfig.default.name || componentConfig.default, componentConfig.default || componentConfig)
7:创建hasfile方法。
/**
* 校验目录下是否有其他文件,注意((?!${name}).)是因为要查询的目录有可能为name/name.vue,而这样可能会导致误判有无children,所以要匹配非name。
* @param {*} file type:string (当前目录路径)
* @param {*} name type:string (目录的文件名 默认等于file参数)
*/
const hasfile = (file, name = file) => new RegExp(file + `/((?!${name}).)`)
8:创建hasVue方法。
/**
* 校验.vue文件
* @param {*} file type:string (当前目录路径)
*/
const hasVue = (file) => new RegExp(file + '.vue')
9:创建createClassify方法。这个方法以一级.vue文件或者文件夹进行分级,这里分级的意义是把同一文件夹下的所有文件收集到一个数组,方便后续操作。代码如下:
const createClassify = () => {
let map = filesKey.reduce((map, cur) => {
let dislodge = cur.match(/\/(.+?)\.vue$/)[1] // 只匹配纯文件名的字符串
let key = dislodge.split('/')[0] // 拿到一级文件的名称
if (!map[key]) {
map[key] = []
}
map[key].push(cur)
console.log(map) // 打印出 { about: ['./about.vue'], home: ['./home.vue'] }
return map
}, {})
}
10:创建getRoutes方法。
/**
* 构建路由
* @param {*} map type:Object
*/
const getRoutes = (map) => {
let res = []
for (let key in map) { // 遍历对象
let level = map[key] // 取出对应value
let text = level.join('@') // 用@把分级数组拼接,这样只是为了方便查找
let expr1 = hasfile(key) // 校验规则,有无子文件
let expr2 = hasVue(key) // 校验规则,有无vue文件
let route = {} // 初始化route
if (text.match(expr1) && text.match(expr2)) { // 有children的route
let max = Math.max(...level.map(v => v.match(/\/(.+?).vue$/)[1].split('/').length)) // 找目录里最深的层级数
let i = 0 // 标记层级
while (i++ < max) { // 按层级来搭建route
level.forEach((item) => {
let wipeOfVue = item.match(/\/(.+?).vue$/)[1] // 匹配纯路径,去除相对路径与.vue
let classArray = wipeOfVue.split('/') // 切割为了方便操作
let len = classArray.length // 深度
if (len === i) {
if (i === 1) { // 如果为第一层,则必带有children
route = {
component: registerComponent(files(item)),
path: getRouteItemPath(item),
children: []
}
} else {
let file = item.match(/(.+?)\.vue$/)[1] // 只匹配目录下.vue之前的路径
let name = classArray[len - 1] // 获取每个路径下具体的文件名
let iteration = classArray.slice(0, len - 1) // 截取文件路径
let childRoute = {
component: registerComponent(files(item)),
path: getRouteItemPath(name)
}
// 从文件的目录下搜索有无子文件,有子文件代表有children属性。 否则无,则直接给route增加name属性
text.match(hasfile(file, name)) && text.match(hasVue(file)) ? childRoute.children = [] : childRoute.name = getRouteItemName(item)
// 通过截取的目录找到对应的parent
let parent = iteration.reduce((map, current, index) => {
let path = index === 0 ? getRouteItemPath(`/${current}.vue`) : getRouteItemPath(`${current}.vue`)
return map.filter(v => v.path === path)[0].children
}, [route])
parent && parent.push(childRoute)
}
}
})
}
res.push(route) // 添加route对象
} else { // 没有children,直接遍历插入
level.forEach(item => {
route = {
component: registerComponent(files(item)),
name: getRouteItemName(item),
path: getRouteItemPath(item)
}
res.push(route) // 添加route对象
})
}
}
return res // 返回整个route对象
}
11:现在来看一下效果。(找个录屏软件真难,不是打广告)
三:结尾。以上就是文章的全部了,感觉自己实现的还不够优雅,算是抛砖引用,在此,谢谢你能看完到最后。完整项目代码:https://github.com/18692959234/free-router
作者:为你敲个代码
github: https://github.com/18692959234
渣渣博客:http://websitejob.xyz/page/pages
上一篇: 【剑指Offer】对称的二叉树
下一篇: 范师兄的面经