# React Native 0.59 在 Node 24 下的启动兼容问题:从报错到一条命令运行

React Native 0.59 在 Node 24 下的启动兼容问题:从报错到一条命令运行

维护旧版 React Native 项目时,更新本机 Node 环境后,可能会先遇到 Gradle 找不到 Node,再遇到 Metro 的 OpenSSL 报错。这次项目就连续遇到了这两个问题。

最终保留了原来的启动习惯:在项目根目录执行 yarn android,自动处理兼容设置、启动 Metro,再由 Gradle 编译安装 Android App。

本文记录具体原因、临时分两步启动的逻辑,以及最后怎样恢复为一条命令。

一、项目环境与两个报错

本次排查环境如下,后面的修改以这个项目为准:

项目 版本或环境
操作系统 macOS
React Native 0.59.10
Node.js 24.4.1,通过 NVM 安装
Node 内置 OpenSSL 3.0.16
Yarn 1.22.22,Yarn Classic
Gradle Wrapper 5.6.4

第一个错误发生在 Gradle 配置阶段:

text 复制代码
A problem occurred evaluating root project 'HaihuiApp'.
Cannot run program "node": error=2, No such file or directory

修复后,自动打开的 Metro 窗口又出现:

text 复制代码
Error: error:0308010C:digital envelope routines::unsupported
    at Object.createHash
    at stableHash (.../metro-cache/src/stableHash.js:18:6)

code: 'ERR_OSSL_EVP_UNSUPPORTED'
Node.js v24.4.1

两个报错发生在不同进程里,需要分别处理。

二、Gradle 为什么找不到 Node?

检查 android/build.gradle,发现项目为了锁定 React Native 依赖版本,在配置阶段执行了一次 Node 命令:

groovy 复制代码
def REACT_NATIVE_VERSION = new File(['node', '--print', "JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim())

这段代码需要先从 Gradle 进程的环境里找到 node,再执行 JavaScript 读取 react-native/package.json。

本机 Node 由 NVM 安装,终端里能执行 node,并不保证 Gradle 进程也能找到同一个可执行文件。这里能确认的是:失败的 Gradle 进程无法通过命令名 node 启动程序;仅凭这段日志,无法确定是哪个环节导致了环境差异。

可以先检查终端里的实际路径:

bash 复制代码
command -v node
node -p 'process.execPath'

本次读取版本号不需要启动 Node。Gradle 自带的 Groovy 就能解析 JSON,因此直接改为:

groovy 复制代码
def REACT_NATIVE_VERSION = new groovy.json.JsonSlurper()
        .parse(file('../node_modules/react-native/package.json')).version

后面的版本锁定逻辑继续使用这个字符串:

groovy 复制代码
force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION

这样,这一处配置不再依赖 Node 的可执行路径,也不用把某台电脑的 NVM 绝对路径写进仓库。项目的 Metro 和 JS 打包流程仍然需要 Node。

三、Metro 的 OpenSSL 报错从哪里来?

查看报错指向的 node_modules/metro-cache/src/stableHash.js,可以看到旧版 Metro 使用了 MD4:

js 复制代码
function stableHash(value) {
  return crypto
    .createHash('md4')
    .update(JSON.stringify(value, canonicalize))
    .digest('buffer');
}

本次 Node 24 使用 OpenSSL 3,而 MD4 默认不可用。当 Metro 计算缓存哈希时,crypto.createHash('md4') 就会触发 ERR_OSSL_EVP_UNSUPPORTED。

Node 官方在 OpenSSL 3 迁移说明中提供了 --openssl-legacy-provider,用于临时兼容依赖旧算法的代码。这里把它用于旧版 Metro 的缓存计算。参考 Node 官方说明。

在当前项目和 Node 版本下,可以这样验证:

bash 复制代码
NODE_OPTIONS=--openssl-legacy-provider node -e \
  'console.log(require("metro-cache").stableHash({check: "metro"}).toString("hex"))'

能够输出哈希值,说明这条缓存哈希调用已能运行。它还不等于整个 App 已经构建、安装成功。

四、为什么当时要先 start,再 android?

先看两条命令分别做什么:

命令 主要作用
yarn start 启动 Metro,转换并通过开发服务提供 JS 代码,默认端口为 8081
yarn android 调用 React Native Android CLI,检查 Metro,并通过 Gradle 编译、安装和启动 App

正常情况下,执行 yarn android 就够了。当 CLI 发现 Metro 没有运行时,会尝试自动启动它。

问题出在自动启动的入口。

这个版本的 CLI 在 macOS 上通过 open 打开 launchPackager.command,再由它执行 packager.sh。自动打开的终端不是可靠的环境变量传递渠道;检查本项目的 CLI 源码后,还能看到它生成的 .packager.env 只写入了端口:

bash 复制代码
export RCT_METRO_PORT=8081

因此,即使只给 yarn android 的当前终端加上 NODE_OPTIONS,也不能保证新窗口里的 Metro 获得相同设置。

最初的处理是先修复 package.json 中的 start:

json 复制代码
{
  "scripts": {
    "start": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --openssl-legacy-provider\" node node_modules/react-native/local-cli/cli.js start"
  }
}

${NODE_OPTIONS:-} 用于保留已有选项,并在变量未设置时使用空字符串。后面追加本次所需的兼容参数。

然后分两个终端操作。

终端 A,在项目根目录启动 Metro,并保持运行:

bash 复制代码
yarn start --reset-cache

等出现 Loading dependency graph, done,在终端 B 的同一项目目录执行:

bash 复制代码
yarn android

这时 CLI 会使用已经启动的 Metro,避开缺少兼容参数的自动启动入口。--reset-cache 用于本次排查时重建转换缓存,日常启动不必每次都添加。

这里先后执行的目的是让带有兼容参数的 Metro 提前就绪,React Native 并不要求开发者始终手动执行两条命令。

也不能直接写成 yarn start && yarn android:Metro 是持续运行的服务,前一个命令正常运行时不会退出,后一个命令就不会开始。

五、最终方案:恢复为一条 yarn android

最终把兼容参数补到自动启动 Metro 时真正执行的 packager.sh 中。

由于这个文件位于 node_modules,单独手改会在重装依赖后丢失。因此把修改逻辑保存在项目脚本里,并通过 Yarn 的前置脚本在每次启动 Android 前检查和应用。

1. 新建准备脚本

新建 scripts/prepare-metro.js:

js 复制代码
/**
 * RN 0.59 opens Metro in a separate terminal on macOS, which does not inherit
 * NODE_OPTIONS. Apply the compatibility option at that terminal's entry point.
 * Running before each Android launch also handles reinstalled node_modules.
 */
const fs = require('fs');
const path = require('path');

const packagerPath = path.join(
  path.dirname(require.resolve('react-native/package.json')),
  'scripts/packager.sh',
);
const original = 'node "$REACT_NATIVE_ROOT/cli.js" start "$@"';
const compatible =
  'NODE_OPTIONS="${NODE_OPTIONS:-} --openssl-legacy-provider" ' + original;
const source = fs.readFileSync(packagerPath, 'utf8');

if (!source.includes(compatible)) {
  if (!source.includes(original)) {
    throw new Error('未找到 Metro 启动入口,请检查 React Native 的 packager.sh。');
  }
  fs.writeFileSync(packagerPath, source.replace(original, compatible));
}

它将原来的启动行:

bash 复制代码
node "$REACT_NATIVE_ROOT/cli.js" start "$@"

替换为:

bash 复制代码
NODE_OPTIONS="${NODE_OPTIONS:-} --openssl-legacy-provider" node "$REACT_NATIVE_ROOT/cli.js" start "$@"

参数直接设置在实际启动 Node 的那一行,新终端无需从旧终端继承该设置。脚本已检查替换后的内容,重复执行不会重复追加参数;如果依赖升级导致目标启动行变化,会明确报错,便于重新检查。

2. 配置 Yarn 的前置脚本

在 package.json 的 scripts 中保留下面几项,其他命令保持原样:

json 复制代码
{
  "scripts": {
    "preandroid": "node scripts/prepare-metro.js",
    "android": "react-native run-android",
    "start": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --openssl-legacy-provider\" node node_modules/react-native/local-cli/cli.js start"
  }
}

在本项目使用的 Yarn Classic 中,执行 yarn android 会先运行 preandroid。执行流程变为:

text 复制代码
yarn android
  → preandroid:检查并应用 Metro 启动入口兼容修改
  → react-native run-android
      → 检查 Metro;未运行时自动打开启动窗口
      → packager.sh 带兼容参数启动 Metro
      → Gradle 编译、安装并启动 Android App

这次"组合命令"利用了 Yarn 的脚本生命周期和 React Native 原有的自动启动能力。Metro 仍然是独立进程,终端仍可能自动弹出,但开发者只需要输入一次命令:

bash 复制代码
yarn android

需要把 package.json 和 scripts/prepare-metro.js 一起提交到仓库。重新安装依赖后,下次执行 yarn android 会重新应用修改,无需提交 node_modules。

直接执行 react-native run-android 会绕过 preandroid。这套项目配置应通过 yarn android 使用;本次也没有为 yarn ios 添加对应前置脚本。

六、如何验证修复?

本次按不同阶段进行了验证,避免把"配置通过"误认为"App 全流程通过"。

Gradle 配置检查

在 android 目录执行:

bash 复制代码
./gradlew help --offline --no-daemon

本机输出 BUILD SUCCESSFUL,说明配置阶段已通过。这不是完整的 Android 编译安装检查。

自动启动入口检查

执行 yarn run preandroid,检查 packager.sh 中是否已包含兼容参数。重复执行准备脚本,确认文件内容不再变化。

随后在未预设 NODE_OPTIONS 的环境中,通过修改后的 packager.sh 临时启动 Metro。测试使用端口 8088,输出包含:

text 复制代码
Loading dependency graph, done.

访问测试服务状态接口:

bash 复制代码
curl http://localhost:8088/status

返回:

text 复制代码
packager-status:running

测试完成后关闭了临时 Metro 进程。还通过 yarn android --help 确认 Yarn 会先运行准备脚本,再调用 Android CLI。

这些检查验证了 Gradle 配置、缓存哈希和 Metro 启动入口。连接设备后,完整的编译、安装和页面运行仍以实际执行 yarn android 的结果为准。

七、这个方案的适用范围

这份修改针对 macOS、React Native 0.59.10、Node 24 和 Yarn Classic 的当前项目。Shell 语法、依赖内启动脚本位置、前置脚本行为都需要与实际环境一致,不能直接当作 Windows 或所有包管理器的通用方案。

--openssl-legacy-provider 是兼容旧依赖的过渡措施,并没有升级 Metro,也不代表 React Native 0.59 的所有代码都已兼容 Node 24。后续升级 React Native 和 Metro、消除对旧算法的依赖后,应重新检查并移除这段兼容逻辑。

相关推荐
天道kabuto7 小时前
入门 React Native:初始化 HelloRN 项目,Android 环境排错全记录
react native
程序员老刘2 天前
Flutter你看看人家RN
flutter·react native·ai编程
是你的小橘呀2 天前
告别各写各的!从 0 到 1 打造 React Native 客户端公共动效基础设施 🏗️
react native
墨狂之逸才2 天前
# React Native iOS 排查记录:从 Yoga 编译报错,到模拟器反复白屏
react native
song5013 天前
React Native for OpenHarmony 实战:三方库 react-native-parallax-scroll-view 的鸿蒙化适配指南
分布式·react native·react.js·transformer·harmonyos
song5013 天前
React Native for OpenHarmony 实战:三方库 react-native-ratings 的鸿蒙化适配指南
javascript·react native·react.js·分类·harmonyos
song5013 天前
React Native for OpenHarmony 实战:三方库 react-native-popup-menu 的鸿蒙化适配指南
css·深度学习·react native·react.js·transformer·harmonyos
梦想的颜色4 天前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
song5015 天前
React Native for OpenHarmony 实战:三方库 react-native-css-transformer 的鸿蒙化适配指南
css·人工智能·深度学习·react native·transformer