Unibest 默认配置下 build:app 产物 manifest.json 缺失 tabBar 等配置导致 wgt 热更新白屏
在使用 Unibest 框架做 App 项目时,CI/CD 流水线打出的 wgt 热更新包安装后,App 直接白屏。本地开发时一切正常,只有在干净环境(git clone + pnpm install + build:app)下才必现。
问题背景
Unibest 通过 vite-plugin-uni-pages 在运行时自动扫描页面并生成 src/pages.json,同时把 tabBar、globalStyle、preloadRule 等配置写在 pages.config.ts 里统一管理。
为了避免自动生成文件污染 Git 历史,这两个文件通常被加入 .gitignore:
src/manifest.json
src/pages.json
这在本地开发时没有问题,因为 dev 启动时插件会先落盘完整的 pages.json 再让 @dcloudio/uni 读取。
问题根因
冷 build(CI 环境)时存在竞态问题:
build:app 启动时,@dcloudio/uni 会在 vite-plugin-uni-pages 完成扫描、写入完整 pages.json 之前就去读取 tabBar 配置。
如果此时 src/pages.json 不存在或只是空文件({}),@dcloudio/uni 就会用一个只含默认配置的 manifest.json 打包,缺少:
plus.tabBar(TabBar 的原生渲染配置)globalStylepreloadRule
最终 dist 目录里的 manifest.json 里没有 plus.tabBar,wgt 包安装后 App 找不到 TabBar 入口,直接白屏。
复现步骤
git clone <your-unibest-project>
pnpm install
pnpm build:app # 产物 dist/.../manifest.json 缺少 plus.tabBar
检查产物:
# 确认缺失
cat dist/build/app/manifest.json | grep -A5 "tabBar"
# 输出为空 → 问题确认
解决方案
方案一(推荐):prebuild 脚本初始化 pages.json(不改 .gitignore)
在 package.json 的 build:app 前挂载一个 prebuild:app 脚本,执行时把 pages.config.ts 里的 tabBar 等关键配置写入 src/pages.json,保证 @dcloudio/uni 读取时文件已经就绪。
在项目根目录新建 scripts/init-base-files.mjs:
// scripts/init-base-files.mjs
// 此脚本用于生成 src/manifest.json 和 src/pages.json 基础文件
// 由于这两个配置文件会被添加到 .gitignore 中,因此需要通过此脚本确保项目能正常运行
//
// 注意:冷 build 时 @dcloudio/uni 会在 UniPages 落盘完整 pages.json 之前就读取 tabBar。
// 因此这里必须把 pages.config.ts 中的 tabBar / globalStyle / preloadRule 写入 pages.json,
// 否则 dist/.../manifest.json 会缺 plus.tabBar,App 热更后白屏。
import fs from 'node:fs'
import path from 'node:path'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '..')
const manifestPath = path.resolve(root, 'src/manifest.json')
const pagesPath = path.resolve(root, 'src/pages.json')
const srcDir = path.resolve(root, 'src')
if (!fs.existsSync(srcDir)) {
fs.mkdirSync(srcDir, { recursive: true })
}
const MIN_SIZE = `{ }`.length
// 如果 src/manifest.json 不存在,就创建它;或者如果文件大小小于等于 MIN_SIZE,也重新创建
if (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size <= MIN_SIZE) {
fs.writeFileSync(manifestPath, JSON.stringify({}, null, 2))
}
/** 去掉 UniPages 可能写入的 // 注释,便于 JSON.parse */
function stripJsonComments(text) {
return text.replace(/^\s*\/\/.*$/gm, '')
}
function readExistingPages() {
if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size <= MIN_SIZE) {
return {
pages: [
{
path: 'pages/index/index',
type: 'home',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
},
],
subPackages: [],
}
}
try {
return JSON.parse(stripJsonComments(fs.readFileSync(pagesPath, 'utf8')))
}
catch {
return { pages: [], subPackages: [] }
}
}
const require = createRequire(import.meta.url)
const jiti = require('jiti')(__filename, {
interopDefault: true,
esmResolve: true,
})
const pagesConfigModule = jiti(path.resolve(root, 'pages.config.ts'))
const pagesConfig = pagesConfigModule?.default ?? pagesConfigModule
if (!pagesConfig || typeof pagesConfig !== 'object') {
throw new Error('[init-baseFiles] failed to load pages.config.ts')
}
const pagesJson = readExistingPages()
if (!Array.isArray(pagesJson.pages)) pagesJson.pages = []
if (!Array.isArray(pagesJson.subPackages)) pagesJson.subPackages = []
if (pagesConfig.globalStyle) pagesJson.globalStyle = pagesConfig.globalStyle
if (pagesConfig.preloadRule) pagesJson.preloadRule = pagesConfig.preloadRule
if (pagesConfig.tabBar) pagesJson.tabBar = pagesConfig.tabBar
else delete pagesJson.tabBar
fs.writeFileSync(pagesPath, `${JSON.stringify(pagesJson, null, 2)}\n`)
if (!pagesJson.tabBar) {
throw new Error('[init-baseFiles] pages.config.ts 未提供 tabBar,冷 build 产物会缺 plus.tabBar')
}
console.log(
`[init-baseFiles] pages.json ready (tabBar=true, pages=${pagesJson.pages.length})`,
)
然后在 package.json 里添加 hook:
{
"scripts": {
"prebuild:app": "node scripts/init-base-files.mjs",
"build:app": "uni build -p app"
}
}
npm/pnpm 会在执行 build:app 之前自动先执行 prebuild:app,无需改动任何构建命令。
方案二(简单粗暴):把两个文件从 .gitignore 移除
直接把 src/manifest.json 和 src/pages.json 纳入 Git 版本控制,CI 环境拿到的是完整的、已经包含 tabBar 的配置文件。
# .gitignore
- src/manifest.json
- src/pages.json
缺点:每次 dev 启动后,vite-plugin-uni-pages 都会重新生成 src/pages.json,Git 会频繁检测到文件变更,容易产生无意义的 diff 提交噪声。
对比
| 方案一(prebuild 脚本) | 方案二(移除 .gitignore) | |
|---|---|---|
| 实现成本 | 需要新增脚本文件 | 改一行 .gitignore 即可 |
| Git 噪声 | 无 | 每次 dev 都会产生 diff |
| 适合场景 | CI/CD 自动化、多人协作项目 | 小型项目或个人项目 |
| 长期维护 | 推荐 | 不推荐 |
总结
这个问题的本质是冷构建时的竞态:@dcloudio/uni 读取配置比 vite-plugin-uni-pages 写入 pages.json 更早,导致 tabBar 等关键配置丢失,最终打出来的 wgt 包缺少 App 运行所需的原生 TabBar 配置,安装后白屏。
推荐方案是在 CI 的 prebuild 阶段执行初始化脚本,提前把 pages.config.ts 中的配置同步到 src/pages.json,既不污染 Git 历史,也能保证构建产物完整。