AI 参与说明(Agent:Codex):本文由 Codex 根据 Expo、React Native、Apple 与 Android 的官方公开文档协助整理,重点核对 Expo SDK、Metro、Hermes、development build、CNG、本地 Xcode 发布、EAS 与 bare React Native 的职责边界。资料核验于 2026-08-14;SDK、CLI、套餐、商店要求与 API 都会变化,动手前请以链接中的当前文档为准。示例使用
com.example等占位符,不含任何真实密钥或账号信息。
适用范围:本文面向已经会一点 React / TypeScript、但不熟悉移动原生工程的开发者。它讲的是 Expo 如何组织 React Native 项目、如何扩展原生能力、如何测试和发布;React Native 的 Fabric、Hermes、JSI 等底层渲染细节请阅读 React Native 技术原理文章。
先说结论#
Expo 是建立在 React Native 之上的框架与工具链,不是 WebView,也不是把 .tsx 自动翻译成 Swift / Kotlin 的编译器。你的 TypeScript / JavaScript 仍由 React Native runtime 执行;Expo 提供一组版本配套的设备 API、路由、应用配置、原生工程生成与开发工具。
EAS(Expo Application Services)是可选的云端交付服务,不等于 Expo 本身。 即使不用 EAS,你仍可使用 Expo SDK、Expo CLI、Expo Router,并在本地用 Xcode / Android Studio 构建;使用 EAS 时,则可把构建、签名协助、上传商店、兼容范围内的 OTA 更新和移动 CI/CD 放到一条工作流中。
最容易记住的划分是:
| 名称 | 它解决什么 | 它不解决什么 |
|---|---|---|
| Expo SDK / CLI | 常见设备能力、依赖兼容、开发服务器、app config | 商店审核、后端业务、任意原生 SDK 的质量 |
| Expo Router | 文件路由、深链、页面布局与 Web URL 约定 | 后端 API、所有导航设计决策 |
| development build | 你的 App 专属开发客户端,可包含任意原生依赖 | App Store / Play 的正式发布 |
| Prebuild / CNG | 从配置可重复地产生或更新原生工程 | 自动理解你手工改过的原生代码 |
| EAS Build | 在云端或本地模式构建、签名、分发原生二进制 | App Review、商店资料与通过审核 |
| EAS Submit | 上传已签名的 .ipa / .aab 到 App Store Connect / Play Console | 截图、metadata、审核提交和对公众发布 |
| EAS Update | 向兼容原生 runtime 下发 JS、样式和资源更新 | 新原生模块、权限、entitlement 或绕过商店规则 |
| EAS Workflows | 用 YAML 编排移动端 CI/CD 作业 | 替你设计测试策略或修复失败的原生构建 |
flowchart TB A["React + TypeScript 业务代码"] --> B["Expo SDK / Router / Metro"] B --> C["React Native runtime\nHermes + Fabric"] C --> D["iOS / Android 原生 App"] E["app.json / app.config.ts\nconfig plugins"] --> F["Prebuild / CNG"] F --> D G["EAS Build"] --> H["签名 .ipa / .aab"] H --> I["EAS Submit → App Store Connect / Play Console"] J["EAS Update"] --> K["兼容 runtime 的 JS / assets"] K --> D
1. Expo 位于哪一层#
一个 Expo 应用有三层。理解它们以后,很多“Expo 能不能做 X”的问题就能自行判断:
- React Native 运行层:React、Hermes、Fabric、平台 Host View、以及 Swift / Objective-C / Kotlin / Java / C++ 原生代码。它负责最终在 iOS 和 Android 上运行。
- Expo 框架层:Expo SDK、Expo CLI、Router、应用配置、config plugins、Prebuild、Expo Modules 和 development build。它降低使用原生能力的日常成本。
- EAS 服务层:Build、Submit、Update、Workflows 等可选服务。它把交付步骤标准化,但不能替代 Apple / Google 账号和审核。
因此,下列说法都不准确:
- “Expo 就是一个在线打包网站”:EAS 是服务,Expo 框架本身可本地使用。
- “Expo 项目不能写原生代码”:可以通过第三方 React Native 库、config plugin、本地 Expo Module,或直接维护
ios/、android/工程扩展。 - “使用 Expo 就永远不用 Xcode / Android Studio”:简单项目可能不常打开它们;复杂崩溃、原生 SDK、签名和性能问题仍离不开平台知识。
- “EAS Update 可以绕过审核发布一切新功能”:它只能更新已存在 native runtime 能执行的非原生部分,还必须遵守商店规则。
2. 从一个最小项目开始#
下面假设使用当前 create-expo-app 默认模板。模板与 SDK 过渡会变化,创建当天请先阅读 Expo 创建项目文档;安装 Expo 依赖时优先使用 npx expo install,由它选择与当前 SDK 相容的版本。本文核验日正处于 SDK 57 过渡,因此使用显式模板;这不是永恒安装命令。
npx create-expo-app@latest my-expo-app --template default@sdk-57
cd my-expo-app
npx expo start启动后,Metro 会监听 TypeScript / JavaScript 与资源变动。对只包含当前 Expo Go 已内置原生能力的学习项目,可用 Expo Go 快速打开;正式项目不要把它当成最终运行环境,下一节会解释原因。但 SDK 57 是本文整理日的过渡版本:最低 Node.js 为 22.13.x,不能假定物理设备上商店安装的 Expo Go 已支持它。 此示例应使用模拟器或 development build;若只想用物理设备的 Expo Go 学习,应按创建项目文档的当期说明使用兼容模板,而不是照抄这里的 SDK 快照。Expo SDK compatibility
当前默认模板通常已经启用 Expo Router,并把路由放在 src/app/。为了验证整个链路,可以将 src/app/index.tsx 改成下面的最小页面:
import { useState } from "react";
import { Pressable, SafeAreaView, StyleSheet, Text } from "react-native";
export default function Index() {
const [count, setCount] = useState(0);
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Expo 正在运行</Text>
<Text accessibilityLiveRegion="polite">已点击 {count} 次</Text>
<Pressable
accessibilityRole="button"
style={styles.button}
onPress={() => setCount((value) => value + 1)}
>
<Text style={styles.buttonText}>增加计数</Text>
</Pressable>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center", gap: 16 },
title: { fontSize: 24, fontWeight: "700" },
button: { borderRadius: 8, backgroundColor: "#4630eb", padding: 12 },
buttonText: { color: "white", fontWeight: "600" },
});这段代码的预期结果是在 iOS / Android 上显示一个计数按钮。它仍是 React Native View、Text、Pressable 等组件;Expo 没有把它转成两份 Swift / Kotlin 页面。运行时渲染原理由 React Native 负责,Expo 负责让项目的依赖、开发与原生配置更易管理。
Metro、Hermes 与原生编译器分别做什么#
npx expo start 启动的是 Expo CLI,其中负责监视、转换和打包 TypeScript / JavaScript 与资源的核心工具是 Metro。Metro 是 Expo 与 React Native 的官方 bundler,不是手机里的运行时,也不会把 .tsx 编译成 Swift 或 Kotlin。Why Metro
用 Vite 类比有助于入门,但要保留目标环境的差异:
| 工具 | 主要输入 | 主要目标环境 | 它不负责什么 |
|---|---|---|---|
| Vite | Web 的 TS / JS / CSS / assets | 浏览器的 JavaScript 引擎、DOM 与 Web API | iOS / Android 原生控件和原生工程编译 |
| Metro | React Native / Expo 的 TS / JS 与 assets | iOS / Android App 中的 React Native JavaScript runtime,生产环境可生成 Hermes bytecode | Swift / Objective-C / Kotlin / Java / C++ 编译、签名与商店发布 |
Hermes 才是 App 内执行 React Native JavaScript 的引擎。 它可用于 iOS 和 Android 的 React Native App,也是当前 Expo 的默认 JavaScript engine。Hermes 由 Meta 开源和维护,不是 Apple 官方框架;Apple 自己提供的是 JavaScriptCore。选择 Hermes 并不意味着绕开 iOS:Hermes 会作为第三方原生依赖随 App 一起由 Xcode 编译、签名和审核,React Native 再通过 Fabric / JSI 等原生边界连接 UIKit 或 Android View。Expo:Using Hermes React Native JavaScript Environment Apple JavaScriptCore
flowchart TB A["TS / JS / JSX"] --> B["Metro\n开发 bundle 或生产 bundle / bytecode"] B --> C["Hermes\niOS 与 Android 内执行 JS"] C --> D["React Native / Fabric"] D --> E["UIKit / Android View"] F["Swift / Objective-C / C++\n原生模块、MediaPipe 等"] --> G["Xcode / Clang"] H["Kotlin / Java / C++\n原生模块、MediaPipe 等"] --> I["Gradle / Android 工具链"] G --> E I --> E
开发和发布时,Metro 的交付方式不同:
- 开发构建:
npx expo start在 Mac 上运行 Metro;手机里的 Expo Go 或 development build 从 Metro 地址读取 bundle,所以停止 Metro、网络不可达或 Mac 离线后,该开发会话通常无法重新加载业务代码。 - Release / 商店构建:Metro 在构建阶段生成生产 bundle,并可为 Hermes 转换 bytecode;这些文件和静态资源被嵌入
.app/.ipa/.aab。安装后的 App 不需要连接 Mac,也不需要常驻 Metro。App 自己调用后端 API 所需的网络是另一回事。 - 原生能力:Swift、Kotlin、MediaPipe 或其他原生 SDK 始终由 Xcode / Gradle 等平台工具链编译。Metro 与 Hermes 只处理 JavaScript 侧,不负责逐帧执行或编译这些原生代码。
因此,更准确的说法是:Vite 为浏览器准备 Web 模块;Metro 为 React Native runtime 准备 JavaScript bundle;Hermes 在 iOS / Android App 内执行这个 bundle;Xcode / Gradle 负责真正的原生二进制。
Expo Router 的最小心智模型#
把 src/app/ 看成 URL 与页面结构,而不是所有业务代码的容器:
src/app/
_layout.tsx # 全局 Stack、Provider、认证边界
index.tsx # /
settings.tsx # /settings
products/[id].tsx # /products/:id
src/
features/ # 业务 feature
services/ # API、通知、存储等适配层
components/ # 项目自己的 UI 组件_layout.tsx 管理导航壳;[id].tsx 代表动态路由;route group 可组织认证前后、Tab、Modal 等页面而不进入 URL。Router 会把深链与文件路由关联起来,但并不会自动处理登录权限、服务端鉴权或数据缓存。Expo Router 介绍
3. Expo Go、development build 与商店二进制#
Expo Go 和 development build 的差异,是初学者最容易走错的地方。
先消除名字误会:Expo Go 不是套餐#
Expo Go 是一个开发用 App,不是 EAS 的付费 plan,也不单独收费。 你在 Expo 定价页看到的 Free、Starter、Production、Enterprise,是可选的 EAS 云服务套餐;它们控制云构建、Update、Workflows 等服务的额度、并发和支持等级,和手机里安装哪一个 Expo Go 没有一一对应关系。你写的“Start”通常是价格页上的 Starter。
| 你看到的名称 | 它属于哪一层 | 是否等于付费套餐 | 正确理解 |
|---|---|---|---|
| Expo Go | Expo 预先发布的通用开发客户端 | 否 | 用于学习和快速打开一个只使用其已内置原生能力的项目;它的 native runtime 固定,不能随你的项目动态加库 |
| development build | 你自己的开发版 App | 否 | 通常是含 expo-dev-client 的项目专属原生 App;可本地构建,也可以消耗 EAS Build 的配额在云端构建 |
| Free / Starter / Production / Enterprise | EAS 的账户计费套餐 | 是,指 EAS 云服务 | 选择的是云构建 / 更新 / CI 等服务的额度与优先级,不会改变 Expo Go 的能力,也不替代 Apple / Google 账号 |
build.production | eas.json 的 build profile key | 否 | profile key 可自定义;但命令省略 --profile 时,EAS CLI 会查找名为 production 的 profile |
channel: "production" | EAS Update 的更新通道 | 否 | 它和 build profile 独立,只决定兼容 binary 从哪个 Update channel 取更新 |
environment: "production" | EAS 环境变量集合 | 否 | 默认有 development、preview、production 三个环境;截至本文核验日,自定义环境仅列在 Production / Enterprise 套餐 |
截至本文核验日,EAS 的 Free 为 $0,Starter 为 $19/月 + 用量,Production 为 $199/月 + 用量;具体额度、价格和可用服务会变化,应以 EAS pricing 为准。Expo 框架、SDK、CLI 和本地原生构建并不要求购买 EAS;用 EAS 云端构建时可以先使用 Free 配额。无论选哪个套餐,TestFlight / App Store 仍需 Apple Developer Program,Google Play 仍需 Play Console 账号。
可以这样记:Expo Go = 别人预装好的通用开发 App;development build = 你自己构建的开发 App;EAS plan = 是否以及以什么额度使用 Expo 的云服务。 而 build.production、channel: "production"、environment: "production" 虽然同名,却分别属于构建 profile、OTA 通道和环境变量三个命名空间;不要把它们和 EAS Production plan 视为绑定关系。EAS build profiles EAS environments
| 产物 | 本质 | 适合做什么 | 关键限制 |
|---|---|---|---|
| Expo Go | Expo 预先发布的通用客户端 | 学习、原型、验证它已包含的 SDK | 不能随项目加入任意原生库、原生配置、推送凭据或自定义 entitlement |
| development build | 通常包含本项目原生依赖与 expo-dev-client 的开发 App | 日常开发、真机联调、原生 SDK、调试 | 含开发工具,不是商店正式产物 |
| internal preview | 可通过链接安装的内部版本 | 设计 / QA / 业务验收 | iOS ad hoc、Android APK 等通常不是 TestFlight / Play 商店测试轨道 |
| store staging / production | 已签名的 .ipa / .aab | TestFlight、Play track、商店审核和正式发布 | 仍需商店账号、资料、审核和发布操作 |
Expo 官方把 development build 描述为“你自己的 Expo Go”:它可以包含任何原生库和原生配置,是面向真实项目的推荐路径。Development builds
先安装开发客户端,再本地编译一次:
npx expo install expo-dev-client
npx expo run:ios
npx expo run:androidexpo run:ios|android 在不存在原生目录时会先调用 Prebuild,再交给 Xcode / Gradle 编译、安装并启动开发服务器。只改 TS / JS 时,一般只需 npx expo start;下面任一变化则要重新生成 / 编译原生二进制:
- 新增、删除或升级带原生代码的库;
- 修改 app config、权限、URL scheme、图标或原生 capability;
- 升级 Expo SDK;
- 新增 config plugin、Expo Module 或原生源码。
不用 EAS:Expo CLI 生成工程,Xcode 本地发布 iOS#
Expo 项目并不必须上传到 Expo 云端打包。只要 Mac 上有兼容的 Xcode,就可以让 Expo CLI 生成 ios/,然后像普通 iOS 项目一样由 Xcode 构建、签名、Archive 并上传。EAS 在这条路径中完全可以不出现。Expo:本地 Release 构建 Expo:使用 Xcode 手动提交 iOS App
如果项目采用 CNG、仓库中还没有 ios/,先执行:
# 从 app config、config plugins 和依赖生成 iOS 原生工程
npx expo prebuild --platform ios
# 用 Xcode 打开生成的 workspace
xed ios如果 ios/ 已经存在且由团队长期维护,不需要每次发布前重复 Prebuild;尤其不要在有未迁移手工原生改动时随意执行 npx expo prebuild --clean,因为 --clean 会重新生成原生目录。之后在 Xcode 中:
- 在 Signing & Capabilities 选择 Apple Team,核对 Bundle ID、版本、build number、权限与 entitlement。
- 在 Product → Scheme → Edit Scheme 中确认 Release 配置,并用 Release 真机版本完成一次脱离 Metro 的 smoke test。
- 选择 Product → Archive。
- 在 Organizer 中选择 Distribute App → App Store Connect,上传到 TestFlight 或提交 App Review;也可以导出
.ipa后通过 Transporter 上传。
下面三个命令代表三种不同目的,不应混为一谈:
# 首次本地调试,或原生依赖 / app config 变化后:编译、安装并启动 Metro
npx expo run:ios --device
# 只改 TS / JS 后:仅启动 Metro,复用手机上已有的 development build
npx expo start
# 在连接设备上检查优化后的本地 Release 行为;这不是可提交商店的 Archive
npx expo run:ios --configuration Release --device“本地构建”不等于所有阶段都能断网:依赖与 CocoaPods 尚未缓存时仍需下载,上传 App Store Connect 也必须联网,并需要 Apple Developer Program。这里的准确含义是:编译过程可以在你的 Mac 和 Xcode 中完成,不依赖 EAS Build,也不需要从 Expo 下载一份已编译 App。
4. CNG、Prebuild、config plugin:原生工程怎样产生#
CNG 不是“没有原生工程”#
Continuous Native Generation(CNG)是一种工作流:把 ios/、android/ 看作可从 Expo SDK 模板、app config 和 config plugins 重复生成的结果。它的目标是让大部分原生配置以版本化的声明存在,降低双端工程漂移与升级成本,而不是禁止原生代码。CNG 官方说明
flowchart LR A["app.json / app.config.ts"] --> D["Prebuild"] B["config plugins"] --> D C["Expo SDK / npm 原生库"] --> D D --> E["ios/ 原生工程"] D --> F["android/ 原生工程"] E --> G["Xcode 编译"] F --> H["Gradle 编译"]
一个最小 app config 片段如下:
{
"expo": {
"name": "Example",
"slug": "example",
"version": "1.0.0",
"scheme": "example",
"plugins": ["expo-router"],
"ios": {
"bundleIdentifier": "com.example.product"
},
"android": {
"package": "com.example.product"
}
}
}app.json 只表示意图;Prebuild 才会把它变成 Info.plist、AndroidManifest、Gradle、Xcode 工程或其他原生文件的实际修改。config plugin 是这个转换过程中的可重复脚本,例如某个库通过 plugin 写入权限文案、URL scheme 或原生 capability。
运行下列命令可显式生成工程:
npx expo prebuild要格外谨慎 npx expo prebuild --clean:它会清理后重生成原生目录,适合采用 CNG 的项目,但会覆盖未被表达为 config plugin / Module 的手工原生改动。当前 Expo SDK 的 Prebuild 行为和默认项会随版本演进;如果团队选择长期手工维护 ios/、android/,就应提交这些目录、审查原生差异,并避免把清理重生当成无风险的格式化命令。
什么时候要写 Expo Module#
按优先级尝试:
- 先找与当前 Expo SDK 相容、维护活跃的 Expo / React Native 原生库;
- 库需要原生配置时,使用它提供的 config plugin;
- 没有合适封装时,用
npx create-expo-module@latest --local创建本地 Expo Module,在 Swift / Kotlin 中只暴露所需能力; - 极端场景再直接维护原生工程、React Native TurboModule / Fabric Component 或 C++ 代码。
本地 Module 的价值是让 TypeScript feature 依赖一个项目自有接口,而不是到处直接依赖第三方桥接包。例如 src/services/secure-key.ios.ts 可调用本地模块,Android / Web 则提供自己的实现。原生模块仍需要 development build 或正式二进制;Expo Go 不会动态下载它。Expo Modules API
5. EAS 到底是什么#
把 EAS 当成一条可选的“移动交付流水线”,而不是新的前端框架。它包含彼此独立、可以按需采用的服务。
EAS Build:构建并签名原生二进制#
EAS Build 在云端(或 --local)运行 iOS / Android 原生构建,产出可安装或可上传的 .ipa、.aab 等文件。它可以协助管理凭据和通过内部链接分发,但不能替代 Apple Developer Program、Play Console 账号或商店审核。
最小初始化流程:
npx eas-cli@latest login
npx eas-cli@latest build:configure
npx eas-cli@latest build --platform android --profile developmenteas.json 用 build profile 描述不同交付目的。下面是适合讲解的最小结构;真实项目还应补 package / bundle ID、更新策略、凭据策略和环境变量。
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"environment": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview",
"environment": "preview"
},
"production": {
"autoIncrement": true,
"channel": "production",
"environment": "production"
}
},
"submit": {
"production": {}
}
}这里的 environment 用于选择 EAS 环境变量集合;channel 用于让 release build 选择 EAS Update 的更新通道。它们不是 CloudKit 的 development / production 环境,也不是 App Store / Play 的审核轨道。不要把 secret 放进 EXPO_PUBLIC_*,因为这类变量会进入客户端 bundle。
EAS Credentials:签名与上传是两类凭据#
EAS 相关凭据至少分为两类,不能混在一个“环境变量”概念里:
| 凭据 | 用途 | 常见形式 |
|---|---|---|
| 应用签名凭据 | 让 binary 成为可安装、可由商店接受的 App | Android keystore / upload key;iOS distribution certificate 与 provisioning profile |
| 商店上传服务凭据 | 让 Submit / CI 有权把 binary 上传给商店后台 | Google Service Account JSON;App Store Connect API Key,或 Apple ID / app-specific password |
EAS 可以生成和托管签名凭据,也可使用团队提供的受控凭据;这不替代 Apple Developer / Google Play 开发者账号。使用 eas credentials --platform ios 或 eas credentials --platform android 管理签名材料。keystore、私钥、credentials.json 和服务账号 JSON 不应提交 Git;应用签名与商店服务凭据放入 EAS Credentials 或受控的本地 / CI 凭据存储,普通构建环境密钥再使用 EAS / CI secret store。iOS Simulator 不需要付费会员,但云端 iOS 真机分发、TestFlight 与 App Store 仍涉及 Apple 的签名与会员权限。EAS app credentials EAS iOS Submit EAS Android Submit
EAS Submit:上传,不是上架完成#
EAS Submit 把已签名的二进制上传给平台:
npx eas-cli@latest submit --platform ios --profile production
npx eas-cli@latest submit --platform android --profile production它的产物和边界很明确:iOS 上传 .ipa 到 App Store Connect,处理后可用于 TestFlight;Android 上传 .aab 到指定 Play track。截图、描述、分级、隐私资料、协议、选择 build、提交 App Review、Play rollout 都仍要在商店控制台完成。上传成功不等于审核通过,更不等于已经对公众发布。EAS Submit
EAS Update:兼容 runtime 内的 OTA 更新#
EAS Update 使用 expo-updates 为已有二进制下发 JavaScript bundle、样式和资源。先把能力编译进二进制:
npx expo install expo-updates
npx eas-cli@latest update:configure
npx eas-cli@latest build --platform all --profile production之后才能发布与它兼容的 update:
npx eas-cli@latest update \
--channel production \
--environment production \
--message "Fix login button alignment"适合 OTA 的内容包括 JS bugfix、文案、翻译、样式、已存在页面的布局和资源;不能用 OTA 完成的是会改变原生代码或原生工程的配置,例如新增原生库、权限、entitlement、原生 capability、URL scheme、Expo SDK 升级及自定义 Swift / Kotlin 代码。只被 JavaScript 消费、且不改变原生接口的配置可以随更新进入 bundle,但仍应验证。OTA 不得用于绕过当时适用的商店审核、支付或可执行代码政策。EAS Update
runtimeVersion 是更新兼容性的闸门。 客户端选择更新时要求平台与 runtimeVersion 字符串精确匹配,再由 build 内嵌的 channel 选择更新通道。推荐先理解 appVersion 策略:原生层发生不兼容改变时,提高 expo.version、重新构建并发布;更新服务只把 JS / assets 发给同一精确 runtime。bundleIdentifier / package 不是 Update 匹配键,单纯换应用标识不会自动隔离更新。runtimeVersion 也不是替代版本管理的魔法字符串,更不会回滚已经执行的数据库迁移或服务端写入。
最小配置形式如下;真实项目仍要让 iOS build number 与 Android versionCode 独立递增:
{
"expo": {
"version": "1.0.0",
"runtimeVersion": {
"policy": "appVersion"
}
}
}autoIncrement: true 管理的是 iOS build number / Android versionCode,不会自动提升用户可见的 expo.version,所以不会自动产生新的 appVersion runtime。不同 dev / preview / staging / production variant 至少应明确使用不同 channel;只要 native JS 接口不同,就必须提升相应 variant 的 expo.version 并重建,或在充分评估后选择其他 runtime policy。EAS runtime versions EAS app version management
EAS Workflows:移动端 CI/CD 编排#
EAS Workflows 用 YAML 运行 build、submit、update、测试等作业。它适合把“PR 产生 preview、main 触发检查、打 tag 构建商店产物、人工批准后更新 production”的过程固化下来。它不是强制使用的服务:GitHub Actions、Bitrise、Codemagic、Xcode Cloud 或企业自建 CI 也可以构建 Expo 项目;关键是把 app config、eas.json、锁文件和可重复的测试都进版本控制,把敏感凭据留在 secret store。EAS Workflows
Xcode Cloud:可替代 iOS 交付链路,不是双端替代#
是的。对拥有稳定 ios/ Xcode project / workspace 的 Expo 或 React Native 项目,Xcode Cloud 可在 Apple 云端执行 Build、Test、Analyze、Archive;Archive 可签名并交付 TestFlight 或上传 App Store Connect。因此,若团队只做 iOS,或已把 iOS CI/CD 放进 Xcode Cloud,那么在 iOS 构建、测试、TestFlight 分发与上传 这一段,通常不必再额外采用 EAS Build、EAS Submit 或 EAS Workflows。Xcode Cloud workflow actions 通过 TestFlight 分发 Xcode Cloud 构建
但它不是 EAS 的双端等价物。Apple 将 Xcode Cloud 定义为面向 Apple platforms 的 CI/CD,文档中的工作流围绕 Xcode scheme 与 xcodebuild actions;因此 Android 构建、Android 签名和 Google Play 提交仍需要另一条 Gradle / CI 流水线,例如 EAS、GitHub Actions、Bitrise、Codemagic 或自建 CI。理论上可在 macOS 环境中编写额外脚本安装工具,但这不是 Apple 文档提供的一等 Android / Play 交付通道,应按自建 CI 维护和验证。Xcode Cloud 概览 依赖与自定义脚本
Expo 采用默认 CNG、且仓库不保留 ios/ 时尤其要先做 POC:Apple 要求 Xcode Cloud 使用持续存在且一致的 Xcode project / workspace,并警告动态生成或修改 project / workspace 可能导致配置或后续构建失败。也就是说,不能假定“纯 CNG 仓库”可零配置接入 Xcode Cloud;应先设计并验证确定性的 iOS workspace 产生与维护方式。首次接入还需要 Xcode、Apple Developer Program、App Store Connect 权限与远程 Git 仓库。Xcode Cloud 接入要求
可以采用混合方式:Xcode Cloud 负责 iOS,EAS Build 或其他 CI 负责 Android;也可以只使用 EAS Update。EAS Update 不依赖 EAS Build,但若 iOS binary 由 Xcode Cloud 产生,必须把 expo-updates 所需的 Expo.plist、runtimeVersion 与 update channel 正确写入原生项目,再由 eas update 或其他 CI job 发布 OTA;原生依赖、权限和原生配置变化仍必须重新 Archive / 发布二进制。不配合 EAS Build 使用 EAS Update EAS Update 配置
6. 不使用 Expo:React Native 的原生发布基线#
不使用 Expo 的 React Native(常称 bare React Native)仍然可以正常上架;它不是“没有原生工程”,而是由团队直接维护 ios/、android/ 工程,并用 Xcode、Gradle 和自建 CI 接管交付。这里特指既不采用 Expo 框架,也不采用 EAS 的传统路径。两者并非必然绑定:EAS Build 是可选服务,bare React Native 项目也可以接入它。
Release 构建时,Metro / Hermes 会把 JavaScript bundle、静态资源和(默认 Hermes 时的)bytecode 放入原生 App;Xcode 与 Gradle 再编译原生代码并进行签名。它们不会自动替你处理 Apple / Google 的账号、商店资料、审核或最终发布。
flowchart TB A["React Native 项目\nTS / JS + ios/ + android/"] --> B["发布准备\n标识、版本、图标、权限、原生依赖"] B --> C["iOS Release\nXcode Archive + distribution signing"] B --> D["Android Release\nGradle bundle + upload-key signing"] C --> E[".ipa → App Store Connect\nTestFlight → App Review"] D --> F[".aab → Play Console\n测试轨道 → 审核 / rollout"]
Android:团队自己配置签名、构建 AAB 并发布#
确定稳定的
applicationId、versionName、versionCode、图标、权限和 release 配置;每次上传 Play 的versionCode必须递增。生成并妥善保管 upload keystore,把 keystore 路径、alias 和密码通过不入库的 Gradle properties 或 CI secret 注入
signingConfigs.release;不要将 keystore 或密码提交到 Git。构建并在真机上测试 release 产物。React Native 当前发布文档给出的 App Bundle 命令是:
npx react-native build-android --mode=release # 产物:android/app/build/outputs/bundle/release/app-release.aab在 Play Console 创建应用、配置 Play App Signing,上传
.aab到 Internal / Closed / Production 等轨道,完成 Data safety、内容分级、商店资料和发布设置后,才可提交审核与 rollout。Google Play 面向用户的 app signing key 与开发者上传用的 upload key 是两种不同角色,应分开理解和保护。React Native:发布到 Google Play Android app signing 创建并设置 Play 应用
iOS:团队自己管理 Xcode Archive、签名与 App Store Connect#
- 在 macOS / Xcode 中配置 Bundle ID、版本和 build number、图标、权限用途文案、capability / entitlement,并选择对应的 Apple Team;首次发布前,在 App Store Connect 创建对应的 app record。创建 app record
- 用 Apple Distribution certificate 与匹配的 provisioning profile(或 Xcode 自动签名)构建 Release;确认 React Native 的 bundle 已随 archive 内置,并在关闭 Metro 的真机上做 release smoke test。
- 在 Xcode 选择 Product → Archive,再执行 Validate App → Distribute App → App Store Connect → Upload。上传后要等待 App Store Connect 处理,才会出现在 TestFlight 或可供选择的 build 列表中。
- 补齐截图、隐私、分级、价格、审核说明等资料;在 TestFlight 验证后,选择 build 并提交 App Review。上传成功不等于已通过审核或公开发布。React Native:发布到 Apple App Store 上传 build 提交审核
没有另接合规的 OTA 方案时,bare React Native 的 JavaScript 修复也要随着新的签名二进制进入 TestFlight / Play 测试轨道,再走商店发布;这正是 EAS Update 这类服务要解决的额外交付问题,而不是 React Native 默认自带的能力。
Expo / EAS 在这条链路上额外做了什么#
| 环节 | 不使用 Expo / EAS 时 | Expo / EAS 增加的工程化能力 | 仍由开发者或商店完成 |
|---|---|---|---|
| 原生配置 | 手工维护 Info.plist、AndroidManifest、Xcode / Gradle、Pods 与原生依赖差异 | Expo 以 app config、config plugin、Prebuild / CNG 表达并重复生成常见配置 | 特殊原生代码、插件质量、权限与合规设计 |
| 构建环境 | 本机或自建 CI 自行维护 macOS / Xcode、JDK、Android SDK、CocoaPods、缓存与脚本 | EAS Build 用 eas.json profile 统一构建意图,可在托管环境构建、保存 artifact 和组织内部交付 | release 测试、原生构建故障诊断、选择可复现的依赖版本 |
| 签名材料 | 自行生成、备份、轮换 Android keystore、iOS certificate / provisioning profile,并安全注入 CI | EAS Credentials 可协助生成和托管兼容的签名凭据,也可使用团队自带凭据 | Apple / Google 开发者资格、密钥所有权与访问控制 |
| 上传与 CI 串联 | Xcode / Transporter、Play Console 或 Fastlane / 自建 API 脚本上传产物 | EAS Submit 可把已签名 .ipa / .aab 上传到 App Store Connect / Play Console;EAS Workflows 可编排 build、submit、update 与测试 | 创建应用记录、截图与 metadata、隐私 / 内容声明、定价、审核、最终 rollout |
| JS 热更新 | 默认没有托管 OTA;若另接第三方或自建方案,需自己处理兼容性、回滚与商店政策 | EAS Update 为兼容 native runtime 的 JS、样式和资源提供 channel / runtimeVersion 管理 | 新原生库、权限、entitlement 或原生代码仍必须重建二进制并遵守商店流程 |
因此,Expo 的核心增量是把常见原生配置与依赖兼容性收敛到框架工作流;EAS 的核心增量是把构建、凭据、上传、更新和 CI/CD 编排集中起来。它们减少的是移动交付的拼装和重复劳动,不会替代商店审核。
选本地原生工具还是 EAS?#
先给结论:EAS 不是另一套原生编译器。 无论本地构建还是 EAS Build,最终仍由 Gradle、Xcode 等平台工具生成和签名原生二进制;差别是由谁维护构建机器、凭据、产物分发与自动化。Expo 可以完全不接 EAS:本地 expo run 会调用已安装的 Android SDK 或 Xcode,编译并安装 development build;只改 JavaScript / TypeScript 时,再以 Metro 复用该二进制即可。本地开发构建 Development builds
| 典型情况 | 优先方案 | EAS 的边际价值 |
|---|---|---|
| 单人、有 Mac 与 Android 工具链、发布不频繁 | 本地 expo run、Xcode / Gradle,配合自己已有的 CI | 通常可以先不使用;你自己维护签名、构建环境与上传流程即可 |
| 多人协作、开发者系统不同,或团队没有可维护的 Mac 构建机 | EAS Build,可按需启用 Credentials | 托管一致的 Android / iOS 环境、构建 profile、缓存、日志和可选的签名凭据协作;非 macOS 开发者也能发起 iOS 云构建 |
| 经常把 preview 发给 QA / 客户,或需要把构建、上传、测试串成流水线 | EAS Build + Submit / Workflows(或同类 CI) | 内部分发 URL、自动上传与可审计的移动 CI/CD,省去人工搬运 .ipa / .aab 和维护脚本的成本 |
| 上线后常有 UI / JS 热修 | EAS Update(或自建兼容更新服务) | 按 channel / runtime 管理兼容二进制的 JS、样式和资源更新;原生库、权限、entitlement、SDK 升级仍必须重新构建并走商店流程 |
所以,先采用 Expo 的 SDK、Router、config plugin 与 CNG,不等于承诺购买 EAS;EAS 也可以只选择其中一项服务。选择它应看团队的交付摩擦,而不是把它误解为 Expo App 能否编译或上架的前提。EAS Build EAS Update
7. 一条更安全的发布流程#
下面是一条面向新项目的建议流程。它把“开发 build”“内部直装”“商店测试”和“正式发布”分开,避免把 preview APK / ad hoc 安装包误当成 TestFlight / Play Closed 测试产物。
flowchart TB A["Typecheck / 单元测试"] --> B["development build\n真机开发"] B --> C["internal preview\n链接直装验收"] C --> D["store staging binary"] D --> E["TestFlight / Play testing track"] E --> F["商店资料 + 审核"] F --> G["production release"] G --> H["兼容 runtime 的 EAS Update\n仅 JS / assets"]
- 先确定稳定标识:iOS bundle identifier、Android package name、URL scheme、Apple / Google 团队归属。不要把临时 ID 带进商店。
- 尽早做 development build:验证登录回调、通知、深链、相机、支付、键盘、安全区和真实设备性能,而不是只看 Expo Go。
- 内部验收与商店验收分开:internal preview 便于链接直装;TestFlight / Play track 必须用商店二进制。新个人 Play 账号还存在 Closed testing 等账号门槛,需以当时规则为准。
- 让 staging 与 production 使用相同的商店标识和原生基线:这样商店测试才真正覆盖生产二进制;如需并行安装的 dev / preview variant,可使用不同标识,但要让其 update runtime 明确隔离。
- OTA 保持保守:将有状态的数据迁移设计为前后兼容的 expand / contract 过程;EAS Update 的“回滚”只能切回 JS / resources,不能撤回用户数据或后端副作用。
8. 测试、诊断与性能应该看什么#
| 层级 | 要验证什么 | 常见工具 / 方法 |
|---|---|---|
| 纯业务逻辑 | 输入、转换、错误模型、权限判断 | TypeScript 单元测试 |
| 组件 | loading / error / accessibility state、用户交互 | React Native Testing Library;Expo 文档已提示 React 19 下不再优先使用 react-test-renderer |
| development build | 原生模块、深链、推送、第三方登录、键盘 | iOS Simulator、Android Emulator、真机与 Dev Menu |
| E2E | 从冷启动到真实核心任务 | Maestro 或团队选定的真机自动化 |
| 性能 | 首屏、列表、图片、JS work、内存、掉帧 | release build + 代表性低端 Android / iPhone;不要以 debug / Expo Go 性能下结论 |
| 发布 | 签名、崩溃、隐私、商店流程 | TestFlight、Play Internal / Closed、监控与分阶段 rollout |
npx expo-doctor 是依赖与项目健康检查的起点,不是对业务正确性、商店合规或所有原生问题的证明。发生 native 构建故障时,先确认 Expo SDK 与 React Native 版本、expo install 选择的依赖、config plugin、锁文件和 build log;不要先删除 ios/、android/ 或盲目升级全部 npm 包。
9. 初学者常见问题#
Expo 是否限制我只用官方 SDK?#
不限制。Expo Go 有限制,因为它是固定的原生客户端;development build 与正式二进制可以加入任意兼容的 React Native 原生库,或自己写 Module。真正的成本在于插件维护、原生配置、双端编译与升级测试。
使用 Expo 后还需要 Apple / Google 开发者账号吗?#
需要。开发学习、Android 本地调试和 iOS Simulator 的要求与商店发布不同,但 TestFlight / App Store 仍需要 Apple Developer Program,Google Play 发布仍需要 Play Console 账号。EAS 只帮助构建、签名和上传,不能代替会员、审核、隐私申报或商店协议。
能不用 EAS 吗?#
可以。Expo 与 Flutter 一样都能把开发版直装到真机:Android 使用 npx expo run:android --device,iOS 使用 npx expo run:ios --device(本地 iOS 构建需要 macOS 与 Xcode)。这两个命令会在需要时执行 Prebuild,再用本机 Android SDK / Xcode 编译、安装并启动 Metro;EAS 不参与。以后只改 JavaScript / TypeScript 时运行 npx expo start 即可,只有原生库、app config 或 Expo SDK 改动才需重新编译原生二进制。本地开发构建
Expo 的 SDK / CLI 不依赖 EAS;可以继续用 Xcode、Gradle 或其他 CI 完成正式构建与提交。iOS 的完整 Archive 步骤见上文“不用 EAS:Expo CLI 生成工程,Xcode 本地发布 iOS”。选择 EAS 的理由是减少跨平台构建、凭据、分发和 OTA 交付的拼装成本,而不是“没有 EAS 就不能发布 Expo App”。
如果项目从一开始也不使用 Expo,而是由 Community CLI 或既有原生宿主维护 React Native,则进入上文的 bare React Native 路线:Metro / Hermes 仍负责 JavaScript bundle,Xcode / Gradle 仍负责原生编译、签名和产物;需要检查 archive 内 bundle、source map 或原生构建细节时,见React Native 技术原理文章。
为什么只改一行 JavaScript 不用重新打包,而改权限就要?#
前者由已内置的 JavaScript runtime 加载;后者会改变二进制中的 Info.plist、AndroidManifest、签名 entitlement 或原生库。旧 App 不可能凭空获得这些原生能力,因此必须产生新 .ipa / .aab,再遵守商店的发布流程。
延伸阅读#
- React Native 技术原理:从 JavaScript 到原生界面、Fabric 与 Hermes
- Flutter 技术原理:Dart、Widget、Engine 与原生发布
- Expo 全面指南:概念、工具栈、上架、HeroUI 跨平台架构与 Flutter 对比
参考资料#
- Expo 创建项目
- Expo CLI
- Expo:本地开发构建
- Expo:本地 Release 构建
- Expo:使用 Xcode 手动提交 iOS App
- Expo:Why Metro
- Expo:使用 Hermes
- Expo Router
- Development builds
- Expo Go 与 development build FAQ
- Continuous Native Generation
- Config plugins
- Expo Modules API
- EAS Build
- EAS Submit
- EAS Update
- EAS runtime versions
- EAS Workflows
- React Native:发布到 Google Play
- React Native:发布到 Apple App Store
- React Native:JavaScript Environment
- Apple JavaScriptCore
- Vite:Why Vite
- Android app signing
- Google Play:创建并设置应用
- App Store Connect:创建 app record
- App Store Connect:上传 build
- App Store Connect:提交审核
- Expo 单元测试