用过 uni-app 的兄弟都知道,每次新建一个页面,都要去 pages.json 里手动注册路由。页面少还行,项目到了几十个页面的时候,一不留神就忘了加,然后编译报错“页面未找到”。这破事我已经犯过 N 次了。
后来实在忍不了,我就琢磨着写一个脚本,专门监听 src/pages 目录,只要新增或删除了 .vue 文件,就自动重写 pages.json。现在开发新页面,只需要在 pages 下新建一个 xxx.vue,其他啥都不用管,路由自动就有了。
今天这篇就是分享一下我这个小工具的思路和完整代码,不是特别复杂,但对懒人非常实用。
1. 先说思路
uni-app 用 Vite 构建的话,项目结构一般是 src/pages 放页面文件,src/pages.json 是全局配置文件。新建一个页面,本质就是在 src/pages 下加一个 .vue 文件。
所以我只要监听 src/pages 目录下的文件变化,然后遍历整个目录,把每个 .vue 文件的相对路径提取出来,转成 pages 数组里的一项,再写回 pages.json。就这么简单。
但直接覆盖写不太行,因为 pages.json 里还有 globalStyle、tabBar 这些配置,所以脚本得先读原来的 pages.json,保留这些字段,只替换 pages 数组。
2. 工具选择:chokidar 加 prettier
监听文件变化我用的是 chokidar,这个库老牌稳定,Webpack 也是用它来监听文件的。写 JSON 的时候顺便用 prettier 格式化一下,不然生成出来的 JSON 挤成一坨心里难受。
当然,如果你想临时快速跑一下,也可以直接用 Node 自带的 fs.watch,但 chokidar 支持递归监听、忽略隐藏文件、防抖,省心很多。
安装依赖:
npm install chokidar prettier
如果你还需要同时跑多个命令,建议再装一个 concurrently:
npm install concurrently
3. 脚本完整代码
我把脚本放在项目根目录,命名为 auto-pages.js。直接看代码:
// auto-pages.js
const chokidar = require('chokidar')
const fs = require('fs')
const path = require('path')
const prettier = require('prettier')
const PAGES_DIR = path.join(__dirname, 'src', 'pages')
const PAGES_JSON_PATH = path.join(__dirname, 'src', 'pages.json')
// 默认页面配置
const DEFAULT_NAV = {
navigationBarTitleText: '默认标题',
enablePullDownRefresh: false
}
// 递归获取 pages 数组
function getPages() {
const pages = []
function walk(dir, parentPath = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true })
entries
.sort((a, b) => a.name.localeCompare(b.name))
.forEach(entry => {
const fullPath = path.join(dir, entry.name)
const relativePath = path.join(parentPath, entry.name).replace(/\/g, '/')
if (entry.isDirectory()) {
walk(fullPath, relativePath)
} else if (entry.isFile() && entry.name.endsWith('.vue')) {
const pagePath = relativePath.replace('.vue', '')
// 读取页面顶部注释,支持自定义标题
const content = fs.readFileSync(fullPath, 'utf-8')
const configMatch = content.match(/<!--s*pages?s*({[sS]*?})s*-->/)
let styleOverrides = {}
if (configMatch) {
try {
styleOverrides = JSON.parse(configMatch[1])
} catch (e) {
console.warn(`解析 ${pagePath} 页面配置失败:`, e)
}
}
pages.push({
path: pagePath,
style: { ...DEFAULT_NAV, ...styleOverrides }
})
}
})
}
walk(PAGES_DIR)
return pages
}
// 写入 pages.json
function writeJson(pages) {
let existing = {}
try {
existing = JSON.parse(fs.readFileSync(PAGES_JSON_PATH, 'utf8'))
} catch (e) {
existing = { globalStyle: {}, tabBar: {} }
}
const newJson = {
...existing,
pages: [...pages]
}
const formatted = prettier.format(JSON.stringify(newJson), { parser: 'json' })
fs.writeFileSync(PAGES_JSON_PATH, formatted, 'utf8')
console.log(`[auto-pages] 更新了 pages.json,共 ${pages.length} 个页面`)
}
async function update() {
const pages = getPages()
writeJson(pages)
}
// 首次启动时先跑一次
update()
// 监听文件变化
const watcher = chokidar.watch(PAGES_DIR, {
ignoreInitial: true,
ignored: /(^|[/\])../,
persistent: true
})
watcher
.on('add', filePath => {
console.log(`[auto-pages] 新页面: ${filePath}`)
update()
})
.on('unlink', filePath => {
console.log(`[auto-pages] 删除页面: ${filePath}`)
update()
})
.on('addDir', dirPath => {
console.log(`[auto-pages] 新目录: ${dirPath}`)
update()
})
.on('unlinkDir', dirPath => {
console.log(`[auto-pages] 删除目录: ${dirPath}`)
update()
})
这段代码核心就是 getPages() 函数。它遍历 src/pages 目录,找所有 .vue 文件,把相对路径拼成类似 order/detail 的字符串,作为 path。同时解析每个文件最顶部的注释,如果注释是 <!-- {"navigationBarTitleText": "订单详情"} --> 这种格式,就用它来覆盖默认的页面标题。
最后把已有的 pages.json 内容扩展一下,替换掉原来的 pages 数组,格式化后写回。
4. 页面里怎么自定义标题等配置
比如你要新建一个订单详情页,不想用默认标题,那就在 order/detail.vue 文件顶部加一行注释:
<!-- { "navigationBarTitleText": "订单详情" } -->
<template>
<view>
<text>这里是订单详情</text>
</view>
</template>
脚本运行后,pages.json 里对应的页面会自动带上这个标题。除了导航栏标题,你还能加 enablePullDownRefresh、backgroundColor 之类的配置,写法就是 JSON 格式。
5. 和 dev 命令一起跑
光有脚本还不够,你得让它和 uni-app 的 dev 进程同时跑。我用 concurrently 来做这事。比如在 package.json 里这样配:
{
"scripts": {
"dev:h5": "concurrently "node auto-pages.js" "uni -p h5"",
"dev:app": "concurrently "node auto-pages.js" "uni -p app"",
"dev:mp-weixin": "concurrently "node auto-pages.js" "uni -p mp-weixin""
}
}
这样启动后,你只管新建 .vue 文件,auto-pages.js 在后台监听到变化,自动改 pages.json。同一时间 uni-app 的编译器也在运行,重新编译后新页面就能直接访问了。
6. 踩过的一些坑
首先,脚本千万不要在 uni-app 编译前跑完了才启动。 最好是用 concurrently 同时跑,不然你后来新建文件它不会监听。我第一次就是单独跑了个 node auto-pages.js,然后忘了放在后台,结果搞半天没动静。
其次,pages.json 里的 pages 数组顺序不能乱来。 虽然 uni-app 没强制要求第一个页面是首页,但如果你 tabBar 里配置了页面,顺序也必须对应。脚本只是把目录里的文件按字母序排了,这可能导致你的 tabBar 页面顺序变乱。所以如果你有 tabBar,我的建议是脚本只负责新增页面,手动调整一下关键顺序,或者干脆在 pages.json 里写一个静态的部分,让脚本合并。但这会复杂很多,我目前也没做特别好的处理,基本靠每次改了以后瞄一眼。
还有,删除文件的时候要小心。 如果你删了一个目录,脚本会触发 unlinkDir,然后重新生成 pages.json,这是对的。但如果目录里还有其他文件,可能会产生不正确的路径。所以我的脚本在删除时直接重新遍历整个目录,保证最终结果是符合当前文件系统的。
最后,关于分包。 我这个简化版没有处理 subPackages,分包里的页面你得手动写在 subPackages 数组里。不过思路可以扩展,判断一下目录名是否在分包列表里,然后分别生成。我项目里暂时没用到,就没折腾了。
7. 实际用下来什么感觉
真香。虽然前期装依赖写脚本花了一小时,但之后每次新建页面都特别顺滑。尤其是从别人仓库拉下来以后,页面很多,不用再去核对 pages.json 缺了哪个。只需要看一眼文件在不在。
有一次同事拉代码,新建了一个页面,结果跑起来报错“page not found”。我说你等两秒,看看 pages.json 里有没有。他说还没呢,我说那就再等一下,脚本要反应。过了两秒他再去找,发现已经自动写进去了。
这感觉,咋说呢,就像请了一个管家,你只管往房间里扔东西,他会自动帮你收拾整齐。
8. 可以改进的地方
如果你想要更完美的方案,可以考虑这些点:
- 支持分包:解析
src/pages下的一级目录作为分包根路径。 - 支持页面配置注释中的
path字段:允许某个页面自定义路由路径,而不是强制用目录结构。 - 加入防抖:短时间内大量新增文件,避免多次写磁盘。
- 直接集成到 Vite 插件里,不过那要单独开发 uni-app 插件了,复杂度高一些。
9. 总结一下
自动生成 pages.json 不是玄学,就是一个 Node 脚本的事。它解决的是“重复劳动”和“低级遗忘”的问题。如果你也在用 uni-app,并且手头项目页面不少,我强烈建议你复制上面的代码改吧改吧用起来。
反正对我来说,自从用了这个脚本,再也没因为漏写 pages.json 被同事喊过“页面呢?”。
代码拿去吧,不用谢。

