我是怎么设计 Cocos Creator 自动化打包工具的

我是怎么设计这个 Cocos Creator 自动化打包工具的

写在前面

说实话,写这个工具纯粹是被逼的。

发版那天晚上,对着10+个渠道的打包列表,我一遍遍地重复着:打开 Creator → 选平台 → 构建 → 等 → 开 Android Studio → 编译 → 签名 → 上传 → 发通知…

到第三个渠道我就开始怀疑人生了,到第七个渠道我开始思考为什么要做游戏开发。

那天晚上我就下定决心:这种狗屎重复劳动必须自动化

一条命令,所有渠道自动打包、自动上传、自动通知。我该干嘛干嘛去,等着收结束消息就行。

一、先聊聊我想解决什么问题

在动手之前,我先梳理了一下痛点:

  1. 渠道太多:Android 官方包、TapTap、抖音APP、微信小游戏、抖音小游戏、支付宝小游戏、华为快游戏、OPPO快游戏、小米快游戏、鸿蒙…

  2. 流程重复:每个渠道的打包流程大同小异,但手动操作每次都要从头来

  3. 容易出错:手动切换配置、手动签名,稍不注意就搞错

  4. 耗时太长:一个渠道打完要好几分钟,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上传到后台
        ];
    }
}

看到没?两个平台共用了 ModifyVersionJsonStepBuildCreatorStep,但后面的步骤完全不同。

这种设计的好处就是:

  • 步骤复用:相同的步骤在不同 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) { ... }
}

为啥要这么设计?

  1. 单一数据源:所有步骤共享同一个 Context,避免参数传递
  2. 解耦:Step 之间不直接依赖,只依赖 Context 中的数据
  3. 可追溯: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 热更新

十、写在最后

说实话,这个工具我写了大概一周,后面又断断续续优化了几天。

核心就这几个东西:

  1. Pipeline 模式:把流程拆成步骤,组装执行
  2. Context 数据容器:步骤间解耦,通过数据通信
  3. buildGroup 共享:同平台渠道共享构建产物,省时间
  4. 工厂模式:数据驱动,消除 if/else

其实架构不复杂,关键是想清楚要解决什么问题。

之后每次发版,我就运行一条命令,然后去喝杯咖啡。等收到飞书通知,所有渠道都打完了。

后来我把它集成到Jenkins后,每次发版,策划直接在网页上点击几下就可以完成打包了。

任务顺利交出去了,反正我再也不用发版当天加班到凌晨了。


如果你也在做游戏开发,也被多渠道打包折磨过,希望这篇文章能给你一些启发。

当然也有Store版本,不想自己动手的可以购买使用

我是【bit-老宫】有问题欢迎交流

微信:G0900901
公众号:
扫码_搜索联合传播样式-标准色版

18赞

是不是因为你贴了二、 维 、码的原因导致那么慢

应该不是吧
之前也贴过,没有审核呐

老宫牛逼啊 :+1:

需求才是生产工具的第一生产力啊

不嫌麻烦,稍微看看,大家都能写出来 嘿嘿嘿

那肯定,先有需求才有解决方案嘛

等一下~老宫:ox::beer:

作者您好小白有个问题问下,就是批量打包,那接入各个渠道的登录、支付这些是怎么来做的?

最容易处理的就是用不同的分支控制
ts代码用同一套,原生部分做到不同的分支里
打包过程中用命令控制切换分支

哈哈被逼的

好的,感谢回复。是不是这样理解,我先把各个渠道的sdk接好,然后依次git checkout切换各个分支,然后再执行打包?

把各渠道的sdk接好
执行打包
构建creator
打包渠道A
切换分支
打包渠道B
切换分支
打包渠道C

这里说的切换分支集成到打包工具中

明白了,感谢老宫解惑

不客气兄弟

我们的处理是,把各个包体的运行时工程保存起来, 在里面做SDK接入,然后每次打包时将assets替换掉就行, 这样creator只需要打包一个(任意平台,目的是为了得到assets

:cow::beer:,用gulp写过类似的, 没这么全面哈哈哈

ios,安卓 鸿蒙可以这样做
小游戏的会有差异,不能共用
工具中也提供了分组构建,可以减少creator构建次数

我最开始用python写的
不过creator用ts js
就重构了一下


没事加个群聊聊天