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

8月 10, 2026
Frontend, React, TypeScript, ByAI

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

  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 负责让项目的依赖、开发与原生配置更易管理。

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 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 或原生源码。

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

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"]
  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,不能撤回用户数据或后端副作用。

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,再遵守商店的发布流程。

延伸阅读#

参考资料#

本文共 7089 字,上次修改于 Aug 10, 2026,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» Expo、React Native 与 Flutter:概念、架构、上架与选型

» React Native 技术原理:从 TypeScript 到原生界面、Fabric 与 Hermes

» React Native 组件库横评:HeroUI Native、Paper、Tamagui 与 gluestack-ui

» 全面掌握 TanStack Query:现代 React 应用的数据管理利器

» 全面掌握 tRPC:端到端类型安全的下一代 API 框架