npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flun/nodejs-mobile-react-native

v2.1.4

Published

Node.js for Mobile Apps React Native plugin (flun fork, based on nodejs-mobile-react-native)

Readme

@flun/nodejs-mobile-react-native

简介

为 React Native 应用提供完整的 Node.js; 让 Node.js 服务端代码与 React Native 前端运行于同一进程的不同线程, 双向通信;

基于 nodejs-mobile-react-native 的 flun fork; Android 与 iOS 各自内置独立的 Node.js 运行时(版本对照见"安装"章节);

可直接用于任何 React Native 项目, 也可由 @flun/node-mobile-app 在构建时自动调用;

  • GitHub: https://github.com/OpenFlun/nodejs-mobile-react-native
  • Gitee: https://gitee.com/OpenFlun/nodejs-mobile-react-native
  • npm: https://www.npmjs.com/package/@flun/nodejs-mobile-react-native

与原版的差异

rn-bridge 的处理

rn-bridge 是 native binding 的 JS 包装, 必须走 CJS; 因本包是 type: module, 入口文件改名为 index.cjs,package.json 的 main 指向它.无论引用方是 CJS 还是 ESM, 都能正常加载;

预编译资源下载(libnode.so / NodeMobile.xcframework)

两套预编译二进制体积较大, 不塞进 npm 包, 改为 postinstall 阶段自动下载并解压到对应目录:

| 资源 | 用途 | | --------------------- | --------------------------------------------------------- | | android-libnode.zip | Android 的 libnode.so(具体架构以 prebuiltAssets 为准) | | ios-nodemobile.zip | iOS 的 NodeMobile.xcframework |

  • 下载源:Gitee 优先, 失败自动回退 GitHub
  • 各 asset 独立处理, 已装版本匹配则跳过(不重复下载)
  • 全部下载失败时打印下载链接,不会中断 npm 安装, 可手动下载覆盖到对应目录

npm 包体积:tarball 不含预编译二进制,装包很快,二进制随后拉取;

需要覆盖版本 / 跳过下载等进阶操作, 见 安装 章节的"首次安装的预编译资源下载";

代码风格

| 项 | 原版 | 本 fork | | ------------------- | ---------------------------------- | ------------------------------------------------- | | 模块系统 | CommonJS | ESM(package.json 的 type: "module") | | 导出方式 | 文件内 module.exports | 文件末尾统一 export { ... } | | 函数声明 | function xxx | const xxx = (...) => {}(class 保留) | | 注释 | 英文 | 中文 | | 依赖 | nodejs-mobile-gyp | @flun/nodejs-mobile-gyp(去停更依赖) | | xcode 依赖 | 保留(RN 0.60 autolinking 前的遗留) | 删除(代码中零引用) | | react-native peer | 强制 | peerDependenciesMeta.optional(避免重复安装) |


特性

  • Node.js 运行时内置 Android 与 iOS, 与 React Native 前端同进程不同线程, 支持双向通信
  • 预编译二进制不经 npm 包分发, 由 postinstall 自动下载(Gitee 优先, 失败回退 GitHub), 包体小、装包快
  • 二进制版本匹配时跳过下载; 全部下载失败时打印链接, 不中断 npm 安装
  • ESM / CJS 双兼容: rn-bridge 走 CJS 入口 index.cjs, 引用方无论 CJS 或 ESM 均可加载
  • 可作为中间层被 @flun/node-mobile-app 自动调用, 也可直接用于任意 React Native 项目

包目录结构

@flun/nodejs-mobile-react-native/
│
├── android/                      # Android 原生工程
│   ├── build.gradle              # gyp 路径探测,abiFilters,NDK 工具链注入,Windows 适配(cmd / sh.exe / 正斜杠)
│   ├── CMakeLists.txt            # 编译 rn-bridge.cpp + native-lib.cpp
│   └── src/main/
│       ├── AndroidManifest.xml
│       ├── cpp/
│       │   ├── native-lib.cpp    # JNI 入口, 设置 NODE_PATH, 启动 Node 引擎, stdout/stderr 重定向
│       │   ├── rn-bridge.cpp     # 注册 native binding(NODE_MODULE_LINKED)与 channel 队列
│       │   └── rn-bridge.h       # 头文件
│       └── java/com/janeasystems/rn_nodejs_mobile/
│           ├── RNNodeJsMobileModule.java  # RN 原生模块:start / sendMessage / registerChannel 等
│           └── RNNodeJsMobilePackage.java
│
├── install/                      # postinstall 复制到宿主项目的运行时资源
│   └── resources/
│       ├── nodejs-assets/nodejs-project/
│       │   ├── sample-main.js       # 用户 main.js 示例(ESM)
│       │   └── sample-package.json  # 用户 package.json 示例(含 type: module)
│       └── nodejs-modules/builtin_modules/rn-bridge/
│           ├── index.cjs            # rn-bridge 运行时模块(CJS,.cjs 强制后缀以兼容 ESM 项目)
│           └── package.json         # main: index.cjs
│
├── ios/                          # iOS 原生工程
│   ├── NodeRunner.hpp            # Node 引擎生命周期管理
│   ├── NodeRunner.mm             # 设置 NODE_PATH, 启动 Node, stdout/stderr 回调
│   ├── rn-bridge.cpp             # iOS 端同名 native binding
│   ├── rn-bridge.h               # 头文件
│   ├── RNNodeJsMobile.h
│   ├── RNNodeJsMobile.m          # RN 原生模块;-r 加载 dlopen override
│   ├── RNNodeJsMobile.xcodeproj/
│   ├── RNNodeJsMobile.xcworkspace/
│   ├── libnode/include/node/     # Node 头文件(随 npm 包分发)
│   └── NodeMobile.xcframework/   # iOS 静态库; 由 postinstall 下载(见简介中的"预编译资源下载")
│
├── scripts/                      # 构建与安装期脚本(全部 ESM)
│   ├── create-node-structure.js     # postinstall: 复制 nodejs-assets 到宿主项目根;
│   │                               宿主项目依赖 @flun/node-mobile-app 时跳过
│   ├── download-prebuilt-assets.js  # postinstall: 从 Gitee / GitHub Release 下载 libnode.so / NodeMobile.xcframework
│   ├── patch-package.js             # 补丁原生模块 package.json(node-gyp-build → node-gyp-build-mobile)
│   ├── ios-copy-nodejs-project.sh   # iOS Script Phase: 复制 Node 项目
│   ├── ios-build-native-modules.sh  # iOS Script Phase: 调用 @flun/nodejs-mobile-gyp 编译原生模块
│   ├── ios-sign-native-modules.sh   # iOS Script Phase: 签名原生模块
│   ├── ios-remove-framework-simulator-strips.sh  # iOS Script Phase: 移除模拟器切片
│   ├── ios-create-plists-and-dlopen-override.js  # 生成 framework Info.plist 与 dlopen 覆盖数据
│   ├── override-dlopen-paths-preload.js          # 运行时 preload, 覆盖 process.dlopen 指向 App 内 Frameworks
│   └── plisttemplate.xml
├── CHANGELOG.md                  # 版本变更记录
├── index.d.ts                    # TypeScript 类型声明
├── index.js                      # JS 桥接层入口
├── LICENSE
├── native.js                     # 实际桥接层:start / startWithArgs / startWithScript / channel
├── nodejs-mobile-react-native.podspec  # iOS Pod 配置(vendored NodeMobile.xcframework)
├── package.json                  # type: module; postinstall 触发结构生成与预编译资源下载
├── react-native.config.js        # RN CLI 配置: 指定 Android sourceDir 与 iOS 的 4 个 Script Phase
├── README.md                     # 本文件
└── types.d.ts                    # 共享类型声明(ChannelCallback, StartupOptions)

安装前准备

允许安装脚本执行

本包在安装时可能触发某些依赖包的自动脚本(如 postinstall 等); 如果你的 npm 全局配置或项目配置禁止了脚本执行(例如设置了 ignore-scripts=true), 可能会导致安装不完整或运行时异常;

推荐在项目根目录的 package.json 中添加 allowScripts 字段, 显式放行本包及其依赖的脚本:

{
  "allowScripts": {
    "@flun/nodejs-mobile-gyp":true,
    "@flun/nodejs-mobile-react-native": true
    // 如果依赖的其它包(如 bcrypt, electron-winstaller 等)也有脚本, 请按需添加, 格式相同
  }
}

系统与配置要求

本包是 @flun/node-mobile-app 与 @flun/nodejs-mobile-gyp 之间的中间层; 完整的宿主平台与工具链要求见 gyp 的"支持的系统与配置要求".本包相关的要点:

| 目标平台 | 宿主平台 | 额外要求 | | -------- | ----------------------- | ----------------------------------------------------------- | | Android | Windows / macOS / Linux | Android NDK;Windows 上还需 Visual Studio 与 Git for Windows | | iOS | 仅 macOS | 完整 Xcode(Command Line Tools 单独不够)+ CocoaPods |

  • Node.js:最低版本见 package.json 的 engines.node 字段
  • Python 3.6+:gyp 构建时需要
  • 首次安装的预编译资源:Android 与 iOS 的运行时二进制由 postinstall 自动下载,无需手动准备

安装

npm i @flun/nodejs-mobile-react-native

iOS 需运行 pod install 链接原生代码.通过 @flun/node-mobile-app 构建时 CLI 会自动执行,直接使用本包时需手动运行:

cd ios && pod install

如果你的依赖是 @flun/node-mobile-app, 那么你不需要手动安装本包, 它由 CLI 在构建时自动调用;

首次安装的预编译资源下载

本包的 npm tarball 仅含代码(不含预编译二进制),安装后 postinstall 会自动从 Gitee / GitHub Release 拉取:

  • Android:android-libnode.zip → 覆盖到 android/libnode/
  • iOS(仅 macOS):ios-nodemobile.zip → 覆盖到 ios/NodeMobile.xcframework/

细节见简介中的"预编译资源下载";

环境变量(按需设置):

# 覆盖当前版本(具体版本号请在 package.json 的 "prebuiltAssets" 查看)
# 具体下载版本由 package.json 的 "prebuiltAssets.version" 决定;同一 tag 下 Android 与 iOS 附件的实际运行时版本可能不同
NODE_MOBILE_PREBUILT_VERSION=你的目标版本号

# 跳过全部下载
NODE_MOBILE_PREBUILT_SKIP=1

# 只跳过 iOS(macOS 上只做 Android 编译时用)
NODE_MOBILE_PREBUILT_IOS_SKIP=1

Node.js 运行时能力对照

Android 与 iOS 采用各自独立的 Node.js 运行时;下载哪个 Release tag 由 package.json 的 prebuiltAssets.version 决定(可用环境变量 NODE_MOBILE_PREBUILT_VERSION 覆盖), 但同一 tag 下 Android 与 iOS 附件的实际运行时版本可能不同--以最终下载到的产物为准:

| 平台 | ICU | Express | 备注 | | ------- | -------------- | ---------------- | --------------------------- | | Android | 完整(full-icu) | 4.x / 5.x 均支持 | 支持 \p{...} Unicode 正则 | | iOS | 暂无 ICU | 仅 4.x | 不支持 \p{...} |

关于 full-icu:

  • 完整 ICU(full-icu)自 nodejs-mobile 基于 Node.js v22.23.2 起支持; 更低基线版本不支持 \p{...} 等 Unicode 属性转义
  • iOS 目前尚未适配 full-icu, 后续会逐步跟进, 请耐心等待
  • 如果你自行编译适配了 iOS 上的 full-icu, 欢迎分享:[email protected]

两个平台的运行时彼此独立; 表中状态为当前发布情况.

函数方式

按运行位置分两层,各层入口如下:

| 入口 | 运行位置 | 用途 | | ----------- | --------------- | ------------------------------------ | | nodejs | React Native 侧 | 启动 Node 线程、收发消息 | | rn_bridge | Node 侧 | 与 React Native 通信、取应用数据目录 |

详细接口见以下 API 三节;

React Native 层 API

从 React Native 代码直接调用:

import * as nodejs from '@flun/nodejs-mobile-react-native';

可用方法:

  • nodejs.start
  • nodejs.startWithArgs
  • nodejs.startWithScript
  • nodejs.channel.addListener
  • nodejs.channel.removeListener
  • nodejs.channel.post
  • nodejs.channel.send

nodejs.channel.send(...msg) 等价于 nodejs.channel.post('message', ...msg), 保留是为了向后兼容;

nodejs.channel 继承自 React Native 的 EventEmitter 类, 去掉 emit, 增加 post 和 send;

nodejs.start(scriptFileName [, options])

| 参数 | 类型 | | -------------- | ---------------- | | scriptFileName | string | | options | StartupOptions |

从 nodejs-project 目录下的某个文件启动 nodejs-mobile 运行时线程;

nodejs.startWithArgs(command [, options])

| 参数 | 类型 | | ------- | ---------------- | | command | string | | options | StartupOptions |

从 nodejs-project 目录下的某个文件启动, 并把额外参数传给它;command 通常包含脚本文件名作为第一个参数, 例如:

command = 'main.js --insecure-http-parser --zero-fill-buffers'

nodejs.startWithScript(scriptBody [, options])

| 参数 | 类型 | | ---------- | ---------------- | | scriptBody | string | | options | StartupOptions |

用一个脚本字符串启动 nodejs-mobile 运行时线程;

nodejs.channel.addListener(event, callback)

| 参数 | 类型 | | -------- | ---------- | | event | string | | callback | function |

为 Node 侧发出的自定义事件注册回调;

nodejs.channel.removeListener(event, callback)

| 参数 | 类型 | | -------- | ---------- | | event | string | | callback | function |

移除之前通过 addListener 注册的回调.callback 必须与注册时传入的是同一个函数引用.

nodejs.channel.post(event, ...message)

| 参数 | 类型 | | ---------- | ---------------------------------------------------------------- | | event | string | | ...message | 任何可用 JSON.stringify 序列化,JSON.parse 反序列化的 JS 类型 |

在 Node 侧触发一个自定义事件;

nodejs.channel.send(...message)

| 参数 | 类型 | | ---------- | ---------------------------------------------------------------- | | ...message | 任何可用 JSON.stringify 序列化,JSON.parse 反序列化的 JS 类型 |

在 Node 侧触发一个 'message' 事件; 是 nodejs.channel.post('message', ...message) 的别名;

StartupOptions: object

| 名称 | 类型 | 默认值 | 说明 | | ---------------------- | --------- | ------ | --------------------------------------------------------------------- | | redirectOutputToLogcat | boolean | true | 是否把 Node 的 stdout/stderr 重定向到 Android logcat(仅 Android 有效) |

Node 层 API(rn-bridge)

从 Node 代码通过 rn-bridge 模块调用:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const rn_bridge = require('rn-bridge');

可用方法:

  • rn_bridge.channel.on
  • rn_bridge.channel.removeListener
  • rn_bridge.channel.post
  • rn_bridge.channel.send
  • rn_bridge.app.on
  • rn_bridge.app.datadir

rn_bridge.channel.send(...msg) 等价于 rn_bridge.channel.post('message', ...msg), 保留是为了向后兼容;

rn_bridge.channel 继承自 Node 的 EventEmitter 类, 去掉 emit, 增加 post 和 send;

rn_bridge.channel.on(event, callback)

| 参数 | 类型 | | -------- | ---------- | | event | string | | callback | function |

为 React Native 侧发出的自定义事件注册回调;

接收 nodejs.channel.send 的消息, 用:

rn_bridge.channel.on('message', listenerCallback)

rn_bridge.channel.removeListener(event, callback)

| 参数 | 类型 | | -------- | ---------- | | event | string | | callback | function |

移除之前通过 on 注册的回调.callback 必须与注册时传入的是同一个函数引用.

rn_bridge.channel.post(event, ...message)

| 参数 | 类型 | | ---------- | ---------------------------------------------------------------- | | event | string | | ...message | 任何可用 JSON.stringify 序列化,JSON.parse 反序列化的 JS 类型 |

在 React Native 侧触发一个自定义事件;

rn_bridge.channel.send(...message)

| 参数 | 类型 | | ---------- | ---------------------------------------------------------------- | | ...message | 任何可用 JSON.stringify 序列化,JSON.parse 反序列化的 JS 类型 |

在 React Native 侧触发一个 'message' 事件; 是 rn_bridge.channel.post('message', ...message) 的别名;

rn_bridge.app.on(event, callback)

| 参数 | 类型 | | -------- | ---------- | | event | string | | callback | function |

注册 App 事件回调; 当前支持 'pause' 和 'resume', 分别在应用进入后台/前台时自动触发;

rn_bridge.app.on('pause', (pauseLock) => {
  console.log('[node] app paused.')
  pauseLock.release()
})
rn_bridge.app.on('resume', () => {
  console.log('[node] app resumed.')
})

'pause' 事件在应用切到后台时触发; iOS 上系统会等待所有 'pause' 处理器返回后才真正挂起应用; 为了让 iOS 知道何时可以安全挂起, 每个 'pause' 监听器会收到一个 pauseLock 参数, 调用它的 release() 表示该监听器已经完成工作; 所有锁都释放后(或 iOS 强制挂起), 应用才会进入挂起状态;

rn_bridge.app.on('pause', (pauseLock) => {
  server.close(() => {
    // 服务器停止监听, 当前连接全部关闭后, 应用才会挂起;
    pauseLock.release()
  })
})

警告:iOS 上应用最终一定会被挂起;pause 事件里应尽快完成清理, 并确保每个监听器都调用 pauseLock.release(), 否则应用会在 iOS 允许的范围内一直占用后台;

rn_bridge.app.datadir()

返回一个可写的持久化数据目录; iOS 上是 NSDocumentDirectory, Android 上是 FilesDir;

Channel 回调:function(arg)

| 名称 | 类型 | | ---- | ---------------------------------------------------------------- | | arg | 任何可用 JSON.stringify 序列化,JSON.parse 反序列化的 JS 类型 |

通道内部用 JSON.stringify 序列化,JSON.parse 反序列化; 这意味着:

  • JS 的 Date 会被转成字符串
  • 函数会从对象里被剔除
  • 只支持 Boolean,Number,String,Object,Array,null 这些类型(符合 JSON 数据交换语法标准)
  • NaN,Infinity,-Infinity 会被 JSON.stringify 转成 null
  • undefined 在对象里会被剔除, 在数组里会被转成 null
  • 循环引用会抛出 TypeError

其他 Node API 说明

os.tmpdir()

  • iOS:os.tmpdir() 返回一个临时目录, 因为 iOS 会把应用的 TMPDIR 环境变量设为 NSTemporaryDirectory 的等价路径;
  • Android:Android 系统没有为应用定义临时目录, 插件会把 TMPDIR 环境变量设为应用上下文 CacheDir 的值;

集成示例

Node.js 项目

@flun/nodejs-mobile-react-native 安装后, 会在你的应用里生成 nodejs-assets/nodejs-project/ 目录; 这个目录会被打进 APK/IPA, Node 引擎启动时会从这里加载 main.js;

目录里包含两个示例文件:

  • sample-main.js
  • sample-package.json

建议把它们重命名为 main.js 和 package.json 后开始开发;

注意:sample-main.js 和 sample-package.json 会在安装/更新 @flun/nodejs-mobile-react-native 时被覆盖, 不要直接改动它们;

main.js 示例内容(ESM + createRequire):

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const rn_bridge = require('rn-bridge');

// 把收到的每条 react-native 消息回显;
rn_bridge.channel.on('message', (msg) => {
  rn_bridge.channel.send(msg);
});

// 通知 react-native:Node 已初始化;
rn_bridge.channel.send('Node was initialized.');

注意:package.json 默认是 type: module,main.js 本身是 ESM, 不能直接写 require('rn-bridge'); 如需加载 rn-bridge, 请用 createRequire:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const rn_bridge = require('rn-bridge');

React Native 的打包器(Metro)会扫描项目目录下的 JS 文件; 为避免它把 nodejs-assets/nodejs-project/ 里的服务端 JS 也当作应用代码解析, 请在 metro.config.js 里用 blockList 排除该目录, 配置示例见下方"故障排查"章节;

Node.js 运行时通过 Unix 风格的路径访问文件; Android 上, Node 项目会在应用首次启动(或更新后)从 APK 的 assets 复制到默认应用数据目录下的 nodejs-project/;

注意: 应用每次更新后项目目录会被覆盖, 不要把它当作持久化存储; 持久化请用 rn_bridge.app.datadir();

为了加速资源解压(避免递归遍历 assets 目录), 编译时会把文件清单 file.list 和目录清单 dir.list 一起打进 assets; 这在 Android 6.x 及更旧版本上能规避 assets manager 的一个严重性能 bug;

Node 模块

在 nodejs-assets/nodejs-project/ 里跑 npm install 即可添加 Node 模块(前提是已有 package.json);

原生模块

Windows / Linux / macOS 上均支持编译 Android 原生模块;编译 iOS 原生模块则必须在 macOS 上进行;

插件会扫描 nodejs-project 目录下的 .gyp 文件, 自动识别原生模块;

前置工具链:

  • Android:需要 Android NDK; 插件会自动定位 NDK 并把工具链路径注入构建环境, 无需手动设置 ANDROID_NDK_HOME;
  • iOS:需要完整 Xcode(仅 macOS;Command Line Tools 单独不够);
  • Windows 额外需要:Visual Studio(含"使用 C++ 的桌面开发"工作负载)与 Git for Windows(提供 make 所需的 POSIX 工具); 完整清单见 @flun/nodejs-mobile-gyp 的系统要求;

运行时版本与开发机的 Node 版本相互独立:

prebuiltAssets.version 指定的是下载用的 Release tag, 与开发机安装的 Node 版本无关;同一 tag 下 Android 与 iOS 附件的实际运行时版本可能不同(以产物为准);只要开发机的 Node 满足 package.json 的 engines.node 要求即可编译原生模块--产物的 ABI 由 NDK 与运行时自带的头文件决定, 开发机的 Node 仅用于驱动构建工具链;

切换运行时版本请设置环境变量 NODE_MOBILE_PREBUILT_VERSION(详见"安装"章节);

Android 上编译原生模块耗时较长(需为每个目标架构分别编译);编译产物 .node 会按架构分开打进应用, 运行时选择正确的那个;

如果想手动控制开关, 可以创建 nodejs-assets/BUILD_NATIVE_MODULES.txt:

  • 内容 1: 强制启用编译
  • 内容 0: 强制关闭编译

例如:

echo "1" > nodejs-assets/BUILD_NATIVE_MODULES.txt
react-native run-android
Prebuilds(预编译产物)

插件会自动检测预编译的 .node 文件并关闭该模块的即时编译; 识别路径:

nodejs-assets/nodejs-project/node_modules/<MODULE_NAME>/prebuilds/<PLATFORM>-<ARCH>/<NAME>.node

支持的组合:

  • PLATFORM = android
    • ARCH = arm
    • ARCH = arm64
    • ARCH = x64
  • PLATFORM = ios
    • ARCH = arm64
    • ARCH = x64

检测到 prebuild 后, 插件会删除该模块的 binding.gyp 并修改 package.json, 让 node-gyp 忽略它;

如果你是原生模块作者, 想为自己的模块提供 nodejs-mobile 的 prebuild, 参考 CLI 工具 prebuild-for-nodejs-mobile;

React Native 应用

先从 React Native 侧导入:

import * as nodejs from '@flun/nodejs-mobile-react-native';

在应用主组件的生命周期里启动 Node:

nodejs.start('main.js')
nodejs.channel.addListener(
  'message', msg => alert('From node: ' + msg)
)

这会告诉原生代码启动一个专用线程, 从 nodejs-assets/nodejs-project/main.js 开始运行 Node, 并在收到 Node 消息时弹窗显示;

注意:Node 项目以单例形式运行在独立线程上, 只有第一次 nodejs.start() 生效, 后续调用不会启动新线程; 这意味着 react-native 的热重载不会影响 Node 项目, 需要重启应用;

发送消息给 Node:

<Button title="Message Node"
  onPress={() => nodejs.channel.send('A message!')}
/>

开发者参考

1. 为什么 loader 补丁按目录定位 rn-bridge, 而不是硬编码入口文件名

改造过程中把 rn-bridge/index.js 改名为 index.cjs 后, 配早期 v22 产物, App 启动即闪退, logcat 显示:

FORTIFY: pthread_mutex_lock called on a destroyed mutex
Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE)

根因: 早期 v22 libnode.so 内置的 CJS loader 补丁按 rn-bridge/index.js 硬编码定位入口; 本包因 type: module 已把入口改名为 index.cjs, 补丁匹配失败, Node 回退到 process._linkedBinding('rn_bridge') 拿裸 native binding(只有 sendMessage / registerChannel / getDataDir, 没有 channel 属性), 触发崩溃;

方案:v22 适配包的 CJS loader 补丁改为按目录 rn-bridge 定位, 入口文件名交给该目录 package.json 的 main 字段解析--无论入口叫 index.js / index.cjs / index.mjs 还是其它, 都能正确加载;

结论:rn-bridge 作为 native binding 的 JS 包装, 必须走 CJS;

2. 为什么用 .cjs 而不是 .js

本包整体是 type: module, 普通的 .js 会被按 ESM 解析;.cjs 后缀是 Node 官方约定的 CommonJS 强制后缀,优先级高于包 type--无论引用方是 CJS 还是 ESM 项目, Node 都用 CJS loader 加载 index.cjs;

因此:

  • import rn_bridge from 'rn-bridge'(ESM)→ default 即 module.exports
  • require('rn-bridge')(CJS)→ 原生

两种写法都能拿到 { app, channel };

3. sample-main.js 与生成的 main.js 为什么模块类型不同

  • sample-main.js(示例, 给用户参考): 用 ESM import; 用户整包复制后,sample-package.json 自带 type: module,import 天然成立;
  • main.js(CLI 生成): 由 @flun/node-mobile-app 的 generate.js 生成, 用 createRequire(import.meta.url) 拿到 require, 再加载 rn-bridge;package.json 默认 type: module,main.js 本身是 ESM, 但通过 createRequire 保证 rn-bridge 一定走 CJS loader 路径(原因见坑 1);

两者并存, 各服务各的场景;

4. peerDependenciesMeta.react-native 为什么标 optional

原版把 react-native 声明为强制 peer, npm 7+ 会自动安装 peer, 造成重复安装;

本包将其标记为 peerDependenciesMeta.optional, 避免用户项目里重复装 RN--react-native 由 @flun/node-mobile-app 的 dependencies 提供, 不会缺失;

常见问题

Android 构建缓存

Android 上 react-native 有时无法正确重建 assets; 如果 react-native run-android 报错, 可以手动清 Gradle 缓存后重试:

Windows:

cd android
gradlew clean
cd ..
react-native run-android

Linux/macOS:

cd android
./gradlew clean
cd ..
react-native run-android

让 Metro 忽略 nodejs-project

React Native 的打包器(Metro)会扫描项目目录下的 JS 文件;nodejs-assets/nodejs-project/ 里是 Node 引擎在设备上加载的服务端代码, 不应被 Metro 当作应用代码解析, 否则可能被误打包或与其他模块发生路径冲突;

解决办法: 在项目的 metro.config.js 里用 blockList 把 nodejs-assets/,android/,ios/ 排除掉(react-native >= 0.87.1):

import exclusionListModule from 'metro-config/private/defaults/exclusionList'
const exclusionList = exclusionListModule.default

export default {
  resolver: {
    blockList: exclusionList([
      /[/\\]nodejs-assets[/\\].*/,
      /[/\\]android[/\\].*/,
      /[/\\]ios[/\\].*/,
    ])
  },
  // ...
}

许可证

ISC


贡献