Skip to content

多端构建与发布 ​

适用端:移动端 | 类型:操作

本页讲从源码到可交付产物这一段:56 个 dev / build 脚本各自的 mode 与产物落点、env/.env* 四层叠加如何决定产物里的常量、manifest.config.ts 如何生成三端清单、两个 app 端专属插件做什么、微信小程序的上传链路,以及实测的产物体积构成。

前置阅读:移动端环境准备与启动(怎么跑起来)、工程结构与约定式路由(pages.json / manifest.json 是谁生成的)。

它解决什么问题 ​

一套代码要出三种形态的产物,而三者的构建入口、mode 语义、产物目录、发布动作全都不一样:

最容易踩的不是命令本身,是三处隐性耦合:

  1. mode 同时决定「加载哪个 .env.[mode]」和「是否压缩 / 是否注入 eruda / 是否弹打包分析」,而不是只有前者;
  2. src/manifest.json 由 manifest.config.ts 生成,但它自己另有一套 mode 推导(解析 process.argv),不复用 Vite 的 mode;
  3. app 端产物还需要两个只在 app 平台生效的插件补资源,其中一个曾经把路径写死成 dist/dev/app(已修,见下文)。

代码在哪 ​

要看什么文件
全部脚本(73 条,其中 56 条是 dev / build)package.json:24-98
Vite 配置:base / 插件链 / server / buildvite.config.ts(base 在 :87,18 个插件在 :88-198,server 在 :226-241,build 在 :245-252)
三端产物清单的真源manifest.config.ts(170 行)
全局页面配置与 easycompages.config.ts
环境变量四层env/.env(79 行,24 项公共基底)、.env.development、.env.test(各 15 行)、.env.production(23 行)
app 端原生插件资源复制vite-plugins/copy-native-resources.ts(199 行)
app 端 manifest plugins 同步vite-plugins/sync-manifest-plugins.ts(67 行)
自动打开小程序开发者工具scripts/open-dev-tools.js(113 行)
微信小程序上传scripts/upload-weixin.js(223 行)
硬编码色守卫scripts/check-hardcoded-color.mjs(pnpm check:color)
上游插件说明vite-plugins/README.md

核心机制 ​

1 mode 决定一切,而 56 个脚本只用了 3 个 mode ​

package.json 的 56 条 dev / build 脚本,mode 只有三种取值:

脚本后缀实际命令Vite mode加载的 env 文件(后者覆盖前者)
无后缀(dev、dev:h5、build:h5…)uni / uni builddev 命令 = development,build 命令 = production.env + .env.development / .env + .env.production
:test(dev:test、build:mp:test…)追加 --mode testtest.env + .env.test
:prod(dev:prod、build:h5:prod…)追加 --mode productionproduction.env + .env.production

非 H5 端的 dev 也是 build command

vite.config.ts:60-64 的注释写明了这一点:pnpm dev:app 得到的 command 是 build、mode 是 development。所以判断「是不是生产」不能看 command,只能看 mode / NODE_ENV——vite.config.ts:251 的 minify: mode === 'development' ? false : 'esbuild' 与 copy-native-resources.ts:100 的 process.env.NODE_ENV === 'production' ? 'build' : 'dev' 都是按这个口径写的。

用 Vite 自己的 loadEnv 逐 mode 实测(node -e "const {loadEnv}=require('vite');…"),三层叠加后的关键取值:

变量developmenttestproduction
VITE_SERVER_BASEURLhttp://localhost:48080/admin-api同左https://demo.enlev.cn/admin-api
VITE_STATIC_BASEURL''''''
VITE_DELETE_CONSOLEfalsefalsetrue
VITE_APP_PUBLIC_BASE///
VITE_APP_PROXY_ENABLEfalsefalsefalse
VITE_COPY_NATIVE_RES_ENABLEfalsefalsefalse
VITE_APP_DEFAULT_LOGIN_USERNAMEadminadminundefined

别把带口令的默认值写进 env/.env

env/.env 是所有 mode 的公共基底,production 也加载它,而 Vite 会把 import.meta.env.VITE_* 静态内联进产物——运行期没有任何开关能把它摘掉。本项目原先把 VITE_APP_DEFAULT_LOGIN_{TENANT_ID,USERNAME,PASSWORD} 放在 .env 里,结果 2026-08-30 那次 H5 构建的产物里能直接搜到明文:

dist/build/h5/assets/pages-core-auth-login.Dx0p7Pwd.js
  … t=O({username:"admin",password:"admin123",captchaVerification:""}) …

消费点是 src/pages-core/auth/login.vue:123-124(无条件填进 formData)与 src/pages-core/auth/components/tenant-picker.vue:50。现已把三项移到 .env.development 与 .env.test,.env 原位留了注释说明为什么不能挪回去;.env.production 不含这三项,实测 production mode 下三者均为 undefined,登录页变为空表单。

推论:任何以 VITE_ 开头的值都要当作「会出现在公开产物里」来对待。同理,env/.env:75-79 的 VITE_APP_API_ENCRYPT_* 把两把 AES 明文密钥直接写在仓库里,它们会被内联进每一端的产物。目前还没有造成实际泄露,是因为请求加密根本没被触发:VITE_APP_API_ENCRYPT_ENABLE = true 只是能力开关,真正的闸门是 src/http/interceptor.ts:83 的 if (options.isEncrypt),而全仓库没有任何调用方传 isEncrypt: true(该字段只出现在类型声明 src/http/types.ts:13)。响应侧的解密由服务端下发的加密头触发(src/http/http.ts:84-94),所以只要后端不加密就也不会解密。真要启用请求加密,必须先把密钥换成运行期下发的方案。

2 只有 3 个脚本挂了 init-baseFiles 前置钩子 ​

src/pages.json 与 src/manifest.json 都被 gitignore(.gitignore:42-43),必须先由 scripts/create-base-files.js 造出桩文件,再由 UniPages / UniManifest 插件在启动时覆盖成真内容。

钩子内容覆盖到的脚本
preinstallnpx only-allow pnpm所有 pnpm install
preparepnpm init-husky & pnpm init-baseFilespnpm install 之后
predevpnpm init-baseFiles只有 pnpm dev
predev:app同上只有 pnpm dev:app
predev:mp同上只有 pnpm dev:mp

npm/pnpm 的 pre<name> 钩子只对脚本名精确匹配生效,所以 dev:h5、dev:test、dev:prod、dev:h5:test、dev:mp-weixin、dev:mp:prod、以及全部 25 条 build:* 都没有前置钩子。目前不出问题,是因为 prepare 在 pnpm install 时已经跑过一次 init-baseFiles——也就是说这条链的兜底在安装期而不是构建期。手工删掉 src/pages.json 再直接 pnpm build:h5 就会踩空。

prepare 用的是单个 &

"prepare": "pnpm init-husky & pnpm init-baseFiles" 是单个 &:在 POSIX shell 下 init-husky 被丢到后台,它失败不会阻断安装,且两条命令并发跑;在 Windows 的 cmd 下 & 才是顺序分隔符。而 init-husky = git init && husky,在一个已经是 git 仓库的目录里再 git init 是多余的。这两处都属遗留(已记入待办),不影响构建产物。

3 build 段的四个开关,其中一个是死的 ​

vite.config.ts:242-252:

ts
esbuild: {
  drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [],
},
build: {
  sourcemap: false,
  // 方便非h5端调试
  // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
  target: 'es6',
  // 开发环境不用压缩
  minify: mode === 'development' ? false : 'esbuild',
},
开关生效条件实测状态
esbuild.dropVITE_DELETE_CONSOLE === 'true'production 为 true → console/debugger 全被摘掉;development/test 为 false
build.minifymode !== 'development'uni -p app(dev)产物不压缩,uni build 系列压缩
build.target恒定 es6三端一致
build.sourcemap恒定 false见下

VITE_SHOW_SOURCEMAP 是死变量

.env.development:6、.env.test:6、.env.production:6 都设了 VITE_SHOW_SOURCEMAP = false,但全仓库唯一读它的地方是 vite.config.ts:248 那行被注释掉的代码。改这个变量没有任何效果,三端产物一律无 sourcemap。要出带 sourcemap 的包,只能改 :246。

4 manifest.config.ts 自己另推一次 mode ​

src/manifest.json 的真源是根目录的 manifest.config.ts,它由 UniManifest()(vite.config.ts:92)驱动。关键在于它拿不到 Vite 的 mode,只能自己解析命令行(manifest.config.ts:8-14):

ts
function getMode() {
  const args = process.argv.slice(2)
  const modeFlagIndex = args.findIndex(arg => arg === '--mode')
  return modeFlagIndex !== -1
    ? args[modeFlagIndex + 1]
    : args[0] === 'build' ? 'production' : 'development'
}
const env = loadEnv(getMode(), path.resolve(process.cwd(), 'env'))

它从 env 里取 5 个值填进清单(manifest.config.ts:15-21):VITE_APP_TITLE → name、VITE_UNI_APPID → appid、VITE_WX_APPID → mp-weixin.appid、VITE_APP_PUBLIC_BASE → h5.router.base、VITE_FALLBACK_LOCALE → locale。

当前生成的 src/manifest.json 实测值:

字段值来源
name启凌智搭env/.env:1 VITE_APP_TITLE
appid__UNI__D1E5001env/.env VITE_UNI_APPID,仍是上游 DCloud appid
mp-weixin.appidwx63c280fe3248a3e7env/.env VITE_WX_APPID,仍是上游小程序 appid
mp-weixin.setting.urlCheckfalsemanifest.config.ts:128-133 写死
mp-weixin.optimization.subPackagestrue同上,开启分包优化
h5.router.base/env/.env:16 VITE_APP_PUBLIC_BASE
versionName / versionCode1.0.0 / 100manifest.config.ts:28-29 硬编码,从不递增
locale缺失VITE_FALLBACK_LOCALE 在任何 env 文件里都没定义
uniStatistics.enablefalsemanifest.config.ts:166-168 写死
app-plus.distribute.androidminSdk 21 / targetSdk 30 / abiFilters: armeabi-v7a, arm64-v8a / 15 条权限manifest.config.ts:62-83

三个身份标识仍是上游的

VITE_UNI_APPID、VITE_WX_APPID、VITE_BAIDU_MAP_KEY 都还是上游演示值。VITE_WX_APPID 尤其要紧——scripts/upload-weixin.js:156-160 直接拿它当上传目标 appid,缺失时 throw,所以不能简单删掉,必须换成自己的。DCloud appid 要在 HBuilderX 里重新获取。这三项已记入待办,等账号信息。

locale 那行等于没写

manifest.config.ts:31 是 'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans',注释暗示期望值,但变量从未定义,实测生成结果里 locale 字段整个不存在(undefined 被 JSON.stringify 丢掉)。uni-app 会退回它自己的默认语言。要固定语言,得先在 env/.env 里补 VITE_FALLBACK_LOCALE = 'zh-Hans'。

Android 权限清单(manifest.config.ts:66-82)整段继承自上游,含 READ_LOGS、WRITE_SETTINGS、READ_PHONE_STATE、GET_ACCOUNTS、MOUNT_UNMOUNT_FILESYSTEMS 这类宽权限。当前 App 端并未打包发布,所以还没有实际后果;真要上架前必须逐条核对,各大应用商店会因无关权限拒审。

5 两个只在 app 端生效的插件 ​

插件挂载点生效条件做什么
createCopyNativeResourcesPluginvite.config.ts:182-187UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true'把根目录 nativeplugins/ 整个复制到 dist/{build|dev}/app/nativeplugins/(copy-native-resources.ts:39-45 的默认源/目标目录名,:100-102 按 NODE_ENV 与 UNI_PLATFORM 定位目标)。当前开关是 false,且仓库里没有 nativeplugins/ 目录
syncManifestPluginvite.config.ts:188apply: 'build',全平台挂载把 src/manifest.json 的 app-plus.distribute.plugins 合并写进产物的 plus.distribute.plugins

第二个插件原先有两处缺陷,本轮已修(提交 5f653c4b):

  • 目标路径写死 ./dist/dev/app/manifest.json,而 uni build -p app 的产物在 dist/build/app——同步结果永远进不了生产产物;
  • mkdirSync 在「有没有 plugins 可同步」的判断之前无条件执行,于是连 H5 构建都会在 dist 下留一个空的 dev/app/。仓库里那个 2026-08-30 造出来的空目录就是这么来的。

修法是:非 app 平台直接返回;build / dev 按 NODE_ENV 推导(与同目录 open-dev-tools.js:26,99 早就在用的口径一致);没有 plugins 时在建目录之前就返回。改完用 esbuild 转出插件、逐组合调用 writeBundle.handler 验过 7 例:h5 / mp-weixin 跳过、app 无插件跳过、app 生产写 dist/build、app 开发写 dist/dev、app-android 写 dist/build、已有 manifest 的 icons 等字段原样保留。

6 打包分析与开发者工具是「会自动弹出来」的 ​

行为触发条件代码
visualizer 生成体积报告并自动打开浏览器UNI_PLATFORM === 'h5' && mode === 'production'vite.config.ts:173-180,输出到 node_modules/.cache/visualizer/stats.html
自动打开微信开发者工具SKIP_OPEN_DEVTOOLS !== 'true' 且 UNI_PLATFORM 含 mp 且是首次构建vite.config.ts:194-197 → scripts/open-dev-tools.js:97-113
注入 eruda 调试面板UNI_PLATFORM === 'h5' && mode === 'development'vite.config.ts:189-191
html-transform 替换 %BUILD_TIME% / %VITE_APP_TITLE%UNI_PLATFORM === 'h5'vite.config.ts:166-171,模板见 index.html:2,17

两条对 CI 有直接影响:pnpm build:h5 会弹出一个浏览器标签(visualizer 的 open: true),pnpm build:mp 会尝试拉起微信开发者工具。前者在无人值守的构建机上只是多一个进程,后者要靠 SKIP_OPEN_DEVTOOLS=true 跳过——scripts/upload-weixin.js:142-150 就是这么做的。开发者工具的 CLI 路径通过 WECHAT_DEVTOOLS_CLI_PATH 配置(env/.env:9,11 有 macOS 与 Windows 两个注释掉的示例),未配置时退回 open-dev-tools.js:43(macOS)/ :53(Windows)的平台默认路径,找不到就打印提示后静默返回,不会让构建失败。

7 微信小程序上传链路 ​

pnpm upload:mp-weixin → scripts/upload-weixin.js,全流程(:153-186):

三个参数的回退顺序:

参数优先级代码
version--version= → WECHAT_UPLOAD_VERSION → readPackageVersion():162;readPackageVersion()(:62-69)取 pkg['yudao-version'] || pkg.version || '1.0.0'
desc--desc= → WECHAT_UPLOAD_DESC → readGitCommitMessage() → 上传于 <时间>:163、:71-80
robot--robot= → WECHAT_UPLOAD_ROBOT → 1;必须是 1–30 的整数,否则 throw:164-165、assertRobot(:130-134)

上传版本号读的是 yudao-version

readPackageVersion() 优先取 package.json:6 的 "yudao-version": "2026.07.0-snapshot",而不是 package.json:4 的 "version": "2026.07-snapshot"。这个键名是上游遗留,本项目刻意保留了它(它记录的是上游合并基线,见 docs/merge/)。后果是:不显式传 --version= 就会把 2026.07.0-snapshot 当小程序版本号上传。正式发布务必显式传 --version=1.0.0 这样的语义化版本,或者改 readPackageVersion() 的取值顺序。

私钥解析(resolvePrivateKeyPath,:112-126)按顺序找:--private-key= → 环境变量 WECHAT_UPLOAD_PRIVATE_KEY_PATH → 根目录 private.<appid>.key → 根目录 private.key;都没有就 throw 并提示前两种配法。私钥不进 git(.gitignore 覆盖),CI 上用 secret 注入环境变量。

loadEnvFile(:43-52)是手写的 .env 解析器(:27-41),只处理 key=value 单行、去掉首尾引号、跳过 # 开头行;不支持多行值、export 前缀、变量插值。它的合并顺序(.env 然后 .env.production)与 Vite 一致,所以拿到的 appid / 版本号跟构建时是同一套。

产物结构与体积 ​

下面的数字来自 2026-08-30 的过期产物

仓库里的 dist/build/h5/ 构建于 2026-08-30 02:44,而当前 HEAD 是 2026-10-25——中间隔了改名(产物 index.html 的 <title> 还是旧品牌 X评云,现在应为 启凌智搭)、E1 的地址修复、以及默认口令的移除。这份产物不能部署,下面的体积数字只能当量级参考,不能作为当前基线。

dist/build/h5/ 的构成:

目录大小
assets/13,424 KB(1,242 个 .js + 584 KB 的 .css)
static/85 KB
index.html4 KB

最大的 8 个分块:

分块大小归属
index-JHpR1tCX.js1,056 KB主包(Vue + uni-app 运行时 + 公共依赖)
yd-chart.vue_…js560 KBecharts,仅被 pages-statistics 引用
pages-im-home-conversation-rtc-call-index.js512 KBlivekit-client,仅被 pages-im 引用
form-create.BbEIUtok.js172 KB在线表单渲染器(BPM + infra 共用)
z-paging.CEwHvMdX.js128 KB分页列表
yd-markdown.DzNPV1Kk.js104 KBMarkdown 渲染
pages-im-home-conversation-message-index.js88 KBpages-im
conversationStore.Zt34oxru.js88 KBpages-im 的 store

按分包前缀汇总(pages-* 分块):

分包文件数大小是否可达
pages-mes1882,076 KB✗ 后端无此模块
pages-im44968 KB✗
pages-mall87772 KB✗
pages-erp61704 KB✗
pages-iot43476 KB✗
pages-crm40412 KB✗
pages-wms29252 KB✗
pages-statistics18208 KB✗
pages-pay22184 KB✗
pages-mp19180 KB✗
pages-member18176 KB✗
pages-system58493 KB✓
pages-ai43424 KB✓
pages-infra42312 KB✓
pages-bpm28244 KB✓
pages-core1596 KB✓(登录 / 注册 / 我的 / 404)
pages-index332 KB✓(工作台首页)

11 个不可达分包的 pages-* 分块合计 6,408 KB,再加上只被 pages-statistics 引用的 yd-chart(560 KB),约 7.0 MB / 13.4 MB ≈ 52% 的 assets 是编译进了包但用户永远点不到的代码(不可达的判据见移动端总览:后端 ainb 已整体删除 crm/erp/im/iot/mall/member/mes/mp/pay/wms 等模块,导航树 system_nav_node 也不给它们建节点)。

想瘦身先动分包清单

vite.config.ts:104-121 的 UniPages({ subPackages: [...] }) 列了全部 16 个分包目录。去掉不可达的那些,产物能砍掉一半左右,src/pages.json 的路由数也会从 764 降到约 190。但这属于产品决策而非构建配置:这些目录还在,是为了将来后端补上对应模块时能直接接回来。真要动,先确认「不打算再要这些模块」,并且同步改 pages.config.ts、src/tabbar/config.ts 里的引用。

多端产物落点(uni-app 约定,dist 整个被 gitignore,.gitignore:12):

命令产物目录
pnpm build:h5 / build:h5:proddist/build/h5/
pnpm dev:h5不落盘(dev server,:9000)
pnpm build:mp / build:mp:proddist/build/mp-weixin/
pnpm dev:mp / dev:mp-weixindist/dev/mp-weixin/
pnpm build:app / build:app:proddist/build/app/
pnpm dev:appdist/dev/app/

发布流程 ​

H5 ​

  1. pnpm build:h5(或 build:h5:prod,二者 mode 相同)。
  2. 把 dist/build/h5/ 整个上传到站点根。
  3. VITE_APP_PUBLIC_BASE 默认 /(env/.env:16),部署到子路径时必须先改成 /子路径/ 再构建——它同时喂给 vite.config.ts:87 的 base 和 manifest.config.ts:34 的 h5.router.base,两处不一致会导致静态资源 404 或路由 base 错位。
  4. 因为是 SPA + history 路由,nginx 要配 try_files $uri $uri/ /index.html;。
  5. VITE_SERVER_BASEURL 在 production 是 https://demo.enlev.cn/admin-api(绝对地址)。若前后端同源部署,改成 '' 走同源、由 nginx 反代 /admin-api 更省事,也省掉 CORS——.env.production:9-11 的注释里写了这个选项。
  6. 产物 index.html 的 <html build-time="…"> 属性由 vite.config.ts:166-171 注入,可以用它确认线上跑的是哪次构建。

微信小程序 ​

  1. 确认 env/.env 的 VITE_WX_APPID 是自己的小程序 appid(当前还是上游的)。
  2. 配好上传私钥:微信后台 → 开发管理 → 开发设置 → 生成「小程序代码上传密钥」,放到根目录 private.<appid>.key,或设 WECHAT_UPLOAD_PRIVATE_KEY_PATH。同时把本机 IP 加进微信后台的 IP 白名单。
  3. 先跑 pnpm upload:mp-weixin --dry-run --version=1.0.0,它会构建 + 校验参数/私钥/产物但不上传(:183-186)。
  4. 正式上传:pnpm upload:mp-weixin --version=1.0.0 --desc="…" --robot=2。
  5. 也可以走手工路:pnpm build:mp → 微信开发者工具导入 dist/build/mp-weixin → 点「上传」。
  6. 上传后到微信后台把该版本设为「体验版」或提交审核。
  7. 服务器域名白名单:小程序后台要把 https://demo.enlev.cn 加进 request / uploadFile / downloadFile 合法域名,否则真机上所有请求被拦。开发阶段可在开发者工具里勾「不校验合法域名」(manifest.config.ts:129 的 setting.urlCheck: false 就是这个开关的产物侧体现)。

App(Android / iOS) ​

  1. pnpm build:app → dist/build/app/。
  2. 用 HBuilderX 打开该目录,或直接在 HBuilderX 里对本仓库执行「发行 → 原生 App-云打包」。
  3. 云打包要用自己的 DCloud appid(manifest.config.ts:26 读的 VITE_UNI_APPID,当前仍是上游值),并配好 Android 签名证书 / iOS 证书与描述文件。
  4. 若引入了本地原生插件:把插件放进根目录 nativeplugins/,在 env/.env 里把 VITE_COPY_NATIVE_RES_ENABLE 改成 true,并在 manifest.config.ts 的 app-plus.distribute 下补 plugins 段——后者是 syncManifestPlugin 唯一会同步的字段。
  5. 上架前逐条核对 manifest.config.ts:66-82 的 Android 权限,删掉用不到的(见上文 warning)。

App 与小程序两条链路本机从未实测

本仓库的开发验证只覆盖 H5(浏览器里能直接看)。HBuilderX 云打包、miniprogram-ci 真实上传、原生插件资源复制、以及各小程序平台(支付宝/百度/字节/QQ…,package.json 里有对应脚本但 manifest.config.ts 只给支付宝与百度写了配置段)都没有在这台机器上跑通过。上面的步骤是源码依据 + 官方文档推出来的,第一次执行时请按「未证实」一节的清单逐项确认。

怎么扩展 ​

加一个新的构建目标平台(例如字节小程序):package.json 里 dev:mp-toutiao / build:mp-toutiao 已经有了(uni -p mp-toutiao),缺的是 manifest.config.ts 里的 mp-toutiao 配置段——目前只有一行 usingComponents: true(:163-165,mp-baidu 同样只有这一行,:160-162)。参照 mp-weixin(:126-143)或 mp-alipay(:144-159)补齐 appid、setting、optimization.subPackages。另外 vite.config.ts:127 的 UniOptimization({ enable: UNI_PLATFORM === 'mp-weixin' }) 只对微信开启,其他平台需要分包优化时要一并放开。

让构建产物带上 sourcemap:改 vite.config.ts:246 的 sourcemap: false,或把 :248 那行取消注释并给三个 env 文件的 VITE_SHOW_SOURCEMAP 赋真值。注意 sourcemap 会泄露未混淆源码,只建议对 test mode 开。

加一个只在某个平台生效的 Vite 插件:照 vite.config.ts:166-171(UNI_PLATFORM === 'h5' && { … })的写法用短路表达式,Vite 会忽略 falsy 项。但要注意 :133 的注释——改 pages.json 的插件必须排在 UniPages 之后、UniKuRoot 之前,插件数组的顺序是敏感的(:89 也标了 UniXXX 需要在 Uni 之前引入)。

在 CI 里跑构建:至少设 SKIP_OPEN_DEVTOOLS=true(否则 build:mp* 会尝试拉起微信开发者工具),并预期 build:h5* 会生成 node_modules/.cache/visualizer/stats.html 且尝试打开浏览器(vite.config.ts:173-180 的 open: true 没有环境变量开关,要在 CI 里彻底关掉只能改代码或让 mode 不等于 production)。

改产物里的品牌名 / 标题:只需改 env/.env:1 的 VITE_APP_TITLE,它同时喂给 manifest.config.ts:25(App 名)与 index.html:17 的 %VITE_APP_TITLE%(H5 页面标题)。

测试 ​

本仓库没有单元测试,构建相关的可执行校验只有三条:

命令校验什么能不能挡住构建问题
pnpm type-check(vue-tsc --noEmit)全仓 TS / Vue 类型能挡住类型错误,挡不住 env 配置错误
pnpm lintESLint(带 cache)只挡风格与被打开的规则
pnpm check:colorscripts/check-hardcoded-color.mjs,硬编码色守卫只挡品牌色

env 与产物的一致性没有任何自动校验,本页用的手法可以直接复用:

bash
# 逐 mode 打印实际生效的变量(用 Vite 自己的 loadEnv,与构建同源)
node -e "const {loadEnv}=require('vite'),path=require('path');
for (const m of ['development','test','production']) {
  const e=loadEnv(m, path.resolve(process.cwd(),'env'));
  console.log(m, JSON.stringify({
    SERVER:e.VITE_SERVER_BASEURL, TITLE:e.VITE_APP_TITLE,
    DROP:e.VITE_DELETE_CONSOLE, USER:e.VITE_APP_DEFAULT_LOGIN_USERNAME }))
}"

# 在产物里反查某个字符串是否被内联进去了
grep -rl "要查的值" dist/build/h5/assets/

第二条是发现「默认口令进了生产包」的办法,也适合用来确认任何 VITE_ 值有没有意外落进产物。

坑与注意 ​

  • dist/ 是 gitignore 的,仓库里那份是 2026-08-30 的过期产物,<title> 还是改名前的旧品牌。别拿它部署,也别拿它的体积当基线。
  • env/.env 里的任何 VITE_ 值都会进生产包,运行期开关摘不掉。口令、密钥、内网地址一律放 .env.development / .env.test。
  • mode 不是 development 就没有 eruda、也不压缩以外的行为差异:dev:test / dev:prod 名字里带 dev,但 mode 是 test / production,产物是压缩过、无 eruda 的。
  • VITE_SHOW_SOURCEMAP 改了没用,唯一的读取点被注释掉了。
  • VITE_FALLBACK_LOCALE 从未定义,manifest.config.ts:31 那行等于空写。
  • manifest.config.ts 自己解析 process.argv 推 mode,跟 Vite 的 mode 是两条独立链路。如果将来加了新的 mode 名(比如 --mode staging),要确认 getMode() 也能拿到,否则 src/manifest.json 会用错 env。
  • versionName / versionCode 硬编码在 manifest.config.ts:28-29,App 端「关于」里显示的版本永远是 1.0.0;versionCode 不递增会导致 Android 无法覆盖安装。
  • 小程序上传的默认版本号取自 pkg['yudao-version'],不是 pkg.version,见上文 warning。
  • build:h5* 会自动弹浏览器(visualizer),build:mp* 会尝试拉起微信开发者工具;CI 里后者用 SKIP_OPEN_DEVTOOLS=true 跳过,前者没有开关。
  • 改 pages.config.ts / manifest.config.ts / env/* 必须重启 dev server:它们生成的是 src/pages.json 与 src/manifest.json,HMR 覆盖不到。vite.config.ts 本身有 ViteRestart(:161-164)会自动重启,其余不会。
  • base 与 h5.router.base 必须同步改:两者都来自 VITE_APP_PUBLIC_BASE,但一个进 Vite 的资源前缀、一个进 manifest 的路由 base,只改一边会得到「资源 404」或「路由 404」两种不同的坏法。
  • server.proxy 默认关闭(VITE_APP_PROXY_ENABLE = false),三端都是直连后端。把它打开只对 H5 dev server 生效(vite.config.ts:230 的注释),小程序与 App 端根本没有 devServer。

→ 本节是贴着上下文的提醒;按症状查的移动端全域清单见移动端坑位,跨端清单见坑位索引。

未证实 ​

  • App 打包全链路:HBuilderX 云打包 / 本地离线打包、Android 签名、iOS 证书与描述文件、nativeplugins/ 复制后的实际生效情况,本机都没有跑过。VITE_COPY_NATIVE_RES_ENABLE 一直是 false,copy-native-resources.ts 的行为只有源码依据(不过它的目标目录推导与 open-dev-tools.js:26 同口径,后者是被真实构建验证过的)。
  • miniprogram-ci 真实上传:只跑过阅读源码与参数校验路径,没有真的上传过(缺自己的 appid 与私钥)。robot 1–30 的约束、setting.minifyWXML 等选项的实际效果都取自官方文档。
  • manifest.config.ts 的 getMode() 在各种 argv 下的返回值:args[0] === 'build' ? 'production' : 'development' 这一支依赖 uni CLI 实际传进来的 argv 形状,没有逐条脚本验证过。已验证的是结果:当前 src/manifest.json 的 name 是 启凌智搭,说明 loadEnv 至少读到了 env/.env。
  • 非微信的其他小程序平台(支付宝 / 百度 / 字节 / QQ / 快手 / 京东 / 小红书 / 快应用):package.json 里有全套 dev:mp-* / build:mp-*,manifest.config.ts 只给支付宝(:144-159)写了带 styleIsolation / compileOptions 的完整配置段,百度(:160-162)与字节(:163-165)各只有一行 usingComponents: true,其余平台连配置段都没有、只有 uni-app 默认值。没有一个被构建验证过。
  • UniOptimization 关闭时的分包行为:vite.config.ts:126-127 的注释说「仅微信小程序开启,避免支付宝小程序运行报错」,这个「报错」没有复现过。

相关页面 ​