@uni-helper/vite-plugin-uni-pages
v0.5.0
Published
File system based routing for uni-app applications using Vite
Downloads
11,592
Keywords
Readme
@uni-helper/vite-plugin-uni-pages
使用 TypeScript 编写 uni-app 的 pages.json。支持约定式路由。
不想看文档?直接问 AI 🤖
请考虑持续赞助以维持该项目的持续健康发展,非常感谢!🙏
安装
pnpm i -D @uni-helper/vite-plugin-uni-pages使用
// vite.config.mts
import Uni from '@uni-helper/plugin-uni'
// 或
// import dcloudioUni from '@dcloudio/vite-plugin-uni'
// const Uni = dcloudioUni.default || dcloudioUni
import UniPages from '@uni-helper/vite-plugin-uni-pages'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
UniPages(), // 需要在 Uni() 之前调用
Uni(),
],
})创建 pages.config.(ts|mts|cts|js|cjs|mjs|json),然后用 TypeScript 编写你的 pages.json。你可以在创建的文件里使用 #ifdef H5 等条件编译语句。👉 pages.config.ts 示例
// pages.config.ts
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
// 你可以手动指定 pages,与 definePage 合并时,pages.config.ts 中的同名属性会覆盖 definePage。
// 如果不手动指定 pages,插件会自动扫描页面并生成 pages.json。
// pages: [],
// 其它属性参考 pages.json,理论上一比一对齐
// 如果发现没有对齐,请提交 issue,谢谢 🙏
// https://uniapp.dcloud.net.cn/collocation/pages.html
globalStyle: {
navigationBarTextStyle: 'black',
navigationBarTitleText: '@uni-helper',
},
})页面级配置 definePage
在页面文件的 <script setup> 中使用 definePage 宏声明页面元数据:
<script setup lang="ts">
definePage({
style: {
navigationBarTitleText: '首页',
},
type: 'home', // 标记为首页
})
</script>
definePage必须写在<script>块的顶层,不要包进if、try等语句块里。插件只在块的顶层查找这个宏:写在里面的调用既读不到配置,也不会在构建时从代码里移除,运行时会报definePage is not defined。
要让编辑器识别 definePage 全局宏,需要在项目的声明文件或 tsconfig.json > types 引用本包的 /client 类型。
// env.d.ts
/// <reference types="vite/client" />
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />// tsconfig.json
{
"include": ["**/*.d.ts"]
}// tsconfig.json
{
"types": ["@uni-helper/vite-plugin-uni-pages/client"]
}自 0.5.0 起,definePage 支持函数式写法,接收 { platform, define } 上下文(详见 平台条件页面配置):
<script setup lang="ts">
definePage(({ platform, define }) =>
define({
style: { navigationBarTitleText: '首页' },
})
.ifdef('mp-weixin', { style: { navigationBarBackgroundColor: '#07c160' } })
.ifndef('h5', { middlewares: ['auth'] }),
)
</script>自 0.5.0 起,返回(或直接传入)null 表示当前平台不注册该页面:
<script setup lang="ts">
definePage(({ platform }) => {
// 仅小程序注册该页面
if (platform === 'h5')
return null
return { style: { navigationBarTitleText: '仅小程序' } }
})
</script>宏的参数里可以引用页面文件顶部导入的值(普通 <script> 块里的导入也算),插件在构建时会把这些导入一起执行;宏参数没用到的导入(比如组件的导入)不会被执行:
<script setup lang="ts">
import { TITLE } from './title.mjs'
definePage({
style: { navigationBarTitleText: TITLE },
})
</script>导入路径要写全扩展名(
./title.mjs、./title.ts都可以)。TS 里习惯的省略扩展名写法(./title)在这里不行——宏代码在构建期用 Node 的require执行,它不做 TS 的扩展名补全。
平台条件页面配置
自 0.5.0 起,函数式 definePage 注入的 define 工厂提供条件 DSL,无需手动读取 process.env.UNI_PLATFORM:
define(base):所有平台共享的基础元数据.ifdef(platforms, partial):仅在列出的平台生效.ifndef(platforms, partial):在列出的平台之外的所有平台生效- 匹配的分支按声明顺序深合并进
base:对象递归合并,数组与原始值直接替换 h5与web是同一平台的别名,分支中写任意一个即同时覆盖两者platform为当前构建的平台标识(如'mp-weixin'、'h5')
<script setup lang="ts">
definePage(({ define }) =>
define({
style: { navigationBarTitleText: '标题', enablePullDownRefresh: false },
})
.ifdef('mp-weixin', { style: { navigationBarBackgroundColor: '#07c160' } })
.ifndef(['h5', 'web'], { middlewares: ['auth'] }),
)
</script>条件定义在扫描阶段即按当前平台解析为普通对象,最终写入 pages.json 的仍是平台无关的标准结构;多终端并发时各平台的差异由插件按条件编译块合并(见 多终端并发开发)。
你也可以导入 虚拟模块 来访问所有页面的元数据。
注意:虚拟模块的导出不保证稳定,可能会在小版本中出现变化。
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />
import { pages } from 'virtual:uni-pages'
console.log(pages)插件配置
interface UserOptions {
/**
* 生成页面路径的 TypeScript 类型声明
* 为 true 时在项目根目录生成 uni-pages.d.ts
* 为 string 时作为自定义输出路径(相对于项目根目录)
* @default true
* @since 0.2.9
*/
dts?: boolean | string
/**
* 页面配置文件的加载源
* 基于 unconfig,支持多配置源合并
* @default 'pages.config'
* @since 0.2.7
*/
configSource?: ConfigSource
/**
* 默认应用入口页面(首页)
* 当没有页面通过 definePage({ type: 'home' }) 标记为首页时使用
* 支持多个路径样式以兼容不同的目录结构
* @default ['pages/index', 'pages/index/index']
* @since 0.1.9
*/
homePage?: string | string[]
/**
* 是否自动扫描目录并合并页面配置到 pages.json
* 关闭后仅加载用户配置文件,不扫描文件系统
* @default true
* @since 0.1.0
*/
mergePages?: boolean
/**
* 主包页面的搜索目录
* 支持 glob 模式,如 'src/{pages,views}'
* 最终结果由 tinyglobby 解析为匹配的目录列表
* @default 'src/pages'
* @since 0.1.0
*/
dir?: string
/**
* 分包页面目录的根目录列表
* 用于 uni-app 的分包加载功能
* 支持字符串格式(目录路径)或对象格式(自定义 pages.json 中的 root)
* 更多上下文参考 <https://github.com/uni-helper/vite-plugin-uni-pages/issues/271>
* @default []
* @since 0.1.8
*/
subPackages?: (string | { dir: string, root: string })[]
/**
* pages.json 所在目录
* 相对于项目根目录,也是计算页面相对路径的基准
* @default 'src'
* @since 0.0.1
*/
outDir?: string
/**
* 排除的文件/目录模式
* 基于 tinyglobby 的 ignore 选项
* @default ['node_modules', '.git', '**\/__*__/**']
* @since 0.0.4
*/
exclude?: string[]
/**
* 是否压缩生成的 pages.json
* @default false
* @since 0.1.6
*/
minify?: boolean
/**
* 是否在 pages.json 末尾插入换行
* @default false
* @since 0.5.0
*/
insertFinalNewline?: boolean
/**
* 生成的 pages.json 的缩进
* 接受空格数量或字符串(如 `'\t'`)
* 当 `minify` 为 `true` 时被忽略
* @default 2
* @since 0.5.0
*/
indent?: number | string
/**
* 生成的 pages.json 的换行符
* 自 0.5.0 起提供
* @default '\n'
*/
eol?: '\n' | '\r\n'
/**
* 启用调试日志
* 为 true 时启用所有分类;为字符串时仅启用特定分类
* 可选分类:hmr | options | pages | subPages | error | cache | declaration | definePage
* 也可通过环境变量 DEBUG=vite-plugin-uni-pages:* 控制
* @default false
* @since 0.1.8
*/
debug?: boolean | DebugType
// 生命周期钩子,在每个阶段前后触发,仅接收该阶段的输入或输出数据
// 自 v0.0.3 起提供;0.5.0 起签名由 (ctx: PageContext) 改为按阶段传入数据,见「0.5.0 破坏性变更」
onBeforeLoadUserConfig?: () => void
onAfterLoadUserConfig?: (pagesGlobConfig: PagesConfig | undefined) => void
onBeforeScanPages?: () => void
onAfterScanPages?: (pages: Map<string, Page>, subPages: Map<string, Map<string, Page>>) => void
onBeforeMergePageMetaData?: (pages: Map<string, Page>, pagesGlobConfig: PagesConfig | undefined) => void
onAfterMergePageMetaData?: (pageMetaData: InternalPages, subPageMetaData: SubPackages) => void
onBeforeWriteFile?: (filePath: string) => void
onAfterWriteFile?: (filePath: string, content: string) => void
}流程说明
插件内部按以下顺序执行生命周期,每个阶段对应一个生命周期钩子:
加载用户配置 → 扫描页面文件 → 合并页面元数据 → 生成并写入 pages.json加载用户配置:通过
unconfig加载pages.config.ts等配置文件,获取手动指定的页面元数据(pages、subPackages、globalStyle等)扫描页面文件(仅在
mergePages: true时):根据dir和subPackages扫描文件系统,为每个页面文件创建Page实例合并页面元数据:将扫描到的页面与用户配置中的页面元数据合并。优先级从低到高:
globalStyle(pages.config.ts中的全局样式)definePage()(.vue文件中通过宏定义的页面配置)pages.config.ts中pages数组里对应 path 的条目(最高优先级,会覆盖definePage的同名属性)
生成并写入:序列化为
pages.json,写入到outDir目录,并生成 TypeScript 类型声明
配置示例
// vite.config.mts
import UniPages from '@uni-helper/vite-plugin-uni-pages'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
UniPages({
dts: 'src/typings/uni-pages.d.ts', // 自定义类型声明路径
dir: 'src/{pages,views}', // 多目录扫描
subPackages: ['src/sub-package-a', 'src/sub-package-b'],
exclude: ['node_modules', '.git', '**/__*__/**', '**/components/**'],
minify: true,
debug: true, // 或 debug: 'pages'
onAfterScanPages(pages) {
console.log(`扫描到 ${pages.size} 个主包页面`)
},
}),
],
})完整类型定义
请查看 types.ts 获取完整定义。
FAQ
这个插件写入配置晚于 uni-app 读取配置,导致无法正常运行
@dcloudio/vite-plugin-uni 在 Vite 的 config 钩子里通过 parsePagesJsonOnce 读取 pages.json,而本插件在更晚的 configResolved 钩子里才写入。
config 早于 configResolved,所以即便本插件设置了 enforce: 'pre',也只能在 configResolved 内部抢先,无法早于 config 钩子。parsePagesJsonOnce 结果被 once 缓存,首次读取后即固定,后续写入对 uni-app 无效。
核心矛盾是时序:必须在 uni-app 进程启动前把 pages.json 生成好。以下按推荐度排序给出方案。
方案一(推荐):使用 @uni-helper/unh
unh 在调用 uni dev/build 前加载配置、扫描页面并写盘,再 spawn 子进程,天然解决时序问题。
// package.json
{
"scripts": {
"dev": "unh dev",
"build": "unh build"
}
}// unh.config.ts
import { defineConfig } from '@uni-helper/unh'
export default defineConfig({
autoGenerate: {
pages: true, // 在 dev/build 前自动生成 pages.json
},
})方案二:自行编写脚本,在 uni 命令前生成
如果不想引入 unh,可以自行编写脚本在 uni 命令前生成 pages.json,再用 && 串联或 npm predev/prebuild 钩子触发。脚本需要复刻本插件的完整流水线(加载 pages.config.ts、扫描页面文件、解析 definePage 宏、合并元数据),维护成本较高,故优先推荐方案一。
支持 JSX/TSX 吗?
不支持,只支持 vue/nvue/uvue 文件。
文件名有限制吗?
文件名内不能带有额外的 . 分隔符,如 index.v1.vue 不合法。这是小程序的限制,并非本插件的限制。
支持 monorepo 吗?
在 monorepo 项目中,如果页面分布在多个 package 中,可以使用 subPackages 配置的对象格式来自定义生成的 root 路径。
// vite.config.mts
import UniPages from '@uni-helper/vite-plugin-uni-pages'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
UniPages({
subPackages: [
// 简写格式(原有功能)
'src/pages-sub',
// 对象格式(monorepo 支持)
{
dir: '../../packages/login/src/pages', // 页面目录路径
root: 'packages/login/src/pages', // 自定义 pages.json 中的 root
},
{
dir: '../../packages/user/src/pages',
root: 'packages/user/src/pages',
},
],
}),
],
})这样生成的 pages.json 中 subPackages.root 将使用自定义的值,而不是基于文件系统计算的相对路径,避免出现 .. 造成路径问题。
更多上下文参考 https://github.com/uni-helper/vite-plugin-uni-pages/issues/271。
0.5.0 破坏性变更
升级到 0.5.0 前请阅读以下变更:
仅支持 ESM:移除了
require条件导出,main指向dist/index.mjs。使用 CommonJS(require())引入本插件的项目需要迁移到 ESM,例如将 Vite 配置文件改名为vite.config.mts。Node.js 版本要求:
^22.22.2 || ^24.15.0 || >=26.0.0(随@babel/*v8 升级)。Babel 8:宏解析底层升级到
@babel/parserv8。import attributes 仅支持with { ... }语法,已废弃的assert { ... }语法不再支持;带有该语法的页面文件将无法解析宏,退化为仅路径元数据。生命周期钩子签名变更:钩子不再接收整个
PageContext,改为仅接收对应阶段的输入或输出数据,部分钩子名称同步调整。对照表:| 0.4.x | 0.5.0 | | --- | --- | |
onBeforeLoadUserConfig(ctx)|onBeforeLoadUserConfig()| |onAfterLoadUserConfig(ctx)|onAfterLoadUserConfig(pagesGlobConfig)| |onBeforeScanPages(ctx)|onBeforeScanPages()| |onAfterScanPages(ctx)|onAfterScanPages(pages, subPages)| |onBeforeMergePageMetaData(ctx)|onBeforeMergePageMetaData(pages, pagesGlobConfig)| |onAfterMergePageMetaData(ctx)|onAfterMergePageMetaData(pageMetaData, subPageMetaData)| |onBeforeWriteFile(ctx)|onBeforeWriteFile(filePath)| |onAfterWriteFile(ctx)|onAfterWriteFile(filePath, content)|依赖清理:
json5、yaml、detect-indent、detect-newline不再是运行时依赖(配置文件仍支持ts/mts/cts/js/cjs/mjs/json,无行为变化)。类型入口:包名的
types入口直指构建产物,仅配置"types": ["@uni-helper/vite-plugin-uni-pages"]不再带入definePage全局与virtual:uni-pages模块声明,请改用/client子路径(见上方「页面级配置 definePage」)。类型修正(静默变更):以下
@uni-helper/uni-pages-types的类型变更不会产生 TS 报错(接口带索引签名),但行为有差异:app-plus.softInputMode/softInputNavBar更名为官方拼写softinputMode/softinputNavBar(App-Harmony 同步更名)。旧字段名不再有类型提示;pages.json中已写的旧字段名请手动改为新拼写。GlobalStyle移除disableScroll/disableSwipeBack。官方文档注明这两个配置只在页面级style中有效、在globalStyle中设置无效,现仅在PageStyle上保留;写在globalStyle里的这两个字段会被忽略。
不可用的
pages.json直接报错:已存在的pages.json若不是普通文件(例如目录)或缺少读写权限,插件现在抛出错误并停止生成;0.4.x 会把它删除并重建为占位内容,静默丢失手写内容。请先修复权限(如chmod u+w)或自行删除该文件。
从 0.3.x 直接升级到 0.5.x 的用户请注意:
<route>自定义块与routeBlockLang选项已在 0.4.0 移除(0.3.24 起运行时会打印废弃提示),请改用definePage宏,迁移说明见 https://uni-helper.js.org/vite-plugin-uni-pages/definePage。
0.5.0 同时带来以下新能力:
definePage(null)按平台跳过页面注册(见上方「页面级配置 definePage」)- 函数式
definePage与define().ifdef()/.ifndef()条件 DSL(见上方「平台条件页面配置」) - 插件生成的过期子包自动清理(见下方「多终端并发开发」)
- tabBar 顶层属性按平台分别记录,多平台取值互不覆盖(见下方「多终端并发开发」)
- 新增
indent、eol、insertFinalNewline格式化选项
多终端并发开发
同一项目里同时运行到多个平台时(例如,两个终端分别运行 pnpm run dev:mp-weixin 和 pnpm run dev:mp-alipay),多个进程会读写同一个 pages.json。
本插件默认会:
- 通过文件锁串行化对
pages.json的「读取已有内容 → 合并当前平台配置 → 写回」整个流程,避免并发写入互相覆盖; - 保留其他平台已经写入的
#ifdef条件编译块,只更新当前平台对应的条目; - 对同一
path(或 tabBar 的pagePath)的条目按内容去重:内容相同的条目会合并为一条,并把两个平台合在一起写(如H5 || MP-WEIXIN),不会因为先后跑过多个平台而产生重复路由; - tabBar 的顶层属性(
color、selectedColor、custom、position、midButton等)同样按平台分别记录:各平台取值不同时,每个平台的取值保存在自己的#ifdef块里(同一属性名会出现多个按平台互斥的副本,uni-app 条件编译后每个平台只剩一份);取值相同时合并为一条不带条件的属性。只有声明了非空list的运行才会写属性,list为空或未配置 tabBar 的运行不会覆盖其他平台已写入的属性。注意minify: true的输出无法携带注释,属性会变成最后写入的说了算,请对同一项目所有平台的构建保持一致的格式化设置; - 手写的
#ifndef块(插件自身只会写#ifdef)表达不了「按平台列表」的规则,会原样保留、不参与合并,tabBar 属性上的#ifndef同样处理。页面与 tabBar 条目按「只包裹紧随其后的那一个条目」解释;tabBar 的外观属性支持一个条件块包住多个属性——块里每个属性会各自带上完整的#ifndef/#endif包裹重新输出(属性之间互相独立,各平台看到的显示/隐藏结果不变),#ifdef多属性块同理按注释里列出的平台归属每个属性。扫描结果与某个#ifndef条目内容相同时,会并进该条目,避免当前平台出现重复路由。注意两个边界:当前平台恰好是被排除的平台时,内容相等的配置按#ifndef语义同样被隐藏;同一 tabBar 属性同时存在#ifndef副本与各平台不同取值的变体时,被多份副本覆盖的平台会出现重复键,按 JSON 规则后面的键覆盖前面的(当前平台自己的取值写在最后、优先生效)。
这样每个终端都能保留各自的平台配置。条件编译注释基于 comment-json 写入,最终 pages.json 同时包含所有平台的分支。
生成标记与过期子包清理
自 0.5.0 起,插件会在 pages.json 的 pages 与每个 subPackages 条目的头部写入生成标记注释(如 // GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN),记录写入过该数组的平台全集。
平台全集始终包含当前运行的平台,即使本次运行没有向该数组贡献任何条目(例如所有页面都在当前平台通过 definePage(null) 跳过)。这样其他平台专属的条目会保持 #ifdef 包裹、不会不加包裹地输出:uni-app 条件编译下,没被包裹的条目所有平台都看得见,不加包裹会把它们漏进当前平台的构建产物。平台全集还会把上一次运行记在标记里的平台算进来,并且只增不减:某平台的条目被它自己的扫描重写后,它也不会从全集中消失,避免多终端交替写入时,残留的条目被误判成盖住全平台、不加包裹地输出。
基于该标记,插件会自动清理由它生成、但在当前平台扫描中已不存在的条目:
- 页面条目:主包
pages中带生成标记但本次扫描没找到的条目,会把当前平台从它的平台列表里去掉;其他平台的#ifdef条目保留。 - 子包条目:带生成标记且本次扫描没找到的子包(所有页面都被
definePage(null)跳过,或目录被删除),当前平台已经看不到它的任何页面,整个子包条目会从pages.json中删除。这里不会保留其他平台条目的空壳:当前平台的构建产物里没有该分包目录,app.json 里留空壳会指向一个不存在的 root。删除不会丢状态——每个平台每次运行都会从自己的扫描结果重写条目,其他平台下次写入时会把子包重新写回。 - 子包
plugins:subPackages配置里的plugins跟着配置走——配置里有就随子包写进pages.json,配置里删掉后就从插件生成的子包(带生成标记)上自动清掉,不会一直留在产物里。手写子包(没有生成标记)的plugins不受影响。 - 用户手写内容:没有生成标记的条目视为用户手写,插件永远不会修改或删除它们。如果你在
pages.config.ts的subPackages里手动维护子包,或直接在pages.json里手写子包条目,它们不受清理影响。在pages.config.ts的子包里写pages时,path按pages.json的惯例相对root书写(如root为pkg时写detail):同一个页面文件扫描到时,两条会按页合并成一条,你写的属性(如style)覆盖扫描结果。
注意:这个约定依赖生成标记的存在。不要删除插件写入的
// GENERATED BY UNI-PAGES, PLATFORM: ...注释行,否则对应条目会被视为用户手写,过期后不再被清理。
平台全集只增不减
平台全集只增不减:某平台写入过一次,它就会一直留在标记里(即使该平台的开发终端之后再也不运行)。多算的平台唯一的影响,是让各平台不相同的条目一直包着 #ifdef——多余但无害,不会漏东西。
如果确实彻底停用了某个平台、想让 pages.json 回到干净状态:不要手动编辑标记行(改错会导致条目被误判为用户手写,或不加包裹地漏出去),直接删除生成的 pages.json,下次运行会从零重新生成。
另外,「用户手写内容永不修改或删除」的保护范围仅指上述清理逻辑覆盖的场景(带生成标记的子包条目)。pages 数组中不带生成标记、也不在本次扫描结果里的手写页面条目,会在合并时因平台列表为空而被丢弃——这是历史行为;需要长期保留的手写页面建议放进 pages.config.ts 的 pages 数组,插件每次都会原样并入。
开发
环境搭建、常用命令、测试与项目结构见仓库根目录的 CONTRIBUTING.md。
架构
插件围绕 PageContext 组织核心流程,流水线阶段顺序固定(加载用户配置 → 扫描页面 → 合并页面配置 → 写入 pages.json),各专项能力放在独立的模块里:
index.ts Vite 插件入口 — configResolved / transform / configureServer / resolveId / load
context.ts PageContext 编排核心 — 配置加载、扫描、合并、监听、虚拟模块与 HMR
pipeline.ts 纯流水线入口 — createPages / generateAll,root 和 platform 从外部传入,测试直接走这里
pages-json.ts pages.json 读-改-写 — 多平台 #ifdef 合并、首页排前、
生成标记与过期子包清理、序列化格式化、文件锁与原子写入
files.ts 文件工具 — 页面扫描、路径解析、文件锁(withFileLock)、pages.json 合法性检查
macro.ts definePage 宏解析 — SFC 解析、单个 script 块失败不影响别的块、
对象 / 函数 / 异步函数三种形态求值
condition.ts 按平台写配置 — define().ifdef()/.ifndef(),扫描时就算成当前平台的普通对象
page.ts 页面实体 — 文件读取、宏求值、变更检测、跳过状态
options.ts 选项解析 — 默认值合并、glob 目录解析、subPackages root 映射
declaration.ts uni-pages.d.ts 生成,为导航 API 提供路径类型检查
config.ts defineUniPages 辅助函数 + 类型重导出
constant.ts 常量 — 虚拟模块 ID、页面文件扩展名(vue / nvue / uvue)
logger.ts 分类 debug 日志
types.ts 公共类型定义(Options / UserOptions / ResolvedOptions)模块依赖关系
index.ts
└─ context.ts(编排核心)
├─ options.ts 选项解析
├─ files.ts 扫描与文件锁
├─ page.ts ── macro.ts ── condition.ts
├─ pages-json.ts read-modify-write(内含文件锁)
└─ declaration.ts
pipeline.ts ── context.ts 测试与外部调用的流水线入口插件生命周期
插件通过 Vite 的生命周期钩子驱动,顺序如下:
- 插件工厂调用(同步)— 预检
pages.json是否存在且合法(此时config.root未知,回退到VITE_ROOT_DIR/process.cwd()解析路径) configResolved(异步)— 创建PageContext,检测是否与vite-plugin-uni-platform协同,执行首次updatePagesJSON()生成 pages.json;build --watch模式下另建 chokidar 监听页面目录configureServer(dev)— 复用 Vite 的server.watcher,把配置文件源(pages.config.ts等)加入监听;变更时重跑完整流水线,失效虚拟模块并通知浏览器 full-reloadtransform— 从 vue / nvue / uvue 文件中移除definePage宏调用,避免运行时报错resolveId/load— 提供virtual:uni-pages虚拟模块,暴露所有页面元数据
关键设计决策
- 统一入口:
PageContext.updatePagesJSON()是唯一的完整流水线入口,阶段顺序封装其内,调用方(Vite 钩子、watcher 回调、测试)无需知道内部阶段。 - read-modify-write + 生成标记:平台状态保存在
pages.json自身(// GENERATED BY UNI-PAGES, PLATFORM: ...标记),无需外部缓存文件;无标记的条目视为用户手写,插件永不修改。过期生成物基于该标记自动清理。 - 文件锁 + 原子写入:整个「读 → 合并 → 写」在同一个
withFileLock内执行,写入走 tmp + rename,多终端并发(如同时跑 mp-weixin 和 mp-alipay)不会互相覆盖或读到写了一半的内容。 - 内容不变就不写:写入前先和磁盘上的内容比一遍,没变化就跳过,避免触发下游不必要的重编译。
- 统一监听:配置文件与页面文件共用同一个 chokidar(dev 下直接复用 Vite 的 watcher,不额外创建);配置变更不做增量,直接重跑完整流水线,配合「内容不变就不写」避免多余的磁盘写入。
- 增量页面更新:页面文件变更时先经
hasChanged()检测页面配置是否真的变化,无变化则跳过整轮流水线。 - 宏解析失败隔离:每个
<script>块独立解析,单块语法错误(如 Babel 8 已移除的assertimport attributes)只影响该块,不会阻断其它块的宏移除。 - 无导入时副作用:所有文件系统操作都在插件生命周期内执行,而非模块导入时,模块可独立测试。
- 可从外部传入的入口:
pipeline.ts的root/platform从外部传入,测试不绑定进程环境变量,平台相关快照结果确定。
感谢
关联项目
- @uni-helper/vite-plugin-uni-pages - 使用 TypeScript 编写 uni-app 的 pages.json,支持约定式路由
- @uni-helper/pages-json-schema - 为 uni-app 的 pages.json 提供 schema
- @uni-helper/uni-pages-types - 为 uni-app 的 pages.json 提供 TypeScript 类型
- uni-helper/vite-plugin-uni-manifest - 使用 TypeScript 来编写 uni-app 的 manifest.json
- uni-helper/vite-plugin-uni-platform - 基于文件名 (.<h5|mp-weixin|app>.) 的按平台编译插件
- uni-helper/vite-plugin-uni-platform-modifier - 为属性、指令提供平台修饰符并按需编译
- uni-helper/vite-plugin-uni-layouts - 为 Vite 下的 uni-app 提供类 nuxt 的 layouts 系统
- uni-ku/root - 解决 uni-app 无法使用根部组件问题
