踩坑··

Unibest 默认配置下 build:app 产物 manifest.json 缺失 tabBar 等配置导致 wgt 热更新白屏

记录使用 Unibest 框架时,默认 install 后执行 build:app,生成的 manifest.json 缺失 pages.config.ts 中 tabBar 等配置,热更新 wgt 包安装后 App 白屏的原因与解决方案。

在使用 Unibest 框架做 App 项目时,CI/CD 流水线打出的 wgt 热更新包安装后,App 直接白屏。本地开发时一切正常,只有在干净环境(git clone + pnpm install + build:app)下才必现。

问题背景

Unibest 通过 vite-plugin-uni-pages 在运行时自动扫描页面并生成 src/pages.json,同时把 tabBarglobalStylepreloadRule 等配置写在 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 的原生渲染配置)
  • globalStyle
  • preloadRule

最终 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.jsonbuild: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.jsonsrc/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 历史,也能保证构建产物完整。

Built with qbimz • © 2026