Skip to content
安装与快速开始

安装与快速开始

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.js

app.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

终端会打印一个二维码。三种打开方式:

  1. Expo Go:iOS/Android 应用商店装 Expo Go,扫码即可在真机看到应用;
  2. 模拟器:Android 模拟器已开的话按终端提示 a,iOS 模拟器需要 macOS + Xcode;
  3. 物理设备连开发服务器: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 命令或显式脚本,不要依赖交互式终端提示。