Expo 技术原理与交付:从 React Native 项目到 EAS 发布

This article is extracted from the chat log with AI. Please identify it with caution.

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”的问题就能自行判断:

  1. React Native 运行层:React、Hermes、Fabric、平台 Host View、以及 Swift / Objective-C / Kotlin / Java / C++ 原生代码。它负责最终在 iOS 和 Android 上运行。
  2. Expo 框架层:Expo SDK、Expo CLI、Router、应用配置、config plugins、Prebuild、Expo Modules 和 development build。它降低使用原生能力的日常成本。
  3. 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 ViewTextPressable 等组件;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 类比有助于入门,但要保留目标环境的差异:

工具主要输入主要目标环境它不负责什么
ViteWeb 的 TS / JS / CSS / assets浏览器的 JavaScript 引擎、DOM 与 Web APIiOS / Android 原生控件和原生工程编译
MetroReact Native / Expo 的 TS / JS 与 assetsiOS / Android App 中的 React Native JavaScript runtime,生产环境可生成 Hermes bytecodeSwift / 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 GoExpo 预先发布的通用开发客户端用于学习和快速打开一个只使用其已内置原生能力的项目;它的 native runtime 固定,不能随你的项目动态加库
development build你自己的开发版 App通常是含 expo-dev-client 的项目专属原生 App;可本地构建,也可以消耗 EAS Build 的配额在云端构建
Free / Starter / Production / EnterpriseEAS 的账户计费套餐是,指 EAS 云服务选择的是云构建 / 更新 / CI 等服务的额度与优先级,不会改变 Expo Go 的能力,也不替代 Apple / Google 账号
build.productioneas.json 的 build profile keyprofile key 可自定义;但命令省略 --profile 时,EAS CLI 会查找名为 production 的 profile
channel: "production"EAS Update 的更新通道它和 build profile 独立,只决定兼容 binary 从哪个 Update channel 取更新
environment: "production"EAS 环境变量集合默认有 developmentpreviewproduction 三个环境;截至本文核验日,自定义环境仅列在 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.productionchannel: "production"environment: "production" 虽然同名,却分别属于构建 profile、OTA 通道和环境变量三个命名空间;不要把它们和 EAS Production plan 视为绑定关系。EAS build profiles EAS environments

产物本质适合做什么关键限制
Expo GoExpo 预先发布的通用客户端学习、原型、验证它已包含的 SDK不能随项目加入任意原生库、原生配置、推送凭据或自定义 entitlement
development build通常包含本项目原生依赖与 expo-dev-client 的开发 App日常开发、真机联调、原生 SDK、调试含开发工具,不是商店正式产物
internal preview可通过链接安装的内部版本设计 / QA / 业务验收iOS ad hoc、Android APK 等通常不是 TestFlight / Play 商店测试轨道
store staging / production已签名的 .ipa / .aabTestFlight、Play track、商店审核和正式发布仍需商店账号、资料、审核和发布操作

Expo 官方把 development build 描述为“你自己的 Expo Go”:它可以包含任何原生库和原生配置,是面向真实项目的推荐路径。Development builds

先安装开发客户端,再本地编译一次:

npx expo install expo-dev-client
npx expo run:ios
npx expo run:android

expo 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 中:

  1. Signing & Capabilities 选择 Apple Team,核对 Bundle ID、版本、build number、权限与 entitlement。
  2. Product → Scheme → Edit Scheme 中确认 Release 配置,并用 Release 真机版本完成一次脱离 Metro 的 smoke test。
  3. 选择 Product → Archive
  4. 在 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#

按优先级尝试:

  1. 先找与当前 Expo SDK 相容、维护活跃的 Expo / React Native 原生库;
  2. 库需要原生配置时,使用它提供的 config plugin;
  3. 没有合适封装时,用 npx create-expo-module@latest --local 创建本地 Expo Module,在 Swift / Kotlin 中只暴露所需能力;
  4. 极端场景再直接维护原生工程、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 development

eas.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 成为可安装、可由商店接受的 AppAndroid 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 ioseas 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.plistruntimeVersion 与 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 并发布#

  1. 确定稳定的 applicationIdversionNameversionCode、图标、权限和 release 配置;每次上传 Play 的 versionCode 必须递增。

  2. 生成并妥善保管 upload keystore,把 keystore 路径、alias 和密码通过不入库的 Gradle properties 或 CI secret 注入 signingConfigs.release;不要将 keystore 或密码提交到 Git。

  3. 构建并在真机上测试 release 产物。React Native 当前发布文档给出的 App Bundle 命令是:

    npx react-native build-android --mode=release
    # 产物:android/app/build/outputs/bundle/release/app-release.aab
  4. 在 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#

  1. 在 macOS / Xcode 中配置 Bundle ID、版本和 build number、图标、权限用途文案、capability / entitlement,并选择对应的 Apple Team;首次发布前,在 App Store Connect 创建对应的 app record。创建 app record
  2. 用 Apple Distribution certificate 与匹配的 provisioning profile(或 Xcode 自动签名)构建 Release;确认 React Native 的 bundle 已随 archive 内置,并在关闭 Metro 的真机上做 release smoke test。
  3. 在 Xcode 选择 Product → Archive,再执行 Validate App → Distribute App → App Store Connect → Upload。上传后要等待 App Store Connect 处理,才会出现在 TestFlight 或可供选择的 build 列表中。
  4. 补齐截图、隐私、分级、价格、审核说明等资料;在 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 Buildeas.json profile 统一构建意图,可在托管环境构建、保存 artifact 和组织内部交付release 测试、原生构建故障诊断、选择可复现的依赖版本
签名材料自行生成、备份、轮换 Android keystore、iOS certificate / provisioning profile,并安全注入 CIEAS 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"]
  1. 先确定稳定标识:iOS bundle identifier、Android package name、URL scheme、Apple / Google 团队归属。不要把临时 ID 带进商店。
  2. 尽早做 development build:验证登录回调、通知、深链、相机、支付、键盘、安全区和真实设备性能,而不是只看 Expo Go。
  3. 内部验收与商店验收分开:internal preview 便于链接直装;TestFlight / Play track 必须用商店二进制。新个人 Play 账号还存在 Closed testing 等账号门槛,需以当时规则为准。
  4. 让 staging 与 production 使用相同的商店标识和原生基线:这样商店测试才真正覆盖生产二进制;如需并行安装的 dev / preview variant,可使用不同标识,但要让其 update runtime 明确隔离。
  5. 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,再遵守商店的发布流程。

延伸阅读#

参考资料#

本文共 13414 字,创建于 Aug 10, 2026

相关标签: Frontend, React, TypeScript, ByAI