Skip to content
雲里
里雾

Expo vs 裸 RN

mindgym 开发 更新于 2026/4/12

Expo 是建立在裸 React Native 之上的一层工具链抽象,核心目标是把”脆弱的可变原生配置文件”变成可用 JavaScript 声明并自动生成的东西。本页梳理这层抽象存在的动机、工作原理、以及它的真实代价。


概述:裸 RN 的原生层真的有多痛?

在裸 RN 项目(如 SaltyFlame)里,写 JS 部分体验还行,但一旦碰到”需要修改原生层”的场景,就会感受到一道隐形的墙:

这就是裸 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 团队意识到”封闭沙盒”的模型走不远。他们做了两件事:

  1. 引入 Bare Workflow:允许开发者 eject 出来,保留原生 ios/android/ 目录,但仍然可以用 Expo SDK 里的模块。这让 Expo 从”托管平台”变成”工具集”。
  2. 推出 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 时会:

你从来不需要手动碰这些文件。app.json 就是唯一的事实来源(single source of truth)。

Config Plugin 的本质是声明式配置对命令式原生修改的封装。你告诉 Expo”我要 splash screen 背景色是白色”,它知道在 iOS 和 Android 上”白色 splash 背景”分别对应哪些文件的哪些字段。

Expo Go vs Expo Dev Client:两种不同的开发容器

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 的真实缺点(不掩盖)

  1. 构建速度:EAS Build 免费计划有队列等待,可能要等 10-30 分钟出包。
  2. 供应商依赖:EAS 是付费服务,免费计划有限制。可以自托管 EAS Server,但需要额外运维成本。
  3. 调试复杂性:原生问题比裸 RN 更难调试,因为多了 prebuild 这一层。
  4. 版本锁定: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.jsonplugins 数组里所有插件依次执行,最终生成 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.Screenoptions 时,用的就是 React Navigation 的 API。两者是包装与被包装的关系,不是替代关系。


版本说明

本页基于 MindGym M0 阶段(Expo SDK 54, RN 0.81.5, 2026-04-13)。

参见

参考