从去年开始,我陆陆续续接手过好几个 React Native / Expo 项目,几乎每个团队的共同痛点都是同一个:打包发布靠人工、靠某一个人的电脑、靠运气。发布日前一天晚上,负责打 iOS 包的同学电脑没电、Xcode 版本不对、证书过期、Google Play 账号密码在另一个人手里——这种场景我经历过太多次。后来我把打包发布整体迁到 EAS + Fastlane 这套组合上,才真正体会到什么叫"工程化终局":不管是 iOS 还是 Android,不管是内测分发还是商店上架,一条命令或者一次 Git 推送就能全部搞定。
这篇文章就把我这套多端自动化打包发布方案完整拆开讲,包括为什么这么选型、EAS 和 Fastlane 各自负责什么、配置怎么写、踩过哪些坑,以及后续怎么把 AI 测试和代码审查也接进来。不管你是独立开发者还是中小团队的技术负责人,这套方案都有很强的参考价值。
1. 把"发布日"从噩梦变成例行公事
1.1 我经历过的手动打包惨状
先说说我为什么要折腾这套东西。之前有个项目,双端都要发版,团队里只有一台 Mac mini 能打 iOS 包。每次发版流程是这样的:
- 开发同学合并完代码,告诉负责打包的同学"可以打了";
- 打包同学拉代码、切分支、装依赖、跑 bundle、开 Xcode 调签名、Archive、等上传;
- 上传完 TestFlight 或者 App Store Connect 之后,还要去后台填审核资料、传截图;
- Android 那边同样来一遍:生成 AAB、签名、上传 Google Play、等审核。
整个过程耗时两三个小时,而且只要中间任何一步报错,前功尽弃。最常见的问题就是:本地 node_modules 装了半年没更新、某次升级之后 CocoaPods 版本不对、证书链里少了一个中间证书、Google Play 服务账号权限没配好。每次出问题,都要花大量时间排查"为什么这台电脑上能编,那台电脑上不能编"。
后来我意识到,问题的根源不在"某一步操作没做好",而在于整个流程过度依赖人、依赖环境。只要打包环境是"某台特定电脑",这个问题就永远存在。所以我把目标定成:打包和发布这件事,必须环境无关、人人可触发、可重复执行。EAS 和 Fastlane 就是冲着这个目标去的。
1.2 先搞清楚 EAS 和 Fastlane 各自该干什么
很多刚接触的人会把 EAS 和 Fastlane 搞混,觉得它们都是"自动化打包工具",选了其中一个就行。实际上它们是不同层次的工具,配合起来才是完整的解决方案。
EAS(Expo Application Services)是 Expo 推出的云端构建服务,核心能力是"把你的项目源码拿到 Expo 的云服务器上编译",Android 产出 AAB/APK,iOS 产出 IPA。它解决的是"编译环境一致性问题"。你在本地不管装了什么稀奇古怪的东西,只要 eas.json 配好了,云端每次都是干净的环境编译。
Fastlane 是一个开源的自动化发布工具链,核心能力是"把发布过程中的所有操作脚本化"。它管的事情更偏向"发布动作":生成和管理签名证书、给 iOS 版本号自增、上传 IPA 到 TestFlight、上传 AAB 到 Google Play、生成商店截图、提交元数据等等。
简单说:EAS 管"把代码变成安装包",Fastlane 管"把安装包变成已发布的版本",以及"让签名这件事不再靠手动"。当然两者也有功能重叠的地方,比如 EAS Submit 也能上传商店,Fastlane 也能在本地打包。但最顺手的分工是:EAS 负责云构建,Fastlane 负责签名管理和富媒体上传。后面我会展开讲这个分工。
1.3 这套方案适合谁
先说清楚,这套方案不是所有项目都必要。如果你的项目只是个人 Demo,或者一个月才发一次版,手动打包虽然烦但也能忍。但如果你符合下面任意一条,我认为就值得迁移:
- 团队超过 2 人,iOS 打包依赖特定某台 Mac;
- 发版频率超过每周一次,甚至每天一次;
- 同时维护 iOS、Android 两个平台;
- 经常需要打"给测试同学的内测包",而不是只发正式版;
- 证书管理混乱,经常出现"我的证书过期了""那个证书在谁的电脑上"这类问题。
迁移成本其实没有想象中高,尤其是 Expo 项目,EAS 基本是开箱即用。哪怕是纯 React Native 裸项目,也可以把 EAS 当独立的云构建服务用。Fastlane 那边主要是第一次配置 match 证书体系比较费劲,配好之后一劳永逸。下面我从头到尾把每一步讲清楚。
2. EAS 云端构建:把"我这台电脑能编"变成"云端永远能编"
2.1 EAS 到底解决了什么
EAS Build 的设计思路一句话就能说清楚:每次构建都从零开始,在云端一个干净的容器里执行"安装依赖 → 生成原生工程 → 编译 → 签名"全流程。它不像在本地打包那样"这次成功可能是因为上次缓存了什么",而是保证同样的代码和配置,在任何时间、任何人触发,产出的包是一致的。
对于 Expo 项目来说,EAS 有个更狠的优势:它连"原生工程"都不需要你维护。你在本地可能从来不需要打开 Xcode 或者 Android Studio,只需要维护好 app.json 和 eas.json,EAS 会帮你生成对应的原生工程并完成编译。这对不熟悉原生开发的团队来说,省掉的功夫是巨大的。
EAS Build 还直接集成了签名能力。iOS 证书、Android 签名密钥都可以上传到 EAS 的凭据管理系统,构建的时候自动使用。也就是说,连"证书装在谁的钥匙串里"这种问题也被云端接管了。
2.2 eas.json 配置拆解
EAS 的核心配置在项目根目录的eas.json。我先给一份我目前在生产环境用的配置,再逐段解释为什么这么写:
{ "cli": { "version": ">= 8.0.0", "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "channel": "development" }, "preview": { "distribution": "internal", "channel": "preview", "android": { "buildType": "apk" } }, "production": { "distribution": "store", "channel": "production", "autoIncrement": true } }, "submit": { "production": {} } }这里有几个关键点:
developmentClient: true:development 这个 profile 是给开发调试用的,需要配合 Expo Go 的 dev client 使用,产出的是开发版应用,方便连本地调试服务。distribution: "internal":preview 和 development 都走内部分发,意味着不需要走商店审核,可以直接给测试同学装。buildType: "apk":preview 在 Android 上打 APK 而不是 AAB。因为内测分发用 APK 最方便,而正式上架 Google Play 需要 AAB。两个 profile 分开打,各干各的。autoIncrement: true:production 构建时版本号自动递增,不用每次手动改。这个非常重要,因为 iOS 上传同版本号会直接报错。
channel 的概念也顺便说一下。Expo 的更新服务可以按 channel 区分不同环境,比如 development 环境连的是开发服务器,production 环境走 OTA 更新。构建时指定 channel,相当于告诉 EAS"这个包是给哪个环境用的",后续发布 JS 更新时也能精准投递。
2.3 环境变量与敏感信息处理
构建过程不是只要代码就能跑,很多项目需要 API 地址、第三方 key、甚至一些私有仓库的访问凭证。EAS 提供了多层级的配置方式:
- 写在
eas.json里的env字段:适合所有环境都一样、且不敏感的信息; - 写在项目根目录
.env文件(会被.gitignore忽略):EAS CLI 构建时如果你加了--profile参数,它可以把本地的.env传给云端; - EAS 后台的 Secrets 面板:适合存真正的敏感信息,比如 API key、上传凭证,一旦创建后不会再明文显示。
我实际的项目中,API 地址这类按环境区分的配置放在app.config.ts里读取,通过EXPO_PUBLIC_前缀暴露给客户端:
export default { name: "MyApp", slug: "my-app", extra: { apiBaseUrl: process.env.API_BASE_URL, enableAnalytics: process.env.ENABLE_ANALYTICS === "true" } };构建时在 EAS Secrets 里配好API_BASE_URL、ENABLE_ANALYTICS,云端构建就会自动读取,代码里也拿得到。这样做的好处是:本地开发、内测包、正式版用的是同一套代码,只是环境变量不同,不会再出现"测试环境好好的,生产环境崩了"这种低级问题。
2.4 EAS 云端构建免费吗
这个问题很多人问,我直接说结论:EAS Build 有免费额度,但比较有限,团队项目大概率要付费。
截至我写这篇文章的时候,EAS 的免费计划每月赠送一定数量的构建时长,个人项目偶尔打几个包完全够用;但如果每天都触发构建、还要同时打 iOS 和 Android,免费额度很容易用完。EAS 的付费档主要有两个维度:构建时长和并发数。免费计划并发基本等于 1,也就是说一个构建没跑完,另一个要排队。团队场景下,这个排队很影响效率。
我的建议是:个人项目先用免费额度,真不够再升级;团队项目直接上付费档,把"等待构建"的时间成本折算一下,你会发现这笔钱花得很值。另外,安卓构建走的是 Linux 容器,速度快,iOS 构建走的是 macOS 容器,速度慢一些,计费也按照实际时长来。控制预算的一个技巧是:内测包不要频繁打 production profile,用 preview 就够了,既能省时间也省钱。
3. Fastlane:签名、内测、商店资料这些"脏活累活"
3.1 Fastlane 在整条流水线中的位置
如果说 EAS 承包了"编译",那 Fastlane 承包的就是"编译之外几乎所有事"。以 iOS 为例,一个正式版发布背后至少有这些动作:
- 创建/更新 App ID;
- 生成和管理 Distribution 证书;
- 创建/更新 Provisioning Profile;
- 版本号自增;
- Archive 并导出 IPA;
- 上传到 TestFlight;
- 上传截图和元数据;
- 提交审核。
如果这些全部手动做,哪怕 EAS 已经帮你把包打出来了,后面的上传和提交资料仍然要耗费大量时间。Fastlane 的价值就是把这一长串动作缩减成一个命令:fastlane beta或者fastlane release。
在 EAS + Fastlane 的架构里,我通常是这样安排的:EAS Build 出包,EAS Submit 做基础上传;Fastlane 负责更复杂的动作——比如带 HTTP 头文件上传、特殊签名的处理、灰度发版的调用、以及本地跑 UI 测试。相当于 Fastlane 是那个"兜底"的瑞士军刀,凡 EAS 没覆盖到、覆盖得不灵活的场景,都由它补上。
3.2 match 管证书:告别"证书又过期了"
iOS 证书管理是所有 iOS 开发者的噩梦,Fastlane 官方后来专门做了match来解决这个问题。它的原理是:把证书和描述文件加密后存到一个 Git 仓库里,所有需要的人拉取这个仓库,按需自动安装到本地钥匙串。
我第一次配置 match 的时候也觉得麻烦,但配完之后发现这才是正道。具体步骤:
- 创建一个私有的 Git 仓库,比如
ios-certificates; - 项目里执行
fastlane match init,填仓库地址; - 执行
fastlane match appstore创建 App Store 分发证书; - 执行
fastlane match development创建开发证书。
之后任何人拉完代码,执行fastlane match development,证书和描述文件就自动装到本地了。不再有人问"证书在谁电脑上"。match 的加密密码也要放进团队的密钥管理里,否则新人入职还是没法自己拉证书。
一个非常重要的经验:在 EAS 上构建 iOS 包时,你可以用 EAS 自己管理的证书,也可以让 EAS 使用你 Fastlane match 仓库里的证书。如果团队已经有一套 match 体系,建议直接用"credentialsSource": "remote"配合appleTeamId配置,让 EAS 读取你上传的证书,而不是在 EAS 后台再维护一套。两边统一使用同一套证书,能避免"EAS 打出来的包签名和本地 Fastlane 上传的不一致"这种诡异问题。
3.3 lane 设计与常见场景
Fastlane 的核心概念是 lane,可以理解成"一个命名好的自动化步骤序列"。我项目的fastlane/Fastfile大概是这样的:
platform :ios do desc "上传到 TestFlight" lane :beta do increment_build_number(xcodeproj: "ios/MyApp.xcodeproj") build_app(scheme: "MyApp", export_method: "app-store") upload_to_testflight end desc "提审 App Store" lane :release do beta upload_to_app_store(skip_metadata: true, skip_screenshots: true) end end platform :android do desc "上传到 Google Play 内测轨道" lane :beta do gradle(task: "bundleRelease") upload_to_play_store(track: "internal") end end这里要特别提醒几点:
increment_build_number必须在打 IPA 之前执行,iOS 的 build number 每次上传都要比之前大,否则 TestFlight 会直接拒绝。- Android 侧,如果 EAS 已经帮你把 AAB 打出来了,其实 Fastlane 不需要重新跑 gradle,直接
upload_to_play_store指向那个 AAB 文件即可。反过来如果你用 Fastlane 全流程本地打包,那gradle这个 lane 是必须的。 skip_metadata: true和skip_screenshots: true在初期配置时强烈建议加上,否则 Fastlane 会尝试把本地没有的截图和描述文件推到商店,直接报错。
Fastlane 还有一个常被忽略的价值:它有一套统一的"预检"机制。比如fastlane beta执行前,会检查项目里有没有未提交的改动、证书是否匹配、版本号是否合法。这些检查虽然简单,但能避免很多低级失误。
4. 双引擎协作:Android/iOS/内测/上架一次跑通
4.1 整体流程设计
说了这么多,落地的时候到底怎么串起来?我把我现在维护的一个项目完整流程画个文字版:
- 开发者把代码推送到 Git 仓库;
- CI(我用的是 GitHub Actions,也可以用自己的 CI)检测到打了
v*的 tag,触发流水线; - 流水线里调用
eas build --platform all --profile production,EAS 在云端分别打 iOS 和 Android 的正式包; - 构建成功后,EAS 自动触发提交,或者 CI 调用
eas submit -p ios和eas submit -p android上传商店; - 内测场景走另一个分支:推送代码到
develop分支时,CI 自动调用eas build --profile preview,生成内测包,并把下载链接通过 webhook 发到企业微信/钉钉/Slack 群里。
这里 EAS 和 Fastlane 的分工是:构建、基础上传归 EAS;如果还需要更复杂的逻辑——比如上传前先跑一轮自动化测试、或者在 app store connect 里设置审核语言、或者失败时发通知——这部分我用 Fastlane 来写。你也可以把 Fastlane 嵌入到 CI 脚本里,构建完成后执行一个fastlane upload_and_notifylane,把上传和通知一起做掉。
有一个细节值得提:EAS Build 的输出可以作为一个"产物"传给 Fastlane。比如在 GitHub Actions 里,eas build完成后拿到的构建 ID 和下载 URL,可以让你直接用fastlane的 lane 去下载这个包再上传到其他平台。这就是我说的"双引擎协作",两边不是竞争,而是接力。
4.2 版本号统一管理
多端自动化发布里,版本号管理是最容易翻车的地方。iOS 和 Android 的版本体系看着像,其实完全不是一回事:
- iOS 有 CFBundleShortVersionString(展示版本号,如 2.1.0)和 CFBundleVersion(构建号,同一版本内每次上传必须递增);
- Android 有 versionName(展示版本号)和 versionCode(整数,每次上传必须递增)。
手动管这三四个数字,几乎必出问题。我在 EAS 里的做法是开启"appVersionSource": "remote",让 EAS 根据构建时间自动生成递增的构建号,同时在配置里用autoIncrement让 EAS 每次基于上一个云端版本号自增。这样本地完全不用改任何配置文件,云端就是版本号的事实来源,再也不会出现"两个人同时打了同一个构建号"的尴尬。
Fastlane 侧也可以做同样的管理:increment_build_number和increment_version_code会自动读取现有版本号并 +1。如果你走的是"EAS 打正式包,Fastlane 打测试包"的混合路线,建议把两个工具的版本号规则统一,用脚本在 CI 里生成一个全局版本号文件,EAS 和 Fastlane 都从同一个文件读取,双端永远一致。
4.3 与 CI 集成触发自动化
EAS 本身不做 CI,它只负责构建;Fastlane 也不做 CI,它只负责执行动作。真正把"提交代码 → 自动打包 → 自动发布"串起来的是 CI 流水线。
我用 GitHub Actions 的配置片段例子:
name: Build Release on: push: tags: - "v*" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx eas-cli build --platform all --profile production env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}EXPO_TOKEN是 Expo 后台生成的访问令牌,放进 CI 的 secrets 里之后,CI 机器就能以你的身份触发构建。构建完成后,你可以继续加一步用eas submit上传,或者在 EAS 后台开启 auto-submit 功能自动上传。
这里我给一个实用建议:内测包的触发不要用 tag,直接用分支更合理。因为内测包频率高、不需要严格打版本号,推一次代码就发一个链接给测试,体验会好很多。正式包才用 tag 触发,保证每次发版都有明确的代码基线。
5. 实战排错:我在迁移过程中踩过的坑
5.1 EAS 缓存污染导致构建结果不一致
EAS 为了加速构建,会默认开启缓存,包括 npm/yarn 的依赖缓存和 CocoaPods 的缓存。听起来是好事,但实际坑了我一次:某次我改了package.json里的依赖版本,本地跑得好好的,EAS 云端构建却一直用旧依赖,导致产出的包里有旧 bug。排查了很久,最后靠eas build --clear-cache解决了。
从此之后我的策略是:日常内测包可以依赖缓存加速;但每次发正式版之前,强制清一次缓存。在 production profile 对应的构建命令里加一个参数,或者在 CI 构建 yaml 里显式传入--clear-cache,确保正式包永远是从零构建的。慢个一两分钟,换来的是确定性,值。
5.2 Fastlane match 与 EAS 凭据的冲突
这个坑我印象很深。最早我们团队用 Fastlane match 管理证书,后来接入 EAS,我顺手在 EAS 后台又生成了一套新证书。结果某次内测,本地的 Fastlane 上传 TestFlight 成功了,EAS 构建出的包却一直报签名错误。最终排查发现:两套证书体系同时存在,EAS 用它自己的证书签名,但这个证书不在我们 match 仓库里,导致真机安装和验证信息对不上。
解决方案就是把凭据来源统一。我在eas.json里指定了:
{ "build": { "production": { "ios": { "credentialsSource": "remote" } } } }并确保 EAS 后台的 iOS 证书就是从 match 仓库同步过来的那套。具体的操作路径是:EAS 登录后,在项目凭据页面选择"使用已有证书",然后填入从 match 仓库导出的p12和 profile 文件。统一之后,本地 Fastlane 上传、云端 EAS 构建、App Store Connect 验证,三者用的都是同一套证书,再也没出过签名问题。
5.3 免费额度的误用与配额管理
前面讲了 EAS 有免费额度,但我见过不少团队把免费额度用光之后,直接在 CI 日志里看到一堆"build has been queued"错误,然后一脸懵。这里有个常见误区:默认的eas build命令会以当前登录账号的身份创建构建,而免费账号的并发是 1,构建多了就排队。如果 CI 里同时触发了 iOS 和 Android 两个构建,免费账号下 Android 只能等 iOS 完成才开始,耗时直接翻倍。
我的建议是:团队使用付费计划前,先在eas.json里给不同 profile 设定好resourceClass。内测包用普通档位就够,正式包换成更强的档位保证编译速度。另外,设置一个"构建触发冷却"机制,比如同一个分支在 30 分钟内重复推送只打一次包,避免无效构建烧配额。
5.4 内测链接失效与 OTA 更新的叠加问题
还有一个容易被忽视的坑:EAS 打出来的内测包,如果项目的 Expo 更新服务开着,用户首次启动时可能会收到 OTA 更新,导致实际运行的 JS 代码和打包时的不一致。测试同学反馈"我装的就是最新包啊,怎么还是旧功能",很多时候都是这个原因。
解决方法是:内测包在previewprofile 里把channel指到一个专用的 preview 频道,并且确保这个频道不发 OTA 更新;正式包走 production 频道。这样测试装的是真正的"包内代码",不会和 OTA 混在一起排查不清。
6. 从"能打包"到"工程化终局":AI 测试与代码审查
6.1 为什么打包自动化只是起点
很多人以为打包发布能自动化,工程化就"到头了"。实际上,打包自动化解决的只是"变更如何安全地到达用户"这个链路的一半。另一半是:怎么保证这次变更真的没问题。手动测试在小项目里能撑住,但一旦发版频率上来了,测试就成了瓶颈。更别提有些变更——比如改了一个全局样式、调了一个底层网络库——影响面之大,人工回归根本测不全。
所以当我把 EAS + Fastlane 这套构建发布链跑通之后,我立刻开始看下一层的工程化:测试自动化和代码审查自动化。这一步做好了,整个流水线才算闭环:提交代码 → 自动审查 → 自动测试 → 自动构建 → 自动发布,全程无人值守。
6.2 AI 自动生成测试用例
测试用例怎么来?传统思路是让开发自己写,但大部分业务代码的测试用例都是重复模板,比如"接口请求成功返回数据""接口失败弹出提示"。这类用例完全可以交给 AI 生成。现在有一些平台已经能把"根据代码变化自动补测试用例"做成日常功能,比如 Harness 这类工程化平台里就集成了 AI 测试生成能力。
我在实际项目中尝试过一个流程:在 PR 提交后,CI 里跑一个基于 AI 的测试生成器,它会读取这次 diff 涉及的函数和组件,自动生成对应的单元测试和冒烟用例,然后直接提交到分支上。开发者只需要 review 生成的用例是否合理,再点击合并。测出来不只是"覆盖率数字变好看了",而是真的能拦住一些低级回归。
给一个我自己的判断标准:AI 生成测试用例最适合的,是那些纯逻辑、输入输出明确的模块,比如工具函数、状态管理、接口数据转换层。UI 层面的测试,AI 生成的用处没那么大,还是需要写一些端到端的关键路径用例来兜底。想无脑把全部测试都丢给 AI,目前不现实,但"把 60% 的重复用例交给 AI",是完全做得到的。
6.3 自动代码审查
代码 review 是保证代码质量的另一个关键环节。人工 review 的问题是:耗时、依赖个人经验、且容易在发版前因为时间紧张而流于形式。AI 代码审查工具可以做到的是:在开发者提交 PR 后几秒钟内,自动拉取 diff,检查明显的反模式、安全隐患、性能问题、未处理的错误分支等等。
我在团队里推行的流程是:AI 审查先过一遍,给出问题列表和修改建议;开发者处理完"机器查得出的问题"之后,再交给人工 reviewer 去关注业务逻辑、架构合理性这些机器暂时不擅长的地方。这样人工 review 的负担大幅下降,质量反而提升了,因为人工可以把精力集中在真正需要人脑判断的问题上。
说实话,AI 代码审查刚开始推的时候,组里有人抵触,觉得"又多了一个挑刺的"。但用了两个月之后,大家普遍承认它确实能抓到一些容易漏掉的问题,比如空指针、资源未释放、安全组配置错误这些。重要的是把它定位成"助手"而不是"法官",建议和结论都需要人来判断。
6.4 把这些接进现有流水线
如果你已经搭好了 EAS + Fastlane 的构建发布链路,把 AI 测试和代码审查接进来其实非常顺。以 GitHub Actions 为例,基本上就是往现有流水线里加两个 job:
reviewjob:在 PR 创建和更新时运行,调用 AI 审查 API,把结果以评论形式贴在 PR 下面;testjob:在 push 和 PR 时运行,先用 Fastlane 执行本地的自动化测试 lane(或者跑 jest / detox),再让 AI 针对 diff 生成补充用例,合并前全部通过才允许进buildjob。
这样你就得到了一个完整的工程化闭环。之前"打包发布自动化"只是让最后一步变得松快,加入 AI 测试和审查之后,整个变更从提交到上线,每一步都有工具兜底,人的角色变成了"决策者"而不是"干活的"。
我对工程化的理解也在不断变化。最初我觉得能用脚本把打包发布跑起来已经很厉害,后来发现签名管理、版本号、环境变量这些细节才是决定自动化能不能长期跑下去的关键;再后来发现,有了稳定的构建和发布管道之后,把 AI 测试和代码审查接进来是一件水到渠成的事。现在再回看那段凌晨发版的经历,我只想说:尽早把规则和工具定好,人才不用去当流程里最不稳定的那个环节。