Skip to content
Expo 最佳实践与陷阱

Expo 最佳实践与陷阱

把全手册散落的"顺带一提"收拢成一张网。这一章假设你已经知道每个功能怎么用(各功能章),这里只回答两个问题:哪里会出事,以及生产项目应该养成什么习惯。所有条目截至 Expo SDK 58 / 2026-10-20,来源为官方 changelog 正文与文档实测。

陷阱汇总表

环境与 Expo Go(详见 00 / 05 章)

陷阱 后果 正确做法
把 Expo Go 当生产开发环境 未内置的原生库、图标/splash/App Links、远程推送都不在能力范围内 官方已多次明说 Expo Go 是教学工具,起步后即迁 development build
以为 npx create-expo-app 装的是最新 SDK 2026-10-20 时点商店 Expo Go 58 未上架,默认创建的是 SDK 57 项目 要 SDK 58 用 npx create-expo-app --template default@sdk-58,并配 dev build(见 00 章)
升级商店 Expo Go 后连不上旧项目 新版 Expo Go 只支持单一 SDK 项目 SDK 与 Expo Go 版本对齐;过渡期用模拟器版或 eas go 自建(见 05 章)
Expo Go 扫 QR 无反应 2026-09 起强制登录:终端与 App 需同一账号 npx expo login + App 内登录;模拟器版与 dev build 不受影响(见 05 章)
用 Expo Go 加载他人 EAS Update / 自托管 HBC 2026-05-12 起仅能加载本组织更新;Hermes 字节码只来自 EAS Update 评审分发走 TestFlight/Play 轨道或 dev build;自托管更新提供纯 JS bundle(见 05 章)
全局安装 expo-cli 已废弃,行为和本地 CLI 不一致 一律 npx expo(见 02 章)
Node 用奇数版本或 22.x 过旧 SDK 58 工具链要求 22 线 ≥22.13、24 线 ≥24.3、26+;23/25 不支持 用 nvm/volta 固定受支持版本(见 00 章)

依赖与升级(详见 02 / 11 章)

陷阱 后果 正确做法
npm i expo-xxx 裸装包 装到与 SDK 不匹配的版本,红屏 “React Native version mismatch” 一律 npx expo install;事后修 npx expo install --fix
升级只 bump expo 版本号 expo-* 全家、react-native、router 版本错位 按官方流水线:expo@^58 → --fix → expo-doctor(见 11 章)
升级后不重建 dev build 旧原生壳 + 新 JS 运行时,行为诡异或直接崩 每次 SDK 升级都重新构建 dev client(见 05/11 章)
认为"没有报错=升级完成" 错过 Strict TS、SceneDelegate、R8 等静默破坏项 逐条对照 changelog 的 Deprecations/Breaking(见 11 章)
手工编辑 android/、ios/ 后继续用 prebuild 下次 --clean 全部蒸发 原生定制只有两个归宿:config plugin 或 Expo Modules/inline module(见 03/09 章)

配置(详见 03 章)

陷阱 后果 正确做法
把密钥写进 extra 或 EXPO_PUBLIC_* 二者都会进 bundle,反编译即泄露 构建期机密用 EAS secrets;运行时密钥永远在服务器侧
SDK 58 后仍用 NODE_ENV=test npx expo start 期望加载 .env.test CLI 强制按命令改写 NODE_ENV,实际加载 .env.development .env 按 development/production 组织;区分环境用 scheme/profile 参数(见 03 章)
改了 plugins/icon/权限字段就以为生效 这些只在 prebuild/构建期消费 本地验证跑 npx expo config --type introspect;native 生效需重建(见 03 章)

Expo Router(详见 04 章)

陷阱 后果 正确做法
升级 58 后沿用 withLayoutContext / expo-router/react-navigation 深导入 前者已弃用,后者大部分被移除 用 integrateWithRouter;自定义导航器按迁移指南改写(见 04 章)
Web 部署升级 58 后行为突变 58 起 web.output 未设置默认 server,按请求渲染而非导出预渲染 想要旧静态行为:显式 static 并配 expo-router 插件 apiRoutes 选项(见 04/10 章)
Web 上依赖 safe-area-context 的 insets 58 起 Router 不再在 web 提供 CSS 用 env(safe-area-inset-*)(见 04/10 章)
useLocalSearchParams 结果直接当 number 用 值类型是 `string string[]`,数组下标漏防会 NaN

构建与发布(详见 06/07 章)

陷阱 后果 正确做法
以为 release 崩了是代码问题查半天 SDK 58 Android 模板默认开 R8,反射依赖被裁剪 排查反射库;加 ProGuard keep 规则,或 expo-build-properties 关 android.enableMinifyInReleaseBuilds(见 06 章)
runtimeVersion 策略随手配 OTA 推给原生不兼容的构建 → 白屏无回退 理解 appVersion/fingerprint 两策边界后再 eas update:configure;58 默认 balanced 指纹只是减少误报,不替你定策略(见 07 章)
以为 EAS Update 什么都能热更 只覆盖 JS 与静态资源 任何原生变化(含新增原生库、改权限)必须走商店版本(见 07 章)
eas update 的 --environment 与产出 bundle 的构建不一致 OTA 内联的 EXPO_PUBLIC_* 值和二进制里配的环境对不上 SDK 55+ 该旗标必传;构建与更新走同一 EAS 环境变量集(见 06/07 章)
不设 buildNumber 自增 提审同号被拒/覆盖 eas.json 配 autoIncrement(见 06/07 章)
在文档/脚本里写死 EAS 额度价格 价格时效数据立刻过期 以 expo.dev/pricing 为准,流程上留出"额度检查"步骤(见 06 章)

官方模块(详见 08 章)

陷阱 后果 正确做法
SDK 58 里沿用 File.write() 当同步用 58 起变异步,不 await 可能读到写一半/丢内容 需同步语义用 File.writeSync()(见 08 章)
File.md5 继续用 58 已弃用 File.digest() 支持 MD5/SHA-1/256/384/512(见 08 章)
expo-sqlite 仍开 libSQL 相关配置 58 彻底移除:syncLibSQL()/libSQLOptions 失效,插件 useLibSQL 无效 纯 SQLite 路径重写;确需 libSQL 要自行带第三方方案(见 08 章)
按旧认知给通知写 handler"必须快否则被丢" 58 起前台通知默认展示(除非 handler 否决),慢 handler 不再丢消息 重读 setNotificationHandler 语义,别保留过时的"秒回"技巧(见 08 章)
iOS 相册视频 URI 升级后"变了" 58 起 Asset.getUri()/AssetInfo.uri 解析 Photos 当前版本 要原始文件传 version: AssetUriVersion.ORIGINAL(见 08 章)
iOS 应用一直挂着 location background mode 只为 geofencing 无实际使用场景会被 App Store 2.5.4 拒 58 起 geofencing 不再要求该 background mode,删掉闲置声明(见 08 章)
依赖方向锁横竖屏 iOS 27 应用可 resize:requireFullScreen 不再豁免、lockAsync 可能无效 按自适应布局设计,把方向锁当提示而非保证(见 08 章)
在 SDK 58 上规划 macOS 目标 官方明确暂不支持,等 react-native-macos 跟上 RN 0.86+ macOS 需求先钉在旧 SDK 或 bare(见 10 章)
新项目还用 expo-av 已被拆分为 expo-audio / expo-video 替代 音视频分别上 expo-audio、expo-video(见 08 章)

两个值得单独讲的风险模式

热更新是双刃剑

EAS Update 的事故几乎都同构:推了一个当前原生运行时跑不了的 JS。防线只有三层,缺一层都要出事。第一层是 runtimeVersion 策略——它回答"哪些原生构建可以接收这批更新",策略要么保守(appVersion,跟着商店版本号走)要么精确(fingerprint,跟着原生内容走);SDK 58 把指纹默认改成 balanced preset,更新 version、动 node_modules 文件不再触发指纹变化,减少的是"该收更新却收不到"的误伤,不是给你乱配兜底。第二层是渐进发布:按百分比放量,配合 Expo Observe 的崩溃率对比再放全量。第三层是回滚预案:出问题先回滚(channel 指回上一版),而不是现场 hotfix。另记一条 58 的配套:updates.maxUpdatesToKeep 可调大本地保留数,频繁切 channel(channel surfing)的团队不必反复重下相同更新。

把 Expo Go 当分发渠道

Expo Go 的定位在 2026 年已经收窄得很明确:登录要求、组织限制、HBC 限制、单 SDK。任何"让客户在 Expo Go 里看我们的 demo/评审我们的 App"的流程都建在沙地上。评审分发用官方列出的三条正路:TestFlight / Play 内部轨道、EAS Build 的 internal distribution(或企业签方案)、dev build + EAS Update。

最佳实践清单

项目与依赖

  • 仓库里不放 android/、ios/(用 CNG 的团队),把它们当构建产物;用 config plugin + Expo Modules 承载全部原生定制。
  • expo、expo-router、expo-*、react-native、React 的版本永远用 npx expo install 取得,CI 里跑 npx expo-doctor 当 lint 用。
  • lockfile 提交并保证本地/EAS 用同一包管理器与版本,避免"本地能构建,云端解析漂移"。

配置

  • app.config.ts 做动态层(品牌/环境/bundle id 按变量切换),app.json 只留静态公共部分,或者干脆全部 ts 化。
  • 权限描述、scheme、bundleIdentifier/package 在项目初始化就定死——它们进商店后改动成本高。
  • 每次改配置先 npx expo config --type introspect 看合并结果,再决定要不要 prebuild。

Router

  • 目录结构一次设计到位:(tabs) + (auth) 分组、+not-found、layout 级错误边界(screenErrorBoundary)都配上。
  • 数据获取优先 loader + 平台 HTTP 缓存(loader 里设 Cache-Control),客户端状态管理再叠加 TanStack Query(见 tanstack-query/)。
  • 深链即一等公民:每个对外入口都在真机上用 launch URL 实测过一遍,别只测应用内跳转。

构建发布

  • eas.json 三档 profile(development/preview/production)职责固定:development 出 dev client,preview 内部分发可热更,production 只对接商店。
  • iOS/Android 凭证用 EAS 托管起步,团队变大后再接 App Store Connect API Key / Play Service Account;eas credentials 定期盘点。
  • release 构建先在真机跑一轮冒烟(R8、ProGuard、原生混淆问题 debug 包暴露不出来)。

热更新

  • runtimeVersion 策略写进文档并评审,新人不许改。
  • 更新先发 preview/staging channel,观察 Observe 指标再进 production channel,带百分比放量。
  • OTA 与商店版本同仓管理:发原生版本时给该 runtimeVersion 的更新"封口"。

可观测性

  • SDK 58 起直接上 Expo Observe(AppMetrics 已弃用),错误自动记录可按需用 errorHandlingEnabled 关掉再接自建通道;Observe.clientId 与自家埋点对齐。
  • 需要产品分析用官方 PostHog 集成(eas integrations:posthog:connect),事件自动带 eas/update_id、eas/channel、eas/runtime_version,排查"哪个更新引入的回归"极快。

上线检查清单

原生版本发布:

  1. npx expo-doctor 全绿;expo install --fix 无待处理差异
  2. changelog 破坏性项逐条核对记录在升级 PR(尤其 Strict TS、SceneDelegate、R8、NODE_ENV)
  3. Android release 真机冒烟(R8 开启态);反射依赖的 keep 规则已验证
  4. iOS 27 目标:scene lifecycle 迁移自检(自定义过 AppDelegate 的必看);方向锁/全屏豁免不再依赖
  5. 权限文案、图标/splash、scheme 与商店条目一致
  6. buildNumber 自增生效、提审元数据同步
  7. 热更新窗口规划:本次原生版本的 runtimeVersion 边界、channel 状态

热更新发布:

  1. runtimeVersion 与目标原生构建匹配复核
  2. 先 preview/staging channel 灰度,Observe 崩溃率/启动指标对比基线
  3. production channel 百分比放量,留观察窗口
  4. 回滚路径演练可用(channel 指回上一 update)
  5. 更新内容不含任何原生/配置变更(有则禁止走 OTA)

调试速查表

症状 先查什么 详见
红屏 “React Native version mismatch” npx expo install --fix 的候选差异 11 章
QR 扫了不加载 终端与 Expo Go 是否同账号登录(57+iOS 起强制) 05 章
加了第三方原生库就崩 还在用 Expo Go?切 dev build;autolinking 日志 05/09 章
debug 正常 release 崩(Android) R8/ProGuard keep 规则 06 章
推送更新后部分用户白屏 runtimeVersion 策略与目标构建是否匹配;先回滚 07 章
prebuild 生成的工程和预期不符 npx expo config --type introspect + 插件执行顺序 03 章
深链冷启动没到目标页 launch URL 的 __expo_ 保留参数与 scheme:// 前缀;Router 对 URL 的解析 04/05 章
Web 导出后页面空白/路由 404 58 默认 server output 与预期 static 不符;apiRoutes 选项 04/10 章
改 app.json 的原生字段没生效 该字段只在构建期消费:重建 dev build 或 prebuild 03 章
升级后 Metro 缓存诡异旧行为 npx expo start --clear + EXPO_PUBLIC_* 在 58 已正确失效缓存 02/11 章

实战建议

用一句话概括这本手册的立场:Expo 的信任成本在 2026 年主要花在"理解平台边界",而不是"能不能做出功能"。功能层面(模块、Router、原生扩展)官方文档已经很好;出事的都是边界认知——Expo Go 是什么、OTA 能覆盖什么、runtimeVersion 承诺了什么、prebuild 什么时候会抹掉你的手工改动。团队落地时建议把这一章打印级对待:新人上手第一天读"环境与 Expo Go"表,第一次发版前过一遍"上线检查清单",升级 SDK 时把陷阱表里带"SDK 58"字样的行当作回归测试项。其余各章在需要时回查即可。