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.json → codegenConfig | 告诉构建系统:去读 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 的原因。
新写法多出来的部分:
- spec 文件 — 类型 + 接口定义
- codegenConfig — 触发代码生成
- 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 件事绑在一起了:
- 新通信机制(TurboModule / JSI,替代旧 Bridge)
- 代码生成(Codegen,替代手写胶水)
- 你的业务(加载 split bundle)
实际上 业务逻辑只在 SplitBundleLoader.mm 的 load 方法里;其余大部分是 RN 0.86 的「入场券」。
如果只记 3 句话
- spec = 在 TS 里写「JS 能调哪些原生方法」
- Codegen = 构建时根据 spec 自动生成 JS ↔ Native 胶水
- TurboModule = 新架构下 JS 找到并调用那个原生模块的方式
你们项目里,这一切只为 SplitBundleLoader.load 这一件事服务。
如果你希望,我可以把这份「四层模型 + 心智模型」整理进 docs/TurboModule + Codegen spec.md,替换现在偏「文件清单」的写法,读起来会更顺。