Expo应用怎么用EAS Build完成从构建到上架的全流程?
从本地开发到用户手机,中间隔了几座山
写完一个Expo应用只是开始。要让用户真正用上它,你需要把JavaScript代码编译成原生二进制包,签名、提交应用商店、再走完审核流程——每一步都有可能卡住。Expo Application Services(EAS)就是Expo团队给出的答案:把构建、提交、更新这三件事分别交给EAS Build、EAS Submit、EAS Update来处理。
本文会从eas.json的每一行配置讲起,覆盖开发构建/预览构建/生产构建的区别、商店提交流程、OTA更新机制、环境变量管理、CI/CD集成,以及最常见的报错和排查思路。
EAS Build的核心配置:eas.json
EAS的所有构建行为都由项目根目录下的eas.json控制。运行eas build:configure会自动生成一份基础配置,但实际项目通常需要自定义。
一个典型的eas.json长这样:
json{ "cli": { "version": ">= 13.0.0", "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "channel": "development", "env": { "APP_ENV": "development" } }, "preview": { "distribution": "internal", "channel": "preview", "env": { "APP_ENV": "staging" } }, "production": { "channel": "production", "autoIncrement": true, "env": { "APP_ENV": "production" } } }, "submit": { "production": {} } }
几个关键字段的含义:
cli.version:约束EAS CLI的最低版本,避免因CLI版本差异导致构建失败。cli.appVersionSource:设为"remote"时,版本号以EAS服务器记录为准,配合autoIncrement自动递增build number。autoIncrement:每次构建自动+1,防止因build number重复被应用商店拒绝。channel:绑定EAS Update的更新通道,决定这个构建能收到哪个通道的OTA推送。env:构建时注入的环境变量,注意这和运行时环境变量是两回事。
开发构建、预览构建、生产构建有什么区别
EAS Build的三种构建类型对应不同的使用场景,搞混了会浪费大量构建时间。
开发构建(Development Build)
json"development": { "developmentClient": true, "distribution": "internal" }
developmentClient: true是关键标志——它会在包里内置Expo Dev Client,支持热重载和开发者菜单。这个构建只用于开发调试,体积比生产包大,性能也差。distribution: "internal"意味着包不经过商店,而是通过链接直接安装。
使用场景:开发者在真机上调试原生模块、测试推送通知等模拟器无法覆盖的功能。
预览构建(Preview Build)
json"preview": { "distribution": "internal", "channel": "preview" }
没有developmentClient,所以是完整的 Release 模式运行,但分发方式仍然是内部的。QA团队和产品经理通常用这个构建来验收。它和生产的区别仅在于分发渠道——预览构建不提交商店,但运行时行为和生产一致。
生产构建(Production Build)
json"production": { "channel": "production", "autoIncrement": true }
最终提交到App Store和Google Play的包。没有developmentClient,没有distribution: "internal",走的是商店分发流程。
三者之间的核心差异:开发构建包含调试工具、体积大、运行慢;预览构建是生产代码但内部分发;生产构建就是上架的最终产物。构建时间上,生产构建因为要做代码混淆和优化,通常比开发构建多花2-5分钟。
EAS Submit:怎么把构建提交到应用商店
构建完成后,下一步是提交商店。EAS Submit把这个过程简化成一行命令。
iOS提交准备
- 在App Store Connect创建应用记录。
- 生成App Store Connect API Key(角色选Admin或Developer),记下Issuer ID、Key ID和.p8文件。
- 在EAS CLI中配置凭证:
basheas credentials
- 提交构建:
basheas submit --platform ios --latest
--latest自动取最近一次成功构建。你也可以指定build ID:eas submit --platform ios --id xxxx。
Android提交准备
- 在Google Play Console创建应用。
- 创建服务账号并下载JSON密钥文件。
- 授予服务账号必要的权限(至少需要"发布到测试轨道"权限)。
- 提交构建:
basheas submit --platform android --latest
在eas.json中预配置提交参数
json"submit": { "production": { "ios": { "ascAppId": "1234567890" }, "android": { "serviceAccountKeyPath": "./pc-api-key.json", "track": "internal" } } }
ascAppId是App Store Connect中的应用ID,填上后提交时不再需要手动输入。track控制发布轨道:internal(内部测试)、alpha(公开测试)、beta(公测)、production(正式发布)。
EAS Update:如何做OTA更新
这是EAS最实用的功能之一。当你只改了JavaScript/TypeScript代码和资源文件,没有动原生依赖,就可以通过OTA直接把更新推到用户手机,跳过整个应用商店审核流程。
运行时版本策略
app.json中配置:
json"runtimeVersion": { "policy": "appVersion" }
这行配置的含义:每当version字段变化,runtime version跟着变。只有runtime version完全匹配时,OTA更新才会生效。这就保证了你改了原生代码后,旧版本的应用不会收到不兼容的JS更新。
另一种策略是"fingerprint",它基于项目原生文件的内容生成指纹,精度更高但需要更频繁地构建新包。SDK 55及以后版本推荐使用fingerprint策略。
发布更新
basheas update --channel production --message "修复登录页闪退问题"
--channel必须和构建时绑定的channel一致,否则用户收不到。--message是更新说明,方便回溯。
渐进式发布
basheas update:rollout --channel production --percent 25
先推给25%的用户,观察错误率,再逐步扩大到50%、100%。这是生产环境必须有的安全网。
常见OTA问题
更新不生效:90%的原因是channel不匹配。用eas channel:list确认构建绑定的channel和发布时的--channel参数一致。
"No compatible updates found":runtime version不匹配。检查构建的runtime version和更新的target runtime version是否相同。
更新下载了但不生效:需要确认expo-updates的checkAutomatically配置,以及是否在合适的时机调用了Updates.reloadAsync()。
App Store和Google Play上架流程
App Store上架
- 构建:
eas build --platform ios --profile production - 提交:
eas submit --platform ios --latest,构建会自动上传到App Store Connect - TestFlight测试:上传后自动出现在TestFlight中,可以邀请内部/外部测试员
- 填写上架信息:在App Store Connect中完成应用描述、截图、隐私政策URL、审核信息等
- 提交审核:Apple审核周期通常24-48小时,首次审核可能更长
- 发布:审核通过后选择"手动发布"或"自动发布"
Google Play上架
- 构建:
eas build --platform android --profile production - 提交:
eas submit --platform android --latest,构建上传到Google Play Console - 内部测试轨道:先推到internal track验证
- 逐步升级轨道:internal → alpha → beta → production
- 填写商店信息:应用描述、截图、内容分级等
- 发布:Google审核通常几小时到几天
两个商店的关键差异:Apple审核严格但可预期,Google审核快但有时会因不明原因拒绝。建议两个平台都先做内部测试再逐步开放。
环境变量和Secrets怎么管理
EAS提供了两层环境变量机制,搞混了会踩坑。
构建时环境变量
在eas.json的env字段中定义:
json"production": { "env": { "APP_ENV": "production", "API_URL": "https://api.example.com" } }
这些变量在云端构建过程中可用,用于构建脚本。但注意:它们不会打包进最终的JS bundle。
Secrets
在Expo后台(expo.dev → 项目 → Environment variables)中创建,勾选"Sensitive"标记。这些值不会出现在git记录中,适合存放API Key、签名密码等敏感信息。
运行时环境变量
要让JS代码在运行时读到环境变量,需要用react-native-dotenv或Babel宏在构建时把值内联到代码中。Expo SDK 49+推荐的做法是:
js// 在app.config.js或app.config.ts中读取环境变量 export default { extra: { apiUrl: process.env.API_URL, sentryDsn: process.env.SENTRY_DSN, }, };
然后在代码中通过Constants.expoConfig.extra访问。这比硬编码安全,也比运行时读取可靠。
EAS Update的环境变量陷阱
eas.json中env定义的变量只在eas build时生效,eas update时不会读取。如果你在OTA更新中依赖某个环境变量,需要在eas update命令中显式传入:
basheas update --channel production --env API_URL=https://api.example.com
CI/CD集成:怎么把构建自动化
手动跑eas build容易忘步骤,接入CI/CD后一切自动化。
GitHub Actions示例
yamlname: EAS Build and Submit on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx eas-cli build --platform all --profile production --non-interactive --no-wait env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
几个要点:
EXPO_TOKEN:在expo.dev生成Personal Access Token,存到GitHub Secrets中。这是CI认证的唯一方式。--non-interactive:CI环境下必须加,否则命令会等待用户输入而挂起。--no-wait:不等待构建完成,EAS构建通常5-20分钟,加了这行CI立刻结束,省CI分钟数。如果需要等构建结果再执行后续步骤(比如自动提交),去掉这个flag。
自动提交商店
构建成功后自动提交:
bashnpx eas-cli build --platform ios --profile production --auto-submit
--auto-submit会在构建完成后自动触发eas submit,使用eas.json中配置的submit profile。
自动OTA更新
对main分支的JS改动自动推送更新:
yaml- run: npx eas-cli update --channel production --message "${{ github.event.head_commit.message }}" env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
完整的CI/CD流程是:push到main → 自动构建 → 自动提交商店 → 自动推送OTA更新给已有用户。
常见报错和排查方法
构建失败:Gradle daemon disappeared unexpectedly
这是OOM(内存不足)的典型表现。Android构建特别容易触发。
解决方案:在eas.json中指定更大的资源规格:
json"production": { "resourceClass": "large" }
large规格提供更多内存,构建耗时略长但稳定性大幅提升。同时用Expo Atlas分析bundle体积,找出过大的依赖。
构建失败:None of the files exist
通常是文件名大小写问题。macOS文件系统默认不区分大小写,但EAS Build的Linux环境严格区分。本地能跑的import路径到了云端就报错。
排查方法:仔细检查import路径和实际文件名的大小写是否完全一致。
提交失败:Missing App Store Connect API Key
iOS提交需要App Store Connect API Key,而且Key的权限必须足够。常见错误是给了只读权限的Key。
解决:重新生成Key,角色选Admin,然后确认Issuer ID和Key ID没有填反。
提交失败:Google Play拒绝:Version code already used
每个版本号只能提交一次。如果之前提交了build number 1的包被拒绝,修改后再次提交必须递增build number。
解决:使用autoIncrement: true自动管理版本号,永远不要再手动填build number。
OTA更新静默失败
用户报告没收到更新,但EAS后台显示发布成功。
排查步骤:
eas channel:list确认channel映射正确- 检查构建的runtime version和更新的target runtime version
- 确认
expo-updates配置了自动检查:checkAutomatically: "ON_LOAD" - 在代码中添加日志:
Updates.checkForUpdateAsync()查看返回结果
构建速度太慢
EAS Build每次都从零开始安装依赖和编译。减少构建时间的方法:
- 锁定依赖版本:用
npm ci代替npm install - 指定构建镜像:
"image": "latest"使用预装了常用工具的镜像 - 减少原生依赖:每多一个原生模块就多一份编译时间
- 合理使用
--no-wait:CI中不阻塞等待
写在最后
Expo EAS把React Native应用从构建到上架的流程拆成了三个独立环节:Build负责编译打包,Submit负责商店提交,Update负责OTA推送。理解eas.json中build profile和channel的关系是掌握EAS的关键——它决定了你的构建类型、分发方式和更新通道。
对于刚上手的团队,建议从development构建开始验证原生功能,用preview构建给QA验收,确认无误后再跑production构建提交商店。接入CI/CD后,构建和提交变成代码推送的自动后续动作,开发者只需要关注代码本身。遇到问题时,先看EAS构建日志中[stderr]前缀的输出,大部分错误的根因都藏在那里。