Skip to content
核心概念

核心概念

这一章建立 Expo 的心智模型:版本号之间到底怎么配对、“eject/managed/bare"这些老概念现在还剩多少真实性、一份配置怎么变成两个原生工程、expo-* 包内部是什么结构、以及 JS 代码经过怎样的路径到达设备。读完你应该能回答一个实战中最常纠结的问题:“我改的这个东西,需要重新做什么?” SDK 58 的新特性不在这里罗列——它们各自归位到对应功能章,本章只讲持续有效的骨架。


SDK 版本模型:一切跟着一个数字走

主版本号就是 SDK 号

Expo SDK 58 意味着:expo 包是 58.x.x,expo-router、expo-image、expo-camera……所有 expo-* 包的主版本号都是 58。2026-10-20 对 npm registry 实测:expo latest 58.0.16、expo-router 58.0.24、expo-image 58.0.16、expo-sqlite 58.0.13,sdk-58 与 latest dist-tag 一致。

这套"同号制"不是从来都有的:expo-image 在 SDK 52 时代还是 2.0.3(实测 dist-tag),SDK 55 起已经是 55.0.11;expo-router 更早是自己的一套 2.x–6.x 编号,SDK 56 起改为 56.2.21 → 57.0.28 → 58.0.24 随 SDK 同号。所以看老教程里的 expo-image@17.x 这类版本号不要照抄——那是统一编号之前的世界。

同号制解决的问题很直白:以前要判断"我的 SDK 54 该配哪个 expo-image”,现在只要包的主版本和 SDK 主版本相同,它就是官方为这代 SDK 测试过的组合。小版本(58.0.16 里的 .16)是 bug 修复滚动发布,可以随手升级。

锚定链:SDK → React Native → React

每个 SDK 大版本锚定一对 RN/React 精确版本。下面是 expo-template-default 各 SDK dist-tag 的 package.json 实测值(2026-10-20,registry):

Expo SDK React Native React 备注
54 0.81.5 19.1.0
55 0.83.10 19.2.0
56 0.85.3 19.2.3
57 0.86.3 19.2.3
58 0.88.0 19.3.0 从 0.86 直升 0.88,一个 SDK 吃下 0.87/0.88 两版变化(官方 changelog 原文确认)

Node 侧同样被锚定:SDK 58 要求 Node 22.13+ / 24.3+ / 26+,奇数大版本不支持(changelog #47202)。这张表的意义不是背下来,而是让你明白:升级 Expo SDK 是一次打包升级——RN、React、Hermes、全部原生依赖的版本同时换血,这正是"自己凑版本组合"的 bare 项目最缺的东西。

发布节奏:平均约 3 个月一版,2026 年明显加密

以 expo 包 X.0.0 的 npm 发布时间实测(registry time 字段):

50 → 2024-01-18    53 → 2025-04-28    56 → 2026-05-20
51 → 2024-05-06    54 → 2025-09-10    57 → 2026-06-30
52 → 2024-11-10    55 → 2026-02-25    58 → 2026-09-29

2024 年 3 版、2025 年 3 版、2026 年前三个季度已出 4 版(56→57 只隔了约 6 周)。老口径"一年约 3 个 SDK"已经偏保守,更准确的说法是大约每季度一版、近期节奏在加快——排期上应当把"每年 3-4 次 SDK 升级"当成固定开销来预留。

还有一个对实际工作影响很大的细节:一个 SDK 存在多个"发布日期",它们不相等。SDK 58 的 expo@58.0.0 于 2026-09-29 发到 npm,官方 changelog 于 2026-10-15 发布,而 Expo Go 商店版截至 2026-10-20 还没上架(changelog 原文"within a few weeks"),所以 create-expo-app 默认还建 57 项目(见 00-概述与安装.md)。同理,旧大版本不会死掉:sdk-57 tag 已经滚到 57.0.30——补丁持续 backport,但新特性只进最新版。判断"能不能上 58"要先分清你指的是哪个"已发布"。

版本矩阵的使用方式

日常你不需要手工维护这张表,三条命令就够了:

npx expo install expo-camera        # 按当前 SDK 挑版本
npx expo install --check            # 列出偏离矩阵的依赖
npx expo-doctor                     # 更全面的依赖/配置体检

升级跨 SDK 时的完整工作流(含 npx expo install --fix、changelog 对账)在 11-升级与诊断.md。

随时确认"我现在到底是谁"

接手陌生仓库或在 CI 里做分支逻辑时,三个地方可以互相印证 SDK 身份:

grep '"expo"' package.json        # 主版本号即 SDK 号(如 ~58.0.16 → SDK 58)
npx expo config --type public     # 解析后的完整配置,含 sdkVersion 字段

sdkVersion 是 app config 的合法字段(官方配置参考页实测在列),正常情况由工具维护,不要手改它去"骗"CLI。Node 底线也可以直接从包元数据读,比翻博客可靠:expo@58.0.16 的 engines 实测为 ^22.13.0 || ^24.3.0 || ^26.0.0 || >=27.0.0——23、25 两个奇数线被 caret 区间天然排除,和 changelog"26 及以后"的口径一致。


managed、bare、eject:已死的概念为什么还在缠着你

历史一段话

早期(约 2018–2021)Expo 是"封闭容器":SDK 模块写死的运行时 + 云构建,想用任意第三方原生库就得 expo eject——把原生工程一次性吐给你,之后永久自管,从此和 Expo 工具链诀别。当时"managed workflow / bare workflow"是两种真实存在、互斥的形态,大量教程、博客、AI 训练语料停留在那个世界。

现状:只有一个项目形态

官方 llms.txt(2026-10-20 实测)的原话口径:"‘Ejecting’ does not exist. The expo eject command was removed in SDK 46 (2022)",并且 “managed vs bare 是过时区分——所有 Expo 项目现在使用同一个架构”。取而代之的是 CNG(Continuous Native Generation):

npx expo prebuild          # 按 app.json + plugins 生成 ios/ 与 android/
npx expo prebuild --clean  # 删掉重新生成(手工改动会丢,这是特性不是 bug)

一个类比:CNG 的原生目录之于 Expo 项目,≈ dist/ 之于前端工程。它是"配置推导出来的构建产物",默认进 .gitignore,可以删、可再生、不该手改;你要改的永远是"源码"那一侧——也就是 app 配置和 config plugins。原生定制的正道是写/用 config plugins(03-应用配置与插件.md),极少数场景才用 patch-project 打补丁(也在那一章)。

再生成也不只有 prebuild 一条触发路径:npx expo run:ios / run:android 和 EAS Build 在原生工程不存在时都会自动先执行 prebuild(SDK 58 changelog 的升级一节原文即如此描述)。所以"删掉 ios/ 目录"永远安全——下一次构建会自己补回来。

反过来,把生成出来的 ios/、android/ 提交进 git 也完全合法——项目就退化成"带着 Expo 工具链的 bare 工程",npx expo start 照常用;这就是官方文档里 “Expo with existing React Native apps” 那条路(react-native/ 手册的构建发布一章对两种形态都适用)。

“产物"心态还有个实操红利:SDK 官方升级指引明确要求,升级前删掉旧版本生成的 android/、ios/ 目录,下次 run:ios/prebuild/EAS 构建会自动按新版本重新生成(SDK 58 changelog 的 Upgrading 一节原话)。既然目录是配置推导出来的,删它就没有任何心理负担——这也是为什么原生目录进 .gitignore 的项目升级反而更快。

实际影响

  • 看到教程说"先 eject 再加原生依赖”——直接丢掉,它是 2021 年的世界。
  • AI 生成的项目里出现 expo-cli(全局安装的旧包,已废弃,用 npx expo)或 managed/bare 二选一的配置注释,按本章的模型纠正。全手册的"旧→新"纠偏清单汇总在 12-最佳实践与陷阱.md。
  • 面试/评审里听到"Expo 项目不想要托管就废了"这类论断,拿 prebuild 现场演示一次即可终结。

项目配置三层:声明 → 插件 → 生成物

Expo 项目的"配置"是一条单向流水线,三层各有各的文件和职责:

app.json / app.config.ts(声明层:我想要什么样的应用)
        ↓  plugins 数组
config plugins(微调层:把声明翻译成对原生工程的具体改动)
        ↓  npx expo prebuild(或 run:ios / EAS Build 自动触发)
ios/ 与 android/(生成物:真正的 Xcode/Gradle 工程)

第一层:app.json / app.config.ts

静态需求写在 app.json:应用名、slug、图标、启动屏、bundle 标识、权限声明、scheme。需要按环境变量或按平台动态计算时改用 app.config.ts(导出一个函数,拿到当前平台上下文再拼配置)——CI 里给 staging/prod 出不同 bundle id 靠它,完整示例见 react-native/00-安装与快速开始.md,本章不重复。

// app.json —— 声明层的典型骨架
{
  "expo": {
    "name": "MyApp",
    "slug": "myapp",
    "scheme": "myapp",
    "version": "1.0.0",
    "plugins": [
      "expo-router",
      "expo-build-properties"
    ]
  }
}

第二层:config plugins

plugins 数组里的每一项都会在 prebuild 时执行,往原生工程里写东西。例如 SDK 58 起 Android 模板默认开 R8 混淆,官方给的不是让你去改 gradle.properties,而是通过 expo-build-properties 插件的 android.enableMinifyInReleaseBuilds 声明开关;expo-notifications 插件配置大图标、expo-splash-screen 配置启动屏,同理。第三方库带的插件也是这一层——npx expo install 装进带原生配置的库时,CLI 会提示把它的 plugin 加进数组。全字段与插件编写见 03-应用配置与插件.md。

第三层:生成物

Info.plist、AndroidManifest.xml、AppDelegate.swift(SDK 58 起还有 scene lifecycle 的 SceneDelegate.swift)都是输出。规则:永远不要直接编辑它们——下次 prebuild 就没了,这正是 CNG 的设计意图;需要"改原生"时回到第二层找插件,没有现成插件就写一个或 patch-project。

三层的存在解释了一个高频困惑:“为什么我在 AndroidManifest 里加的权限消失了?"——因为你改的是生成物,不是配置。

为什么非要设计成这样

因为云端构建和团队复现要求配置必须是完整自洽的。EAS Build 把项目源码传到一台干净的云机器上重建原生工程,它看不到你本地 ios/ 目录里的任何手工改动——凡是没写进 app.json/plugins 的东西,在云端就"不存在”。这条约束反过来保护了本地开发者:配置即事实,任何人在任何机器 prebuild 出的工程都一样。想明白这一点,就理解了官方为什么把"手改原生目录"列为反模式——不是禁你,而是改完你就把自己锁回了"只有你这台机器能构建"的世界。


expo-* 包的内部:expo-modules-core 与 autolinking

每个 expo-* 包都是"TS 门面 + 原生实现"的双层结构。以 expo-device 为例:JS 侧 import Device from 'expo-device' 拿到的是类型完备的 TypeScript 门面;真身在 Swift(iOS)和 Kotlin(Android)里,两边通过 JSI 直连(无 JSON 桥序列化)。把它们粘在一起的是 expo-modules-core:负责模块注册、JS↔原生调用分发、生命周期事件转发,以及给模块作者的一套 Swift/Kotlin DSL。

三个工程细节,了解即可:

  • 调用成本在持续压低。 SDK 56 起 iOS 去掉了 Objective-C++ 中转层(调用提速 1.6–2.3 倍)、Android 用 Kotlin 编译器插件替代运行时反射;SDK 58 再削 iOS 侧的简单值传递与长字符串成本。你升级 SDK 时无感享受,没有要迁移的 API。
  • SDK 58 起 Android 的 libexpo-modules-core.so 随 npm 包预编译分发(四种 ABI、debug/release 各一份),不再每次从 C++ 源码编译;官方实测空白项目四 ABI 干净构建从约 80 秒降到约 39 秒(#50452,M3 Pro)。想退回源码编译用 expo-build-properties 的 android.buildExpoModulesCoreFromSource。
  • **Expo Modules 2.0(beta)**允许用注解标注普通 Swift/Kotlin 方法来暴露给 JS,替代 DSL,微基准全面快于 1.0 与 TurboModule——这是写原生模块话题,见 09-Expo-Modules与原生扩展.md。

autolinking 是另一个"为什么装个 npm 包就完事"的答案:prebuild/构建时,Expo 的自动链接器扫描 node_modules 里带 expo-module.config.json 标记的包,把它们的原生代码挂进 Gradle/CocoaPods 工程——等价于当年手动 link + pod install 的全套动作。这就是为什么 npx expo install react-native-maps 之后什么都不用做就能 import(个别需要 API key、权限声明的库除外,提示会告诉你)。

与 RN 官方 TurboModule/Fabric 体系的关系(同一层竞品还是互补、存量库怎么选)见 react-native/09-原生模块与平台代码.md;所有 SDK 模块的分类速查在 08-SDK模块速查.md。


Metro、Hermes 与打包:代码怎么到达设备

开发回路:Metro dev server

npx expo start 本质是启动一个包着 Metro 的 dev server(默认端口 8081),按请求即时编译模块、供 Fast Refresh 增量更新。设备连上它的三种方式——LAN(默认,要求同网段)、--localhost(仅本机/模拟器)、--tunnel(公网隧道,SDK 58 起是自家 @expo/ws-tunnel,不再依赖 ngrok、需要登录;旧 ngrok 隧道用 --tunnel ngrok 保留)——怎么选、连不上怎么查,全在 05-Expo-Go与开发构建.md。顺带一提 SDK 58 修了一个经典痛点:manifest 现在支持相对 URL,dev server 在反代/远程机器后面时 development build 不再被指到够不着的地址(#47255)。

定制打包行为的地方是 metro.config.js,正确姿势是扩展默认配置而不是重写——Expo 的默认配置里包着平台判断、资产后缀、monorepo 发现等一大套逻辑,重写等于把它们全丢掉:

// metro.config.js —— 官方文档的扩展模式
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push('sql'); // 例:把新文件类型当模块打包

module.exports = config;

另一个和 dev server 缓存相关的 58 变化值得知道:内联进产物的 EXPO_PUBLIC_* 变量现在会记录进 Metro 的 transform cache,改值会让缓存产物正确失效(#47750)——在此之前"改了 .env 但 bundle 里还是旧值"是经典玄学事故。缓存仍出问题时的三板斧:npx expo start -c(清缓存重启)、删 node_modules/.cache、重装依赖。

生产产物:JS bundle 与 Hermes 字节码

发布时 Metro 把全部模块打成单个 JS bundle,然后 Hermes(Expo/RN 的默认引擎)把 bundle 进一步编译成 **HBC(Hermes Bytecode)**字节码打进原生壳。为什么要字节码:省去真机上现场解析/预热 JS 文本的时间,启动更快、内存占用更低。expo export 有个 --no-bytecode 选项,官方文档的措辞相当重:它只用于分析 bundle 体积,永远不要把未编译的 UTF-8 bundle 发到原生平台——启动时间会显著劣化。

字节码还有一个安全维度的变化:2026-05-12 起,Expo Go 里加载 Hermes 字节码 bundle 只允许来自 EAS Update,自托管更新服务器必须提供纯 JS bundle;同时 Expo Go 只能加载你自己/你组织发布的更新(来源:官方 changelog)。

OTA 的完整机制(channel、runtimeVersion 如何把"某个原生壳"和"某批 JS 更新"绑在一起、灰度与回滚)在 07-EAS发布与热更新.md 展开;这里只需要建立一条铁律:OTA 只移动 JS/资源层,动到原生就必须出壳。

NODE_ENV:58 起由 CLI 强制覆盖

这是个新变化但属于基础模型,必须提前建立认知:SDK 58 起 Expo CLI 按命令语义强制设置 NODE_ENV,你传进去的其他值会被替换(changelog #48554/#48741)。实测口径:

  • NODE_ENV=test npx expo start 现在加载的是 .env.development 而不是 .env.test;
  • 调试构建一律 development,其他所有构建(包括你自己命名的 Staging 配置)一律 production;
  • EAS Build、expo config、expo prebuild、expo-doctor 都按 production 模式解析配置。

结论:从 58 起别再把 NODE_ENV 当多环境开关用。区分环境走 EXPO_PUBLIC_* 变量、EAS 环境变量和 app.config.ts 里的自定义逻辑,完整玩法见 03-应用配置与插件.md 与 06-EAS构建.md。

Strict TS API 随 58 落地

RN 0.87(被 SDK 58 打包)起 Strict TypeScript API 是默认:从 react-native/Libraries/* 深导入会直接报类型错误,ref 类型换成 ViewInstance/TextInputInstance 这类实例类型。tsconfig.json 里设 "customConditions": ["react-native", "react-native-legacy-deep-imports"] 可以暂缓,但这个出口在 RN 0.89 之后会移除——官方明说"在 SDK 58 周期内完成迁移"。迁移指引和踩坑记录见 11-升级与诊断.md。


目录约定

默认模板的目录不是强制规范,但它是官方推荐组织方式的物化,照着摆工具最顺(58 模板实测为 src/ 布局,tarball 核验;app/ 顶层布局同样被官方支持,见 04-Expo-Router.md):

MyApp/
├── src/
│   ├── app/              # expo-router 路由目录:文件=屏幕,_layout=布局(见 04)
│   │   ├── _layout.tsx
│   │   └── index.tsx
│   └── components/       # (模板示例组织,非强制;constants/、hooks/ 同级)
├── assets/               # 图标/启动屏/字体,被 app.json 引用
├── scripts/
│   └── reset-project.js  # npm run reset-project:一键清掉模板示例、留最小路由目录
├── ios/  android/        # prebuild 生成物,默认 .gitignore(见 CNG 一节)
├── .expo/                # CLI 本地状态缓存,不要提交
├── app.json / app.config.ts
├── babel.config.js       # babel-preset-expo,一般不动
├── metro.config.js       # 引 @expo/metro-config,打包行为定制处
├── tsconfig.json         # extends expo/tsconfig.base
└── eslint.config.js

两个容易忽略的点:

  • 想在项目根之上再收一层,官方支持顶层 src/ 目录(app/ 放进去也成立),路由章有配套说明;没有硬性要求,团队一致最重要。
  • npm run reset-project 只存在于 create-expo-app 的默认模板(实测 scripts 包含它),blank 模板没有;新项目扫完示例代码用它还原干净起点,比手动删安全。

入口在哪:为什么找不到 App.tsx

从 bare RN 迁过来的第一件事是找入口,而默认模板里没有 App.tsx、也没有 index.js + AppRegistry.registerComponent。实测模板 package.json 的 main 字段是 expo-router/entry:这个入口模块负责把 app/ 目录的路由树注册为 RN 根组件并接管渲染。也就是说"入口"从你手里移交给了路由系统——想理解首屏为什么长这样,看 04-Expo-Router.md;想恢复手写注册流程,用 blank 模板(不推荐默认模板项目走回头路)。


旧→新速查:校验你手里教程的年代

读完本章,用这张表给任何 Expo 资料"断代"(右列均为 2026-10-20 时点实测/官方原文口径):

旧口径 现状 一句话依据
expo eject 后自管原生 eject 命令已删(SDK 46,2022) 官方 llms.txt:“Ejecting does not exist”
managed / bare 两种工作流二选一 只有同一个架构,prebuild 按需生成原生目录 同上
全局装 expo-cli 只用 npx expo(本地 CLI),旧全局包已废弃 官方 llms.txt
Expo Go 支持多 SDK / 随便扫码 新版 Expo Go 只支持单一 SDK,且强制登录 SDK 58 changelog、2026-09 官方公告
expo-image@17.x、expo-router@3.x 式版本号 全部 expo-* 主版本 = SDK 号 registry dist-tags 实测
一年一个大版本、年底发 约每季度一版(2026 年已 4 版) npm X.0.0 发布时间实测
隧道靠 ngrok 默认 @expo/ws-tunnel,需登录;--tunnel ngrok 保留旧路 SDK 58 changelog
用 NODE_ENV 切多环境 CLI 强制按命令语义覆写 NODE_ENV SDK 58 changelog(#48554)

这张表的用途不是挑刺,而是防御:移动端知识的半衰期比 Web 还短,AI 生成代码和两年前的博客会高频地端出左列的东西。


心智模型收尾:Expo 应用 = JS 运行时选择 + 原生壳 + 配置即代码

把前面所有东西压成一句话:你写的永远是 JS/TS;Hermes 是它的运行时;原生壳是配置生成的产物;壳与 JS 之间用不同的发布通道移动。

  • JS/TS 层:React + RN 组件 + expo-* 模块门面,经 Metro 打包、Hermes 执行。
  • 原生壳:RN runtime + Hermes + 你声明的原生模块,由 app.json + plugins → prebuild 生成(或交给 EAS 云端),编译进 APK/IPA。
  • 配置即代码:壳的内容是"声明出来的",所以可重生成、可复现、可在 CI 与本地之间保持一致。

日常开发 95% 的问题都能翻译成"这个改动落在哪一层":

你改了什么 需要做什么 不需要做什么
组件/逻辑/样式(纯 JS) Fast Refresh 即时生效 什么都不用
新增一个 Expo Go 支持的官方/适配模块 npx expo install <pkg>,重启 dev server 不用重新编译壳
新增需要编译原生代码的库 重新编译 development build(run:android / run:ios 或 EAS) 不用碰商店
改 app.json 声明(图标、权限、plugins) 重新 prebuild + 重编壳(生成物才会更新) 不用手改 Info.plist/Manifest
修一个线上 JS bug eas update 发 OTA(仅 JS/资源) 不用过商店审核(边界见 07)
升级 SDK / 改原生版本锚定 按 11-升级与诊断.md 工作流重编壳、重发版 —

分不清层,就分不清"为什么我的改动没生效"和"为什么改个图标也要重新出包"。想清这一张表,Expo 的绝大多数行为都是可预测的。


实战建议

版本矩阵交给工具,不要人肉记忆。 expo install + --check + expo-doctor 三条命令覆盖日常全部版本决策;团队 wiki 里写"装包只用 npx expo install“比贴版本表管用。

把原生目录当 dist/ 对待。 ios/、android/ 进 .gitignore,定制一律走 plugins;哪天真的要接管原生工程,把目录提交即可,工具链不会惩罚你——但接管之后每次 SDK 升级的手工 diff 也归你了,这是 CNG 帮你免掉的债。

按季度节奏规划 SDK 升级。 实测 2024–2026 平均约 3 个月一个大版本、补丁持续 backport 到旧主线,所以"落后一两个版本"完全健康,“两年不升"才会把一次例行升级变成大手术。生产应用别追 npm 首发——等商店版 Expo Go 与生态跟进(参考 00 章 SDK 58 的时间线),升级路径见 11-升级与诊断.md。

旧概念纠偏要说给 AI 听。 让编码代理参与 Expo 项目时,把"没有 eject、没有 managed/bare 之分、只用 npx expo"这三条写进你的项目指令(如 CLAUDE.md),能省掉一类高频错误;全量纠偏清单在 12-最佳实践与陷阱.md。

官方文档可以整页抓成 Markdown。 docs.expo.dev 的任意页面在 URL 后面加 .md 就是纯 Markdown(如 curl -s https://docs.expo.dev/workflow/continuous-native-generation.md),全站页面索引在 llms.txt——查证本章任何一个"官方口径"时用它,比搜索引擎快照新鲜,也适合直接喂给你的编码代理。