Expo 是建立在裸 React Native 之上的一层工具链抽象,核心目标是把”脆弱的可变原生配置文件”变成可用 JavaScript 声明并自动生成的东西。本页梳理这层抽象存在的动机、工作原理、以及它的真实代价。
概述:裸 RN 的原生层真的有多痛?
在裸 RN 项目(如 SaltyFlame)里,写 JS 部分体验还行,但一旦碰到”需要修改原生层”的场景,就会感受到一道隐形的墙:
- 想改启动页?要分别编辑
android/app/src/main/res/里的 XML,和 iOS 的LaunchScreen.storyboard。 - 想用推送通知?要在 Xcode 里手动开 Capability,在
AppDelegate.m里加代码,在 Android Manifest 里加权限,再单独走一遍 Firebase / APNs 配置。 - 升级 RN 版本?要对着升级指南,逐行比对
android/build.gradle、ios/Podfile、android/app/src/main/java/.../MainActivity.kt……这些文件里有多少是你写的业务代码,有多少是 RN 框架本身的样板?大部分是样板。
这就是裸 RN 开发者最真实的痛点:原生配置文件是脆弱的可变样板。它们不归你管,但你要对它们负责。每次需要原生能力,你就必须知道 iOS 和 Android 两套原生工程的细节。
Expo 的出发点非常直接:把这些原生配置文件变成可以用 JavaScript 描述和自动生成的东西。
历史背景:从玩具到基础设施
2015 年,Exponent 时代
Expo 的前身叫 Exponent,由 Charlie Cheever 和 James Ide 在 RN 刚发布时创立。最初的想法很激进:让开发者完全不碰 Xcode 和 Android Studio——把代码传到 Exponent 托管的服务器,App 动态加载。这就是 Expo Go 的前身:一个”壳 App”,里面预装了所有常用原生模块,开发者只写 JS。
这个模型有一个致命缺陷:你只能用 Expo 预装的模块。要接入微信登录、要做蓝牙、要用 Face ID?对不起,不支持。Expo Go 的沙盒边界就是它的天花板。
2020-2021 年,Bare Workflow 和 EAS 的转折
Expo 团队意识到”封闭沙盒”的模型走不远。他们做了两件事:
- 引入 Bare Workflow:允许开发者 eject 出来,保留原生
ios/和android/目录,但仍然可以用 Expo SDK 里的模块。这让 Expo 从”托管平台”变成”工具集”。 - 推出 EAS(Expo Application Services):把”在云端构建原生 App”这件事工程化。开发者不需要有 Mac 才能构建 iOS 包。
2022 年,Config Plugins 和 Prebuild
最关键的架构转变。Expo 引入了 app.json + Config Plugins + expo prebuild 机制——这就是”把原生配置文件变成 JS 描述”的具体实现。
现在的 Expo SDK 54
新架构(Fabric + TurboModules)默认开启,app.json 里一行 "newArchEnabled": true 就够了(MindGym 就是这样配置的)。EAS 提供 Build / Submit / Update 三套服务。Expo Router v4 把文件路由带进 RN 生态。
工作方式
Managed vs Bare 工作流:不是非此即彼
很多人以为 Expo 有两种模式需要”选一个”。实际上这是一个连续谱:
| 模式 | ios/ android/ 目录 | 能用自定义原生模块? | 谁维护原生配置? |
|---|---|---|---|
| Managed | 没有(由 prebuild 生成) | 通过 Config Plugin 间接能 | Expo + 你的 app.json |
| Bare | 有,提交到 git | 直接编辑原生文件 | 你自己 |
MindGym 用的是 Managed Workflow:项目里没有 ios/ 和 android/ 目录。这不是限制——而是”原生配置由工具管理,不由你手写”。
Config Plugins:app.json 到原生配置的桥梁
这是 Expo 最重要的基础设施。
直觉模型:Config Plugin 是一个函数,接受当前的原生配置(Expo 内部叫 ExpoConfig),返回修改后的配置。expo prebuild 命令把所有这些函数依次跑一遍,最终生成 ios/ 和 android/ 目录。
看 MindGym 的 app.json(第 30-44 行):
// app.json:30-44
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
]
]
"expo-splash-screen" 这个 Config Plugin 在 expo prebuild 时会:
- 修改
ios/MindGym/SplashScreen.storyboard里的图片引用和背景色 - 修改
android/app/src/main/res/values/colors.xml里的颜色 - 生成对应的 drawable 资源文件
你从来不需要手动碰这些文件。app.json 就是唯一的事实来源(single source of truth)。
Config Plugin 的本质是声明式配置对命令式原生修改的封装。你告诉 Expo”我要 splash screen 背景色是白色”,它知道在 iOS 和 Android 上”白色 splash 背景”分别对应哪些文件的哪些字段。
Expo Go vs Expo Dev Client:两种不同的开发容器
- Expo Go:App Store 上可下载的通用 App。扫码即可运行你的项目。限制:只能用 Expo 预装的模块,不能用自定义原生模块(比如
react-native-mmkv就不行,因为它有原生代码)。 - Expo Dev Client:你自己构建的开发版 App。它包含你项目里所有的原生依赖,但保留了 Expo 的热更新和 QR 码连接能力。等于”你的项目专属的 Expo Go”。
MindGym 用了 react-native-mmkv(原生模块),所以必须用 Dev Client,不能用 Expo Go。
EAS:三件事,三套工具
| 服务 | 做什么 | 解决什么痛点 |
|---|---|---|
| EAS Build | 在云端构建 .ipa / .apk | 不需要 Mac 构建 iOS;CI 不需要 Apple 证书本地配置 |
| EAS Submit | 把构建产物自动提交到 App Store / Google Play | 省去手动上传和填表格 |
| EAS Update | 热更新 JS bundle,跳过应用商店审核 | 修 bug 不等一周审核,OTA 推送 |
注意 EAS Update 的边界:它只能更新 JS 层。涉及原生代码变更(新增原生模块、升级 RN 版本)时,必须走完整的 EAS Build 流程。
Expo Router:文件路由的最小心智模型
你在 SaltyFlame 里用 React Navigation,需要手动维护一个路由注册表——在某个文件里写 <Stack.Screen name="Training" component={TrainingScreen} />,路由和组件是分离的两份代码。
Expo Router 的核心思想只有一句话:文件存在 = 路由存在。
app/
index.tsx → "/"
train/
[module].tsx → "/train/schulte"、"/train/stroop"、…
result.tsx → "/train/result"
history/
index.tsx → "/history"
_layout.tsx 是特殊文件,它不生成路由,而是为同级和子级页面提供包装层(导航容器、全局 Provider、主题)。
Stack 底层仍然是 React Navigation 的 Native Stack Navigator——Expo Router 只是在它之上加了文件路由的自动发现层。这意味着:如果你熟悉 React Navigation 的 Stack.Screen 配置 API,在 Expo Router 里完全通用。
对比其他方案
| 方案 | 适合场景 | 主要 trade-off |
|---|---|---|
| Expo Managed(MindGym 的选择) | 新项目、团队没有深度 iOS/Android 经验、想专注 JS 层 | 遇到 Expo 不支持的原生能力,需要写 Config Plugin(有学习成本);构建依赖 EAS 云服务 |
| 裸 RN | 已有原生团队、需要深度定制原生层、不想依赖 Expo 服务 | 每次 RN 升级要手动维护原生配置;没有 EAS 就需要自建 CI/CD |
| Flutter | 需要真正跨平台一致的 UI(连原生控件都不用)、Dart 团队 | 生态不如 RN 大;Dart 而非 JS/TS;学习成本高;与 Web 技能不可迁移 |
| 原生 iOS/Android | 性能极致要求、深度原生功能(ARKit、CarPlay 等) | 要维护两套代码库;开发效率低;与 Web 生态完全割裂 |
Expo 的真实缺点(不掩盖):
- 构建速度:EAS Build 免费计划有队列等待,可能要等 10-30 分钟出包。
- 供应商依赖:EAS 是付费服务,免费计划有限制。可以自托管 EAS Server,但需要额外运维成本。
- 调试复杂性:原生问题比裸 RN 更难调试,因为多了 prebuild 这一层。
- 版本锁定:Expo SDK 的升级周期(约每年 2-3 次)有时比 RN 社区慢,你会晚几个月才能用最新 RN 特性。
代码示例
一行开启新架构(app.json):
// app.json:10
"newArchEnabled": true
裸 RN 开启新架构需要修改 android/gradle.properties 里的 newArchEnabled=true,以及 iOS 的 Podfile 里的 fabric_enabled 标志。Expo 把这两处修改统一成 app.json 里的一个字段。
Expo Router 根入口(app/_layout.tsx):注意文件里的 export default——这是 Expo Router 在 app/ 目录下唯一要求 default export 的原因:它在编译期用 require() 扫描文件并读取默认导出。其他所有文件(组件、工具函数、Store)都应该用具名导出。
面试视角
Q: Expo 和裸 React Native 的区别是什么?
核心区别不是”有没有 Expo Go”,而是原生配置的管理方式。裸 RN 要求开发者直接维护
ios/和android/目录里的原生文件;Expo Managed Workflow 通过app.json+ Config Plugins +prebuild把这些原生配置变成声明式的、可自动生成的东西。代价是多了一层抽象,遇到 Expo 没有覆盖的原生能力时需要自己写 Config Plugin。
Q: 什么时候不应该用 Expo?
当你的项目需要大量深度原生定制(比如做音视频 SDK、需要嵌入 native view hierarchy、需要 CarPlay/Android Auto 支持)的时候,Expo 的抽象层会成为阻力。另一个场景是团队有成熟的原生 iOS/Android 工程师,他们更愿意直接维护原生工程——这时 Expo 的抽象层反而造成沟通摩擦。
Q: Config Plugin 是什么?它是怎么工作的?
Config Plugin 是一个函数:
(config: ExpoConfig) => ExpoConfig。它接受当前的 Expo 配置,对原生工程做修改(写文件、改 XML、添加权限…),然后返回更新后的配置。expo prebuild命令会把app.json的plugins数组里所有插件依次执行,最终生成ios/和android/目录。这让原生配置变成了可声明、可重复、可 code review 的东西。
Q: EAS Update 和 App Store 更新有什么区别?能绕过审核吗?
EAS Update 只能推送 JS bundle 的变更(UI 改动、业务逻辑修复、文案更新)。只要没有新增原生模块、没有修改原生代码,就不需要走审核——因为 App 的二进制文件没变,只是里面加载的 JS 变了。但如果你修改了原生层(新装一个有原生代码的 npm 包、改了 Permissions 等),必须重新构建并过审。Apple 的审核规则明确禁止用热更新改变 App 的核心功能,所以 EAS Update 应该用于修 bug 和小改动,不是用来绕过审核推大功能。
Q: Expo Router 和 React Navigation 是竞争关系吗?
不是。Expo Router 底层就是 React Navigation——它只是在 React Navigation 之上加了一层文件路由的自动发现机制。你在 Expo Router 里配置
Stack.Screen的options时,用的就是 React Navigation 的 API。两者是包装与被包装的关系,不是替代关系。
版本说明
本页基于 MindGym M0 阶段(Expo SDK 54, RN 0.81.5, 2026-04-13)。
参见
- Router
- Tailwind
- 2026-04-13-React Native 新架构概览
- 2026-04-13-Metro Hermes 与 JSI
- 2026-04-13-NativeWind 与 Tailwind 设计哲学
参考
- Expo 官方文档 — Config Plugins
- EAS 架构说明(重点看 EAS Build 的”How it works”部分)
- Expo Router RFC(GitHub Discussion) — Evan Bacon 写的文件路由最初设计草案