Skip to content
EAS 构建

EAS 构建

移动端工程做到最后都绕不开两件事:把 JS 和原生代码编译成能安装的二进制,把它变成商店里可下载的东西。EAS(Expo Application Services)就是 Expo 官方托管的这一段服务:云端构建(EAS Build)、提审(EAS Submit)、热更新(EAS Update)、观测(EAS Observe)等。这一章聚焦 EAS Build——eas-cli 怎么用、eas.json 怎么配、签名凭证怎么管、CI 怎么接,以及 SDK 58 带来的构建侧变化。发布与热更新见 07-EAS发布与热更新.md。

先回答一个常见质疑:我本地 npx expo run:ios 也能出包,为什么要用云端构建?

EAS 是什么,和 expo CLI 怎么分工

一句话原则:本地能跑通的不必上云,上云是为了消灭"你机器和我机器不一样"。

  • npx expo start / npx expo run:android|ios 是 Expo CLI 的活,全在本地,不经过任何云服务。
  • EAS Build 把同样一条流水线搬到 Expo 提供的 macOS/Linux 构建机上跑:远端 npm ci、npx expo prebuild 生成原生工程(CNG 项目甚至不需要把 ios/、android/ 提交进仓库)、跑 CocoaPods 和 Gradle、签名、产出 .ipa/.aab/.apk。

值得上云的三个理由:

  1. 不养一堆 Mac。团队做 iOS 构建,传统方案是维护几台 macOS 机器或自建 fastlane;EAS 构建机自带各版本 Xcode/Ruby/CocoaPods,按 image 选环境即可。
  2. 构建环境可复现。本地偶发的 “clean 一下就好了” 在云端不存在——每次都是干净机器加受控缓存;团队协作时 Android Studio 版本差异之类的噪音直接被抹平。
  3. 签名凭证托管。keystore、发布证书、描述文件由 EAS 加密保管(可选自管),新同事入职不用互相传 .p12。

类比一下:CNG 之于原生工程 ≈ 构建产物之于 dist/;EAS Build ≈ “托管的 CI runner + 签名保险柜 + 产物分发页”。

截至 2026-10-20 的时点事实:eas-cli npm 最新版为 25.2.0(registry 实测);SDK 58 构建要求 Node 22 线 ≥22.13、24 线 ≥24.3 或 26+(奇数版本不支持)。定价与构建额度随账号类型不同,一律以 expo.dev/pricing 为准,本文不写具体数字。

安装与登录

官方文档推荐全局安装:

npm install --global eas-cli
# 或者不想装全局,每次用 npx(CI 里常见):
npx eas-cli build -p android --profile preview

账号相关三条命令:

eas login        # 浏览器登录(SDK 58 起 expo login 默认走浏览器,EAS CLI 同理)
eas whoami       # 确认当前账号
eas logout

CI 上没有浏览器,用 personal access token:在 expo.dev 账号设置里生成,注入环境变量 EXPO_TOKEN,所有 eas-cli 命令会自动用它认证,无需 eas login。注意这个坑:token 属于账号而不只是项目,泄露等于把整个 EAS 交出去,团队 CI 建议用专用账号或组织级权限最小化(docs.expo.dev 的 Programmatic access 章节)。

eas init:projectId 绑定

EAS 的一切资源都挂在一个 project 下。首次使用:

eas init          # 创建新 EAS 项目或链接已有项目,交互选择所属账号
eas build:configure  # 顺便生成/完善 eas.json(老项目常用这条一起跑)
eas project:info  # 查看当前绑定的 projectId/slug

eas init 做的事是把 projectId 写进 app config 的 extra.eas.projectId。为什么 projectId 不该随意换:

  • 云端构建的凭证、环境变量、渠道(channel)、构建历史全部按 projectId 存储,换 ID 等于一切从零;
  • 更致命的在 OTA:eas update:configure 写入的二进制更新地址是 https://u.expo.dev/<projectId>,已经装在用户手机里的老版本只认这个地址。换 projectId 后,旧客户端永远收不到任何热更新,只能等商店包覆盖。

要重置只能 eas init --id <新项目ID> --force,生产项目基本不会这么干。顺带一提:eas.json 里 profile 名字可以随便起,projectId 不行。

eas.json 全解

eas.json 放在项目根目录(monorepo 里放 Expo 项目目录),三个顶层键:

{
  "cli": { "version": ">= 12.0.0", "appVersionSource": "remote" },
  "build": { "development": {}, "preview": {}, "production": {} },
  "submit": { "production": {} }
}

注意:不少旧文章写成 buildProfiles/submitProfiles,现行 schema 是 build 与 submit(字段全表见 docs.expo.dev/eas/json.md)。以 docs 为准。

cli 段

docs schema 列出的字段:version(semver 范围,团队里有人 eas-cli 太旧会直接报错,相当于给 CLI 版本上锁)、requireCommit(要求工作区干净才允许构建,防止"构建成果对不上任何 commit")、appVersionSource(remote 或 local,下文构建号自增详解)、promptToConfigurePushNotifications。

build 段:三档 profile 的语义

eas build:configure 生成的默认三档不是摆设,它们对应三种用途(development 档需要 expo-dev-client,见 05-Expo-Go与开发构建.md):

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": { "APP_VARIANT": "development" }
    },
    "development-simulator": {
      "extends": "development",
      "ios": { "simulator": true }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "android": { "buildType": "apk" }
    },
    "production": {
      "autoIncrement": true,
      "channel": "production",
      "env": { "APP_VARIANT": "production" }
    }
  }
}
  • development:developmentClient: true 把 Gradle 任务切成 :app:assembleDebug、iOS 配置切成 Debug,产物带开发者工具,永远不上商店。iOS 真机与模拟器需要两个 profile(模拟器加 "ios": { "simulator": true });Android 一个 .apk 真机模拟器通吃。
  • preview:不带 dev tools 的内部测试包,distribution: "internal" 意味着产物拿到直接安装的分享链接(必须是 apk/ipa 形态才有意义)。它是热更新预览的载体,channel 字段指向哪个渠道就收哪批 OTA(详见 07-EAS发布与热更新.md)。
  • production:distribution 默认 store,产物是上传商店用的 .aab/.ipa,下载链接不可分享。商店包无法直接安装到设备,这是和 preview 的本质区别。

extends 支持 profile 间继承(平台无关),是避免四五个 profile 复制粘贴的正规手段。

profile 级常用字段

真正高频的这批,按用途分组过一遍(完整语义以 docs.expo.dev/eas/json.md 为准):

工具链与环境

  • node / yarn / pnpm / bun / corepack:云端构建机的版本钉。node: "22.23.3" 这种写法对 CI 差异排查非常重要——本机 Node 与构建机不一致是隐形事故源,SDK 58 还改了 Node 支持矩阵(22.13+/24.3+)。
  • image:构建机基础镜像(default/latest 或具名镜像)。镜像决定了 OS 版本、Xcode 版本等没法单独指定的工具链;SDK 项目一般不用碰,EAS 会按 SDK 版本挑合适镜像,特殊需求查 build-reference/infrastructure.md 的镜像清单。
  • ios 下还有 bundler / fastlane / cocoapods / ndk(Android):对应工具的版本覆盖。
  • env:构建期间注入的环境变量。只放可以进 git 的值,密钥类应走 EAS 环境变量(见下文 secrets 小节),文档原话是"仅用于你会提交到仓库的值"。
  • environment:取值为 development / preview / production,告诉 EAS 构建时套用哪一组服务端管理的环境变量。SDK 58 起 expo config/prebuild/expo-doctor 在 EAS Build 上按 production 加载 .env(changelog 实测条目),配合使用别搞混。

产物与分发

  • distribution: store / internal;android.buildType: app-bundle(.aab,默认)/ apk;gradleCommand 可指定任意 Gradle 任务(优先级高于前两者,慎用)。
  • ios.simulator(模拟器 .app)、ios.enterpriseProvisioning(企业签 universal/adhoc)。
  • scheme / ios.buildConfiguration:多 Xcode scheme 时必填 scheme,否则 EAS 会交互问你。
  • buildArtifactPaths / applicationArchivePath:产物 glob 搜索路径,一般只有 custom builds 才需要动。
  • uploadSourceMaps:设为 true 时把 JS source map 传上 EAS,供 EAS Observe 符号化线上 JS 报错堆栈(云端构建专属,local build 不上传)。做生产应用建议开,埋一行配置换线上可读堆栈,见 07-EAS发布与热更新.md 的 Observe 部分。

OTA 相关

  • channel:这个 profile 产出的二进制去哪个更新渠道拉 OTA。developmentClient: true 时该字段无效(dev build 可以加载任意渠道的兼容更新,这正是它适合验证 OTA 的原因)。
  • autoIncrement:构建号自增。Android 默认 bump android.versionCode,iOS bump ios.buildNumber 最后一段;也接受显式值 "versionCode"/"buildNumber"/"version"。商店拒包的高频原因就是"重复构建号",production 档开 autoIncrement: true 一劳永逸。

性能

  • resourceClass: default / medium / large(large 免费计划不提供,docs 原话)。构建慢/内存爆时先升档再查原因。
  • cache: { key, paths, disabled }——构建产物级缓存,成功后保存、下次构建开始时恢复;改 key 即整体失效。注意官方提示:node_modules 不适合放这里(缓存是远端的,下载速度约等于从 npm registry 重装)。
  • credentialsSource: remote(默认,EAS 托管凭证)/ local(用本地 credentials.json,见下文)。
  • withoutCredentials: 允许无凭证构建(dev/debug 产物常见),默认 false。
  • config:指定自定义构建工作流(.eas/build/xxx.yml),属于 custom builds 进阶,见 docs custom-builds 章节,本文不展开。

构建号管理:remote vs local

cli.appVersionSource 决定 autoIncrement 往哪写:

  • remote(EAS CLI 12.0.0+ 的推荐姿势):版本号存在 EAS 服务器,构建时注入原生工程,本地 app config 里的 versionCode/buildNumber 会被忽略——你可以放心把它们从 app.json 删掉,git diff 干净,多人/CI 并行构建不会互相覆盖。
  • local:EAS 直接改你项目里的数字,每次构建后要 commit,否则下次又回到旧值。CI 场景很难协调,基本只在"公司政策必须本地留档"时用。

两个配套命令:eas build:version:set(把商店里已有的版本号同步为远端起点,首次从旧项目迁移到 remote 时必做,否则从 1 开始撞商店)、eas build:version:sync(把远端值拉回本地,供本机 Xcode/Android Studio 出包时用同一版本号)。

一个与下一章强相关的限制:远端 autoIncrement 与 runtimeVersion 的 nativeVersion 策略互斥(docs app-versions.md 原文列为 Limitation);如果要用它,runtimeVersion 请用 appVersion 或 fingerprint 策略——为什么,07-EAS发布与热更新.md 展开。

缓存全景与 Gradle cache

EAS 的构建提速是分层做的,容易配混,梳理一次:

  • 依赖缓存内置:npm 缓存服务器(npm/Yarn 2+ 默认启用;Yarn 1 要按 docs 的 package.json workaround)、Maven 缓存(maven-central/google/plugins-gradle)、CocoaPods 产物缓存。对应的关闭开关:EAS_BUILD_DISABLE_NPM_CACHE / EAS_BUILD_DISABLE_MAVEN_CACHE / EAS_BUILD_DISABLE_COCOAPODS_CACHE 设 "1"(写在 profile 的 env 里)。
  • 锁文件策略:默认按 npm ci / --frozen-lockfile 安装;想用可变安装设 EAS_NO_FROZEN_LOCKFILE=1(这也是"lockfile 与 package.json 对不上直接构建失败"的解闸阀)。
  • ccache(C/C++ 编译缓存):EAS_USE_CACHE=1 同时恢复+保存,EAS_RESTORE_CACHE / EAS_SAVE_CACHE 单独控制。生产构建的安全姿势见 docs caching.md:共享 token 触发构建时用户级缓存会互相污染,建议 production profile 设 EAS_RESTORE_CACHE: "0" + EAS_SAVE_CACHE: "1",只由受信任的 job 产出干净缓存。
  • Gradle build cache(2026-04-30 上线):设 EAS_GRADLE_CACHE=1 开启。官方 changelog 的结论:Gradle 任务输出按缓存 key 复用,缓存 key 取自包管理 lockfile 的哈希;验证是否命中看构建日志 “Run Gradle” 步骤里标 FROM CACHE 的任务。原生模块越多的项目收益越大,与 ccache 两层叠加。
  • 自定义产物缓存走 cache.paths,典型用法是把 ./ios/Podfile.lock 加进去换取确定性构建(docs 提醒:缓存它偶尔会带来需要清缓存的构建错误,权衡使用);eas build --clear-cache 是无脑重置入口。

eas build 命令族

eas build -p android|ios|all        # -p 平台
eas build -e production             # -e/--profile,缺省时若 eas.json 定义了 production 就用它
eas build -m "fix: 登录页闪退"       # 给构建加备注,dashboard 可见
eas build --platform all --profile production --auto-submit          # -s:构建成功直接提审
eas build --auto-submit-with-profile=production --profile preview    # 分别指定构建/提审 profile
eas build --platform ios --local    # 本机跑云端同款流水线
eas build --profile production --non-interactive --no-wait           # CI 触发姿势
eas build --profile production --clear-cache
eas build --json                    # 机器可读输出(隐含 --non-interactive)

几个值得单独说的:

  • 没有 --id 这个旗标(旧文常见);要操作某个构建,用 eas build:view <BUILD_ID> / eas build:cancel <BUILD_ID> / eas build:download --build-id <ID> / eas submit --id <ID>,ID 从 eas build:list 或 dashboard 拿。
  • 非交互不是环境变量而是旗标:--non-interactive(--json 自动带上)。docs 上没有 EAS_NON_INTERACTIVE 这种写法。前提是首次构建已在本地交互跑过(凭证、scheme 选择等配置项都已落定),CI 只是复用这些配置。
  • --what-to-test 配合 --auto-submit 直接写 TestFlight 的"What to test"文案;--freeze-credentials 防止非交互构建"顺手"改了凭证;ad hoc 内部分发想带上新注册的设备,用 --refresh-ad-hoc-provisioning-profile。
  • 排队与取消:eas build:cancel(可按 -p/-e 过滤)、eas build:cancel <ID>;eas build:list --status in-queue 看队列状态(new|in-queue|in-progress|pending-cancel|errored|finished|canceled)。

–local:出本地包的正确姿势

eas build --local 在自己机器上跑与云端同一套流水线(含 prebuild、fastlane、凭证收集),产物的 ios/、android/ 生成方式和参数与云端逐字节一致。它的定位(docs local-builds.md):

  • 调试"云端挂了本地复现不了"的构建失败——官方推荐第一现场;
  • 公司政策禁止第三方 CI:整个编译都在自己基础设施上,与 EAS 的通信仅为确认项目存在和下载托管凭证。

限制要知道:一次只能一个平台(all 禁用);node/image/fastlane 等字段被忽略(用的是你本机环境,“复现 CI"与"复现本机"是两码事);不支持缓存;secret 可见性的环境变量不下发(本地自己 export);需要本机装齐 Node、Android SDK/NDK,iOS 另需 fastlane 和 CocoaPods;Windows 官方不支持本地构建(macOS/Linux 才行)。产物默认落在当前目录,可用 EAS_LOCAL_BUILD_ARTIFACTS_DIR 指定,调试时 EAS_LOCAL_BUILD_SKIP_CLEANUP=1 + EAS_LOCAL_BUILD_WORKINGDIR 保留工作目录看 Xcode 日志。

和 npx expo run:ios --configuration Release 的区别:后者是原生工具链视角——直接调本机的 Xcode/Gradle,你用什么姿势配了原生工程就是什么产物;--local 是 EAS 流水线视角,验证的是"云端这套流程在我机器上是否成立”。排查商店包问题用 --local,日常本地 release 测试用 run:* 更轻。

查看构建内容:build:inspect 等

  • eas build:inspect -p ios -s archive|pre-build|post-build -o ./inspect-dir:把某阶段的项目文件系统快照到本地目录,验证"prebuild 完原生工程到底长什么样"“ipa 里打进去哪些资源"这类问题。
  • eas build:download --build-id <ID> --all-artifacts:连 Xcode 日志、产物归档一起拉下来。
  • eas build:resign:给已有 .ipa 换签名重打包(比如给内部测试 ipa 加一台新设备,省一次完整构建)。
  • eas build:dev:按 fingerprint 匹配已有的模拟器/仿真器 dev-client 构建并安装运行,找不到再触发新构建(默认选 development-simulator 档,配合 05-Expo-Go与开发构建.md)。

凭证体系:先让 EAS 自动生成

默认 credentialsSource: "remote",首次 eas build 会交互式引导创建全部凭证,之后复用。推荐姿势就是让 EAS 生成,除非有明确理由走 local/existing 路线:

eas credentials        # 交互式面板:查看/生成/删除/上传各平台凭证
eas credentials:configure-build

面板里能做的事:下载凭证到本地 credentials.json(credentials.json: Upload/Download... 选项)、管理 FCM/APN key、清理过期 iOS 凭证。想让新队友也能构建,把人加进组织即可——iOS 凭证生成后,协作者不需要 Apple 开发者账号权限就能发起构建(凭证在 EAS 侧)。

iOS

三种凭证(都挂在 Apple Developer 账号下,docs app-credentials.md 的要点):

  • Distribution Certificate:跟人(一个账号一套),过期不影响已上架 App,重新生成后才能再传新包。
  • Provisioning Profile:跟 App,与发布证书绑定,12 个月过期——过期同样不影响线上,下次 eas build -p ios 或 eas credentials 提示重建即可。
  • APN Key(推送密钥):运行期用,撤销后推送即断;上限 2 个,换新 key 不会改变用户已有的 Expo push token。

eas credentials 的删除只删 EAS 服务器上的副本,不删 Apple 侧的——想真正清掉要去 Apple Developer 控制台。要换用已有证书(.p12/.csr 路线)见 docs 的 existing-credentials.md;本地凭证路线是 credentialsSource: "local" + 项目根 credentials.json(不是 eas-credentials.json,注意文件名)。

App Store Connect API Key 什么时候必须:CI 非交互提审(eas submit/--auto-submit)时无法交互输凭证——docs submit/ios.md 明确:非交互模式下没有预先分配的 ASC API key 会直接报 App Store Connect API Keys cannot be set up in --non-interactive mode.,且 profile 必须有 ascAppId。构建侧同理:凭证损坏需要修复时,CI 要注入 EXPO_ASC_API_KEY_PATH、EXPO_ASC_KEY_ID、EXPO_ASC_ISSUER_ID,内部分发还会用到 EXPO_APPLE_TEAM_ID、EXPO_APPLE_TEAM_TYPE(docs building-on-ci.md)。所以哪怕构建不用 ASC key,只要 CI 提审,key 就早配好;也可以在 expo.dev 的账号设置或 eas integrations:asc:connect 预先关联。

Android

首次构建自动生成 keystore,存 EAS 服务器。现状(docs app-credentials.md):Google Play 的 App Signing by Google Play 是默认流程——eas build 产出的包用 upload key 签,商店侧用另一把 app signing key 落地重签;upload keystore 丢了可以通过 Play 支持重置,app signing key 丢了应用就死了。所以 EAS 托管的 keystore 按 upload key 理解即可,eas build 不区分两者。

红线只有一条:生产 keystore 绝不进仓库(debug keystore 是唯一例外,它不上商店)。凭证与 Google 解绑类事故的处理链(下载 credentials.json → keytool -export 导 pem → 找 Google 支持换 key)docs 有完整步骤,真出事照抄。

环境变量与 secrets:eas env 家族

旧教程里的 eas secret:create 已从当前 CLI 参考中消失,现行体系是 EAS environment variables(docs eas/environment-variables.md),一条命令覆盖创建与更新:

# 单条:给 production 环境建一个构建/更新期注入的变量(第一个位置参数是环境名)
eas env:set production --name EXPO_PUBLIC_API_URL --value https://api.example.com --visibility plaintext

# 批量:把本地 .env 文件推上去 / 拉下来(默认路径 .env.local)
eas env:push production --path .env.production
eas env:pull production --path .env.local

三种 visibility:plaintext(处处可见)、sensitive(仅服务端可见、CLI 打码)、secret(只有 EAS 服务器上的 job 能读,构建日志里做混淆——docs 原文举例:装私有包的 NPM_TOKEN)。scope 分 project(默认)/ account。环境名默认 development|preview|production 三个,自定义环境名是更高档计划的能力。

与构建/热更的接线:build profile 设 "environment": "production" 即套用它;eas update 用 --environment 旗标(SDK 55+ 必传,见 07)。

和 03-应用配置与插件.md 的 EXPO_PUBLIC_ 边界呼应一下,这里有个容易误解的点:eas env 的 secret 控制的是变量在服务侧的可见性,管不住"值会不会被打进客户端 bundle”。凡是最终会被用户设备上跑的 JS 读到的值(EXPO_PUBLIC_*、写进 app config 被内联进二进制的任何值),对抓包拆包的人就是明文——docs 原话:“Secrets do not provide any additional security for values that you end up embedding in your application”。真正的密钥只该留在你的后端,别指望任何客户端侧的加密手段。

config plugins 与云端构建

CNG 项目下 EAS Build 在远端跑 npx expo prebuild(--platform、--non-interactive 由引擎自动加,可用 prebuildCommand 微调),所以:

  • 原生定制一律走 config plugins(见 03-应用配置与插件.md),手改 ios/ 不进版本库的内容云端构建拿不到;
  • lockfile 必须与 package.json 一致——云端 npm ci/frozen 安装,不一致直接红;本地能跑不代表云端能跑,先 eas build --local 验证;
  • 想排除某些文件不参与上传,用 .easignore(docs build-reference/easignore.md);私有 npm 包、git submodule、monorepo 各有专页(build-reference 章节)。
  • SDK 58 的 iOS scene-based lifecycle 意味着 prebuild 会生成 SceneDelegate.swift 等新模板文件,ios/ 若被提交过旧版本,升级后删掉让云端重新生成(sdk58 changelog 升级指引)。

SDK 58 的构建侧新事实

以下均来自 SDK 58 changelog(2026-10-15,expo.dev/changelog/sdk-58),直接影响构建配置:

  • Android release 构建默认开启 R8:模板在 gradle.properties 写入 android.enableMinifyInReleaseBuilds=true,用 proguard-android-optimize.txt 预设做 shrink/优化/混淆,包体更小、启动更快。依赖反射的库(部分序列化/事件总线/老三方 SDK)可能因缺 keep 规则在 release 包里崩,debug 一切正常的事故就是这么来的。回退方式:expo-build-properties 插件设 android.enableMinifyInReleaseBuilds: false(见 03-应用配置与插件.md);更该做的是给具体库补 ProGuard keep 规则而不是全局关掉。
  • expo-modules-core 预编译 .so:npm 包直接带四 ABI 的 debug/release 预编译产物,不再每次从源码编 libexpo-modules-core.so(过去 Expo Android 构建里最贵的一步)。官方空白项目实测:Apple M3 Pro 上四 ABI debug 全量构建约 80 秒 → 39 秒,核少的 CI 机器收益更大。要回退源码编译:expo-build-properties 设 android.buildExpoModulesCoreFromSource。
  • fingerprint 默认 preset 改为 balanced:升版本号、改 node_modules 内文件不再改变指纹,rebuild 被误触发的次数明显下降(dev build 复用、EAS Update 兼容判定都吃这条)。三种 preset(strict/balanced/relaxed)及与 runtimeVersion 的关系在 07-EAS发布与热更新.md 展开,这里只需知道:升级到 58 后你的构建复用率会变高,属于"默认更聪明了"。
  • Node 版本门槛变化(22.13+/24.3+),eas.json 里钉了旧 node 字段的记得同步。

构建时长预期与常见失败

正常预期(云端、冷启动、无缓存):小型空白项目单端几分钟量级,中大型 App 十几分钟到几十分钟;重复构建因 npm/Maven/CocoaPods 缓存和 ccache/Gradle cache 命中显著变快。以下高频失败按出现频率排:

  1. lockfile 与 package.json 不一致:云端 frozen install 直接失败。本地 npm install 改依赖没带 lock 变更就推上去了,先本地重装验证,或临时 EAS_NO_FROZEN_LOCKFILE=1 定位(别长期带着)。
  2. pod install 相关:CocoaPods 版本与镜像不匹配、私有源缺 .netrc(会绕过 CocoaPods 缓存)、缓存里的 Podfile.lock 过期。解法顺序:看 iOS 构建日志的 pod 阶段 → --clear-cache 重试 → 固定 cocoapods 版本字段。
  3. R8/Proguard(SDK 58 新增项):release Android 包运行期崩在反射处,debug 正常——先按上一条加 keep 规则定位库。
  4. fastlane 报错被吞:加 --verbose-logs 或 --build-logger-level trace;iOS 完整日志用 eas build:download --all-artifacts。
  5. 内存/OOM:升 resourceClass(免费计划无 large),Android 侧配合 Gradle 内存配置。
  6. 环境差异:node 字段没钉版本,构建机默认与你本机不同步;以及 expo config 在云端按 production 模式取环境变量(SDK 58 起 NODE_ENV 由 CLI 强制),本地 .env.development 的值在云端根本不会生效这类"配置消失"问题,根多在此。

排障总入口:docs.expo.dev 的 build-reference/troubleshooting.md。

CI 集成

先说结论:老文章里的 expo/eas-build-action 已不存在(2026-10-20 用 api.github.com 验证返回 404,只剩第三方同名仿制仓库)。现在官方给的两条路:

路线一:EAS Workflows(Expo 自己的 CI/CD,免维护 runner)。.eas/workflows/build.yml:

name: Build
on:
  push:
    branches: [main]
jobs:
  build_android:
    name: Build Android App
    type: build
    params:
      platform: android
  build_ios:
    name: Build iOS App
    type: build
    params:
      platform: ios

路线二:GitHub Actions 裸 eas-cli 步骤(docs building-on-ci.md 现行示例,expo/expo-github-action@v8 已验证为活跃官方仓库,也可不用 action 直接 npx):

# .github/workflows/eas-build.yml
name: EAS Build
on:
  workflow_dispatch:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: npm
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: npm ci
      - run: eas build --platform all --non-interactive --no-wait

--no-wait 让 CI 触发成功即收工(不计 EAS 构建时间),后续看 dashboard 或 webhook(eas webhook:create --event BUILD)。GitLab/Bitbucket/CircleCI 同理,核心都是 EXPO_TOKEN + npx eas-cli build --non-interactive --no-wait。

另有一个零配置选项:Expo GitHub App(docs build/building-from-github.md)——在 expo.dev 关联仓库后,PR 评论触发构建、自动预览构建,subdirectory 项目要配 “Base directory”,适合不想写 yml 的团队。

实战建议

发版前的配置层自检,按这个顺序过:

  1. eas.json 三档 profile 起步(development/preview/production),团队约定 profile 名即语义,extends 收敛公共字段;cli.version 锁最低 eas-cli 版本,requireCommit 建议 production 档开。
  2. appVersionSource: "remote" + production autoIncrement: true;从老项目迁移先 eas build:version:set 对齐商店已有版本。
  3. 凭证一律交给 EAS 托管起步,出问题再走 existing/local;CI 提审项目预先配好 ASC API key 与 Google service account key。
  4. 构建加速三层:内置依赖缓存(默认开)→ EAS_GRADLE_CACHE=1 + ccache → 最后才考虑 resourceClass 升档;自定义 cache.paths 只放编译产物类目录,别放 node_modules。
  5. uploadSourceMaps: true,SDK 58 的 Observe 符号化全靠它。
  6. 任何"本地好好的云端红",第一步 eas build --local 复现,第二步 eas build:inspect -s pre-build 对比原生工程,第三步再怀疑玄学。
  7. 上云前先问一句这件事是否值得:跑 dev build 验证 npx expo run:ios 更快,云端构建的价值在"别人也要这个产物"和"环境必须可复现"的时刻。