在Vite中使用glob完成自动生成Router规则
-
我们在工作当中,其实会遇到很多的体力活,它没有什么技术含量,但你还不得不做,比方说像这个路由配置:
import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', name: 'index', component: () => import('../views/index.vue'), meta: { title: '首页', menuOrder: 1 } }, { path: '', name: '', component: () => import('../views/about/index.vue'), meta: { title: '关于', menuOrder: 1 } }, { path: '', name: '', component: () => import('../views/contact/index.vue'), meta: { title: '联系', menuOrder: 1 } }, ] export const router = createRouter({ history: createWebHashHistory(), routes })
-
有没感觉路由配置就是一个纯粹的体力活,但你又不得不干的体力活?
-
如果你的心思足够细腻,你还会感觉到这个玩意儿有点像重复代码,但是你又说不好哪里重复了。你的感觉是非常正确的,它就是重复代码,它跟谁重复了呢?跟我们的目录结构重复了。看看对比:
-
为什么要把它写一遍呢?
-
其实我们软件工程里边早就出现了一个概念,叫做:
约定大于配置
,就是在我们实际开发中,公司往往会给予很多的开发规范,在这些规范里边其实就已经暗含了配置了 -
比如about页面,需要建一个文件夹,它的页面组件是
/views/about/index.vue
,contact页面也同理。按照这样的规范去写出来的目录,它其实就是路由配置 -
那我现在要想一想了,既然如此啊,我们能不能找到一个办法,就是在代码里边自动帮我们生成路由配置,不用去手动写;
-
手动写的还经常写错,单词写错,大小写写错,然后呢,导入路径写错,增加了不少的错误几率,而且将来这个目录结构一改,如果说你这里忘记了更改,还要造成很多的隐患,那么如果说能够自动生成的话,这些问题都不复存在了,那怎么来做呢?
-
此时我想,能否对
/router/index.js
文件内定义的routes
这个路由配置做文章? -
参考小程序的做法,为每一个页面呢写上一个配置,用json或js都行,导出一个配置
meta
中缺失的配置export default { title: 'Home', menuOrder: 1 }
-
同样的道理呢,我们把这个配置呢,应用到每一个页面,每一个页面带一个配置,换句话说,只要有这个配置,它就是一个页面,没有这个配置它就不是一个页面,
-
-
这么配置后,当每次新建页面,把这个配置给它带上就完事了。这么一来,信息就完整了,我们就可以大胆的不再使用手动的书写方式了啊,为了避免报错呢,暂时的给
/router/index.js
文件内导出的routes
配置为一个空数组,然后将routes
自动生成 -
怎么生成?如果说你用的是
vue-cli
,由于它背后使用了Webpack
,那么你可以利用里边的**require.context()
**来进行批量导入,用它可以把views
文件夹下边的所有你需要的文件都一起导入进来,比方说你要找到它里边的所有page.js
可以全部导入进来。 -
由于我这里用的是
viet
,它背后不是Webpack
,上述方法用不了。在vite里边呢,它也有相同的方式来做同样的事情,这种方式呢,叫做glob; -
通过查看官方文档发现,这是一个函数,给它传入一个匹配规则,它就能找到这个文件夹下边的所有的相应文件,把这些文件导入进来,生成一个模块的集合。
-
比方说我要匹配不
views
文件夹下边的所有的page.js
的文件,const pageModules = import.meta.glob('../views/**/page.js') console.log(pageModules)
-
观察打印结果发现,对象的属性名是对应的路径,值为动态导入函数,这些就是我们想要的配置啊
-
如果你希望对象的值不是动态导入的函数,官方文档上有一个
globEager
方法```javascript const pageModules = import.meta.globEager('../views/**/page.js') ```
-
在或者你指向要这个模块集合里的
ddefault
对象,那就稍微有点复杂了,不过,抱着死磕到底的心态,绝对尝试下,结果真给我搞出来了。下面是具体实现代码:const pages = Object.fromEntries(Object.entries(pageModules).map(([path, pageModule]) => [path, pageModule.default || {}]))
-
首先,我们使用
Object.entries
将模块对象pageModules
转换为一个键值对数组,其中每个键值对都包含模块路径作为键和模块对象作为值。 -
接下来,我们使用
map
方法对这个数组进行遍历,对于每个键值对,我们都将其转换为一个包含模块路径作为键和模块对象default
属性值作为值的键值对数组。这里使用了解构语法将path
和pageModule
分别指定为数组项的第一个和第二个元素,然后使用数组字面量的方式返回一个包含path
和pageModule.default
的新数组。 -
最后,我们使用
Object.fromEntries
将新数组转换为一个对象字面量,其中每个键值对都由原来的模块路径和default
属性值组成。由于一些模块可能没有default
属性值,所以我们使用|| {}
语法将其设置为空对象,以防止在后续操作中出现undefined
值的情况。 -
这样就处理好了我想要的数据,打印出来看下:
-
再来观察下这个对象,有3个对应的路径的属性和模块内容,说明我们应该有三个对应的路由配置。
-
这里通过
Object.entries()
方法将pages
转为数组:const routes = Object.entries(pages)
-
接下来需要通过数组的
map
方法,将数组内的每一项,映射成路由配置,每一次映射,都要返回一个路由配置,比如下面这个:return { path: '/', name: 'home', component: () => import('../views/index.vue'), meta: { title: 'Home', menuOrder: 1 } }
-
先找简单的,观察通过数组内的成员,
config
这个配置对应的就是meta
对象,替换即可return { path: '/', name: 'home', component: () => import('../views/index.vue'), meta: config }
-
接下来是路径,要把
../views/about.page.js
这个数组成员截取头尾,只留下/about
,路径就出来了,首页的路径需要做点出来,以下是实现代码: -
const routes = Object.entries(pages).map(([pagePath, config]) => { const path = (pagePath.replace('../views', '').replace('/page.js', '') || '/') return { path, name: 'home', component: () => import('../views/index.vue'), meta: config // config 就是 meta 的值,直接就能用 } })
效果:
-
然后是路由name,具体看公司规定了,这里就简单点吧,比如
/about
,就将其命名为about
const routes = Object.entries(pages).map(([pagePath, config]) => { const path = (pagePath.replace('../views', '').replace('/page.js', '') || '/') const name = path.split('/').filter(Boolean).join('-') || 'home' return { path, name, component: () => import('../views/index.vue'), meta: config } })
看效果:
-
最后是
component
,数组对象内貌似有这个玩意,想这直接用replace
方法替换,结果运行时编辑器终端报错。。。const routes = Object.entries(pages).map(([pagePath, config]) => { const path = (pagePath.replace('../views', '').replace('/page.js', '') || '/') const name = path.split('/').filter(Boolean).join('-') || 'home' const compPath = pagePath.replace('page.js', 'index.vue') return { path: path, name, component: () => import(compPath), meta: config } })
component
对应的属性值变成了经过vite
转换后的字面量,这不是我们想要的。 -
查阅资料后发现,因为
vite
是用rollup
进行打包的,在这种环境内打包,有一个要求,就是import
内不能放变量,要放字面量,不然会影响到其的静态分析。 -
这里想到在导入页面模块的时候,再去导入组件的模块,在
import
里就有我们想要的字面量了,这下就简单了,只需去取组件模块内的import字面量,替换到map
方法内,就行了。const routes = Object.entries(pages).map(([pagePath, config]) => { const path = (pagePath.replace('../views', '').replace('/page.js', '') || '/') const name = path.split('/').filter(Boolean).join('-') || 'home' const compPath = pagePath.replace('page.js', 'index.vue') return { path: path, name, component: compModules[compPath], meta: config } })
-
再来看下数据结构:
-
最后,把先去为防止报错而赋值成空数组的routes去掉,大功告成了,附上
/router/index.js
文件完整代码:import { createRouter, createWebHashHistory } from 'vue-router' // const routes = [ // { // path: '/', // name: 'home', // component: () => import('../views/index.vue'), // meta: { title: 'Home', menuOrder: 1 } // }, // { // path: '/about', // name: 'about', // component: () => import('../views/about/index.vue'), // meta: { title: 'about', menuOrder: 10 } // }, // { // path: '/contact', // name: 'contact', // component: () => import('../views/contact/index.vue'), // meta: { title: 'contact', menuOrder: 100 } // }, // ] const pageModules = import.meta.globEager('../views/**/page.js') const pages = Object.fromEntries(Object.entries(pageModules).map(([path, pageModule]) => [path, pageModule.default || {}])) const compModules = import.meta.glob('../views/**/index.vue') const routes = Object.entries(pages).map(([pagePath, config]) => { const path = (pagePath.replace('../views', '').replace('/page.js', '') || '/') const name = path.split('/').filter(Boolean).join('-') || 'home' const compPath = pagePath.replace('page.js', 'index.vue') return { path: path, name, component: compModules[compPath], meta: config } }) export const router = createRouter({ history: createWebHashHistory(), routes })
-
页面效果:
-
App.vue完整代码:
<script setup> import { watchEffect, ref } from 'vue' import { router } from './router' const routes = ref(router.getRoutes()) watchEffect(() => (routes.value = router.getRoutes)) const onUpdated = () => (routes.value = router.getRoutes()) router.afterEach(onUpdated) </script> <template> <nav> <router-link v-for="route in routes" :key="route.path" :to="route.path">{{ route.meta.title }}</router-link> </nav> <router-view></router-view> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } nav { text-align: center; } nav > a { margin-left: 20px; text-decoration: none; } </style>
-
在此基础上,如果需要新增页面的时候,只需在
views
文件夹加内新建对应的页面文件并带上page.js
配置文件,就可不用再手动配置路由了。
转载自:https://juejin.cn/post/7206604884055949349