Skip to content
升级与诊断

升级与诊断

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 升级到底动了什么(官方排查指南把范围讲得很清楚,先建立预期):

  1. expo 包及其依赖的核心行为变化;
  2. 一批 expo-* 官方模块换版本(expo-updates、expo-camera、expo-dev-client……);
  3. 其他关键配套包换版本(react-native-reanimated、react-native-screens,用 Router 时还有它内嵌的 React Navigation 层);
  4. 一次 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 的时效声明):

  1. 新版 Expo Go 只支持单一 SDK:商店里的"SDK 58 版"只认 58 项目;旧 SDK 项目装不了新 Expo Go。官方同时重申 Expo Go 是教学/起步工具,生产应用应迁 development build。
  2. 商店审核造成的时间窗: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"。
  3. 需要指定版本的 Expo Go 时:Android 设备/模拟器与 iOS 模拟器可从 https://expo.dev/go 或 expo-go CLI 安装;被 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 核实):

  1. 关掉所有残留的 dev server 进程(ps -A | grep expo;多项目同开时 SDK 58 端口自动递增后,以前"两个 8081 打架"的问题会变成"App 连错服务器",逐个关干净);
  2. app.json 里若写了 sdkVersion,删掉或改成与 package.json 的 expo 一致;
  3. npx expo-doctor 看 react-native 应装哪个版本 → npx expo install --fix;
  4. 重新出 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 节的流水线走一次不超过一两天;拖三个大版本再升,就是把一两天换成一个月。