欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-17 16:55:51
...

前言

写过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创建后默认带有的文件)

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

二:在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

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

2:但上面仅仅只是基础路由,我们创建动态路由、嵌套路由与动态嵌套路由,为了逻辑的区分,我们删除原有的pages里的文件,再新增文件,目录结构如下:

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

# 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是什么?

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

看到现在的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:现在来看一下效果。(找个录屏软件真难,不是打广告)

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

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

作者:为你敲个代码

github: https://github.com/18692959234

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