miniprogram-ci 实战指南:把小程序上传做成一条命令
Posted on Sat 14 November 2026 in 技术 • Sucre Liu
本文基于 miniprogram-ci 2.1.31 编写。它是微信官方从开发者工具中抽离出来的编译模块,让我们可以在没有图形界面的服务器上完成小程序代码的编译、预览与上传。
一、它解决什么问题
日常开发中,「上传体验版」这件事一直绑在微信开发者工具上:打开工具、点上传、填版本号、等进度条。一旦团队要做自动化——合并到主干自动出包、提测自动生成二维码、每晚自动跑包体积检查——这条路就走不通了。
miniprogram-ci 把这部分能力做成了 Node 模块,同时提供命令行版本。它能做:
- 上传代码(对应开发者工具的「上传」)
- 预览代码(生成体验二维码)
- 构建 npm(对应「工具 → 构建 npm」)
- 获取本地编译后的代码包,排查体积异常
- 拉取最近上传版本的 sourceMap
- 代码质量扫描、静态依赖分析
- 上传云函数、静态网站、云存储,创建云托管版本
- 小游戏 jsserver 上传、多端 Apk/Ipa 构建
它不能做的是:提交审核、发布上线、灰度、回退版本。这些仍然要去公众平台人工操作。所以定位要清楚——CI 负责把包送到微信后台,人负责发布决策。
二、准备工作
2.1 安装
全局安装用于命令行调用:
npm install -g miniprogram-ci
miniprogram-ci --help
项目内安装用于脚本调用:
npm install miniprogram-ci --save-dev
Node 版本建议 18 及以上。包体积较大(解压后约 20MB),首次安装稍慢属正常。
2.2 获取代码上传密钥
用小程序管理员账号登录「微信公众平台 → 开发 → 开发设置 → 小程序代码上传」,生成并下载密钥,文件名形如 private.wx1234567890.key。
三个要点:
- 密钥拥有预览和上传权限,等同于上传能力,不要提交到代码仓库。
- 平台不保存明文,遗失只能重置。
- 第三方平台代开发场景从 1.0.28 起同样支持,调用方式不变。
2.3 配置 IP 白名单
仍在「小程序代码上传」页面,把 CI 机器的公网出口 IP 加进去。没有白名单,preview 和 upload 一律失败,报错通常是权限或 IP 不合法。
云厂商的 NAT 出口 IP 可能漂移,建议固定弹性 IP,或把出口网段一次性加全。
2.4 规划 robot 编号
robot 取值 1~30,与开发者工具共用。它的作用是在后台「版本管理」里区分是谁上传的。实践建议:
| robot | 用途 |
|---|---|
| 1 | 测试环境自动构建 |
| 2 | 预发环境 |
| 3 | 生产发布 |
| 4~30 | 各开发者本地调试 |
编号冲突不会报错,但排查问题时会很痛苦。
三、核心对象 Project
所有 API 的第一个参数都是 Project 实例:
const ci = require('miniprogram-ci')
const project = new ci.Project({
appid: 'wx1234567890abcdef',
type: 'miniProgram',
projectPath: './dist',
privateKeyPath: './private.key',
ignores: ['node_modules/**/*'],
})
| 字段 | 必填 | 说明 |
|---|---|---|
| appid | 是 | 小程序 / 小游戏 appid |
| projectPath | 是 | project.config.json 所在目录 |
| privateKey / privateKeyPath | 二选一 | 密钥内容或密钥文件路径 |
| type | 否 | miniProgram / miniProgramPlugin / miniGame / miniGamePlugin,默认 miniProgram |
| ignores | 否 | 排除规则,glob 写法 |
三个容易踩的点:
- projectPath 不是源码根目录,而是构建产物里 project.config.json 所在的目录。用 Taro / uni-app 时通常是
dist/或dist/dev/mp-weixin/。 - 创建时机:Project 在实例化时扫描文件树。必须等
npm run build结束后再 new,否则会报「找不到文件」。若文件在实例化后才变化,调用project.updateFiles()刷新。 - ignores 只影响打包,不影响本地编译。node_modules 一定要排除,否则包体积会莫名其妙涨几 MB。
四、上传代码
const result = await ci.upload({
project,
version: '1.2.0',
desc: 'feat: 新增订单详情页',
robot: 1,
setting: { useProjectConfig: true },
threads: 4,
onProgressUpdate: console.log,
})
console.log(result.subPackageInfo)
参数说明:
| 参数 | 说明 |
|---|---|
| version | 必填,自定义版本号 |
| desc | 备注,会显示在后台版本列表 |
| setting | 编译设置,见第五节 |
| robot | 1~30 |
| threads | 本地编译线程数 |
| useCOS | 异步上传;代码包超过 5MB 时默认开启 |
| onProgressUpdate | 进度回调,适合打日志 |
返回值里 subPackageInfo 最有用:数组元素 name 为 __FULL__ 表示整包,__APP__ 表示主包,其余为分包名,size 是字节数。把它打印进 CI 日志,包体积变化一目了然。
版本号建议用 v1.2.0+构建号 或 git tag。微信允许同版本号重复上传,但后台会堆出多条同名记录,回溯困难。
五、编译设置 setting
setting 不传就按默认;传 useProjectConfig: true 表示继承 project.config.json,下列字段可单独覆盖:
| 键 | 说明 |
|---|---|
| useProjectConfig | true 时以项目配置为基准 |
| es7 | 对应「将 JS 编译为 ES5」,es6 已废弃 |
| minify | 上传时压缩全部代码 |
| minifyJS / minifyWXML / minifyWXSS | 分项压缩,优先级高于 minify |
| codeProtect | 上传时进行代码保护 |
| autoPrefixWXSS | 上传时样式自动补全 |
| compileWorklet | 编译 worklet 代码 |
| disableUseStrict | 增强编译下禁用严格模式 |
推荐组合:
// 生产包
setting: { useProjectConfig: true, es7: true, minify: true, codeProtect: true }
// 调试包:关压缩,保留可读堆栈
setting: { useProjectConfig: true, es7: true, minify: false, codeProtect: false }
注意 es6 只是历史遗留,效果等同 es7,不必再写。
六、预览代码
预览生成一个体验二维码,扫码即可在真机打开:
await ci.preview({
project,
desc: 'PR #128 预览',
robot: 1,
qrcodeFormat: 'image',
qrcodeOutputDest: './qrcode.png',
pagePath: 'pages/order/detail',
searchQuery: 'id=10086&from=ci',
scene: 1011,
})
| 参数 | 说明 |
|---|---|
| qrcodeFormat | image / base64 / terminal,默认 terminal |
| qrcodeOutputDest | 二维码保存路径,必填 |
| pagePath | 启动页面路径 |
| searchQuery | 启动参数,命令行里 & 要写成 \& |
| scene | 场景值,默认 1011 |
| bigPackageSizeSupport | 预览时主包、分包上限放宽到 4M |
CI 中的典型用法:在 PR 评论里贴二维码图片,测试同学直接扫码验收,不必等打包同事。
七、构建 npm
对应开发者工具的「构建 npm」:
const warnings = await ci.packNpm(project, {
ignores: ['**/__tests__/**'],
reporter: (infos) => console.log(infos),
})
返回的 warnings 是数组,每项含 msg、code、jsPath、startLine、endLine,可以格式化成易读的告警列表。
当 npm 依赖不在小程序项目内(monorepo 常见),用 packNpmManually:
await ci.packNpmManually({
packageJsonPath: '../lib/package.json',
miniprogramNpmDistDir: './dist/',
})
它会读取指定 package.json 对应的 node_modules,把 miniprogram_npm 构建到目标目录。
流水线里的顺序是固定的:
npm ci → npm run build → pack-npm → preview / upload
顺序错了,上传的包里就没有 miniprogram_npm。
八、排查包体积与质量问题
8.1 拿到最终代码包
当后台提示「代码包超出限制」,但你在源码里怎么找都找不到大文件时:
const compiled = await ci.getCompiledResult({
project,
version: '1.2.0',
setting: { useProjectConfig: true },
}, './compiled.zip')
第二个参数是 zip 保存路径,返回值是 Record<string, string | Buffer>。解开 zip 直接看最终上传的文件清单,比猜快得多。
8.2 静态依赖分析
const report = await ci.analyseCode(project)
输出 files 与 modules 两张表。每个模块有唯一 id(形如 Page:pages/index/index),通过 parentDeps 和 deps 描述依赖关系。适合定位「谁引用了这个 300KB 的库」。
8.3 代码质量扫描
const result = await ci.checkCodeQuality(project)
这个接口不读取私钥,可以放心在本地或任意 CI 里跑。返回数组,每项包含 name、success、text、docURL、detail。
11 个检查项:
| 检查项 | 关注点 |
|---|---|
| PACKAGE_SIZE_LIMIT | 主包 / 分包体积 |
| JS_COMPRESS_OPEN | JS 是否压缩 |
| WXML_COMPRESS_OPEN | WXML 是否压缩 |
| WXSS_COMPRESS_OPEN | WXSS 是否压缩 |
| LAZYCODE_LOADING_OPEN | 是否开启按需注入 |
| IMAGE_AND_AUDIO_LIMIT | 图片音频体积 |
| CONTAINS_OTHER_PKG_JS | 主包里只被单个分包引用的 JS |
| CONTAINS_OTHER_PKG_COMPONENTS | 主包里只被单个分包引用的组件 |
| CONTAINS_UNUSED_PLUGINS | 未使用的插件 |
| CONTAINS_UNUSED_COMPONENTS | 未使用的组件 |
| CONTAINS_UNUSED_CODES | 无依赖文件 |
把它接成 CI 闸门:任意一项 success === false 就失败,能挡住绝大多数「包越做越大」的回归。
8.4 拉取 sourceMap
后台错误监控里的堆栈是压缩后的,需要 sourceMap 还原:
await ci.getDevSourceMap({
project,
robot: 1,
sourceMapSavePath: './sourcemap.zip',
})
它取的是该 robot 最近一次上传的版本,所以要在上传成功后立刻拉取并归档。
九、云开发相关能力
云函数上传:
await ci.cloud.uploadFunction({
project,
env: 'prod-1g2h3j4k',
name: 'orderNotify',
path: './cloudfunctions/orderNotify',
remoteNpmInstall: true,
})
remoteNpmInstall: true 时云端装依赖,不上传 node_modules;false 则全量上传。生产建议 true,包更小、构建更稳定。
定时触发器:
await ci.cloud.createTimeTrigger({
project,
envId: 'prod-1g2h3j4k',
functionName: 'orderNotify',
triggersConfig: [{
name: 'dailyReport',
type: 'timer',
config: '0 0 2 * * * *',
}],
})
注意:目前只支持一个触发器,数组只能填一项。
静态网站与云存储:
await ci.cloud.uploadStaticStorage({
project, env: 'prod-1g2h3j4k',
path: './web/dist', remotePath: '/',
})
await ci.cloud.uploadStorage({
project, env: 'prod-1g2h3j4k',
path: './assets', remotePath: '/assets',
})
云托管新建版本参数较多(CPU、内存、副本数、扩缩容阈值、端口等),适合做成发布脚本,不在流水线里逐次手调。
十、命令行模式
不想写 Node 脚本时,CLI 更直接:
# 上传
miniprogram-ci upload \
--pp ./dist/ \
--pkp ./private.key \
--appid wx1234567890abcdef \
--uv 1.2.0 \
-r 1 \
--use-project-config true
# 预览
miniprogram-ci preview \
--pp ./dist/ \
--pkp ./private.key \
--appid wx1234567890abcdef \
--uv 1.2.0 \
-r 1 \
--qrcode-format image \
--qrcode-output-dest /tmp/qrcode.png
# 质量检查
miniprogram-ci check-code-quality \
--pp ./dist/ \
--pkp ./private.key \
--appid wx1234567890abcdef \
--save-path ./quality.json
# 构建 npm
miniprogram-ci pack-npm --pp ./dist/ --pkp ./private.key --appid wx1234567890abcdef
常用缩写:
| 参数 | 含义 |
|---|---|
| --pp | projectPath |
| --pkp | privateKeyPath |
| --uv | upload version |
| -r | robot 编号 |
| --use-project-config | 继承项目配置 |
子命令还有 get-compiled-result、get-dev-source-map、pack-npm-manually、cloud functions upload、cloud staticstorage upload、cloud storage upload、cloud container create。
十一、接入 GitHub Actions
一个可直接用的流水线:打 tag 时自动构建、检查、上传。
name: miniprogram-ci
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- name: 写入上传密钥
run: echo "${{ secrets.MP_PRIVATE_KEY }}" > private.key
- name: 构建 npm
run: npx miniprogram-ci pack-npm --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key
- name: 包体积与质量检查
run: npx miniprogram-ci check-code-quality --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key --save-path quality.json
- name: 上传体验版
run: npx miniprogram-ci upload --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key --uv ${GITHUB_REF_NAME#v} -r 1 --use-project-config true
- name: 清理密钥
if: always()
run: rm -f private.key
几个约定:
- 密钥放
Secrets,用 echo 写入临时文件,job 结束前删掉。 - 出口 IP 必须提前加进白名单;GitHub 托管 runner 的 IP 会变,建议用自建 runner 或固定出口。
- 版本号直接取 tag,去掉前缀
v。 - 质量检查放上传之前,不通过就不进入上传步骤。
十二、常见问题排查
1. invalid ip / 权限不足
白名单没配,或出口 IP 变了。先 curl ifconfig.me 确认当前出口,再回后台核对。
2. 编译时报找不到文件
Project 实例化早于构建完成。把 new ci.Project() 移到 npm run build 之后。
3. 包体积突然变大
先 getCompiledResult 解包看清单,再跑 checkCodeQuality 看 PACKAGE_SIZE_LIMIT 和 CONTAINS_UNUSED_*。常见原因:node_modules 没被 ignores 排除、source map 被一起打包、图片没走 CDN。
4. 上传后小程序里没有 miniprogram_npm
packNpm 没执行,或执行顺序在 build 之前。注意 packNpm 写在 build 之后。
5. 命令行里 searchQuery 传不进去
& 被 shell 吃掉了,写成 \& 或整体加引号。
6. 多流水线互相覆盖版本
不同环境用不同 robot;版本号带环境前缀,如 test-1.2.0-45、prod-1.2.0。
7. 代理环境上传超时
miniprogram-ci 会依次读 HTTPS_PROXY、https_proxy、HTTP_PROXY、http_proxy、npm 的 proxy 配置。也可以显式指定 --proxy,或调用 ci.proxy()。务必把 servicewechat.com 加进 no_proxy,否则请求会被代理拦截。
8. 进程跑完不退出
旧版本问题,2.1.5 起已修复;若仍遇到,检查是否有未关闭的句柄,并在脚本末尾显式 process.exit(0)。
十三、能力边界与最佳实践
能做与不能做,前面已经说过。这里给几条落地建议:
- 上传不等于发布。CI 的终点是「体验版就绪」,审核与发布永远留人工一步。
- 包体积设闸门。checkCodeQuality 接进流水线,比事后优化便宜得多。
- 密钥最小暴露。只在需要的步骤落盘,用完即删,禁止打进镜像。
- 版本号可追溯。tag + commit sha,后台记录和代码仓库能一一对上。
- sourceMap 归档。上传成功立刻拉取,按版本号存到对象存储,线上报错才有得查。
- robot 分环境。测试、预发、生产各用各的,日志和版本列表才不会混。
小结
miniprogram-ci 的价值不在于「多了一个上传接口」,而在于把小程序的交付过程变成了可编排的代码。一旦上传、预览、包体积检查都进了流水线,团队就不必再为「谁传的包」「为什么这次大了 300KB」浪费时间。
先把 preview 跑通,再接 checkCodeQuality,最后接 upload——按这个顺序落地,问题最容易定位。
参考:miniprogram-ci 官方 README(v2.1.31)与微信开放文档。