Skip to content

vite-plugin-uni-pages-json 源码解读

一、适用场景

vite-plugin-uni-pages-json 是一个为 uni-app 项目量身打造的 Vite 插件,它会在编译时src/pages.json 解析为一个名为 virtual:uni-pages-json 的虚拟模块,业务侧可以直接 import { pages, tabbar, globalStyle } from 'virtual:uni-pages-json' 拿到结构化数据。需要满足以下所有前提:

前提说明
构建工具Vite ≥ 5(依赖 resolveId / load / handleHotUpdate 钩子)
框架uni-app(基于 @dcloudio/vite-plugin-uni
pages.json 路径默认 src/pages.json(相对 config.root
业务代码需要通过 virtual: 前缀导入(TypeScript 需配 shim)

不适合

  • 非 Vite 工程(webpack 项目需另写 loader / 自定义 resolver)
  • 没有 pages.json 配置约定的项目(如纯 Vue/React 工程)
  • 需要运行时动态改 pages.json 的场景(虚拟模块只在编译时存在)

本插件与姊妹篇 vite-plugin-uni-root-frame 功能正交:root-frame 负责"按 pages.json 写入布局组件",pages-json 负责"把 pages.json 读取为 ESM 模块"。


二、背景

uni-app 业务层经常需要在运行时拿到 pages.json 中的数据:导航栏标题、TabBar 列表、globalStyle 默认值。原生限制是:

限制点说明
运行时无读取 APIpages.json 是编译时配置,运行时 uni-app 不暴露
类型支持缺失直接解析后无 TypeScript 类型提示
热更新困难修改后无统一刷新机制
配置分散页面样式需在每个页面 style 中重复声明

Vite 提供的虚拟模块能力恰好解决这一矛盾:把 pages.json 在编译时"物化"成一个真实可 import 的 ESM 模块,业务代码以透明导入的方式拿到结构化数据。

虚拟模块原理、虚拟 ID 与 \0 前缀的细节,系列第二章 Vite 虚拟模块 已有完整推导,本篇不再重复。


三、与 root-frame 的"读 / 写"分工

两个插件同源(同仓库 vite-plugins/ 目录下),但语义完全不同。把它们放在一起看:

维度vite-plugin-uni-pages-jsonvite-plugin-uni-root-frame
方向读(pages.json → ESM 模块)写(按 pages.json → 包裹 .vue)
核心钩子resolveId + load + handleHotUpdateconfigResolved + transform + handleHotUpdate
产物虚拟模块(消费者是业务代码)转换后的 .vue 代码(消费者是 Vite)
触发时机load 时按需解析每个 .vue 文件 transform 时调用
典型配置位置vite.config.js 顶部同上,紧随其后
降级行为try/catch 返回空模块try/catch 仅 console.error,wrapPages 为空

vite.config.js 中两者通常顺序注册:

js
import uniPagesJson from './vite-plugins/vite-plugin-uni-pages-json.js'
import uniRootFrame from './vite-plugins/vite-plugin-uni-root-frame.js'

export default defineConfig({
    plugins: [
        uniPagesJson(),                  // 1. 先把 pages.json 物化为模块
        uniRootFrame({ componentName: 'AppLayout' }), // 2. 再按 pages.json 注入根框
        uni(),                           // 3. 最后走 uni 官方插件
    ],
})

为什么要 pages-json 在前? pages-json 不依赖任何其他插件,但 root-frame 的 configResolved 会立即读取 pages.json;如果在 pages-json 还没准备好(极端情况:多实例冲突)就被调用,wrapPages 会回退为空。


四、错误韧性

这是这个插件最值得作为开源范例的地方:用最朴素的 try/catch 提供降级,而不是抛错中断构建

4.1 load 钩子的双轨返回值

js
load(id) {
    if (id === resolvedVirtualModuleId) {
        try {
            const content = fs.readFileSync(pagesJsonPath, 'utf-8');
            const jsonString = content
                .replace(/\/\/.*$/gm, '')
                .replace(/\/\*[\s\S]*?\*\//g, '');
            const config = JSON.parse(jsonString);

            return `
                export const pages = ${JSON.stringify(config.pages || [])};
                export const tabbar = ${JSON.stringify(config.tabBar || {})};
                export const globalStyle = ${JSON.stringify(config.globalStyle || {})};
                export default { pages, tabbar, globalStyle };
            `;
        } catch (e) {
            console.error('[vite-plugin-uni-pages-json] Failed to parse pages.json', e);
            return `
                export const pages = [];
                export const tabbar = {};
                export const globalStyle = {};
                export default { pages: [], tabbar: {}, globalStyle: {} };
            `;
        }
    }
}

两个分支返回结构相同的 ESM 模块,唯一的差别是数据多少。这是有意为之:

失败原因行为
pages.json 不存在readFileSync 抛 ENOENT → catch 兜底返回空模块
非法 JSON(注释未去除干净)JSON.parse 抛 SyntaxError → catch 兜底
config.pages 缺失`
config.tabBar 拼写错误(如 tabbar静默回退到空对象,业务侧用 .list 时返回 undefined

为什么不直接抛错? 业务侧的 import { pages } from 'virtual:uni-pages-json'静态依赖——一旦抛错,构建失败,业务代码不可调试。降级到空模块至少能让 dev server 起来,开发者能从控制台报错定位问题,而不是面对一个红屏。

开源建议:作为通用插件范式,错误降级应满足"形态一致、数据有损、构建不挂"三条原则。本插件是教科书级别的实现。

4.2 注释去除的鲁棒性

js
const jsonString = content
    .replace(/\/\/.*$/gm, '')           // 行注释
    .replace(/\/\*[\s\S]*?\*\//g, '');  // 块注释
写法解释
/\/\/.*$/gm// 后到行尾的所有字符,m$ 匹配每行行尾
/\/\*[\s\S]*?\*\//g/* ... */ 块注释,[\s\S]*? 非贪婪跨行匹配

pages.json 本身不支持注释,但 uni-app 生态里大量页面用 "// xxx": true 这种伪注释。两次正则去注释后再 JSON.parse,即可兼容真实仓库用法(参考 src/pages.jsonglobalStyle.layout 里就有 "// showNav": true)。

鲁棒性边界:如果注释出现在字符串字面量内(如 "// not comment"),本正则会误伤。这是已知简化,业务侧约定不在字符串里写 //


五、虚拟模块在 moduleGraph 中的角色

虚拟模块是 Vite 内部的一等公民,与物理文件走同一条 moduleGraph 流水线。本插件用到的虚拟模块相关字段:

字段作用
virtualModuleId'virtual:uni-pages-json'用户 import 时使用的 ID
resolvedVirtualModuleId'\0' + virtualModuleIdVite 内部标识,加 \0 防止与真实文件冲突

5.1 三段生命周期

关键点

  1. \0 前缀不会出现在用户代码中,仅作为 moduleGraph 的内部缓存键。
  2. load 只执行一次——模块内容稳定后 Vite 会缓存。下次再 import,直接走 moduleGraph。
  3. 依赖反向追踪:当 A 模块 import 了虚拟模块,A 会被记录为该虚拟模块的"依赖者",虚拟模块 invalidate 时 A 也会被联动 invalidate。

5.2 handleHotUpdate 的失效策略

js
handleHotUpdate({ file, server }) {
    if (normalize(file) === normalize(pagesJsonPath)) {
        const mod = server.moduleGraph.getModuleById(resolvedVirtualModuleId);
        if (mod) {
            server.moduleGraph.invalidateModule(mod);
            server.config.logger.info(
                `\x1b[32m[vite-plugin-uni-pages-json]\x1b[0m pages.json updated, reloading...`,
                { timestamp: true }
            );
            return [mod];
        }
    }
}

三个关键动作:

动作作用
getModuleById('\0virtual:uni-pages-json')拿到 moduleGraph 中的虚拟模块实例
invalidateModule(mod)标记失效,下次访问时重新 load
return [mod]告诉 Vite 这些模块需要立即重编译

为什么是 full-reload 而不是精细 HMR? 系列第二章已经讲过:pages.json 变更属于"全局配置变更",可能影响页面标题、TabBar 列表等跨模块状态。本插件早期版本里其实有:

js
// server.ws.send({ type: 'full-reload', path: '*' });

这一行被注释掉了。现在仅依赖 invalidateModule + moduleGraph 反向追踪:当虚拟模块失效后,所有 import 它的业务模块都会重新求值,相当于"业务侧自动重渲染"。

这是更优雅的策略:不再显式发全量刷新信号,而是让 moduleGraph 自然驱动重渲染。前提是业务模块没有持久化不可重算的状态(如全局计数器)。


六、TypeScript shim 协作

直接 import from 'virtual:uni-pages-json' 在 TypeScript 项目里会报错:"找不到模块"。需要在 src/shims-uni.d.ts(或任意 .d.ts 文件)中声明模块:

ts
declare module 'virtual:uni-pages-json' {
    export const pages: Array<{
        path: string
        style?: Record<string, any>
    }>
    export const tabbar: {
        list?: Array<{
            pagePath: string
            text?: string
            iconPath?: string
            selectedIconPath?: string
        }>
    }
    export const globalStyle: Record<string, any>
    const _default: {
        pages: typeof pages
        tabbar: typeof tabbar
        globalStyle: typeof globalStyle
    }
    export default _default
}

这样 IDE 与 tsc 都能识别虚拟模块,编辑器智能提示生效。

本插件本身不强制类型(虚拟模块的 load 返回的就是 any),类型由 shim 层负责定义。这是一个清晰的边界划分:插件只负责产数据,类型是消费方的责任


七、可拓展点

这是本插件最容易被低估的一面:作为一个仅 ~70 行的微插件,它的"形态一致性"使得扩展成本极低。常见扩展场景:

7.1 暴露 easycom

js
return `
    export const pages = ${JSON.stringify(config.pages || [])};
    export const tabbar = ${JSON.stringify(config.tabBar || {}))};
    export const globalStyle = ${JSON.stringify(config.globalStyle || {})};
    export const easycom = ${JSON.stringify(config.easycom || {})};
    export default { pages, tabbar, globalStyle, easycom };
`;

业务侧即可:

js
import { easycom } from 'virtual:uni-pages-json'
// 自定义 easycom 校验、生成 .d.ts、构建时扫描

7.2 支持 subPackages(分包)

js
const subPackages = (config.subPackages || []).map(sp => ({
    root: sp.root,
    pages: sp.pages || [],
}))
// 同样通过 JSON.stringify 导出

7.3 平台分支裁剪

js
const platform = process.env.UNI_PLATFORM || 'h5'
const tabbarList = (config.tabBar?.list || []).filter(item => {
    if (!item[platform]) return true
    return item[platform].visible !== false
})

扩展原则:保持 ESM 模块的"导出字段稳定、形状可预测"。一旦在某个版本删字段,所有消费方都会编译失败。


八、使用示例

8.1 vite.config.js

js
import { defineConfig } from 'vite'
import uniPlugin from '@dcloudio/vite-plugin-uni'
import uniPagesJson from './vite-plugins/vite-plugin-uni-pages-json.js'
import uniRootFrame from './vite-plugins/vite-plugin-uni-root-frame.js'

const uni = uniPlugin.default || uniPlugin

export default defineConfig({
    plugins: [
        uniPagesJson(),
        uniRootFrame({ componentName: 'AppLayout' }),
        uni(),
    ],
})

8.2 业务侧导入

vue
<script setup>
import { pages, tabbar, globalStyle } from 'virtual:uni-pages-json'

const activePagePath = ref('')
// 用 pages 判断当前路径是否在已注册页面里
const isKnownPage = computed(() =>
    pages.some(p => p.path === activePagePath.value)
)
</script>

8.3 排错清单

现象排查方向
import 报"找不到模块"检查 shims-uni.d.ts 是否声明了 virtual:uni-pages-json
控制台报 Failed to parse pages.json检查 pages.json 是否合法 JSON(插件已支持 // 注释)
改了 pages.json 没生效确认 dev server 是否在运行;尝试手动刷新(早期版本需要)
tabbar.list 是 undefinedpages.json 中字段是 tabBar(大写 B),不是 tabbar

九、边界与不适用场景

边界说明
字符串内的 //正则会误伤,约定业务侧不写
自定义 pages.json 路径当前硬编码 src/pages.json,如需自定义需改源码
运行时改 pages.json不支持,虚拟模块是编译时产物
TypeScript 类型由 shim 层声明,插件本身无强类型
SSR 模式行为未验证(与 @dcloudio/vite-plugin-uni 的 SSR 模式需额外测试)

十、小结

vite-plugin-uni-pages-json 是一个仅 ~70 行的微插件,但完整覆盖了 Vite 插件的核心能力:虚拟模块、热更新、错误降级。它的设计哲学可以总结为三点:

原则实现
失败形态一致try/catch 返回结构相同的空模块
数据集中、形态稳定ESM 命名导出 + 默认导出双重暴露
副作用最小无 transform、无运行时注入,仅在 load 与 handleHotUpdate 两点活动

作为姊妹篇,vite-plugin-uni-root-frame 负责"按 pages.json 写",本插件负责"把 pages.json 读为 ESM"。两者协同,可让一个 uni-app 项目的所有 pages.json 配置同时具备"编译时驱动"与"运行时可读"两种能力。

虚拟模块原理与 \0 前缀机制详见系列第二章 Vite 虚拟模块

源码:uni-app/vite-plugins/vite-plugin-uni-pages-json.js