升级与诊断
Expo 的 SDK 升级是这套框架里被工程化程度最高的流程:一条 --fix 对齐依赖矩阵,一条 expo-doctor 扫已知坑,剩下的体力活集中在消化破坏性变更和重出原生包。这一章给你完整的升级工作流(以 SDK 57→58 为实例)、58 的破坏性变更逐项拆解、expo-doctor 检查项清单,以及升级翻车后的排查手册。基线为 2026-10-20,凡"SDK 58 起"的断言均对照官方 changelog / docs / 源码核实。
1. 为什么 Expo 的升级痛感低
先讲清楚问题本身。裸 React Native 项目的升级为什么疼:RN 的 npm 包不承诺向后兼容,一个 react-native-xxx 原生库只有碰上特定 RN 版本才编译得过;三方依赖各配各的版本,升级等于全矩阵重新试错。Expo SDK 把这件事反着做:一个 SDK 版本号 = 一张预先验证过的版本矩阵——SDK 58 意味着 expo 58.0.16 + React Native 0.88.0 + React 19.3.0 + 几十个官方模块的精确配对(默认模板 dependency 全表 npm 实测)。你要做的一切只是把整个项目"换到另一张已验证的矩阵上",而工具链知道每张表长什么样:
npx expo install装包时按当前矩阵挑版本(见02-命令行操作.md);npx expo install --fix把项目里所有与矩阵错位的包(你手动npm i过的、上一代遗留的)批量修正;npx expo-doctor校验"现在装的"与"矩阵要求的"是否一致,外加二十多项静态检查;- CNG 让原生工程本身也是可再生品——矩阵里原生那半张表,用
prebuild重新生成即可,不用手工 diff(非 CNG 项目才需要动原生工程,见第 2 节分支)。
类比:SDK 版本像 Next.js 的大版本或发行版 Linux——你升级的是"整套已搭配好的栈",而不是逐个 apt 包赌兼容性。这就是为什么 Expo 官方敢说"生产应用建议跟着最新 SDK 走"。
一次 SDK 升级到底动了什么(官方排查指南把范围讲得很清楚,先建立预期):
expo包及其依赖的核心行为变化;- 一批
expo-*官方模块换版本(expo-updates、expo-camera、expo-dev-client……); - 其他关键配套包换版本(react-native-reanimated、react-native-screens,用 Router 时还有它内嵌的 React Navigation 层);
- 一次 react-native 大版本升级,连带 Metro。
第 4 条是重点:你实际上同时在升 React Native,所以你的 JS/TS、自写原生代码、三方库都得兼容新 RN——升级出问题时,排查方向就是这四类里"哪个包的哪个新版本",而不是笼统的"升完坏了"。
反过来说,升级翻车的第一大原因也出在矩阵上:日常用裸 npm i 往项目里塞了"latest 版"的原生依赖,等于手动在矩阵上打孔。docs 的 CLI 参考页对此说得很直白(“React Native is not backwards compatible”,包版本必须与项目里的 RN 精确配对),所以 npx expo install 才要取代 npm i。平时靠它守矩阵,升级时 --fix 才有东西可修。
2. 官方升级工作流(SDK 57 → 58 实例)
官方建议一次只升一个 SDK 大版本,出问题时能精确定位是哪一代引入的。先给老手一张全程命令卡(每步的细节在后面小节):
git switch -c upgrade/sdk-58
npx expo install expo@^58.0.0 --fix # expo 本体 + 全矩阵对齐(changelog 官方写法)
npx expo-doctor # 静态检查,先修到这里全绿
rm -rf android ios && npx expo prebuild # CNG 项目:重生成原生工程(-p 单平台亦可)
npx expo run:ios # 重出 development build(Android 同理)
# 以下按项目形态追加:
# 非 CNG: npx pod-install + docs.expo.dev/bare/upgrade/?fromSdk=57&toSdk=58 逐文件应用 diff
# Web 项目:npx expo export --platform web # 验证新的 server output 默认值
# Router 自定义 navigator:过 router/migrate/sdk-57-to-58标准流程分五步:
2.1 开分支
git switch -c upgrade/sdk-58这不是客套:升级分支要能随时扔回主分支继续干活,验证完所有目标平台再合并(checklist 见第 8 节)。另外,如果你还在用 Expo Go 开发,官方明确建议升级前先迁到 development build——Expo Go 模拟不了你的生产壳,升级前后要有可对比的基线(迁移方法见 05-Expo-Go与开发构建.md)。
同一次分支里还藏着一条纪律:别把两个大开关捆在一起。官方排查指南专门举过例子——如果项目计划同时"升 SDK + 开某个实验性大配置"(当年是 New Architecture,今天是 scene 生命周期、R8 这类),把它拆成两次合入:先升 SDK 跑通,再开新行为。出了 bug 你才能回答"是哪一步引入的",否则回归二分法要从头做。
2.2 对齐依赖矩阵
SDK 58 changelog 给的升级命令是一条合并写法:
npx expo install expo@^58.0.0 --fix语义:先把 expo 本体升到 58,再按新矩阵修正其余所有依赖。docs 的升级 walkthrough 把它拆成两步(npm install expo@^58.0.0 然后 npx expo install --fix),两种等价。升完立刻:
npx expo-doctor把静态层面的错位先清掉,再进入下一步——doctor 报的依赖问题先修,后面的红屏会少一大半。
2.3 逐项消化破坏性变更
读 changelog 的 “Notable breaking changes” 与 “Deprecations” 两节(SDK 58 的重点项见第 3 节),对照自己项目逐条处理。整份 58 的破坏性变更都收在 https://expo.dev/changelog/sdk-58;Expo Router 的项目还要额外过一遍迁移指南 https://docs.expo.dev/router/migrate/sdk-57-to-58/ 。
2.4 按项目形态更新原生工程
-
CNG 项目(android/ios 由 prebuild 生成):删掉本地这两个目录,下次
npx expo run:ios/npx expo prebuild/ EAS Build 时按 58 模板自动重生成。这是 CNG 升级体验的核心红利——原生那半张矩阵表不用你手工维护。rm -rf android ios && npx expo prebuild -
非 CNG 项目(手工维护 android/ios):有 ios 目录的先
npx pod-install,然后逐项应用 Native project upgrade helper 的 diff——https://docs.expo.dev/bare/upgrade/ ,页面里选 from SDK 57 / to SDK 58(形如bare/upgrade/?fromSdk=57&toSdk=58),对照改基础原生文件,“一行差很多”。注意:旧教程里常见的upgrade-helper.expo.dev子域名已不存在(2026-10-20 实测该域名返回 Expo 的 “Subdomain not found” 页),官方入口就是上面这个 docs 页面。RN 框架本身的升级差异另见 React Native Upgrade Helper(react-native-community.github.io/upgrade-helper)。 -
用了 expo-dev-client:升级后必须重出一个新的 development build——旧壳里的原生运行时还是上一代矩阵。
-
还在 Expo Go:考虑趁机迁到 development build(官方对生产应用的统一口径)。
2.5 重建、回归、验证
本地 run:ios / run:android 出新的 dev build,四个方向都点一遍:启动、核心链路、崩溃日志、Web(如果有)。卡在玄学错误上,按第 7 节排查。
时间分配上有个经验值:命令本身五分钟跑完,80% 的耗时在"读 changelog + 验证 release 构建"。把 3.3(R8)和 3.4(NODE_ENV)这类"debug 一切正常、release/CI 才炸"的项放在验证清单最前面,它们才是升级 PR 的真正风险面。
3. SDK 58 破坏性变更逐项拆解
以下每条都核验自 changelog 正文(标注 PR 号)。痛感从"编译不过"到"行为悄悄变了"排序。
3.1 React Native Strict TypeScript API 成为默认(最高优先级)
RN 0.87 起严格 TS API 默认开启:对 react-native/Libraries/* 的深导入直接变成类型错误,部分类型形状改变(最显眼的是 ref 改用 ViewInstance、TextInputInstance 这类实例类型)。官方给了逃生门——在 tsconfig.json 里:
{
"compilerOptions": {
"customConditions": ["react-native", "react-native-legacy-deep-imports"]
}
}("react-native" 必须保留,expo/tsconfig.base 本来就设了它。)关键时间窗:这个 opt-out 在 React Native 0.89 之后移除——也就是说 SDK 58 是"可以拖"的最后一年,必须在本周期内完成迁移,否则下一跳 59 会硬着陆。迁移指南:reactnative.dev/docs/strict-typescript-api(“迁移指南"一节),或者把 RN 社区的 migrate-to-strict-api skill(skills.sh)交给 agent 批量改。深导入常见于自研 UI 库和从旧教程抄来的 NativeModules 底层 hack,先跑 tsc 收集全量报错再定工作量。
3.2 iOS 转向 scene-based 生命周期(SceneDelegate)
iOS 27 SDK 要求 UIScene 生命周期,npx expo prebuild 现在会生成 SceneDelegate.swift 并在 Info.plist 写入 UIApplicationSceneManifest;UIWindow 改由 scene delegate 创建,不再是 didFinishLaunchingWithOptions 里那一个(#46733/#46734)。Expo 把 URL、user activity、Handoff、quick action 等事件转发给 ExpoAppDelegate 的 overrides 和 subscribers,所以纯 config plugin / expo module 路子写的代码不受影响;受影响的三类人——手改过 AppDelegate.swift 的、完全手工管理 ios/ 目录的、直接覆写 app delegate 方法的——必读迁移指南 https://expo.fyi/ios-scene-lifecycle 。
3.3 Android 模板 release 默认开 R8
expo prebuild 生成的 android/gradle.properties 现在默认 android.enableMinifyInReleaseBuilds=true(#50108),用 proguard-android-optimize.txt 预设。收益是包体积与启动时间;风险是反射密集、靠 Class.forName/序列化找类的三方库在 release 包里运行时崩(debug 完全正常,最阴的那种)。需要 keep 规则就加进 android/app/proguard-rules.pro——但 CNG 项目记住这个文件本身要用 plugin 维护,或者先用 expo-build-properties 把 android.enableMinifyInReleaseBuilds 设回 false 止血(配置见 03-应用配置与插件.md)。升级 PR 里务必包含一次 release 构建的真机回归。
3.4 NODE_ENV 被 CLI 强制接管
SDK 58 起 Expo CLI 按命令把 NODE_ENV 覆盖为 development 或 production,外部传入的其他值一律丢弃(#48554/#48741):NODE_ENV=test npx expo start 现在加载的是 .env.development 而不是 .env.test;debug 构建走 development,其余一切(包括自定义的 Staging configuration)走 production;EAS Build 上 expo config、expo prebuild、expo-doctor 统一按 production(#48938/#48845)。如果你有依赖 NODE_ENV 自定义值来切环境变量的土办法,这次必须改成 .env.<mode> + APP_ENV 那类正经姿势(环境文件规则见 03-应用配置与插件.md)。
3.5 expo-router:导航核心重写 + Web 默认 server output
Router 与 SDK 同号(58.x.x,npm 实测 58.0.24)。这一代变化最多:
- 导航核心重写:导航状态与 route key 变为确定性、导航 dispatch 推迟到 commit 之后,并砍掉了一大块 fork 自 React Navigation 的 API 面。普通项目无感,用了自定义 navigator / 深度依赖内部行为的项目要过迁移指南。
withLayoutContext废弃,改用integrateWithRouter(#50337)。- Web 默认
output: "server":web.output未设置时按 server 处理(每请求渲染 HTML),不再是导出时预渲染;要保留 57 行为显式设"static"(详见10-Web与托管.md)。 - async routes 默认开启;data loaders 走标准 HTTP 缓存(
Cache-Control)。 @expo/ui、expo-symbols降为 optional peer dependencies。
对照清单:https://docs.expo.dev/router/migrate/sdk-57-to-58/ 。
3.6 API 级破坏变更(小但疼)
| 模块 | 变更 | 应对 |
|---|---|---|
| expo-file-system | File.write() 变异步返回 Promise(#45992) |
同步语义改 File.writeSync();调用点补 await |
| expo-sqlite | libSQL 支持整体移除:syncLibSQL()、libSQLOptions 删除(#48543) |
用过的迁标准 API;useLibSQL 插件属性成为 no-op |
| expo-notifications | 前台通知默认展示,除非你的 setNotificationHandler 另有决定;慢 handler 不再导致通知被丢弃(#49072) |
以前"前台收到但被我们吞掉"的行为会变,检查 handler 返回值 |
| @expo/fingerprint | 默认 preset 变为 balanced(#47668) |
版本号变更、node_modules 内文件变化不再触发新 fingerprint,OTA 的 native-version 判定更稳;要旧语义看 presets 文档(07-EAS发布与热更新.md) |
| RN API 移除 | InteractionManager(改用 requestIdleCallback)、Touchable 根导出、NativeMethods 类型、Modal.animated、StatusBar 的 backgroundColor/translucent/networkActivityIndicatorVisible;ImageBackground 废弃(用绝对定位 Image + View) |
全局 grep 一遍,多为 lint 可得 |
| expo login | 默认浏览器授权流(#46832) | 脚本用 --no-browser 或 --username/--password |
| expo serve | 拒绝伺服含 server 代码的 expo export --dev 产物(#50522) |
重新 export 不带 --dev |
| 工具链 | Node 要求 22 线 ≥22.13、24 线 ≥24.3、26+,奇数版本(23/25)不支持(#47202);AGP 9 兼容 | 升 SDK 前先 node -v |
另外:macOS 在 SDK 58 暂不支持,要等 react-native-macos 追上 RN 0.86+——macOS 目标的项目别升 58。顺带一句非破坏的行为变化:React Native 0.87/0.88 一次收进(58 从 0.86 直升 0.88),其中 npx react-native spm 只是 opt-in 预研,CocoaPods 仍是默认路径。
4. 官方新途径:expo-upgrade agent skill
SDK 58 changelog 的升级指引第一句就是推荐用 expo-upgrade skill 搭配 Claude Code(或同类 agent)执行升级。安装官方 Expo Skills 插件(Claude Code,2026-10-20 docs 实测命令):
claude plugin install expo@claude-plugins-official
# 或在 Claude Code 会话里:/plugin install expo@claude-plugins-official其他 agent 环境用 npx skills add expo/skills。skill 本体在 github.com/expo/skills(plugins/expo/skills/expo-upgrade),内容是第 2 节流程的可执行版:跑 install --fix、过 doctor、逐项修依赖错位。定位说清楚:这是新途径而非替代——agent 负责体力活(对齐版本、修 import),破坏性变更的业务判断(3.3 的 R8 keep 规则、3.5 的自定义 navigator)仍要你亲自验收 diff。
5. 版本对照与 Expo Go 时间窗
SDK 与 React Native 的对应关系(npm dist-tags + changelog 双重核验):
| Expo SDK | React Native | 备注 |
|---|---|---|
| 54 | 0.81 | |
| 55 | 0.83 | |
| 56 | 0.85 | |
| 57 | 0.86 | |
| 58 | 0.88(React 19.3.0) | 跳过 0.87 单列 SDK,0.87+0.88 变化一次收进 |
Expo Go 侧要记住三条(截至 2026-10-20 的时点口径,变化很快,动手前先看 05-Expo-Go与开发构建.md 的时效声明):
- 新版 Expo Go 只支持单一 SDK:商店里的"SDK 58 版"只认 58 项目;旧 SDK 项目装不了新 Expo Go。官方同时重申 Expo Go 是教学/起步工具,生产应用应迁 development build。
- 商店审核造成的时间窗:58 的 Expo Go 尚未上架(changelog 原话"within a few weeks”),这之前
npx create-expo-app默认创建的是 SDK 57 项目;想直接起 58,用npx create-expo-app --template default@sdk-58并配 development build。SDK 56 起 create-expo-app 会让你选"兼容商店版 Expo Go"还是"最新 SDK"。 - 需要指定版本的 Expo Go 时:Android 设备/模拟器与 iOS 模拟器可从 https://expo.dev/go 或
expo-goCLI 安装;被 App Store 审核卡住时,eas go可自建 Expo Go 走 TestFlight 分发(需 Apple 开发者会员)。
顺带把"要不要急着升"的答案也给了:EAS 服务对旧 SDK 的向后兼容窗口比 Expo Go 长得多,但不是永久——落后两个大版本以上,欠下的破坏性变更会按年复利。
6. expo-doctor 检查项清单
npx expo-doctor(独立包,2026-10-20 实测 latest 1.21.5)跑二十多项静态检查。以下清单取自 expo/expo 主仓库 packages/expo-doctor/src/checks/ 源码目录(2026-10-20),按用途分组:
依赖与版本矩阵(升级期最容易红的一组)
- 已装包与当前 SDK 矩阵是否匹配(
installed dependency versions)——核心项,--fix能修的它先报 - 依赖被
overrides/resolutions改写、peer dependencies 缺失、重复安装(autolinking duplicates) - 不该直接安装的包被显式装进依赖(如与 expo-router 共存的
@react-navigation/*包——有专门的 Router 检查项) - 不兼容的 support packages、expo/vector-icons、Hermes V1 回归涉及的 SDK 版本
- 本地装了旧全局 CLI 遗留物(
expo-cli出现在本地依赖里它都会报)
配置与工程
- package.json 常见问题、lockfile 缺失、
.env本地环境文件被提交进 git - app config schema 校验 + 常见问题检查;非 CNG 项目里 app config 字段与原生工程不同步(比如改了 bundleId 但 ios 工程里没动——这条对第 2.4 节非 CNG 分支尤其有用)
- Metro 配置问题、通用 project setup 问题
兼容性与发布
- 对 React Native Directory 校验三方库(默认启用,可在 package.json
reactNativeDirectoryCheck配置) - 应用商店提交版本要求(versionCode / buildNumber 是否够新)
- 包管理器与 Node/Xcode 等原生工具链版本(
native tooling versions)
常用开关:EXPO_DOCTOR_SKIP_DEPENDENCY_VERSION_CHECK=1 跳过版本矩阵检查(自管矩阵时);package.json 里的 reactNativeDirectoryCheck / appConfigFieldsNotSyncedCheck 两个字段可分别配置 Directory 校验与"配置-原生工程同步"检查;行为细节看 npx expo-doctor --help,也可以带路径参数体检不在 cwd 的项目。挂进 CI 的推荐组合(两条都用非零退出码说话,CI=1 保证它们不弹交互):
CI=1 npx expo install --check # 依赖矩阵错位即失败
CI=1 npx expo-doctor # 静态检查失败即失败升级 PR 里这两行变绿,第 2.2 节的手工确认就可以省了。
7. 常见升级故障排查
官方专门有一篇排查指南:Troubleshooting your SDK upgrade(https://expo.fyi/troubleshooting-sdk-upgrades )。下面按出现频率整理主干,外加两条官方 docs 的专项。
7.1 红屏 “React Native version mismatch”
升级后最高频的红屏,格式是 JavaScript version: X / Native version: Y。本质:终端里 Metro 用的 JS 版 react-native 和设备上装的原生壳不是同一张矩阵。处理顺序(docs.expo.dev/troubleshooting/react-native-version-mismatch 核实):
- 关掉所有残留的 dev server 进程(
ps -A | grep expo;多项目同开时 SDK 58 端口自动递增后,以前"两个 8081 打架"的问题会变成"App 连错服务器",逐个关干净); - app.json 里若写了
sdkVersion,删掉或改成与 package.json 的expo一致; npx expo-doctor看 react-native 应装哪个版本 →npx expo install --fix;- 重新出 development build——99% 的 mismatch 到此解决:JS 会跟着 dev server 走,原生壳不会自己变。
7.2 缓存三件套:Metro / node_modules / watchman
依赖大版本换动后 node_modules 可能处于包管理器自己都"不认识"的中间态。按代价从低到高:
npx expo start -c # 1. 只清 Metro 缓存,最便宜
watchman watch-del-all # 2. 文件监听索引(mac/Linux 与 Windows 均可,没装 watchman 跳过)
rm -rf node_modules && npm install # 3. 重装依赖(Windows PowerShell:Remove-Item -Path node_modules -Recurse -Force)Windows 的 haste-map / metro 缓存路径是 %localappdata%\Temp 下的 haste-map-* 与 metro-cache(docs 的 Windows 清缓存页核实);macOS/Linux 在 $TMPDIR。删 lockfile 重装是最后手段——会顺手动到所有无关依赖,monorepo 里尤其克制(官方原话)。
7.3 原生残留:prebuild –clean 与派生缓存
CNG 项目的"原生代码玄学错误"(Gradle 配置自相矛盾、podspec 陈旧、Xcode Build Phase 残留),先 npx expo prebuild --clean 全量再生成一次;本地编译问题再清 DerivedData / android/app/build,或直接用 npx expo run:ios --no-build-cache。排查时先想"是不是旧生成物在作祟",再怀疑新代码。
7.4 EAS 构建与本地不一致:lockfile
EAS 云端按仓库里的 lockfile 安装依赖:本地你删了 lockfile 重装碰巧修好的版本,云端根本拿不到——表现是"本地绿、CI 红"或"构建产物行为怪异"。规矩:本地动依赖后把 package.json + lockfile 一起提交,别单独提交其一;构建日志里核对实际安装的版本。EAS 侧构建错误的专项排查见 06-EAS构建.md 与 https://docs.expo.dev/build-reference/troubleshooting 。
7.5 三方库不兼容与"旧 workaround 反噬"
- 报错点名某个三方库时,去它的 repo 看 releases 和 issues——你的错误大概率是别人升 58 时踩过的;EAS 构建挂在 Gradle/pod 步骤且指向某库,多半是它还没适配 RN 0.88。
- 回头清理历史 workaround:为老 SDK 钉过的
kotlinVersion(expo-build-properties)、手写的 proguard 规则、expo.install.exclude白名单,升级后可能正是问题本身。官方排查文里"检查不再需要的旧 workaround"值得单独抄在 PR 描述里过一遍。 - 最小复现:
npx create-expo-app@latest起干净项目,只搬到能复现问题的最小量——复现不了说明是你自己代码/配置的锅,复现了带着它去提问(expo/expo issues 或对应库的 issues),提问写具体报错文本而不是"不工作了"。
7.6 错误在 JS 层还是原生层:先分诊再深挖
官方排查指南给的分诊办法——红屏/逻辑错走 JS 工具链:dev server 的 Terminal UI 里按 J 开 React Native DevTools(或 M 呼出设备端开发菜单进调试路径),在 “Unhandled exceptions” 上打断点,看报错前的调用栈与状态;升级期大量"旧 workaround 反噬"类问题在这一步现形。装了不崩、跑着崩走原生日志:Android 用 adb logcat,iOS 模拟器用 macOS 的 Console.app 盯原生崩溃输出——三方库没适配新 RN 的编译期/链接期错误,只有原生日志会告诉你真相(更系统的调试手法见 react-native/10-调试测试与性能.md)。
7.7 Expo Go 侧的版本错位
“Project is incompatible with this version of Expo Go” 红屏:项目 SDK ≠ 商店 Expo Go 支持的单一 SDK。按第 5 节的时间窗处理:要么项目退回/对齐商店版支持的 SDK,要么迁 development build(推荐,见 05-Expo-Go与开发构建.md);iOS 真机上还有"CLI 与 Expo Go 必须登录同一账号"的 2026-09 新政,症状与排查同见该章。
8. 升级 PR checklist(实战建议)
按这个清单核对一个 SDK 升级 PR,过了就敢合:
-
package.json的expo与所有 expo-/react-native- 版本对齐矩阵,npx expo install --check退出码为 0 -
npx expo-doctor全绿(CI 里留痕) - changelog “Notable breaking changes” 逐条比对,处理不了的记 issue 而不是默默跳过;Router 项目过完 sdk-57-to-58 迁移指南
- 全量
tsc通过,Strict TS API 的深导入/类型改动清零(别把 legacy opt-out 带过 58 这一代) - 有 Android release 需求:R8 开启下真机跑过核心链路,keep 规则补进 plugin 管理
- iOS:没手改过 SceneDelegate/AppDelegate;手改过则按迁移指南补齐并解释保留原因
-
.env加载在新 NODE_ENV 规则下行为验证过(dev/staging/prod 三档各一次) - CNG:
android/、ios/已删除重生成;非 CNG:upgrade helper diff 逐文件勾掉 - lockfile 与 package.json 同 commit;EAS 上出一版 development build 真机回归
- notifications/文件系统/sqlite 若在用:前台展示行为、
File.write调用点、libSQL 残留各扫一遍 - 回滚方案写明:旧 dev build 还能装、旧 OTA channel 不覆盖(
07-EAS发布与热更新.md)
最后一条心法:升级的恐惧来自欠账。每代 SDK 的破坏性变更在 changelog 里写得清清楚楚,按第 2 节的流水线走一次不超过一两天;拖三个大版本再升,就是把一两天换成一个月。