仿nuxt.js,自动构建路由,释放你的双手?!

1,856 阅读5分钟

前言

写过nuxt.js的同学,肯定也对nuxt的路由规则有一定的了解,在pages目录下创建文件,即可以自动构建路由,本文来带领大家实现一下在vue里怎么去自动构建路由。我这里使用的是Vue-cli 3.X版本进行初始化。cli 2.X版本也是一样只不过初始化出来的文件目录不一。

做前准备

了解require.context:

它是webpack里一个重要的API,它可以通过一个目录进行搜索,useSubdirectories指定是否搜索子目录,以及与文件匹配的正则表达式。很好理解,它可以对指定目录进行搜索,所以我们可以用它来自动构建路由,也可以用它来构建组建。

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

了解nuxt.js路由规则

这里就简单讲一下,不做详诉。想要深入了解:nuxt.js官网地址: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:创建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}).)`)

7:创建hasVue方法。

/**
 * 校验.vue文件
 * @param {*} file type:string (当前目录路径)
 */
const hasVue = (file) => new RegExp(file + '.vue')

8:创建createClassify方法。这个方法以一级.vue文件或者文件夹进行分级,这里分级的意义是把同一文件夹下的所有文件收集到一个数组,方便后续操作。代码如下:

const createClassify = () => {
  let map = filesKey.reduce((map, cur) => {
    let dislodge = cur.match(/\/(.+?)\.vue$/)[1] // 只匹配纯文件名的字符串
    let key = dislodge.split('/')[0]; // 拿到一级文件的名称
    (map[key] || (map[key] = [])).push(cur)
    return map
  }, {})
  return getRoutes(map)
}

9:创建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: files(item).default,
                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: files(item).default,
                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: files(item).default,
          name: getRouteItemName(item),
          path: getRouteItemPath(item)
        }
        res.push(route) // 添加route对象
      })
    }
  }
  return res // 返回整个route对象
}

10:现在来看一下效果。(找个录屏软件真难,不是打广告)

三:结尾。以上就是文章的全部了,感觉自己实现的还不够优雅,算是抛砖引用,在此,谢谢你能看完到最后。完整项目代码:github.com/18692959234…

作者:为你敲个代码

github: github.com/18692959234

渣渣博客:websitejob.xyz/page/pages