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、消除对旧算法的依赖后,应重新检查并移除这段兼容逻辑。