Skip to main content

TurboModule 与 Codegen 机制

你的直觉是对的:TurboModule + Codegen spec 不是「一个功能」,而是 RN 新架构里「JS 调原生」的一整套机制。跨度大,是因为中间叠了 4 层。下面用「从业务出发、一层层往下」的方式拆开。


先忘掉名词,只看业务

你真正想做的事只有一件:

await SplitBundleLoader.load('/path/to/bundle.js', 42);

意思是:JS 告诉 iOS 原生:「把这个 split bundle 文件加载进当前 RN runtime」

Dev 用 Metro 热加载;Release 没有 Metro,必须走原生。所以需要一个 JS → Native 的桥


两种桥:旧 vs 新

旧方式(Legacy)新方式(TurboModule + Codegen)
JS 怎么调NativeModules.SplitBundleLoader.load(...)TurboModuleRegistry.getEnforcing('SplitBundleLoader')
原生怎么接手写 RCTBridgeModule + RCT_EXPORT_METHOD继承 Codegen 生成的基类 + 实现 getTurboModule
类型安全基本没有,字符串拼方法名spec 文件定义接口,构建时生成胶水代码
RN 0.86 Bridgeless容易 crash(你们踩过 bridge selector 问题)官方推荐路径

Codegen 的作用:你写一份 TypeScript「合同」(spec),构建时自动生成 JS ↔ Native 之间的胶水,不用手写大量样板代码。

可以把它想成:

spec.ts = API 文档(合同)
Codegen = 根据合同自动生成接线员
TurboModule = 新架构下的「电话线」
原生 .mm = 真正干活的实现

四层模型(从外到内)

┌─────────────────────────────────────────┐
│ 第 4 层:业务代码 │
│ bundleLoader.ts 调用 load() │
└──────────────────┬──────────────────────┘
│ 就是普通 async 函数调用
┌──────────────────▼──────────────────────┐
│ 第 3 层:JS 入口(spec 文件) │
│ NativeSplitBundleLoader.ts │
│ 「我要一个叫 SplitBundleLoader 的模块, │
│ 它有 load(url, id) 方法」 │
└──────────────────┬──────────────────────┘
│ TurboModuleRegistry 查找已注册模块
┌──────────────────▼──────────────────────┐
│ 第 2 层:Codegen 生成的胶水(构建产物) │
│ NativeSplitBundleLoaderSpecJSI 等 │
│ 「把 JS 的 load() 调用翻译成 ObjC 调用」 │
└──────────────────┬──────────────────────┘
│ getTurboModule 返回 JSI 实例
┌──────────────────▼──────────────────────┐
│ 第 1 层:你的原生实现 │
│ SplitBundleLoader.mm │
│ 真正执行 registerSegmentWithId / 加载 JS │
└─────────────────────────────────────────┘

你日常只需要关心第 4 层和第 1 层;第 2、3 层是 RN 新架构要求的「接线规范」。


每个文件到底在干什么(一句话版)

文件一句话
NativeSplitBundleLoader.ts合同:定义 load(url, id) 长什么样
package.jsoncodegenConfig告诉构建系统:去读 src/specs,生成 RNSplitBundleLoaderSpec
build/generated/.../RNSplitBundleLoaderSpec.*自动生成的接线员(你不手写)
SplitBundleLoader.h声明:「我是 SplitBundleLoader,继承 Codegen 基类」
SplitBundleLoader.mm实现load 里真正加载 bundle;getTurboModule 把模块注册进 RN
splitBundleLoader.ts薄封装,方便业务 import
bundleLoader.ts业务:Release OTA 时调用 SplitBundleLoader.load

和「普通原生模块」对比

如果不用 TurboModule + Codegen,Legacy 写法大概是:

// JS
NativeModules.SplitBundleLoader.load(path, segmentId)

// ObjC
RCT_EXPORT_METHOD(load:(NSString *)fileUrl segmentId:(double)segmentId ...)

看起来更简单,但在 RN 0.86 + Bridgeless(fabric: true)下,Legacy 模块访问 self.bridge 会 crash——这就是你们改成 TurboModule 的原因。

新写法多出来的部分:

  1. spec 文件 — 类型 + 接口定义
  2. codegenConfig — 触发代码生成
  3. getTurboModule — 把模块挂到新架构上

多写这些,是为了在新架构下稳定运行。


心智模型:把它当成「带类型的 NativeModules」

Legacy: JS ──(字符串)──> Bridge ──> ObjC 方法
TurboModule: JS ──(spec 合同)──> JSI ──> ObjC 方法

Codegen 自动生成中间层

对你写业务代码的人:用法几乎一样,还是 await SplitBundleLoader.load(...)

对你写原生的人:除了实现 load,还要接 Codegen 基类 + getTurboModule(基本是固定模板)。

对构建系统pod install / Android build 时跑 Codegen,生成那些 NativeXxxSpecJSI 文件。


为什么感觉「跨度大」

因为 RN 把 3 件事绑在一起了:

  1. 新通信机制(TurboModule / JSI,替代旧 Bridge)
  2. 代码生成(Codegen,替代手写胶水)
  3. 你的业务(加载 split bundle)

实际上 业务逻辑只在 SplitBundleLoader.mmload 方法里;其余大部分是 RN 0.86 的「入场券」。


如果只记 3 句话

  1. spec = 在 TS 里写「JS 能调哪些原生方法」
  2. Codegen = 构建时根据 spec 自动生成 JS ↔ Native 胶水
  3. TurboModule = 新架构下 JS 找到并调用那个原生模块的方式

你们项目里,这一切只为 SplitBundleLoader.load 这一件事服务。


如果你希望,我可以把这份「四层模型 + 心智模型」整理进 docs/TurboModule + Codegen spec.md,替换现在偏「文件清单」的写法,读起来会更顺。