编译与构建配置
当前项目统一在根目录 oak.config.ts 中描述 Web、微信小程序和 Server 的编译行为:
import { CreateCompilerConfig } from '@xuchangzju/oak-cli/lib/createConfig';
export default CreateCompilerConfig({
vite: {
common: {},
web: {},
mp: {},
},
server: {},
});
vite.common 是共享层,vite.web、vite.mp 是目标覆盖层。alias、dedupe、插件和其它配置会按框架规则合并;目标配置优先级更高。不要同时维护一份旧 configuration/compiler.js 和一份相互矛盾的 oak.config.ts。
插件层次
每个 Vite 目标的插件分成三类:
plugins: {
pre: [],
buildin: {},
post: [],
}
pre:项目插件,在 Oak 内置插件前执行;buildin:配置 Oak 已提供的 CDN、PWA、Node polyfill、图片压缩等能力;post:项目插件,在 Oak 内置插件后执行。
优先使用 buildin 的结构化选项。只有框架没有提供相应能力时才增加项目插件,并确认插件在 Vite 8 / Rolldown 下可运行。
Web CDN
Web CDN 是显式 opt-in:只有 staging / production build 且配置了 vite.web.plugins.buildin.cdn 才会 external 对应依赖。未配置时 React、ReactDOM 等依赖正常进入 bundle。
CDN 配置支持多个候选源、超时、依赖顺序以及 global/UMD、ESM、CJS 模块。配置时至少确认:
- package key 与真实 import 一致;
- global/UMD 模块声明正确的全局变量;
- ESM 源允许跨域并能被 import map 或 preload 使用;
- 多源 fallback 不会执行同一个有副作用脚本两次;
- 生产页面在 CDN 全部失败时有明确降级行为。
Desktop production 会强制保持离线 bundle,不消费 Web CDN external。
一个带依赖关系和双源竞速的最小配置如下:
export default CreateCompilerConfig({
vite: {
web: {
plugins: {
buildin: {
cdn: {
moduleStrategy: 'parallel',
sourceStrategy: 'race',
timeout: 8000,
react: {
version: '19.2.4',
format: 'umd',
global: 'React',
sources: [
'https://cdn-a.example/react@{version}.js',
'https://cdn-b.example/react@{version}.js',
],
},
'react-dom/client': {
package: 'react-dom',
external: ['react-dom', 'react-dom/client'],
version: '19.2.4',
format: 'umd',
global: 'ReactDOM',
dependsOn: ['react'],
sources: [
'https://cdn-a.example/react-dom@{version}.js',
'https://cdn-b.example/react-dom@{version}.js',
],
},
},
},
},
},
},
});
moduleStrategy: 'parallel' 只并行加载彼此无依赖的模块。dependsOn 使用 CDN 配置 key,不是 npm 包名猜测;未知 key 和循环依赖会在构建配置阶段失败。
sourceStrategy: 'race' 会同时准备同一模块的候选源,但只执行最先成功的一个。global/UMD/IIFE 与 ESM 使用 preload 竞速,CJS 使用 fetch 竞速;它会增加瞬时请求量,适合跨 CDN 容灾,不应无条件开启。也可以只在某个模块上配置 sourceStrategy,并为 root、module 或单个 source 分别设置 timeout。
React 19 官方包不再提供传统 UMD 产物。使用 UMD 时必须确认 CDN 提供的真实文件和全局变量;使用官方 CJS 产物时要显式声明 dependsOn。构建成功不代表浏览器运行成功,至少要在 production 页面验证裸 import、CJS require、fallback 和全部 CDN 失败路径。
Web PWA
Vite Web 保留了可选 PWA 接入,但当前模板不会安装 vite-plugin-pwa:现有发布版与 Vite 8 baseline 存在 peer dependency 冲突。缺少该包时,production / staging 构建会提示 PWA 被跳过,不影响普通 Web 构建。
项目不使用 PWA 时,建议显式关闭并消除提示:
export default CreateCompilerConfig({
vite: {
web: {
plugins: {
buildin: {
pwa: false,
},
},
},
},
});
当前实现只可靠消费 pwa: false 或 { enabled: false } 这个关闭语义;其它 pwa 对象字段尚未传给 VitePWA(...)。不要在文档或项目配置中把 manifest/workbox 对象当作已经生效的合同。确实需要 PWA 时,应先确认一个与 Vite 8 兼容的插件版本,并用项目 pre / post 插件显式接入和验证 service worker、manifest、scope、start URL、图标及缓存策略。开发模式不会注册内置 PWA service worker,避免缓存干扰 HMR。
小程序 Node polyfill
Vite MP 使用 vite-plugin-node-polyfills 兼容部分第三方包。默认保留 process、global、Buffer 与 node: protocol import,但不自动引入体积和真机风险较高的 crypto、assert 完整兼容链。
确实需要 Node built-in 时使用完整白名单:
export default CreateCompilerConfig({
vite: {
mp: {
plugins: {
buildin: {
nodePolyfills: {
include: ['path', 'util'],
},
},
},
},
},
});
非空 include 是完整白名单,不是“在默认集合上追加”。加入 crypto 或 assert 前,应检查最终主包依赖图,并在真机验证随机数、加密、正则和启动阶段;微信开发者工具不能覆盖所有 JS 引擎差异。
小程序图片压缩
图片压缩作用于最终小程序 dist,因此能覆盖主包、分包和复制后的资源:
npm install -D sharp
export default CreateCompilerConfig({
vite: {
mp: {
plugins: {
buildin: {
compressImage: {
quality: 82,
include: ['assets/', /images/],
minSize: 4096,
skipIfLarger: true,
},
},
},
},
},
});
支持 PNG、JPEG、WebP、AVIF。sharp 必须安装在消费项目中;未安装时构建只给 warning 并跳过。保留 skipIfLarger: true,避免压缩结果反而扩大包体。
小程序 route map
小程序 Vite 构建会把 package.config.ts、页面配置和分包信息编译成 virtual:oak-mp-route-map。Oak navigator 使用 canonical route,例如:
this.features.navigator.navigateTo({
url: '/frontend/order/detail',
});
构建器负责把它解析为真实主包或分包路径。不要把生成路径保存进实体、菜单或共享常量。显式 route map 存在时,缺失 route 会报错或 warning,不再猜测 /pages/.../index。
Bundle 分析
Web 和小程序构建均支持:
oak-cli build --target web --mode production --vite --analyze
oak-cli build --target mp --mode production --vite --analyze
报告包含 bundle treemap、源码目录、NPM 依赖和反向依赖图;小程序还会显示主包、分包统计。先从报告确认大模块的真实 importer,再决定拆包、按需导入或调整 polyfill,不要只根据包名猜测。
Server bundle
oak.config.ts 的 server 只服务 opt-in 的 esbuild runtime bundle:
export default CreateCompilerConfig({
server: {
nodeModules: {
bundle: false,
},
esbuild: {
sourcemap: true,
},
},
});
server.nodeModules.bundle: false 时,普通第三方依赖保持裸 package import,部署目录需要安装生产依赖;设为 true 才会尝试把第三方运行依赖带入 bundle。数据库驱动是动态装载项,构建后必须确认目标环境需要的 mysql2、pg 或 better-sqlite3 已包含或可从外部解析。
Server bundle 的完整发布方式见部署。
严格 TypeScript 构建
当前模板的 build:es 默认执行:
oak-cli build --target tsc --configFile tsconfig.es.json --noEmit --enable-xml-check --emit-injection-types --check-style-less
这些检查分别覆盖 XML/WXML、编译器推导的 render props 声明、Less Module class 与真实 JSX/XML 作用域。报错应回到 properties、formData、methods、模板表达式和真实样式结构修复,不要通过扩大手写 props、空样式或关闭检查掩盖合同错误。
tscBuilder compiler plugin
oak-domain 的 tscBuilder 还提供程序化 TscCompilerPlugin,供 oak-cli 这类编译工具在创建 TypeScript Program 前替换 host、增加 rootNames 或映射 diagnostics:
import type { TscCompilerPlugin } from 'oak-domain/lib/compiler/tscBuilder';
const plugin: TscCompilerPlugin = {
prepareProgram(context) {
return {
host: context.host,
rootNames: context.rootNames,
mapDiagnostic(diagnostic) {
return diagnostic;
},
};
},
};
这个接口属于编译工具集成,不是普通 Oak 应用的 Vite 插件配置。普通项目不应为了隐藏诊断注册一个返回 undefined 的 mapper;需要扩展时,应保持 host、rootNames、source map 和 watch 行为可复现,并用独立 fixture 同时验证一次性构建和 watch。