公司需要启动一个新的ssr项目,就选用nuxt搭建,搭建的过程也踩过不少的坑,后面有空再写一篇nuxt的坑点和开发注意事项,今天主要讲关于nuxt自动生成router.js的一个bug(可以直接拉到最后有图展示),和通过源码分析找到原因的过程。说实话个人感觉自动生成路由这个功能有点鸡肋这里就不展开为啥鸡肋,直接进入主题。
nuxt有个自动生成路由或者动态路由的功能,这里引用下官网的例子
项目pages下的文件结构:
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue
将自动生成route
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
}
]
}
总结就是
-
路径为/pages/a/_id.vue,自动处理成参数选填的动态路由/pages/a/:id?
-
路径为/pages/a/_id/index.vue,自动处理成参数必填的动态路由/pages/:id
-
路径为/pages/a/index.vue,自动处理成普通路由/pages/a
用起来还是十分简单的,后面用着用着发现有bug
pages/
--| order/
-----| index.vue
--| order-detail/
-----| _id.vue
上面目录,按照规则应该生成两个动态路由分别为(为了简便这里省略其他name、component等相关字段)
routes: [
{ path: '/order' },
{ path: '/order-detail/:id?' }
]
但是却生成了
routes: [
{ path: '/order' },
{ path: '/order-detail/:id' }
]
id后面的?不见了,baidu、google都没找到答案,好吧,开始扒源码,项目下载下来,找到了他生成路由的方法,在/packages/utils/src/route.js中的createRoutes方法
export const createRoutes = function createRoutes ({
files,
srcDir,
pagesDir = '',
routeNameSplitter = '-',
supportedExtensions = ['vue', 'js'],
trailingSlash
}) {
...
}
这里主要关心files参数,整个流程可以概括成获取pages下的文件目录存到files数组中,传递到当前这个方法中处理成最终生成的route表。比如上面生成files数组值为
[
'pages/order/index.vue',
'pages/order-detail/_id.vue'
]
nuxt会对他们先做一次处理,生成一个初步的route表,如下:
routes: [
{
path: '/order',
name: 'order'
},
{
path: '/order-detail/:id?',
name: 'order-detail-id'
}
]
然后nuxt会在二次处理这个routes。遍历整个刚生成的routes,将包含有index.vue或者index.js的文件路径用/分割,并去掉文件后缀,最后push到routesIndex的二维数组中,像这样(order文件夹包含index.vue所以会被push进去)
[
['page', 'order', 'index']
]
然后把path中带?的route分别在生成paths数组和names数组,生成规则如下:
route.path.split('/')分割成paths数组
['page', 'order-detail', '_id?']
route.name.split('-')分割成names数组
['page', 'order', 'detail', 'id']
然后做第三次处理,循环routes数组,把每一个带?的route再和routesIndex数组的每一项循环做比较,假设routesIndex的循环项为r,找到r中index字符串的索引i,如图代表索引i为2
// r数组, index字符串索引为2,所以i为2
['page', 'order', 'index']
// paths数组
['page', 'order-detail', '_id?']
如果i值小于paths数组,则执行循环,循环中和names做匹配,如果前i - 1项的内容都相等,则将paths的最后一项?去掉
if (i < paths.length) {
for (let a = 0; a <= i; a++) {
if (a === i) {
paths[a] = paths[a].replace('?', '');
}
// 遇到和names中不相等的项直接中断
if (a < i && names[a] !== r[a]) {
break
}
}
}
那么为什么nuxt要做这一步处理呢,因为上面有提到,nuxt需要支持生成带?的动态路由,同时也要支持不带?的动态路由,所以他会先把所有动态路由先变成带?的,然后通过上面那个循环做处理,将和index文件同层级带?的路由去掉?。问题就出在这里,order-detail在names数组中会被拆解成order和detail,导致上面循环不会跳出,所以运行到最后把问号去掉了。
// order-detail对应的names
['pages', 'order', 'detail', '_id']
// order-detail对应的paths
['pages', 'order-detail', '_id?']
// order对应的routeIndex, index索引为2, 并且index之前的项和names刚好
// 一一对应,所以会一直循环到最后,最终将paths[2]的?去掉
['pages', 'order', 'index']
贴一下核心的源代码,有兴趣可以去nuxt github上看看
routes.forEach(route => {
// ...省略
// 如果是带?的route和routesIndex做比较
if (route.path.includes('?')) {
routesIndex.forEach(r => {
// 找到index的索引i
const i = r.indexOf('index');
if (i < paths.length) {
for (let a = 0; a <= i; a++) {
// 循环到最后一项,清除?
if (a === i) {
paths[a] = paths[a].replace('?', '');
}
// 遇到和names中不相等的项直接中断
if (a < i && names[a] !== r[a]) {
break
}
}
}
});
}
});
贴两张直观图,这样会引发bug,生成route中的?没了
改成这样就不会了
所以如何避免呢?这边也总结了两小点
-
动态路由文件命名需要带有-时,-之前的字符串不要和其他包含index的文件名重复,比如上面order和order-detail,可以命名调整成orders和order-detail。
-
比较极端,直接重写一份route,从nuxt.config.js中引入,覆盖掉nuxt自己生成的routes。
结语:
后面有空给nuxt团队提个issue