深度解析:React Native 0.88.0 是如何适配到开源鸿蒙的
深度解析:React Native 0.88.0 是如何适配到开源鸿蒙的
这篇不谈排期、不谈心情,只谈一件事:React Native 0.88.0 走到 OpenHarmony 上,中间那层适配是怎么做的。
所有数据与代码片段都来自仓库当前状态(上游基线
v0.88.0,提交5c35c82cf),可以逐条对照。
一、先把问题问对:适配不是移植
“把 React Native 适配到鸿蒙”,最容易想歪的一步,是把它当成"移植"——把 RN 的代码搬过来、改吧改吧编译到鸿蒙上。
真那样做,第一个版本也许能跑,代价是从此与上游分道扬镳:上游每发一版,你都要重新搬一次,而且永远搬不干净。
这套适配选择的是另一条路:不搬源码,叠补丁。
结构上就是一个朴素的序列:
上游 React Native 定版源码(pristine,v0.88.0)
+ 包补丁层(packages/react-native.patch)
+ 平台覆盖层(rnoh 侧的 .harmony.* 与委托实现)
= 一份能跑在 OpenHarmony 上的 React Native
选择"补丁"而不是"照搬",带来三个必须同时成立的约束——它们决定了后面所有的设计:
- 对非鸿蒙平台必须是行为中性的。 打完补丁的源码如果拿去编译 iOS / Android,行为应该与上游一致;
否则这就是一个分叉,而不是一层适配。 - 必须能跟着上游走。 补丁要尽量贴着上游的既有写法去打,避免与上游的下一版正面冲突。
- 必须能一键复原与重建。 任何时刻都能"清干净 → 重放补丁 → 得到同一份结果",
否则构建就不可复现,排查问题会变成考古。
本文后面的每一个机制,都是在回答"如何在满足这三条的前提下,把鸿蒙塞进去"。
二、架构总览:五层,以及一条解析链
适配不是一个目录,而是五层各司其职。先看全景,再看每一层。
| 层 | 位置 | 职责 |
|---|---|---|
| ① 上游源码层 | packages/react-native(子模块,v0.88.0) | 定版原始码,保持 pristine,随时可 reset |
| ② 补丁层 | packages/react-native.patch | 把鸿蒙接进上游源码:平台分支、委托契约、C++/CMake 增强 |
| ③ JS 包层 | packages/react-native-harmony | 同步补丁后的 Libraries/ src/ types/,并提供 .harmony.* 平台覆盖、metro 解析、JS 胶水 |
| ④ 原生层 | packages/tester/harmony/react_native_openharmony/src/main/cpp 与 .../ets | RNOH C++ 内核(含 NAPI 桥)、ReactCommon 同步副本、ArkTS/ArkUI 侧运行时 |
| ⑤ 工具链层 | react-native-harmony-cli、@rnoh/hvigor-plugin、scripts/*.ts | init / codegen / bundle / run、构建插件、补丁重放与上游同步 |
层与层之间靠两条链贯穿:
(一)构建期的"重放链"——由 scripts/integrate-upstream-code.ts 串起,顺序是:
清空子模块(reset --hard + clean -dfx)
→ git apply react-native.patch
→ 应用 boost 补丁
→ 同步 delegates / Libraries / src / types
→ 同步 ReactCommon(→ tester 侧 third-party/rn/ReactCommon)
→ 收尾:把子模块复原
"收尾复原"这一步很关键:它保证了 packages/react-native 始终是干净的上游码,
补丁是唯一的事实来源——补丁对,结果就对;补丁错,一眼可见。
(二)运行期的"解析链"——由 metro 与宿主环境串起:
JS 里 import ... from './delegates/ButtonDelegate'
→ metro 以 platform = 'harmony' 解析
→ 命中 ButtonDelegate.harmony.ts(平台扩展优先)
→ 内部状态经 NAPI 桥进入 C++ 内核
→ 落到 ArkUI 的组件树上完成渲染
看懂这两条链,后面所有细节都只是它们的展开。
三、补丁层解剖:133 个文件、45 个新增,落在哪里
先给规模。packages/react-native.patch 在 0.88.0 上的实测数据:
| 指标 | 值 |
|---|---|
| 改动文件 | 133 |
| 新增文件 | 45 |
| 行数 | +13663 / -231 |
补丁内出现 harmony | 66 次 |
补丁内出现 OHOS / ohos / rnoh | 62 次 |
按区域分布(改动文件数):
| 区域 | 文件数 | 说明 |
|---|---|---|
Libraries/ | 72 | JS 组件与工具:平台分支、委托调用点 |
ReactCommon/ | 52 | C++ 内核:CMake、JSI、渲染器、Hermes、Yoga |
types_DEPRECATED/ | 7 | 类型声明(0.88 上游把 types 改名,见第六节) |
src/ | 1 | setUpDefaultReactNativeEnvironment.js 的环境装配 |
delegates/(包根) | 1 | DelegateError.ts:委托层的错误约定 |
再看那 45 个新增文件的构成,这是最能说明设计取向的一组数据:
- 绝大多数是
delegates/下的三件套——按组件成对出现,例如
Components/delegates/BaseButtonDelegate.ts+ButtonDelegate.ts、
Components/ScrollView/delegates/BaseScrollViewDelegate.ts+ScrollViewDelegate.ts+types.ts、
Components/TextInput/delegates/(含状态类委托)、Components/Touchable/delegates/(三组)、
以及Alert/、AccessibilityInfo/、Keyboard/、RefreshControl/、StatusBar/、
DrawerAndroid/、ReactNative/(Bridgeless、I18nManager)、NativeComponent/(ViewConfigIgnore)、Share/等。 - 少量是运行时增强:
ReactCommon/react/renderer/uimanager/UIManagerNodeOperationHook.h、
ReactCommon/react/renderer/components/view/platform/cxx/.../HostPlatformViewProps.cpp。 - 个别是能力补齐:
Libraries/RegisterPageName/RegisterPageName.ts(页面登记,鸿蒙多实例/多窗口场景要它)、
delegates/DelegateError.ts(统一"平台不支持"的错误类型)。 - 还有一处容易被忽略但很有代表性的:
Libraries/Renderer/implementations/ReactFabric-prod-batch.js——
Fabric 渲染器的产物文件。也就是说这套适配连渲染器产物都在补丁内对齐,而不是靠构建时现场生成。
把这 45 个文件读成一个句子:
补丁的主要工作不是"改鸿蒙的实现",而是"给上游装上可被鸿蒙替换的挂载点"。
挂载点就是委托(delegate)。下一节展开。
四、JS 层机制(一):Platform.select 里的 harmony 分支与"委托三件套"
上游 RN 的组件里,平台差异本来就有一个标准写法:
const styles = StyleSheet.create({
button: Platform.select({
ios: { /* iOS 样式 */ },
android: { elevation: 4, backgroundColor: '#2196F3', borderRadius: 2 },
}),
});
加一个平台,最直接的做法是在这里再插一个 harmony: 分支。补丁确实这么做了,但它没有把鸿蒙的样式值直接写进来,
而是把值的来源抽出去,交给一个"委托"对象:
// packages/react-native-harmony/Libraries/Components/Button.js(节选,实测行号)
import ButtonDelegate from './delegates/ButtonDelegate'; // :29
const DELEGATE = new ButtonDelegate({}); // 组件外实例化一次
// :280
const innerViewFocusable = DELEGATE.getInnerViewFocusable();
// :321-365
const styles = StyleSheet.create({
button: Platform.select({
harmony: DELEGATE.getButtonStyle(),
ios: {},
android: { elevation: 4, backgroundColor: '#2196F3', borderRadius: 2 },
}),
text: {
textAlign: 'center',
margin: 8,
...Platform.select({
harmony: DELEGATE.getTextStyle(),
ios: { color: '#007AFF', fontSize: 18 },
android: { color: 'white', fontWeight: '500' },
}),
},
buttonDisabled: Platform.select({
harmony: DELEGATE.getButtonDisabledStyle(),
ios: {},
android: { elevation: 0, backgroundColor: '#dfdfdf' },
}),
textDisabled: Platform.select({
harmony: DELEGATE.getTextDisabledStyle(),
ios: { color: '#cdcdcd' },
android: { color: '#a1a1a1' },
}),
});
注意:上游的 iOS / Android 分支一个字节都没动,只是并列多了 harmony。
而被 import 的那个 ./delegates/ButtonDelegate,在不同平台上会解析到不同的文件——这是这套机制的核心,
也是"三件套"的由来:
| 文件 | 角色 | 内容要点 |
|---|---|---|
delegates/BaseButtonDelegate.ts | 抽象契约 | 声明 5 个方法:getButtonStyle / getTextStyle / getButtonDisabledStyle / getTextDisabledStyle / getInnerViewFocusable |
delegates/ButtonDelegate.ts | 默认实现(补丁新增,在上游树里) | 样式一律返回 {},getInnerViewFocusable() 返回 true |
delegates/ButtonDelegate.harmony.ts | 鸿蒙覆盖(rnoh 侧提供) | 返回鸿蒙观感:背景 #317aff、圆角 8;禁用态 opacity: 0.4;文字白色 16px;getInnerViewFocusable() 返回 false |
// rnoh 侧的真实实现(ButtonDelegate.harmony.ts,节选)
export default class ButtonDelegate extends BaseButtonDelegate {
override getButtonDisabledStyle(): ViewStyle { return { opacity: 0.4 }; }
override getButtonStyle(): ViewStyle {
return { backgroundColor: '#317aff', borderRadius: 8 };
}
override getTextStyle(): TextStyle {
return { color: '#FFFFFF', fontSize: 16 };
}
override getInnerViewFocusable(): boolean { return false; }
}
这一处分层,恰好一次回答了第一节里那三条约束:
- 行为中性——iOS / Android 拿到的仍是
ButtonDelegate.ts(默认实现),返回值与改动前一致; - 贴着上游写法——上游组件只多了两行(一行 import、一行实例化),没有引入新的抽象层,也没有重写组件逻辑;
- 可预期——鸿蒙的差异被集中在一个明确的地方,想改样式不必回到上游文件里翻找。
补丁里 harmony 一词出现 66 次,绝大多数就是这种"并列一个分支 + 把值交给委托"的形态。
五、JS 层机制(二):.harmony.* 平台扩展与 metro 解析链
委托解决的是"值级差异",还有一类差异是整个文件级的——某个模块在鸿蒙上需要完全不同的实现。
RN 对此早有约定:foo.ios.js / foo.android.js 会在对应平台上替换 foo.js。
这套适配把 harmony 变成了同等级的平台名,于是在 rnoh 包里能看到 37 个平台专属文件,覆盖的面相当广:
| 文件 | 替换掉的上游实现 | 鸿蒙要处理的问题 |
|---|---|---|
Core/setUpPlatform.harmony.js | 平台初始化 | 平台初始化时机与宿主环境 |
NativeComponent/BaseViewConfig.harmony.js | ViewConfig | 属性映射与原生组件描述 |
Utilities/NativePlatformConstantsHarmony.harmony.ts | 平台常量 | Platform.constants 的鸿蒙取值 |
StyleSheet/NativePlatformColor.harmony.ts、PlatformColorValueTypes.harmony.ts | 平台颜色 | 系统色与颜色类型转换 |
Image/Image.harmony.ts、AssetSourceResolver.harmony.ts | 图片与资源解析 | 资源定位(rawfile / 沙箱路径)而非 bundle 路径 |
Alert/AlertManager.harmony.ts、AlertDelegate.harmony.ts | 弹窗 | 系统弹窗能力对接 |
Vibration/VibrationDelegate.harmony.ts、Settings.harmony.ts | 振动、设置 | 系统能力对接 |
NativeComponent/delegates/ViewConfigIgnoreDelegate.harmony.ts | ViewConfig 例外项 | 鸿蒙不支持的属性静默忽略 |
而让 harmony 能被解析到,靠的是 metro 侧的一段配置
(packages/react-native-harmony/metro.config.js,实测节选):
const HARMONY_PLATFORM_NAME = "harmony";
// ...
// 请求解析时按 harmony 平台处理
if (REQUEST_RESOLUTION_LATEST_PLATFORM === "harmony") { /* ... */ }
// 包名重定向:默认值仍需是 "react-native-harmony"(改成 @react-native-oh/... 会是 breaking change)
options?.reactNativeHarmonyPackageName ?? "react-native-harmony"
const harmonyResolutionResult = resolveHarmonyPlatformRequest(/* ... */);
if (harmonyResolutionResult) { return harmonyResolutionResult; }
配套地,打包链路上平台名是显式传入的——CLI 的 bundle-harmony 命令在调用 metro 时写死 platform: 'harmony',
产物是 bundle.harmony.js(Hermes 引擎下为 hermes_bundle.hbc):
// packages/react-native-harmony-cli/src/commands/bundle-harmony.ts
platform: 'harmony',
两条机制合起来,就是这套适配的 JS 层全貌:
文件级差异用平台扩展(
.harmony.*),值级差异用委托(delegate);两者都不修改上游的 iOS / Android 分支。
顺带一个容易被忽略的细节:metro 配置里还留了一段模块重定向的注释,用于把某些上游模块
重定向到 interop 包(react-native-harmony-61-interop),再从中转回 rnoh 包——
这是为跨版本兼容留的后门:老版本产物的引用路径可以在不改上游的前提下被接管。
六、类型层:0.88 把 types 改名了,鸿蒙怎么接
0.88 有一个静悄悄的破坏性变更:上游把弃用类型的声明目录从 types 改名为 types_DEPRECATED。
对"只认路径"的同步脚本来说,这会让鸿蒙侧的类型目录一夜蒸发。
处置方式是在同步环节做一次显式映射,并把这个决定写进代码注释(scripts/integrate-upstream-code.ts):
// 0.88 起上游把弃用类型目录从 types 改名为 types_DEPRECATED;
// rnoh 包自己的目录名保持 types 不变。
syncJS("types_DEPRECATED", "types");
也就是说:上游改它的名,鸿蒙对外仍是 types——使用者的 import 路径不受影响。
补丁在这个目录下改了 7 个文件,都是"给类型补上鸿蒙这一支":
Libraries/Utilities/Platform.d.ts、Libraries/Lists/FlatList.d.ts、Libraries/Lists/VirtualizedList.d.ts、
Libraries/Components/ScrollView/ScrollView.d.ts、Libraries/EventEmitter/NativeEventEmitter.d.ts、
Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts、public/DeprecatedPropertiesAlias.d.ts。
类型层常被当成"文档工作",但它的作用很实际:TypeScript 用户看到的 API 形状,就是这几个文件决定的。
Platform.OS 是否包含 'harmony'、Platform.select 是否接受 harmony 键,都在这里落地。
七、C++ 层:ReactCommon 那 52 个文件改了什么
JS 层的差异只是"表层"。RN 0.88 的运行时是新架构——Fabric 渲染器 + TurboModules + JSI,
这些在鸿蒙上最终要落到 NAPI 与 ArkUI,中间那一大段是 C++。补丁里 52 个 ReactCommon 文件,
按主题可以归成下面几类(其中 10 个是 CMakeLists.txt,说明构建系统本身也要跟着改):
| 主题 | 代表文件 | 要解决的问题 |
|---|---|---|
| 构建系统 | jserrorhandler/CMakeLists.txt、jsinspector-modern/{cdp,network,tracing}/CMakeLists.txt、react/nativemodule/{core,defaults}/CMakeLists.txt、react/renderer/{core,components/view,uimanager}/CMakeLists.txt | 目标依赖与头文件搜索路径——鸿蒙侧的链接关系与上游不同,必须显式声明 |
| JSI / cxxreact | jsi/jsi/jsi.h、jsi/jsi/jsi.cpp、cxxreact/TraceSection.h | 桥接层的基础类型与追踪埋点 |
| 渲染器核心 | renderer/core/{Props,RawProps,RawPropsParser,ShadowNode,ShadowNodeFamily}、renderer/uimanager/{UIManager,UIManagerBinding,primitives.h}、renderer/mounting/TelemetryController | 属性解析、ShadowTree、挂载与遥测管线 |
| 组件与文本 | renderer/components/scrollview/ScrollEvent、renderer/components/text/ParagraphShadowNode、renderer/components/textinput/TextInputState.h、renderer/attributedstring/{ParagraphAttributes,conversions.h} | 滚动事件、段落与输入态——鸿蒙组件的对应关系 |
| 图形与布局 | renderer/graphics/platform/cxx/.../Float.h、renderer/components/view/YogaLayoutableShadowNode、yoga/{YGConfig,config/Config,node/Node} | 浮点精度与 Yoga 配置,布局结果必须与 ArkUI 对得上 |
| NativeModule | react/nativemodule/defaults/DefaultTurboModules.cpp | 默认 TurboModule 集合的取舍 |
| Hermes 运行时 | hermes/executor/HermesExecutorFactory.cpp、react/runtime/hermes/HermesInstance.cpp | 内存水位与异常上报(下一节展开) |
| 新增能力 | renderer/uimanager/UIManagerNodeOperationHook.h、renderer/components/view/platform/cxx/.../HostPlatformViewProps.cpp | 节点操作钩子、平台 ViewProps——上游留白处补位 |
这里有一个值得单独点出的"新增":UIManagerNodeOperationHook.h。它不是改上游的某个函数,
而是在 UIManager 的节点操作链上插了一个钩子。这类设计的价值在于——鸿蒙侧可以在不改上游逻辑的前提下,
观察甚至介入每一次节点操作(用于调试、性能分析或一致性校验),而上游的默认路径完全不变。
再看 ViewPropsInterpolation、YogaLayoutableShadowNode、graphics/platform/cxx/Float.h 这几个:
它们是"同一份布局代码要在两套坐标系/精度下算出同一个结果"的战场。布局差一点,真机上就是错位,
这类改动通常不会出现在功能清单里,但它决定了渲染的"像不像"。
八、Hermes:从内存水位到鸿蒙的异常上报链路
上面 52 个文件里,有两个专属于 Hermes:HermesExecutorFactory.cpp 与 HermesInstance.cpp。
它们承载了一条完整的链路——把 Hermes 的堆内存水位,接到鸿蒙系统的内存异常上报框架上。
这条链接起来是这样的:
Hermes 每次 GC 回调 onHermesGCAnalytics(event)
→ 取 event.size.after 作为当前堆大小
→ 与 rnoh::getHermesMemoryWaterMarkBytes()(水位阈值)比较,未超则返回
→ 超阈值:tryMarkHermesWaterMarkExceededForProcessLifetime() 保证"进程生命周期内只报一次"
→ 记录 WARN 日志(LOG_TAG = "HermesGC")
→ dlsym 动态取 OH_HiAppEvent_ReportFrameworkMemAnomaly,调用上报
├─ 系统 API 低于 26 → 记一条 WARN 后跳过
├─ 符号不存在(老设备/老系统)→ 记一条 WARN 后跳过
└─ 返回 0 视为成功,其余按失败打 ERROR
几个工程细节值得注意(均在 HermesExecutorFactory.cpp / HermesInstance.cpp 中):
- 用
dlsym(RTLD_DEFAULT, ...)而不是直接链接符号,并且用std::call_once只解析一次。
这样在缺少该能力的系统上不会链接失败,而是降级为"记一条警告、跳过上报"。 - API 版本门禁:
rnoh::IsAtLeastApi26()不满足时直接返回,并通过OH_GetSdkApiVersion()把实际版本打进日志——
线上排查时能一眼看出"是系统不支持,还是水位没到"。 - 上报去重:
tryMarkHermesWaterMarkExceededForProcessLifetime()把"已上报"记为进程级状态,
避免 GC 频繁触发时把系统上报接口打爆。 - 框架标识:上报时使用
FRAMEWORK_TYPE_REACT_NATIVE_HERMES = 1,让系统侧能区分是 Hermes 的问题还是宿主的问题。 - 两个入口各一处:
.withAnalyticsCallback(onHermesGCAnalytics)在
HermesExecutorFactory.cpp:282(桥接模式)与HermesInstance.cpp:169(Bridgeless 模式)各挂一次,
两条运行时路径都要覆盖。
配套的版本与体积问题也在这里处理:
- Hermes 子模块固定在
3477757eb(Prepare fixes for Hermes 250829098.0.17),
根package.json再用overrides把hermes-compiler锁到250829098.0.17,确保编译器与运行时不漂移; - 体积上,
cpp/patches/hermes-trim-cmake.patch(27 行)由构建脚本幂等应用
(先git apply --reverse --check判断是否已应用,已应用则跳过),配合三方库与 include 目录裁剪,
把不需要的产物从 HAR 里剔出去——日志里的 Third-party trimmed / include directory hermes trimmed
就是这一步。
九、原生层:三套补丁机制,与一个 127 文件的内核
到这里,JS 与 C++ 都讲完了,但还差一块:鸿蒙自己的运行时内核。它不在上游源码里,而是这套适配的"下半身"。
先补一个容易混淆的点:这套适配其实用了三种不同形态的补丁机制,各管一摊。
| 机制 | 载体 | 作用对象 | 为什么用这种形态 |
|---|---|---|---|
| ① git 补丁 | packages/react-native.patch | 上游 JS 源码 + ReactCommon | 需要"可复现重放、可一键复原",git 补丁最合适 |
| ② include 覆盖层 | cpp/patches/react_native_core/(33 个源文件,不是 .patch) | 被编译的 ReactNative 核心 C++ | 需要整文件替换(如文本布局的实现),靠头文件搜索顺序生效 |
| ③ 体积裁剪补丁 | cpp/patches/hermes-trim-cmake.patch | Hermes 的 CMake | 需要幂等地反复应用,“已应用则跳过” |
第 ② 种最值得说。它不是"打补丁",而是把整个文件塞进编译器的头文件搜索路径前面:
# packages/react-native-harmony/scripts/resources/react-native-harmony.cmake(节选)
${RNOH_CPP_DIR}/patches/react_native_core
${RNOH_CPP_DIR}/patches/react_native_core/react/renderer/textlayoutmanager/platform/harmony/
于是当编译某个上游 C++ 目标时,#include "react/renderer/textlayoutmanager/TextLayoutManager.h"
命中的是覆盖层里的鸿蒙实现(TextLayoutManager.h / .cpp),而不是上游版本——
上游源码一个字节没改,行为却在鸿蒙侧被整段替换。覆盖层的 33 个文件主要集中在
文本布局(textlayoutmanager)、文本输入(textinput/TextInputState、conversions、ComponentDescriptor)
这些"与平台排版引擎强耦合"的地方,这也说明了鸿蒙适配真正难啃的骨头在哪。
至于最厚的那一层:RNOH 的 C++ 内核,位于 tester 的
src/main/cpp/RNOH/——仅顶层就有 127 个 .h/.cpp(整个 cpp 目录排除三方库后共 1269 个)。
从文件命名就能读出它的职责分工:
| 组别 | 代表文件 | 职责 |
|---|---|---|
| 实例与生命周期 | RNInstance.cpp/.h、RNInstanceCAPI、RNInstanceInternal | RN 实例创建、销毁、多实例管理 |
| ArkTS 桥 | ArkTSBridge、ArkTSChannel、ArkTSMessageHub、ArkJS | ArkTS ↔ C++ 的双向通道与消息分发 |
| TurboModule | TurboModule、TurboModuleFactory、TurboModuleProvider、ArkTSTurboModule、StubModule | 让 TurboModule 落在 ArkTS 上 |
| 组件与挂载 | ComponentInstance、CppComponentInstance、RNArkTSComponentDelegate、MountingManagerArkTS、MountingManagerCAPI、MutationsToNapiConverter、ShadowViewRegistry | ShadowTree 变更转成 ArkUI 的节点操作 |
| 文本与字体 | TextMeasurer、TextStorage、TextStorageBuilder、FontRegistry、TextConversions、TypographyStyleWrapper | 与 ArkUI 排版/字体对齐 |
| 稳定性 | HiCollieFreezeHandler、FreezeStackCollector、ExceptionStackHandler、ParallelCheck、ApiVersionCheck.h | 冻屏检测、异常栈、并行校验、API 版本门禁 |
| 渲染时序 | VSyncListener、EventBeat、HarmonyTimerRegistry、HarmonyAnimationChoreographer、SchedulerDelegate、TaskExecutor | 与鸿蒙的 VSync/定时器/任务队列对接 |
| 调试与性能 | Inspector、InspectorHostTarget、InspectorNetworkReporter、Performance/、NativeLogger、LogSink | DevTools 协议、网络上报、日志 |
ArkTS/ArkUI 侧则是 src/main/ets/ 下的 37 个 .ets 文件:RNApp.ets、RNSurface.ets、
RNOH/RNAbility.ets(Ability 生命周期与 RN 实例的绑定)、LogBoxDialog.ets、RNOHErrorDialog.ets
以及 RNOHCorePackage/、HttpClient/、RemoteImageLoader/ 等。C++ 负责"算",ArkTS 负责"接":
一边是 Fabric 的 ShadowTree 与 JSI 调用,另一边是 ArkUI 的组件树与系统能力。
十、工程依赖层:0.88 的包拆分,带来两个真踩到的坑
RN 0.88 把原先塞在一个包里的东西拆成了一串 @react-native/* 作用域包。rnoh 包因此要同时对齐
assets-registry、js-polyfills、normalize-colors、virtualized-lists、babel-preset、
metro-config、typescript-config、eslint-config、codegen,并把 react-native 的 peer 版本锁到 0.88.0。
版本对齐本身是体力活,但这次真正卡住的是下面两件事——它们都不是"代码问题",却能让适配停摆。
坑一:react-native 本体还没上架,但 peer 已经点名要它。
发布当天的时间线很微妙:@react-native/* 系列包先在 19:40 上架,而 react-native 本体直到 22:08 才上架。
偏偏 @react-native/virtualized-lists@0.88.0 的 peerDependencies 里写死了 react-native: "0.88.0",
而 pnpm 默认从工作区根解析 peer——三条凑在一起,pnpm install 直接以
ERR_PNPM_NO_MATCHING_VERSION 失败。报错只说"某个直接依赖失败",并不指名道姓是谁在要这个版本,
最后是把各 @react-native/* 包的依赖声明逐个翻过之后才在 virtualized-lists 的 peer 上抓到。
处置是先"临时桥接"(把定版源码打成随仓包、并在工作区根声明同名依赖),等本体上架后切回 registry 并删除临时包。
坑二:pnpm 10 不再读 package.json 里的 pnpm 字段。
仓库里那处 overrides 一直处于"写了但没生效"的状态——它待在 package.json 的 pnpm 字段里,
而 pnpm 10 已经改为从 pnpm-workspace.yaml 读取。已迁移过去。
此外还有两个"版本号一动就会咬人"的细节,记在这里当路标:
minimumReleaseAge与新版本的关系。 工作区配置了 3 天的发布延迟(minimumReleaseAge: 4320),
这是防供应链投毒的合理设置,但它意味着上游刚发版的 3 天内,任何"重新解析"都会被过滤掉。
已提交锁定文件的安装不受影响(走锁定结果),但只要有人删掉锁文件或新增依赖触发解析,就会撞上。
这次生成锁文件时把它临时置 0,完成后再恢复。- hvigor 插件包是按版本号命名的构建产物。
hvigor-config.json5引用的
rnoh-hvigor-plugin-0.88.0.tgz并不入库,需要重新生成;忘了它,构建会一路走到 hvigor 阶段才抛ENOENT。
十一、构建与验证:一条能反复跑通的流水线
适配做得好不好,最终由"能不能稳定构建出来"裁决。这条链路是:
pnpm init-ws
├─ 初始化子模块(含三方库)
├─ pnpm i
├─ _integrate-upstream-code ← 重放补丁 + 同步上游代码(第二节的那条链)
├─ _recreate-hvigor-plugin ← 生成按版本号命名的 hvigor 插件包
└─ 各包 setup
pnpm build:rnoh
└─ scripts/build-har.js
├─ 定位 DevEco 自带的 cmake / ninja
├─ cmake 配置
├─ 幂等应用 hermes-trim-cmake.patch
├─ 裁剪三方库与 include 目录(Third-party trimmed / …hermes trimmed)
├─ 版本号占位符替换(__VERSION_MAJOR__ 等 → 0.88.0)
└─ hvigorw assembleHar(module=react_native_openharmony@default, debug)
其中 codegen 与 autolink 是构建期由 hvigor 插件驱动的,不是人工步骤。本次构建的实测输出:
- codegen 生成 5 个文件:
RNOHGeneratedPackage.h、generated/{components/ts.ts, index.ets, ts.ts, turboModules/ts.ts}; - autolink 更新 4 个文件:
RNOHPackagesFactory.h、autolinking.cmake、RNOHPackagesFactory.ets、oh-package.json5(linked 0、skipped 2); - 编译耗时分布:ArkTS 编译 约 12–13 秒,C++ 构建(
BuildNativeWithNinja)约 2 分 26 秒–2 分 32 秒,
打包 HARPackageHar1–4 秒,整体退出码 0。
C++ 侧还有一条不依赖真机的验证通路:cpp/tests 下的宿主 gtest
(RNInstanceTest、ComponentInstanceTest、ArkUISurfaceTest、RNOHAppNapiBridgeTest,
以及 animated/、arkui/、core/、e2e/、image_cache_interceptor_test/ 等分组),
用 clang 的源码级覆盖率采集(.profraw → llvm-profdata → llvm-cov,同时统计行与分支),
另有两个质量闸门:scripts/gtest-quality-check.js(断言/隔离/稳定性规则)与
scripts/gtest-coverage-check.js(覆盖率阈值)。
一条经验,值得单独说明:这次把依赖从"临时随仓包"切回 registry 之后,构建是重新跑了一遍的。
原因是随仓包是从打过补丁的源码目录打出来的,而 registry 上是上游原始包,两者内容并不相同——
“能装上"不等于"能构建”,只有实跑才能确认。
这篇文章的数据怎么复核
本文所有数字都可以在仓库里一条命令验出来:
cd packages/react-native-harmony/.. # 仓库根
git ls-tree HEAD packages/react-native # → 5c35c82cf(v0.88.0)
grep -c "^diff --git" packages/react-native.patch # → 133
grep -c "^new file mode" packages/react-native.patch # → 45
grep -ci harmony packages/react-native.patch # → 66
find packages/react-native-harmony -name "*.harmony.*" | wc -l # → 37
find packages/tester/harmony/react_native_openharmony/src/main/cpp/patches/react_native_core -type f | wc -l # → 33
ls packages/tester/harmony/react_native_openharmony/src/main/cpp/RNOH/*.h \
packages/tester/harmony/react_native_openharmony/src/main/cpp/RNOH/*.cpp | wc -l # → 127
十二、为什么这套机制能做到"0day"
回到最初那个问题:上游 0.88.0 定版(npm 上架 22:08),适配在 16 分钟后落地(22:24)并随即发版(22:28)。
这不是手速,而是结构决定的:
- 补丁是唯一事实来源。 上游源码永远是 pristine 的,所有鸿蒙改动都在补丁与覆盖层里。
于是"切换上游版本"退化成"重放补丁"这一件确定的事——本次在 0.88.0 上重放,133 个文件零冲突。 - 补丁是上游友好的。 它给上游加
harmony分支、装委托挂载点,不改 iOS / Android 的行为。
好处不只是"不冒犯上游",更实际的是——下一次上游改动不会正面撞上我们的改动。 - 验证不依赖设备。 锁文件安装、
assembleHar构建、宿主 gtest,都在本机完成。
发版不必等真机排队,这一步省下来的时间,比写代码省下来的更多。 - 发版的"形状"是固定的。 发布说明、变更记录、tag 与 Release 都有既有格式可循,
不占适配的关键路径。
也要说清楚它的边界:0day 不是"零工作量",而是把工作量前移到 rc 阶段。
0.88 这条线从 rc.0 一路跟到定版,每一版都在修构建阻塞、补类型、对齐上游改动——
那些"看不见的铺垫",才是定版当晚能 16 分钟收尾的真正原因。
另外,并非所有环节都能提前演练:这次"本体未上架、peer 却已点名"的依赖时序问题,
就是在发版窗口里才暴露的。它已经被写进发布说明与变更记录,下一次遇到就有路可循。
最后一点观察。很多人把"打补丁"默认当成技术债,仿佛只要改上游就等于埋雷。
这件事恰好提供了一个反例:补丁是不是债,取决于三件事——它是否贴着上游的写法、是否可复现重放、是否有测试兜底。
这三条成立时,补丁不是债,而是一层可以长期维护、并在每个上游版本上"当场兑现"的适配层。
上游每发一版,鸿蒙这边有人在同一天接住——这一次,是 16 分钟。
https://atomgit.com/CPF-RN/ohos_react_native
更多推荐



所有评论(0)