安装与快速开始
React Native(RN)让你用 React 和 TypeScript/JavaScript 编写 iOS 与 Android 原生应用:JSX 最终渲染成平台原生视图(UIView、android.view.View),而不是网页。本手册基线为 React Native 0.88(2026-10 发布)/ Expo SDK 58 / TypeScript-first,假设读者已经会写 React(前端向的 React 知识可回顾本知识库 react/ 手册),从零补齐移动端这一环。
这一章解决三件事:环境怎么搭、项目怎么起、第一个能跑的东西什么时候出来。架构原理放到 01-核心概念与架构.md,这里不展开。
手册全文各章:00 安装、01 架构、02 核心组件、03 样式与布局、04 导航、05 数据获取与网络、06 表单与交互、07 动画与手势、08 存储与文件、09 原生模块与平台代码、10 调试测试与性能、11 构建与发布、12 常见陷阱与最佳实践。
官方推荐的两条路:Expo 工作流 vs bare CLI
搭 RN 项目有两个入口,官方文档现在把 Expo 工作流放在推荐位,但 bare(裸 RN CLI)路径一直是第一等公民,两者产物都是真正的原生工程。
Expo 不只是"托管平台"
很多老教程对 Expo 的偏见来自早期"Expo 等于托管、加不了原生代码"的印象,这个印象早已过时。今天的 Expo 是一整套围绕 RN 的工具链和库集合:create-expo-app 脚手架、expo start 开发服务器、对相机/文件系统/传感器等原生能力的 TypeScript 封装(expo-modules-core,SDK 56 起用 Swift 重写)、以及可选的云服务(EAS 构建、Expo Updates、崩溃上报等——这些云服务是可选项,不用它们 Expo 照样开发)。
关键退路在于 Continuous Native Generation(CNG):npx expo prebuild 可以根据配置文件随时重新生成 ios/ 和 android/ 原生目录。也就是说你平时不碰原生工程,需要时它就在;旧教程里的 eject 基本可以忘掉,用 prebuild 就够了。想完全接管原生工程时,把生成出来的目录纳入版本管理,项目就等价于一个 bare 项目,Expo 的工具链继续为你服务。
bare 是什么
bare 指用 @react-native-community/cli 初始化项目,从第一天起就拿到完整的 ios/(Xcode 工程)和 android/(Gradle 工程),自己跑 pod install 和 Gradle。适合已有原生工程要混编 RN、需要深度定制构建、或团队已有成熟的 RN 运维经验的场景。值得注意的一点:RN 0.88 的官方模板本身就预装了 @react-native/new-app-screen、react-native-safe-area-context 这类核心团队的 npm 包——“官方模板"和"社区维护"的边界已经模糊,两条路径没有身份高低。
怎么选
| 维度 | Expo 工作流 | bare CLI |
|---|---|---|
| 上手成本 | 一条命令 + 扫码即跑,Expo Go 免编译 | 需本机配齐 Xcode/Android Studio 才能跑起来 |
| 原生工程 | 默认不生成(prebuild 按需生成) | 第一天就有,全手动 |
| 依赖版本 | npx expo install 按 SDK 对齐 |
自己保证 RN 与 React 版本配对 |
| 自定义原生模块 | 支持(development build / prebuild 后随便加) | 支持,无限制 |
| 升级 | 跟 Expo SDK 大版本走,工具自动对齐 | 逐版本跟 RN 官方 diff 手改 |
| 典型人群 | 新 app、全 JS 团队、要 OTA/云服务 | 存量 RN 项目、原生混编、深度定制构建 |
没有历史包袱的新项目,直接选 Expo。这不是偏好问题:npx expo start 让你不装 Xcode 就能在真机上看到第一屏,这个开发循环的启动速度是 bare 给不了的。
注意:选 Expo 不等于被锁死。prebuild 之后你就在 bare 世界上;反过来,bare 项目也没有"低人一等”,11 章的构建发布对两条路径都适用。
环境要求
先把硬件和工具链需求摆在桌面上,避免装到一半发现平台不满足。
Node.js
- RN 0.87/0.88 的 core engines 实测为
^22.13.0 || ^24.3.0 || >=26.0.0。0.87 起不再支持 Node 20(0.81~0.86 的口径是>=20.19.4)。 - Expo SDK 58 的工具链同口径:Node 22.13+ / 24.3+ / 26+,不支持 23、25 这类奇数号实验线。
- bare 模板 package.json 的 engines 写得略松(
>= 22.11.0),按核心引擎的最严格口径来即可,不要取模板下限。
实操建议:锁 22 LTS 的最新补丁版(22.13.0 及以上),用 volta 或 nvm 管理版本,理由见文末"实战建议"。
node --version # 确认 >= 22.13.0(或 24.3+)iOS 开发
编译 iOS 必须在 macOS 上,且需要:
- Xcode 16.1 或更高(RN 0.88 模板实测要求的最低 Xcode)
- CocoaPods(Ruby gem 或 Homebrew 安装均可)
- iOS 部署目标最低 15.1(RN 0.88 模板脚本实测值)
- 若尝试 0.87 起实验性的 Swift Package Manager 集成:0.88 的 SPM autolinking 会从工程的
IPHONEOS_DEPLOYMENT_TARGET推导下限,Expo 项目当前要求 16.4
Android 开发
Android 不需要 macOS,Windows/Linux/macOS 三平台都可用,需要:
- JDK 17 或更高(Gradle 工具链要求;注意是编译期要求,与产物支持的 Android 版本无关)
- Android Studio(自带 SDK Manager、模拟器;模板实测会拉 compileSdk 37 / targetSdk 36 / minSdk 24 / buildTools 37.0.0 / NDK 27.1.12297006,这些版本以模板生成的 build.gradle 为准,不用手工凑)
- 可选:物理安卓机 + USB 调试,比模拟器流畅得多
Windows 的现实
Windows 开发 Android 完全可用——create-expo-app、Metro、Android 模拟器、真机调试一条龙都没问题。但 Windows 无法编译 iOS,这是苹果许可限制,不是工具问题。在 Windows 上要跑 iOS 只有三条路:借一台 Mac(本地或云主机)、用 CI/云构建的 macOS runner(见 11-构建与发布.md),或者在 Expo 云端构建 iOS 产物。团队协作时常见配置是"全员 Windows/Linux 开发 Android,一台 Mac mini 或云端 macOS 负责 iOS 出包"。
路径 A:Expo 快速开始
创建项目
npx create-expo-app@latest MyApp
cd MyApp这会拿到对齐 Expo SDK 58 的默认模板:TypeScript、Expo Router(expo-router 版本已与 SDK 号对齐,58 的包是 58.0.x)、文件即路由的 app/ 目录。想看模板列表可加 --template 参数指定,默认模板即够用。
生成出来的目录长这样(此时还没有 ios/、android/,这是正常的):
MyApp/
├── app/ # expo-router:一个文件 = 一个路由
│ ├── _layout.tsx # 根布局(Stack/Tabs 在这里声明)
│ ├── index.tsx # 路由 "/",默认导出一个组件就是首屏
│ └── modal.tsx # 例:注册进 Stack 的模态页
├── assets/ # 图片、图标、字体等静态资源
├── node_modules/
├── .vscode/ # 推荐的编辑器设置(设置里可一键应用)
├── app.json # Expo 配置:应用名、图标、版本、权限声明
├── package.json # main 指向 "expo-router/entry"
├── tsconfig.json # extends "expo/tsconfig.base"
└── eslint.config.jsapp.json 是 JSON 静态配置;需要按环境变量、按平台动态生成配置时,改名/追加一个 app.config.ts:
// app.config.ts —— 配置即代码,CI 里按环境注入
import { ExpoConfig, ConfigContext } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: process.env.APP_ENV === 'staging' ? 'MyApp Staging' : 'MyApp',
ios: {
bundleIdentifier:
process.env.APP_ENV === 'staging'
? 'com.example.myapp.staging'
: 'com.example.myapp',
},
android: {
package:
process.env.APP_ENV === 'staging'
? 'com.example.myapp.staging'
: 'com.example.myapp',
},
});app.json 里声明的权限、图标、版本号这些字段,最终会被 prebuild 写进两个原生工程——这就是"配置驱动原生工程"的含义,也是 Expo 项目平时不需要碰 ios/、android/ 的底气。配置字段全表见 11-构建与发布.md。
启动开发服务器
npx expo start终端会打印一个二维码。三种打开方式:
- Expo Go:iOS/Android 应用商店装 Expo Go,扫码即可在真机看到应用;
- 模拟器:Android 模拟器已开的话按终端提示
a,iOS 模拟器需要 macOS + Xcode; - 物理设备连开发服务器:Expo Go 模式下手机和电脑在同一局域网即可。
Expo Go 的边界
Expo Go 是一个预装了当前 SDK 全部官方原生模块的"壳 App",方便到不用编译,但边界要清楚:
- Expo Go 只跟进最新 SDK。SDK 54 之前创建的旧项目,其 Expo Go 已失效,这类项目要用 development build 自测(或者直接升 SDK)。
- 不能加自定义原生模块。Expo Go 里没有你指定的第三方原生库,也没有需要修改原生配置的库;用到这类能力时,Expo Go 会提示你转向 development build。
- 它是开发期工具,不能拿 Expo Go 当分发渠道。
development build:Expo Go 装不下时的正确姿势
当你要加需要编译原生的库(比如某些 SDK 类依赖),或者 Expo Go 版本对不上时,生成一个自带原生工程、预装你全部依赖的开发用 App:
# 生成 ios/ 与 android/ 原生目录(CNG 的核心命令)
npx expo prebuild -p android # 只生成 Android;不带 -p 则两个都生成
npx expo run:android # 编译并安装 development build 到设备/模拟器
npx expo run:ios # 需要 macOS + Xcode 16.1+prebuild 生成的目录默认进 .gitignore,每次能从配置重新生成;如果项目里有大量手工原生改动,把生成目录纳入 git 并提交即可退化为 bare 模式。后续 npx expo start 时连 development build 而不是 Expo Go,日常开发体验不变。
真机上的注意事项(Expo Go 和 development build 通用):
- Android:手机开启开发者选项与 USB 调试,
adb devices能看到设备即可直接装;无线调试各版本入口不一,新手先用 USB。 - iOS:development build 装到物理 iPhone 需要签名——Xcode 里给工程配一个个人 Apple ID 即可免费跑调试机(受限证书 7 天过期);上架与企业分发走
11-构建与发布.md。 - development build 和 Expo Go 可以共存,它们是不同的 App;
npx expo start起来后二维码对两者都有效,终端会显示当前连接的是哪一种。
注意:
expo install装的第三方原生模块需要重新编译 development build 才生效,改完依赖记得重跑run:android/run:ios(或让 Cloud 构建)。
路径 B:bare 快速开始
创建项目
npx @react-native-community/cli@latest init MyApp
cd MyApp当前 CLI 版本为 20.x,会落到 0.88 模板:预装 react 19.3.0、react-native 0.88.0、@react-native/new-app-screen(首次启动的欢迎页,跑通环境用,之后删掉)、react-native-safe-area-context;devDependencies 带 @react-native/babel-preset、@react-native/metro-config、@react-native/typescript-config、@react-native/eslint-config、@react-native/jest-preset(均为 0.88)、typescript ^6.0.3、jest ^29、eslint ^8。其中 @react-native/jest-preset 是 0.85 起从 RN 主包拆出来独立发布的——升级旧项目时别忘了把测试配置一起迁移。
bare 工程从第一天起就是两个完整的原生项目:
MyApp/
├── index.js # JS 入口:AppRegistry.registerComponent 在这里把 App 挂上
├── App.tsx # 组件树根,默认由 @react-native/new-app-screen 提供欢迎页
├── android/
│ ├── build.gradle # compileSdk 37 / minSdk 24 / Kotlin 2.2.0 / AGP 9(模板实测值)
│ ├── gradle.properties # newArchEnabled / hermesEnabled / edgeToEdgeEnabled 均为 true
│ ├── gradlew(.bat) # Gradle wrapper 9.4.1
│ └── app/ # 应用模块:MainApplication、MainActivity、AndroidManifest.xml
├── ios/
│ ├── Podfile # platform :ios, min_ios_version_supported(0.88 实测下限 15.1)
│ ├── MyApp/ # AppDelegate.swift(0.77 起模板为 Swift)、Info.plist
│ └── MyApp.xcodeproj # pod install 后请用生成的 .xcworkspace 打开
├── metro.config.js # require('@react-native/metro-config')
├── babel.config.js # presets: ['@react-native/babel-preset']
├── tsconfig.json # extends '@react-native/typescript-config'
└── Gemfile # 固定 CocoaPods 版本,保证团队 pod 一致环境出问题时先跑 npx react-native info(Expo 项目对应 npx expo info),把输出版本号、Node/Cocoapods/JDK 一栏贴进 issue 或搜索框,比盲猜省一小时。
iOS(仅 macOS)
cd ios
bundle exec pod install # 模板带 Gemfile 时用这句;没有 Gemfile 就直接 pod install
cd ..
npm run ios # 模板 scripts,等价于 react-native run-ios;或手动用 Xcode 打开 MyApp.xcworkspace 后点运行一个对老 RN 开发者影响很大的变化:自 0.84 起 iOS 默认使用预编译的 RN 框架二进制(React 核心与 Hermes 都不再从源码编译),pod install 的时间从"泡杯咖啡"降到几十秒量级。
Android
npm run android # 底层就是 Gradle;也可用 Android Studio 打开 android/ 目录同步后点运行模板实测关键配置(生成后以文件为准):minSdk 24、targetSdk 36、compileSdk 37、NDK 27.1.12297006、Kotlin 2.2.0、Gradle wrapper 9.4.1、AGP 9。gradle.properties 里 newArchEnabled=true、hermesEnabled=true、edgeToEdgeEnabled=true 是默认值——新架构自 0.76 起默认开启、0.82 起旧架构彻底移除,这些开关没有"关掉回到旧世界"的选项了,edge-to-edge 开关也只为过渡保留。
在 Windows 上跑 bare Android
和 Expo 路径一样,Android Studio + JDK 17 就绪即可,npm run android 直接出包到模拟器/真机。iOS 目录在 Windows 上只是躺在仓库里编译不了,不影响 Android 开发。
依赖安装:expo install vs npm i
这是两条路径最容易混用出错的地方。
Expo 项目一律用 npx expo install 装依赖:
npx expo install expo-camera react-native-maps它的作用不只是装包:Expo 维护着"每个 SDK 版本对应的包版本矩阵",expo install 会按当前 SDK 自动挑选兼容版本,纯 JS 库也会顺带把 peer 依赖对齐。事后可以用体检命令核对版本是否偏离矩阵:
npx expo install --check # 列出与当前 SDK 不匹配的依赖
npx expo-doctor # 更全面的配置/依赖体检bare 项目用 npm i(或 yarn/pnpm),但你要自己盯住 RN 与 React 的版本配对——0.88 必须配 React 19.3.0,错一个版本就可能出现各种诡异运行时崩溃(对照表在 01-核心概念与架构.md 的版本一节)。第三方原生模块的版本也要按它自己的 README 对 RN 版本的要求来选。
注意:在 Expo 项目里养成习惯用
npm i装react-native-*开头的包,十次有八次会装上与 SDK 不匹配的版本。混用不是禁止,但装完必须--check。
第一个可运行组件
下面这个计数器不依赖任何第三方库,两条路径都能直接跑。bare 项目替换 App.tsx(@react-native/new-app-screen 的欢迎页就是被它替换掉的对象);Expo 默认模板放进 app/index.tsx(保持默认导出即可,路由部分见 04-导航.md)。
import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
export default function CounterApp() {
const [count, setCount] = useState(0);
return (
<View style={styles.container}>
<Text style={styles.title}>Hello, React Native 0.88</Text>
<Text style={styles.subtitle}>这是渲染出的原生 Text 视图,不是网页</Text>
<Pressable
onPress={() => setCount((c) => c + 1)}
accessibilityRole="button"
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}>
<Text style={styles.buttonText}>点我 +1,当前:{count}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0b1220',
padding: 24,
},
title: { color: '#e2e8f0', fontSize: 24, fontWeight: '700' },
subtitle: { color: '#94a3b8', fontSize: 14, marginTop: 8 },
button: {
marginTop: 32,
backgroundColor: '#2563eb',
borderRadius: 12,
paddingHorizontal: 24,
paddingVertical: 14,
},
buttonPressed: { opacity: 0.7, transform: [{ scale: 0.97 }] },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});和 Web React 的区别一眼就能感受到:没有 div/span,只有 View/Text/Pressable;样式写在 StyleSheet.create 里,布局是 Yoga 提供的受限 Flexbox(没有 display: grid,flexDirection 默认 column)。组件全表见 02-核心组件.md,样式心智见 03-样式与布局.md。
跑起来验证三件事:改代码后 Metro Fast Refresh 立即生效;按 d 能唤起 React Native DevTools(0.76 起的标准调试器);删掉 App 能正常卸载——Expo Go 的话直接退出即可。
项目结构对照
两条路径的目录长得像,但职责差别不小,这张表值得存着:
| 文件/目录 | bare(0.88 模板) | Expo(SDK 58) | 说明 |
|---|---|---|---|
App.tsx / index.ts |
组件树入口 | 默认模板由 expo-router 接管入口 | Expo 默认模板没有 App.tsx,路由入口在 app/ |
app/ |
无 | 核心目录:文件即路由 | _layout.tsx 定义嵌套与布局,页面按路径命名 |
app.json / app.config.ts |
无 | Expo 配置(应用名、图标、权限、构建参数) | .config.ts 变体允许用代码动态生成配置 |
index.js + registerComponent |
有 | 通常没有 | bare 的注册入口;Expo 由 expo-router/entry 替代 |
ios/ |
Xcode 工程,手工维护 | 默认不生成,prebuild 产出 | 生成后提交进 git 即转 bare 模式 |
android/ |
Gradle 工程,手工维护 | 同上 | 模板里的 newArch/hermes/edgeToEdge 开关在此 |
babel.config.js |
引用 @react-native/babel-preset |
babel-preset-expo(内部包着 RN preset) |
一般不动它 |
metro.config.js |
引用 @react-native/metro-config |
@expo/metro-config 包装 |
改打包行为的地方,原理见 01-核心概念与架构.md Metro 一节 |
react-native.config.js |
可选 | 少见 | RN 依赖自动链接的平台级配置 |
tsconfig.json |
extends @react-native/typescript-config |
extends expo/tsconfig.base |
0.87 起 Strict TS API 默认,深导入 react-native/Libraries/* 会报类型错误 |
Gemfile |
锁 CocoaPods 版本 | 可选(prebuild 后才有意义) | iOS 构建环境用 bundler 固定更稳 |
.expo/ |
无 | 开发服务器状态缓存 | 不要提交 |
常见安装报错与排查思路
安装期报错大多集中在四类,逐个给排查路径。
Watchman(macOS/Linux 文件监听)
症状:npx expo start 或 Metro 启动时报 Watchman 相关错误,或改文件后 Fast Refresh 不生效。
- macOS:
brew install watchman,装完watchman watch-del-all清掉旧监听再重启 Metro。 - Linux(含 Docker 跑 RN):inotify 句柄不足时报错,需要调大
fs.inotify.max_user_watches;容器里 Watchman 经常不可用,直接禁用(设WATCHMAN_DISABLE相关环境变量走轮询监听)或改用物理机开发。 - Windows 不需要 Watchman,此错误通常不会遇到;遇到多半是从别处复制的脚本带了 Linux 路径。
- 万能三板斧:重启 Metro、
watchman watch-del-all、清 Metro 缓存(npx expo start -c/ bare 的npx react-native start --reset-cache)。
CocoaPods 与 Ruby
症状:pod install 失败,常见三类原因。
- Ruby 版本混乱:系统 Ruby 与 gem 装的 CocoaPods 版本打架。用
brew install cocoapods或 rbenv 固定 Ruby 后在干净环境里gem install cocoapods,模板带 Gemfile 时全程bundle exec pod install。 - Pod 仓库陈旧:
pod repo update后重试;RN 的 podspec 依赖本地模板目录,务必在ios/里执行而不是项目根。 - Xcode 太旧:RN 0.88 实测要求 Xcode 16.1+,App Store 命令行工具路径不对时用
xcode-select -p检查,必要时sudo xcode-select -s /Applications/Xcode.app/Contents/Developer。
Android SDK 许可证与 JDK
症状:Gradle 报 “Failed to install the following Android SDK packages as some licences have not been accepted”,或 JDK 版本错误。
- 接受许可:
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses(Windows 在 SDK 目录的cmdline-tools\latest\bin\sdkmanager.bat),或在 Android Studio 的 SDK Manager 里点 Accept。 - SDK 路径没被发现时检查环境变量
ANDROID_HOME,或android/local.properties里的sdk.dir(后者别提交进 git,每台机器不同)。 - JDK 必须是 17+:报 “Unsupported class file major version” 基本就是 Gradle 在用旧 JDK,检查
JAVA_HOME和 Android Studio 设置里的 JDK 一致性。模板 pin 了 NDK 27.1.12297006,SDK Manager 里缺了它就去装对应版本,不要手动改模板值去凑。
Node 版本不匹配与 react/react-native 配对错误
症状分两类,都很好认:
- 构建期:CLI 报 engines 相关警告或直接语法报错,常见于 0.87+ 项目还在用 Node 20(0.87 起 Node 20 出局),或用了 23/25 这类 Expo 58 明确不支持的奇数线。
node --version确认,用 volta/nvm 切到 22.13+ 或 24.3+。 - 运行期:App 启动即崩、
Invalid hook call、白屏加 invariant 报错——十有八九是 react 与 react-native 版本没配对(0.88 必须配 react 19.3.0,完整映射在01-核心概念与架构.md)。Expo 项目先跑npx expo-doctor和npx expo install --check,按它给的版本修;bare 项目手工核对两个包的精确版本(建议不带^,用精确锁定,见下文版本策略)。
Metro 端口与调试连接
症状:Port 8081 is already in use,或真机连不上开发服务器。
- 端口占用:macOS/Linux 用
lsof -i :8081找进程,Windows 用netstat -ano | findstr :8081;多项目并行时给第二个指定--port 8082。 - 真机连不上:Expo Go 模式下手机与电脑必须同网段(公司 WiFi 隔离客户端时,用
npx expo start --tunnel走隧道;SDK 58 的 CLI 隧道比之前更快)。物理安卓设备也可用adb reverse tcp:8081 tcp:8081把设备端口直接回环到本机 Metro,最稳。 - iOS 模拟器连不到:模拟器网络一般直通宿主 8081,若失败先确认
xcode-select路径与 Xcode 已完整同意许可协议(sudo xcodebuild -license accept)。
装机后自检清单
装完环境别急着开项目,花两分钟把工具链验一遍,能把后面一半的"玄学报错"掐死在源头:
node --version # >= 22.13.0(或 24.3.0+;奇数号大版本慎用)
npm --version # 随 Node 即可
watchman --version # 仅 macOS/Linux;Windows 跳过
java -version # Android 需要 17+
adb --version # 能跑说明 platform-tools 在 PATH 里
xcodebuild -version # 仅 macOS;RN 0.88 要求 Xcode 16.1+
pod --version # 仅 macOS;配 iOS 时需要每条命令的输出贴进团队 wiki 留档,新人入职直接对照自己的输出,差异点就是问题点。再跑一次 npx create-expo-app@latest ProbeApp && cd ProbeApp && npx expo start 用 Expo Go 扫码看首屏——这条路径不依赖 Xcode/Android Studio,是验证 Node 与网络配置正确性的最快方式(国内网络给 npm 配镜像代理的经验同样适用于 Expo CLI)。
实战建议
新项目直接上 Expo 工作流。 官方推荐、SDK 版本矩阵帮你挡掉大部分依赖地狱、development build 保留了全部原生退路。从 Expo 开始不会限制你走到多远——真要手工接管原生工程时 prebuild 一下,产物就是 bare。
存量 bare 项目不用慌,更不值得为此重写。 bare 是官方维护的第一等路径,新架构默认(0.76+)后两条路径的运行时已经完全一致;升级只是节奏问题,参照 01 章的版本映射表按 RN 版本走即可。
用 volta 或 nvm 锁 Node 版本。 RN 工具链对 Node 版本敏感(0.87 起 Node 20 直接出局、23/25 奇数线 Expo 不支持),团队里"我这能跑你那不能跑"的根因一半在这。Volta 会把版本要求写进 package.json,对 CI 和新人最友好:
{
"volta": {
"node": "22.13.0"
}
}用 nvm 的话在仓库根放 .nvmrc,内容就是 22.13.0 这样的确切版本号,配合 shell 自动切换;只写 22 这种大版本等于没锁——补丁版之间的工具链行为也可能有差异。
给 CI 的最低配置提醒: iOS job 必须在 macOS runner 且镜像里带 Xcode 16.1+、CocoaPods(用 Gemfile/bundler 固定);Android job 用 JDK 17 + 预装模板同款 NDK 27.1.12297006 的镜像,跑一次 sdkmanager --licenses 后缓存 $ANDROID_HOME 与 ~/.gradle;全平台统一 Node ≥ 22.13.0。Expo 项目可以用 EAS 云构建把"没有 Mac"的问题整体外包(见 11-构建与发布.md)。CI 里永远用 npx expo run:android 的等价 Gradle 命令或显式脚本,不要依赖交互式终端提示。