
移动端发布这件事我前前后后手动搞了三年直到版本节奏被需求方逼到墙角才咬牙把 EAS 和 Fastlane 组合起来把 iOS、Android 两端的打包发布彻底变成一句命令。今天这篇就把这套工程化方案的思路、完整配置和踩坑记录写清楚不灌水全是能直接拿去用的东西。先说结论EAS 负责云构建、签名托管和环境隔离Fastlane 负责上架、TestFlight、Play 内测轨道、版本号、发布说明这些偏发布侧的动作两者不是替代关系而是把“从代码到商店”这条链路拆成两段各管各的。这套方案对使用 Expo 或 React Native 的团队尤其合适个人开发者一个人维护多个环境时也能省掉大量重复劳动。如果你现在还在用 Xcode 和 Android Studio 手动点导出、手动填账号密码上传那这篇文章就是给你写的。1. 手动发布为什么会成为瓶颈1.1 一次安卓打包背后有多少事很多人觉得安卓打包就是 Android Studio 里点一下 Build Bundle然后登录 Play Console 上传就行。实际真跑过发布流程的人都知道这中间藏着一堆手工环节先要确认 JDK、SDK、NDK 版本和队友一致否则本地构建出来的 AAB 可能是另一个环境下的产物然后要处理 Gradle 依赖下载、资源优化、多渠道包配置打完包还要检查签名是否有效、版本号是否冲突、发布说明有没有更新上传到 Play Console 后还要选轨道、填更新说明、确认审核状态。这些操作第一次做还好第二次开始就会烦到了每周固定发版的时候人就成了流水线上最不稳定的那个环节。我见过团队里因为某个人本机缓存了旧依赖构建出了一个没有包含最新代码的“假包”然后在 Play 内部测轨道里发给测试排查了整整半天。这类问题本质上不是个人能力问题而是手动流程里必然会出现的信息差和状态漂移。1.2 iOS 和安卓从来就不对称安卓的痛点集中在环境不一致和渠道多而 iOS 的痛点集中在权限和签名。iOS 打包必须要 macOS 和 Xcode这套东西只在自己的机器上能用CI 上跑要么租 Mac 实例要么忍受本地排队每次构建都要面对 distribution certificate、provisioning profile、bundle identifier 这一串 Apple 生态的名词配置错了就是各种 Itms-90034、Missing Provisioning Profile 之类的报错。更麻烦的是证书和描述文件是多人协作时的重灾区。一个人导出了证书另一个人电脑里没有私钥于是自己又生成了一份结果旧的描述文件失效线上用户更新时直接闪退。这种问题一旦发生修复成本极高因为涉及到已经发布的版本、Apple 后台的记录、团队里每个人本地的 keychain。我在很长一段时间里对 iOS 发版都有一种“又要开始渡劫”的恐惧感直到后来把签名和描述文件交给 EAS 托管这件事才算彻底翻篇。1.3 自动化的目标在动工具之前要先把自动化到底想解决什么定义清楚。我给自己列的目标只有四条第一任意一个代码提交或版本标签都能触发一套可重复的构建流程不在依赖任何人的电脑第二iOS 和安卓的构建产物必须从同一套代码、同一个配置文件里产生避免两端出现偏差第三签名、证书、密钥这类敏感信息不能散落在个人电脑里必须集中托管第四上传商店、创建 TestFlight 版本、更新内部测试轨道这些操作要么全自动要么一键确认不要每次手工登录后台点十几下。后面所有关于 EAS 和 Fastlane 的配置都是围绕这四个目标展开的。理解了为什么需要自动化你才能在自己的团队里分清哪些环节该切到 EAS哪些环节该交给 Fastlane而不是照着网上的模板抄一遍就完事。2. EAS 云构建到底负责什么2.1 EAS 不是只有云构建EAS 的全称是 Expo Application Services它是 Expo 官方提供的一套应用服务不只是“云构建”这一个功能。它包含 EAS Build、EAS Submit、EAS Update 和 EAS Hosting。Build 是在云端把 React Native / Expo 项目打包成 IPA 或 AABSubmit 是把构建产物提交到 App Store Connect 或 Google Play ConsoleUpdate 是做 OTA 热更新Hosting 是托管更新产物和相关资源。这整套服务里我们日常用得最多的是 Build 和 Submit。Build 最大的价值不是“不用自己电脑”而是它把构建环境标准化了你在本地再怎么折腾依赖云端永远是用一个干净的 macOS 或 Linux 环境从零开始拉代码、装依赖、跑构建。这一点对 React Native 项目来说非常重要因为很多本地方能复现的“坏包”问题本质上都是本机环境脏了而云端构建天然免疫这个问题。2.2 eas.json 就是发布策略的“配置文件EAS 构建的行为完全由项目根目录的 eas.json 控制。这个文件定义了若干个 profile每个 profile 对应一种构建场景比如开发包、预览包、生产包。你可以在不同 profile 里指定不同的构建方式、分发渠道、资源规格和自增版本号。下面是一个我目前在生产环境使用的 eas.json 示例参数不多但每个字段都很关键{ cli: { version: 5.0.0 }, build: { development: { developmentClient: true, distribution: internal, android: { buildType: apk } }, preview: { distribution: internal, android: { buildType: apk } }, production: { autoIncrement: true, env: { API_ENV: production } } }, submit: { production: {} } }development 这个 profile 用于开发调试developmentClient 设为 true 意味着需要配合 Expo Go 或开发客户端使用它不会生成最终商店包distribution 设为 internal 表示产物只用于内部分发可以安装到注册的设备上。android.buildType 为 apk 时预览包可以直接发给人安装不用走 AAB 的 Google Play 签名流程。preview 和 development 的区别在于 preview 不需要开发客户端它可以作为独立 App 安装到测试机上。production profile 是最关键的。autoIncrement 会在每次构建时自动递增 build number不会再出现版本号撞车的问题env 里可以注入构建时期的环境变量比如 API_ENV。注意这里注入的是构建时的环境变量不是运行时变量所以不要在 env 里放需要保密的密钥。真正敏感的 Secret 应该通过 EAS 的 Secrets 功能管理构建时会注入到进程中但不会写进产物里的明文配置。2.3 云端构建免费吗额度、排队与计费逻辑“EAS 云端构建免费吗”这个问题几乎是每个刚接触 EAS 的人都会问的。答案很简单EAS Build 本身有免费额度但它是限额的不是无限白嫖尤其是对 iOS 构建有比较明显的共享资源限制。免费额度大约是按月计算会包含一定次数的构建Android 和 iOS 分开计算同时免费档还会限制并发数。也就是说如果你一个月只发几个版本免费额度完全够用但如果你频繁触发多平台打包或者团队并发构建多很快就会发现构建排队时间越来越长。免费档用的构建队列是共享的高峰期可能要等很久这也是很多人觉得“EAS 云构建不够快”的原因。哪些情况建议升级付费我的判断标准很简单如果构建开始成为你发版流程的核心依赖并且排队时间影响了交付节奏那就值得花钱买更多的并发和更高优先级的队列。如果只是个人项目、学习 Demo、偶尔发版免费档先跑起来完全没压力。具体价格和每月次数每年都会调整不要看旧博客的截图直接打开官网的 pricing 页面核对才靠谱。3. Fastlane 在流水线里的位置3.1 Fastlane 是“发布动作集合器”Fastlane 是发布侧的老牌自动化工具它本身不打包而是通过上百个 action 把发布相关的零散步骤串成 lane。在 Fastlane 的语境里lane 就是一条可复用的发布流水线比如“打安卓测试包并上传 Play 内部轨道”可以是一条 lane“推送 TestFlight 内测版”可以是另一条 lane。为什么已经有 EAS 还需要 Fastlane因为 EAS 的强项是云构建和托管签名但在上架发布这个环节Fastlane 的生态更细。它可以设置 App Store Connect API Key可以管理 TestFlight 的测试组和发布说明可以处理 Play Console 的轨道切换还能做应用截图和商店元数据。如果你用过 Fastlane 的 deliver 和 pilot你会明白这些细节用常规后台逐个点开要花多少时间。我的做法是把 Fastlane 当作发布动作的统一入口EAS 当作底层构建引擎两者通过命令行和配置文件衔接起来。这样既能享受 EAS 云构建的标准化环境又能使用 Fastlane 在发布侧积累的完整工具链。3.2 编写第一条 lanesiOS 上 TestFlight在项目里装了 Fastlane 之后你会在工程目录下看到一个 fastlane 文件夹里面最关键的是 Fastfile 和 Appfile。Fastfile 用 Ruby 语法定义 laneAppfile 定义应用标识和账号信息。iOS 侧最常用的一条 lane 是把一个已经构建好的 IPA 上传到 TestFlight。因为我已经用 EAS 构建出产物这里的 lane 不需要做编译只需要做上传和版本处理platform :ios do desc 上传最新构建产物到 TestFlight lane :upload_testflight do api_key app_store_connect_api_key( key_id: ENV[ASC_API_KEY_ID], issuer_id: ENV[ASC_API_ISSUER_ID], key_filepath: ENV[ASC_API_KEY_FILEPATH], in_house: false ) pilot( api_key: api_key, ipa: ENV[IPA_PATH], skip_waiting_for_build_processing: true, distribute_external: false, groups: [Internal] ) end end这里最关键的是不要再用 Apple ID 密码加验证码的方式认证。Fastlane 官方早已支持 App Store Connect API Key也就是在 Apple Developer 后台创建一个 .p8 格式的密钥文件配合 key_id 和 issuer_id 直接上传。这样既绕开了双重验证码带来的交互问题也让 CI 环境里的自动化成为可能。api_key 对象可以在同一份 lane 里传给 pilot 和其他上传 action不用每次都重新读取。pilot 里的 skip_waiting_for_build_processing 参数很实用它的意思是上传成功后不阻塞等待苹果处理配合测试组使用可以省掉几分钟的轮询时间。distribute_external 设为 false表示先只发给内部测试组不直接对外发布生产环境要不要外发可以再加参数控制。3.3 Android 内测轨道上传安卓侧没有 TestFlight 这个概念它的主要发布渠道是 Google Play 的各个轨道内部测试、封闭测试、公开测试、生产。Fastlane 的 upload_to_play_store action 可以帮你把 AAB 上传到任意一条轨道并设置发布状态。如果你也在用 EAS Build 构建安卓产物EAS 会在构建完成后返回一个可下载的 AAB 路径你把下载后的文件丢给 Fastlane它负责上传和轨道管理。下面是一条上传内部测试轨道的 laneplatform :android do desc 提交 AAB 到 Play Console 内部测试轨道 lane :upload_internal do upload_to_play_store( aab: ENV[AAB_PATH] || android/app/build/outputs/bundle/release/app-release.aab, track: internal, json_key: ENV[PLAY_JSON_KEY] || play-service-account.json, release_status: draft ) end endPlay Console 的认证方式和 Apple 不同它不是用 API Key而是用 Google Cloud 的服务账号 JSON 文件。你需要先在 Google Play Console 后台设置 API 访问权限创建一个服务账号然后把生成的 JSON 文件下载下来。这个文件就是安卓上传的钥匙等同于 iOS 侧的 .p8 文件务必妥善保管不要提交到 Git 仓库。release_status 设为 draft 可以让上传不触发正式审核等测试结果出来后手动改状态适合内部测试场景。3.4 签名怎么办match 与 EAS Credentials 的选择Fastlane 体系里有一个很出名的签名方案叫 match它的原理是用一个加密的 Git 仓库统一存放 iOS 的证书和描述文件团队成员需要时自动从仓库拉取并安装到本地。这个方案对纯 React Native CLI 项目和传统 iOS 团队很好用解决了签名文件散落在各人电脑上的问题。但对于使用 EAS 的 Expo 项目我不建议同时启用 match 和 EAS Credentials因为两套签名体系并存容易造成冲突。EAS Build 自己有一套云端签名管理项目第一次执行 eas build 时如果你选择让 EAS 自动管理签名它就会生成 iOS 的描述文件和安卓的 keystore并安全保存在 EAS 服务器上。所有在云端发起的构建都会自动引用这套签名根本不需要你的构建机安装证书。什么时候需要切到 match如果你还想保留完全本地构建的能力比如在 Mac 上使用 Fastlane 的 gym 打包 IPA那就需要本机有签名文件这时 match 才值得引入。我的建议是只要你的主构建路径已经切到 EAS就用 EAS Credentials 一条路走到黑别在本地再留一套签名。混合使用的结果往往是云构建用了一组证书、本机签名用了另一组最后上架时发现包签名对不上。4. 把 EAS 和 Fastlane 接成一条完整流水线4.1 分工别硬套谁构建、谁上传、谁管证书把工具接到一起之前先要把分工想清楚。我见过不少团队绕了很多弯路原因就是两套工具边界模糊互相抢占职责。我的分工原则是构建归 EAS上传归 EAS Submit 或 Fastlane证书归 EAS 或 match版本号归构建系统运营动作归 Fastlane。具体来说代码提交后触发 EAS Build 云端构建产出 IPA 和 AAB构建成功后如果需要对外分发到 App Store 和 Play Store可以直接用 EAS Submit 一键上传也可以用 Fastlane 做更精细的轨道控制而商店截图、发布说明、测试组管理这类工作Fastlane 仍然是不可替代的主力。所以“EAS 替代 Fastlane”和“Fastlane 替代 EAS”这两种说法都是不对的。EAS 的优势在构建基础设施Fastlane 的优势在发布侧的业务逻辑。同一个项目里可以用 EAS 构建加 EAS Submit 走完主要流程再用 Fastlane 处理特殊场景比如固定版本号、生成商店截图、清理测试组。4.2 用 Fastlane 做统一入口如果你团队的同学已经习惯敲 fastlane 命令不想让他们学一套新的 EAS 命令可以用 Fastlane 包一层 shell 调用让所有操作都从一个 lane 进去。这种做法的好处是团队发布文档不用频繁更新无论底层是 EAS 还是其他工具对外暴露的入口始终是 fastlane release 这样一个稳定的命令。下面是一个简单的编排 lane它不去复制 EAS 的活而是把 EAS 云构建和 EAS Submit 串起来lane :release do sh(npx eas build --platform all --profile production --non-interactive) sh(npx eas submit --platform all --profile production --non-interactive) end如果你的团队还想在发布前跑一轮自动化测试也可以在这个 lane 前面加上 scan 或 gradle 相关动作。核心思路是什么都往 lane 里放但实际执行时仍然让专业工具做自己擅长的事。Fastlane 在这里更像一个导演而不是演员。4.3 CI 触发GitHub Actions 与标签发布手工敲命令和完全自动化的差异体现在 CI 上。我的习惯是用 Git 标签作为发布触发器开发人员打一个 v1.4.0 这样的标签GitHub Actions 就开始执行构建和提交。这样的好处是版本记录和发布过程天然绑定谁在什么时间发布、对应哪个 commit都能追溯到。下面是一个最简可用的 GitHub Actions workflow。它只负责调用 EAS不做本地构建因为云端打包不需要也不想占用你的 CI 机器name: Release on: push: tags: - v* jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 cache: npm - run: npm ci - name: EAS Build run: npx eas build --platform all --profile production --non-interactive env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - name: EAS Submit run: npx eas submit --platform all --profile production --non-interactive env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}EXPO_TOKEN 是 Expo 账号下的 access token需要在 Expo 开发者后台生成然后配置到 GitHub 仓库的 Secrets 里。这样 Actions 环境里不需要任何本地账号登录状态也不存在把密码写进代码的问题。注意 eas build 默认会等待构建完成才会进入下一步如果高峰期排队很长时间Actions 任务可能因为超时而失败。这时你可以给 eas build 加上 --no-wait让命令触发后立刻返回再通过 EAS 的 webhook 或轮询接口确认构建状态但这套方案复杂度会高不少。我的建议是先用默认的等待行为跑起来等真的遇到排队瓶颈再优化不要一开始就把流水线搞复杂。4.4 分支与环境映射dev/preview/prod 怎么走自动化流程一旦跑起来环境管理就成了新的问题。开发分支的每次提交都打正式包那肯定不行。我的做法是三个环境对应三种触发方式dev 分支每次推送触发 development profile 构建用于开发自测preview 分支或 pull request 触发 preview profile用于给测试人员和产品验收主分支打 tag 触发 production profile流向应用商店。这个映射关系既可以通过 GitHub Actions 的分支过滤实现也可以在 eas.json 的不同 profile 里配置不同的 env 和构建参数。比如 dev 构建注入 APP_DEBUGtruepreview 构建注入 APP_ENVstagingproduction 构建注入 APP_ENVproduction。这样同一套代码在不同阶段拿到不同的行为发布链路才能做到既自动化又不失控。5. 实操从零配置一个 Expo 项目5.1 前置准备与 eas-cli 初始化开始配置之前你需要本地已经装好 Node.js 和 npm。如果还没初始化项目先执行 create-expo-app 建一个空项目如果已经是 React Native 项目也可以直接用 EAS它支持检测原生项目结构。初始化好项目后安装 eas-cli 并登录npm install -g eas-cli eas login如果是 CI 环境不执行 eas login而是把 EXPO_TOKEN 配置到环境变量即可。登录成功后在项目根目录执行eas build:configure这个命令会检测你的项目并生成 eas.json。它还会提示你配置 app.json 里的 android.package 和 ios.bundleIdentifier这两个字段是应用在商店里唯一标识必须提前想清楚因为上线后很难修改。我的建议是使用反向域名格式比如 com.yourcompany.yourapp。5.2 配置 app.json、eas.json 与环境变量先看 app.jsonExpo 项目的基础配置都在这里{ expo: { name: MyApp, slug: my-app, version: 1.0.0, orientation: portrait, ios: { bundleIdentifier: com.yourcompany.myapp, supportsTablet: true }, android: { package: com.yourcompany.myapp, versionCode: 1 }, extra: { apiUrl: https://api.example.com } } }version 是面向用户的版本号android.versionCode 和 ios.buildNumber 是面向商店的构建号。我建议不要在 app.json 里手动维护构建号而是交给 EAS 的 autoIncrement 处理它在每次 production 构建时自动加一非常省心。构建时的环境变量尽量集中在 eas.json 的 profile 里配置。对于真正的 Secret比如后端密钥、三方服务凭证不要写在 app.json应该在 EAS 后台或通过 eas env 命令创建项目级 Secret。构建过程会把 Secret 注入到环境变量中供原生构建代码使用。运行时需要读取的公网地址、开关配置才放在 app.json 的 extra 字段里。5.3 Fastfile 与 Appfile 落地接下来初始化 Fastlane。传统做法是执行 fastlane init 交互生成文件但对 Expo 项目我更喜欢手动创建因为生成出来的模板经常带很多用不上的注释。项目根目录创建 fastlane 文件夹放两个文件Appfile 和 Fastfile。Appfile 用来声明应用信息app_identifier com.yourcompany.myapp apple_id ENV[APPLE_ID] team_id ENV[APPLE_TEAM_ID] json_key_file ENV[PLAY_JSON_KEY]Fastfile 可以定义多条 lane覆盖不同的发布场景。如果只是需要上传已经构建好的产物Fastfile 里面写 iOS 的 upload_testflight 和安卓的 upload_internal 两条 lane 就足够日常使用了。当你跑 bundle exec fastlane ios upload_testflight 时它就会读取 .p8 密钥把指定路径的 IPA 上传到 TestFlight跑 bundle exec fastlane android upload_internal 时它就会用服务账号 JSON 把 AAB 推送到 Play 内部轨道。5.4 首次构建和排错配置完成后第一次执行构建通常是最磨人的eas build --platform all --profile production执行后EAS 会检查项目的签名配置。iOS 需要注意开发者账号里没有可用的 distribution certificate它会引导你登录 Apple 账号并生成对应证书和描述文件安卓则是确认是否让 EAS 生成新的 keystore。这个过程只需要交互一次之后所有云端构建都会复用这套签名不再需要你处理。首次构建没有意外的话会看到构建进度日志里面包含 EAS 在云端执行的每一步拉取依赖、执行 prebuild、编译原生代码、打包产物。如果构建失败不要慌先把完整日志下载下来看90% 的错误集中在两个地方一是依赖安装阶段因为网络原因拉取超时二是原生编译阶段因为内存不足被 kill。这两种问题都可以通过 EAS 后台调整资源规格或清理依赖缓存来解决。6. 上线前必须知道的坑与排查清单6.1 证书和描述文件过期证书和描述文件过期是自动化流程里最隐蔽的坑。证书文件在本地看起来还在但云端签名时一一校验到期就会失败。使用 EAS Credentials 自动管理签名的时候EAS 会在证书接近过期时提示你续期但如果你不是频繁发版很容易忽略。我建议在发布日历里加一个周期检查项每季度登录一次 Expo 后台和 Apple Developer 后台确认所有证书、描述文件的有效期。Fastlane match 用户也要关注 match repo 里证书的到期时间最好写一个巡检 lane定期调用 match 的验证功能。6.2 上传失败App Store Connect 认证与 Play Console上传失败是最让人烦躁的问题因为错误提示常常不够直白。App Store Connect 侧最常见的失败原因是 .p8 密钥对应的权限不足或者密钥被删除。使用 API Key 认证时默认只有上传 App 的权限如果你需要在 Fastlane 里读取测试员列表、管理用户就要在创建密钥时勾选对应的权限范围。权限不够的表现往往是调用了某个 action 却报 403。Play Console 侧的上传失败多半是服务账号 JSON 的权限没配对。很多人把服务账号创建出来了但没有在 Play Console 后台把账号添加为项目的用户这时 Google API 就会拒绝访问。还有一个高频问题是 AAB 的签名和应用在 Play Console 里的签名不一致导致上传被拒。改为 EAS 托管签名后这个问题基本不会出现因为 EAS 会使用与应用注册一致的签名。6.3 构建阶段卡死pod install、内存、超时EAS 云端构建虽然环境干净但也会卡。我看过最多的就是 react-native 项目在 pod install 阶段一直转圈。这种问题通常是 CocoaPods 拉取依赖网络波动导致解决办法是在构建前用自定义构建脚本设置镜像源或重试逻辑或者在 eas.json 里开启缓存避免每次从零拉依赖。另一个常见问题是 iOS 构建内存不足尤其是使用免费档的小资源规格时。遇到这种情况先尝试在 eas.json 的 ios.resourceClass 里换成更贵的 m 系列实例或者把构建任务拆分先构建安卓确认代码没问题再执行 iOS 构建减少同时构建两个平台的压力。6.4 一点心得日志、幂等性、分级发布整个流程跑顺之后我最后想说的是三条经验。第一日志必须单独保存EAS 构建日志和 Fastlane 的输出不要只打印到终端建议默认落地到文件并让 CI 自动归档。出问题的时候拿得出完整日志排查和拿不出日志完全两种心境。第二所有 lane 和流水线都要设计成可重复执行同一套流程跑十次和跑一次结果一致这是流水线的基本功。第三尽量走分级发布先发内部测试组再发封闭测试、公开测试最后生产不要把自动化把所有环境一步推到底。我在这套体系上线后最明显的感受是发布不再是一个让人紧张的事件而是一个按部就班的过程。所有证书、密钥、依赖、版本号都有固定归属步骤可追溯、产物可回滚人只需要在关键节点做确认。最后再分享一个小习惯每次上线后把构建号和发布说明归档到一个 changelog 文件里下次有问题回溯时你会感谢现在的自己。