Expo 技术原理与交付:从 React Native 项目到 EAS 发布
8月 10, 2026
AI 参与说明(Agent:Codex、Lorentz):本文由 Agent 根据 Expo 与 React Native 的官方公开文档协助整理,重点核对 Expo SDK、development build、CNG 与 EAS 的职责边界。资料核验于 2026-08-10;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 负责让项目的依赖、开发与原生配置更易管理。
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 预先发布的通用客户端 | 学习、原型、验证它已包含的 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 或原生源码。
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
6. 一条更安全的发布流程#
下面是一条面向新项目的建议流程。它把“开发 build”“内部直装”“商店测试”和“正式发布”分开,避免把 preview APK / ad hoc 安装包误当成 TestFlight / Play Closed 测试产物。
flowchart LR 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,不能撤回用户数据或后端副作用。
7. 测试、诊断与性能应该看什么#
| 层级 | 要验证什么 | 常见工具 / 方法 |
|---|---|---|
| 纯业务逻辑 | 输入、转换、错误模型、权限判断 | 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 包。
8. 初学者常见问题#
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 的 SDK / CLI 不依赖 EAS;可以本地通过 npx expo run:ios|android、Xcode、Gradle 或其他 CI 完成构建与提交。选择 EAS 的理由是减少跨平台构建、凭据、分发和 OTA 交付的拼装成本,而不是“没有 EAS 就不能发布 Expo App”。
为什么只改一行 JavaScript 不用重新打包,而改权限就要?#
前者由已内置的 JavaScript runtime 加载;后者会改变二进制中的 Info.plist、AndroidManifest、签名 entitlement 或原生库。旧 App 不可能凭空获得这些原生能力,因此必须产生新 .ipa / .aab,再遵守商店的发布流程。
延伸阅读#
- React Native 技术原理:从 JavaScript 到原生界面、Fabric 与 Hermes
- Flutter 技术原理:Dart、Widget、Engine 与原生发布
- Expo 全面指南:概念、工具栈、上架、HeroUI 跨平台架构与 Flutter 对比