前言
说实话,写这个工具纯粹是被逼的。
发版那天晚上,对着10+个渠道的打包列表,我一遍遍地重复着:打开 Creator → 选平台 → 构建 → 等 → 开 Android Studio → 编译 → 签名 → 上传 → 发通知…
到第三个渠道我就开始怀疑人生了,到第七个渠道我开始思考为什么要做游戏开发。
那天晚上我就下定决心:这种重复劳动必须自动化。
一条命令,所有渠道自动打包、自动上传、自动通知。我该干嘛干嘛去,等着收结束消息就行。
一、先聊聊我想解决什么问题
在动手之前,我先梳理了一下痛点:
-
渠道太多:Android 官方包、TapTap、抖音APP、微信小游戏、抖音小游戏、支付宝小游戏、华为快游戏、OPPO快游戏、小米快游戏、鸿蒙…
-
流程重复:每个渠道的打包流程大同小异,但手动操作每次都要从头来
-
容易出错:手动切换配置、手动签名,稍不注意就搞错
-
耗时太长:一个渠道打完要好几分钟,10个渠道就是半天起步
所以我的目标很明确:一条命令搞定所有事情。
二、核心设计思路:Pipeline 模式
2.1 为什么选 Pipeline?
最开始我也想过用一个大函数把所有逻辑写进去,但很快就发现不行。
不同平台的打包流程差异很大:
- Android:Creator 构建 → 修改配置 → Gradle 编译 → 签名 → 上传
- 微信小游戏:Creator 构建 → 上传远程资源 → 调用微信CI上传
- 鸿蒙:Creator 构建 → 修改配置 → hvigorw 编译 → 签名 → 上传
如果用一个大函数,代码里全是 if (platform === 'android') {...},维护起来简直是噩梦。
于是我想到了 Pipeline 模式:把构建流程拆解为独立的步骤(Step),通过 Pipeline 串联执行。
2.2 Pipeline 长什么样?
说白了就是把打包流程拆成一个个小步骤,然后按顺序执行:
// Android 打包流程
class BuilderAndroid {
buildPipeline() {
return [
new ModifyVersionJsonStep(), // 修改版本号
new BuildCreatorStep(), // Creator 构建
new ModifyMainJsStep(), // 修改 main.js(热更新用)
new GenerateManifestStep(), // 生成 manifest
new ModifyNativeConfigAndroid(), // 修改 Android 原生配置
new CompileAndroid(), // Gradle 编译
new CopyArtifactStep('apk'), // 拷贝 APK
new SignApkStep(), // 签名
new UploadArtifactStep() // 上传 CDN
];
}
}
// 微信小游戏打包流程
class BuilderWechat {
buildPipeline() {
return [
new ModifyVersionJsonStep(), // 修改版本号
new BuildCreatorStep(), // Creator 构建
new UploadRemoteResStep(), // 上传远程资源到 CDN
new CompileWechat() // 调用微信CI上传到后台
];
}
}
看到没?两个平台共用了 ModifyVersionJsonStep 和 BuildCreatorStep,但后面的步骤完全不同。
这种设计的好处就是:
- 步骤复用:相同的步骤在不同 Builder 中复用
- 易于扩展:新增渠道 = 组装新 Pipeline,不用改已有代码
- 流程清晰:看 Pipeline 就知道整个构建流程
三、核心组件设计
3.1 Context:数据容器
这是我从 Linus 那句话学来的:数据结构比代码重要。
Context 就是一个数据容器,所有步骤共享同一个 Context,通过它传递数据:
class Context {
#data = new Map(); // 用 Map 存储,性能更好
constructor(params) {
// 存储构建参数
this.set('channel', params.channel); // 渠道
this.set('platform', params.platform); // 平台
this.set('version', params.version); // 版本号
this.set('mode', params.mode); // debug/release
this.set('build', params.build); // 构建号
// ...
}
// 基础操作
get(key) { return this.#data.get(key); }
set(key, value) { this.#data.set(key, value); }
// 步骤结果管理
setStepResult(stepName, result) { ... }
getStepResult(stepName) { ... }
}
为啥要这么设计?
- 单一数据源:所有步骤共享同一个 Context,避免参数传递
- 解耦:Step 之间不直接依赖,只依赖 Context 中的数据
- 可追溯:Context 记录了整个构建过程的所有信息
举个例子,CompileAndroid 步骤编译完 APK 后,把产物路径写入 Context:
context.set('OUTPUT_NAME', 'app-release.apk');
后面的 SignApkStep 直接从 Context 读取:
const apkPath = context.get('OUTPUT_NAME');
Step 之间完全解耦,互不依赖。
3.2 Step:步骤基类
每个 Step 只做一件事,保持简单。我用了模板方法模式:
class Step {
// 步骤名称(子类必须实现)
get name() {
throw new Error('子类必须实现 name');
}
// 判断是否可跳过(子类可选实现)
canSkip(context) {
return context.isStepExecuted(this.name); // 默认:已执行过则跳过
}
// 执行步骤(模板方法,统一处理日志和错误)
async execute(context) {
if (this.canSkip(context)) {
Logger.log(`⊳ 跳过步骤: ${this.name}`);
return context.getStepResult(this.name);
}
try {
Logger.log(`▶ 开始执行: ${this.name}`);
const startTime = Date.now();
const result = await this.run(context); // 调用子类实现
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
Logger.success(`✓ 完成: ${this.name} (耗时: ${duration}s)`);
context.setStepResult(this.name, result);
context.markStepExecuted(this.name);
return result;
} catch (error) {
Logger.error(`✗ 失败: ${this.name}`);
throw error;
}
}
// 实际执行逻辑(子类必须实现)
async run(context) {
throw new Error('子类必须实现 run()');
}
}
基类帮你处理了日志、计时、错误处理、结果缓存,子类只需要专注于业务逻辑。
3.3 Pipeline:执行器
Pipeline 的实现超级简单,就是按顺序执行 Step 数组:
class Pipeline {
#steps = [];
constructor(steps = []) {
this.#steps = steps;
}
async execute(context) {
Logger.blue('==================== Pipeline 开始执行 ====================');
Logger.log(`总步骤数: ${this.#steps.length}`);
const startTime = Date.now();
for (let i = 0; i < this.#steps.length; i++) {
const step = this.#steps[i];
Logger.log(`\n[${i + 1}/${this.#steps.length}] ${step.name}`);
await step.execute(context); // 每个步骤共享同一个 context
}
const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2);
Logger.success(`✓ 总耗时: ${totalDuration}s`);
}
}
3.4 BuilderFactory:工厂类
这里我用了数据驱动的方式,消除了一堆 if/else:
class BuilderFactory {
// 平台 → Builder 映射表
static #builderMap = {
'android': BuilderAndroid,
'ios': BuilderIOS,
'harmonyos-next': BuilderHarmony,
'wechatgame': BuilderWechat,
'alipay-mini-game': BuilderAlipay,
'bytedance-mini-game': BuilderBytedance,
'huawei-quick-game': BuilderHuaweiQuick,
'oppo-mini-game': BuilderOppoQuick,
'xiaomi-quick-game': BuilderXiaomiQuick,
'vivo-mini-game': BuilderVivoQuick
};
static createByPlatform(platform, params) {
const BuilderClass = this.#builderMap[platform];
if (!BuilderClass) {
throw new Error(`不支持的平台: ${platform}`);
}
return new BuilderClass(params);
}
}
新增平台只需要往 Map 里加一行,不改逻辑。
四、关键优化:buildGroup 共享机制
这个优化真的让我省了不少时间。
4.1 问题背景
Android 平台有多个渠道:官方包、TapTap、抖音APP…
它们都要先执行 Creator 构建,而 Creator 构建是最耗时的(5分钟起步)。
如果打3个 Android 渠道,就要执行3次 Creator 构建,15分钟就没了。
但实际上,同平台的渠道可以共享 Creator 构建产物!
4.2 解决方案
在配置文件里,给同平台的渠道设置相同的 group:
[
{ "channel": "official", "platform": "android", "group": "android" },
{ "channel": "taptap", "platform": "android", "group": "android" },
{ "channel": "douyin", "platform": "android", "group": "android" }
]
然后在 BuildCreatorStep 里实现跳过逻辑:
class BuildCreatorStep extends Step {
canSkip(context) {
const channel = context.get('channel');
const buildGroup = DataHelper.channels.getBuildGroup(channel);
// 检查该 buildGroup 是否已经构建过
if (buildGroup && BuildCache.isGroupBuilt(buildGroup)) {
Logger.log(`跳过 Creator 构建:buildGroup [${buildGroup}] 已构建(共享产物)`);
return true;
}
return false;
}
async run(context) {
// 执行 Creator 构建
await runCreatorBuild(...);
// 标记 buildGroup 已构建
const buildGroup = DataHelper.channels.getBuildGroup(channel);
if (buildGroup) {
BuildCache.markGroupBuilt(buildGroup, channel);
}
}
}
效果:
- 打包
official 渠道:执行 Creator 构建,标记 android 组已构建
- 打包
taptap 渠道:检测到 android 组已构建,跳过 Creator 构建
- 打包
douyin 渠道:检测到 android 组已构建,跳过 Creator 构建
3个渠道只需要1次 Creator 构建,从15分钟优化到5分钟!
4.3 BuildCache 实现
class BuildCache {
static #builtGroups = new Map(); // 已构建的 buildGroup
static isGroupBuilt(buildGroup) {
return this.#builtGroups.has(buildGroup);
}
static markGroupBuilt(buildGroup, channel) {
if (!this.#builtGroups.has(buildGroup)) {
this.#builtGroups.set(buildGroup, {
builtBy: channel, // 首次构建的渠道
sharedBy: [] // 共享的渠道列表
});
} else {
const info = this.#builtGroups.get(buildGroup);
info.sharedBy.push(channel);
}
}
static clear() {
this.#builtGroups.clear(); // 每次批量构建开始时清空
}
}
批量打包结束后,还会打印共享汇总:
==================== Creator 构建共享 ====================
[android]
构建者: official
共享者: taptap, douyin (节省 2 次 Creator 构建)
五、命令行入口设计
我设计了两种使用方式:
5.1 交互式打包
运行 npm run build,会弹出交互式界面:
? 请选择渠道类型: (Use arrow keys)
━━━━━━━━ 渠道分组 ━━━━━━━━
🌍 全部渠道 (all)
🎮 小游戏 (minigame)
⚡ 快游戏 (quickgame)
📱 原生平台 (native)
━━━━━━━━ 单个渠道 ━━━━━━━━
❯ 官方android (official)
TapTap (taptap)
微信小游戏 (wechatgame)
...
按上下键切换选中渠道,回车确认,继续输入版本号、构建号、选择模式等。
这种方式适合开发时用,直观方便。
5.2 命令式打包(CI/CD)
npm run build:jenkins -- \
-p official,taptap \
-v 1.0.0 \
-b 100 \
-d false \
-r 15 \
-m "修复bug" \
-n true
参数都通过命令行传入,适合 Jenkins 等 CI 系统调用。
5.3 渠道分组
为了方便批量打包,我还设计了渠道分组:
-p all # 所有渠道
-p minigame # 小游戏渠道(微信、抖音、支付宝)
-p quickgame # 快游戏(华为、oppo、vivo、小米)
-p native # 原生平台(Android、iOS、鸿蒙)
-p official,taptap # 指定多个渠道
实现也很简单:
class ChannelParser {
static parseChannels(channelInput) {
const inputs = channelInput.split(/[,\s]+/);
const channelSet = new Set();
for (const input of inputs) {
// getChannelsByGroupKey 会根据关键字返回对应的渠道列表
const channels = DataHelper.channels.getChannelsByGroupKey(input);
channels.forEach(ch => channelSet.add(ch));
}
return Array.from(channelSet); // 去重后返回
}
}
六、通知系统
打包完成后,自动发送飞书通知。
6.1 消息收集
每个渠道打包完成(成功或失败),都会把结果收集到 MessageHelper:
// BuilderBase.start()
async start() {
try {
await pipeline.execute(this.context);
// 成功,收集消息
const message = this.context.getBuildMessage();
message.succeed = true;
MessageHelper.addMessage(message);
} catch (error) {
// 失败,也收集消息
const message = this.context.getBuildMessage();
message.succeed = false;
message.message = error.message;
MessageHelper.addMessage(message);
}
}
6.2 批量发送
批量打包时,所有渠道都打完后,统一发送一条飞书卡片消息:
// build-core.js
async function startBatchBuild(params) {
// 初始化
MessageHelper.initBaseInfo('build', version, mode, '', buildCode);
// 逐个打包
for (const channel of channels) {
try {
await startBuild({ ...params, channel }, true); // true = 跳过单独通知
} catch (error) {
// 继续打包下一个
}
}
// 统一发送通知
await MessageHelper.send();
}
这样打包10个渠道,只会收到1条汇总消息,不会被通知轰炸。
6.3 飞书卡片
针对不同平台,飞书卡片的展示也不一样:
- Android/鸿蒙:显示下载按钮 + CDN 链接
- 微信/抖音小游戏:显示二维码图片
- 打包失败:显示错误信息
代码里用不同的方法处理:
#parseBuildMessageElement(messageType, info) {
if (!info.succeed) {
return this.#buildFailMessage(info);
}
if (DataHelper.platforms.isQRCode(info.platform)) {
return this.#buildMessageQrcode(info);
}
if (info.platform === "harmonyos-next") {
return this.#buildMessageHarmony(info);
}
return this.#buildMessageDefault(info);
}
七、热更新支持
除了完整打包,我还支持了热更新流程。
热更新的 Pipeline 更简单:
class BuilderHotUpdate extends BuilderBase {
buildPipeline() {
return [
new ModifyVersionJsonStep(), // 修改 version.json
new BuildCreatorStep(), // 构建 Creator 项目
new ModifyMainJsStep(), // 修改 main.js
new GenerateManifestStep(), // 生成热更新 manifest
new UploadHotUpdateResStep() // 上传热更新资源到 CDN
];
}
}
复用了 Creator 构建相关的 Step,只是后面的步骤不一样。
八、配置文件设计
所有配置都放在 config/ 目录下,模版在 template/ 目录:
config/
├── base.json # 基础配置(Creator 路径、项目路径)
├── channels.json # 渠道配置(渠道列表、分组)
├── platforms.json # 平台配置(构建路径、打包配置文件)
├── certificates.json # 证书配置(Android/鸿蒙签名证书)
├── oss.json # OSS 配置(上传地址)
├── hotupdate.json # 热更新配置
└── notification.json # 通知配置(飞书 webhook)
配置和代码分离,不同项目只需要改配置文件,不用改代码。
九、目录结构
最后放一下项目的目录结构:
src/
├── core/ # 核心框架
│ ├── Context.js # 数据容器
│ ├── Step.js # 步骤基类
│ ├── Pipeline.js # 执行器
│ └── BuildCache.js # 构建缓存(buildGroup 共享)
├── builders/ # 构建器
│ ├── BuilderBase.js # 构建器基类
│ ├── BuilderFactory.js # 工厂类
│ ├── BuilderAndroid.js # Android
│ ├── BuilderWechat.js # 微信小游戏
│ ├── BuilderHarmony.js # 鸿蒙
│ └── ...
├── steps/ # 步骤
│ ├── creator/ # Creator 相关
│ │ ├── BuildCreatorStep.js
│ │ ├── ModifyVersionJsonStep.js
│ │ ├── ModifyMainJsStep.js
│ │ └── GenerateManifestStep.js
│ ├── compile/ # 编译相关
│ │ ├── CompileAndroid.js
│ │ ├── CompileWechat.js
│ │ └── ...
│ ├── sign/ # 签名相关
│ └── upload/ # 上传相关
├── config/ # 配置加载器
├── notification/ # 通知系统
├── oss/ # OSS 上传
├── utils/ # 工具类
└── bin/ # 入口脚本
├── build.js # 交互式打包
├── build-jenkins.js # CI/CD 打包
├── hotupdate.js # 交互式热更新
└── hotupdate-jenkins.js # CI/CD 热更新
十、总结
说实话,这个工具我写了大概一周,后面又断断续续优化了几天。
核心就这几个东西:
- Pipeline 模式:把流程拆成步骤,组装执行
- Context 数据容器:步骤间解耦,通过数据通信
- buildGroup 共享:同平台渠道共享构建产物,省时间
- 工厂模式:数据驱动,消除 if/else
其实架构不复杂,关键是想清楚要解决什么问题。
之后每次发版,我就运行一条命令,然后去喝杯咖啡。等收到飞书通知,所有渠道都打完了。
后来我把它集成到Jenkins后,每次发版,策划直接在网页上点击几下就可以完成打包了。
任务顺利交出去了,反正我再也不用发版当天加班到凌晨了。
如果你也在做游戏开发,也被多渠道打包折磨过,希望这篇文章能给你一些启发。
十一、扩展阅读
当然也有Store版本,不想自己动手的可以购买使用: