React Native for OpenHarmony 实战:三方库 react-native-tooltip 的鸿蒙化适配指南
本文记录把 react-native-tooltip(长按弹出的操作菜单)在 HarmonyOS 上验证的完整过程。
这一篇的结论和之前不同:它的核心功能在鸿蒙上【不可用】。
但实测过程推翻了一个流传的说法 —— 它的交付包文档写着"导入即失败",而实测【导入、打包、构建、页面渲染全都成功】。
所以本文分三层给结论:
| 层次 | 结论 | 证据 |
|---|---|---|
静态层(import / 打包 / HAP 构建 / 读取契约) | ✅ 完全可用 | bundle exit=0、BUILD SUCCESSFUL 3分45秒、页面正常渲染 |
渲染层(把 <ToolTip> 放进页面) | ⚠️ 未验证(预期会失败) | 它要渲染一个 iOS 原生组件 RCTToolTipText,见第二节 |
| 功能层(长按弹出菜单) | ❌ 必然不可用 | NativeModules.ToolTipMenu 实测是 null ⇒ 调 .show() 必然抛 TypeError |
为什么仍然值得写这一篇:这种"看起来能用、实际用不了"的库最容易坑人 ——
它 import 得进来、typeof 是个 function、propTypes 和 defaultProps 都读得到,
只有真正去调 showMenu() 时才会崩。本文把这条边界划清楚。
★★★★★ 一、实测推翻了交付包文档的「导入即失败」
交付包自带的 README.OpenHarmony_CN.md 里有这么几句:
「
main指向ToolTip.ios.js,通过NativeModules.ToolTipMenu调用 iOS 原生组件;鸿蒙没有对应实现。」
「ToolTip… 鸿蒙:不支持」
「导入即失败,见注意事项」
"导入即失败"这个说法,实测不成立。
第一步:打包
★ bundle exit=0
⇒ Metro 打包成功 ✓
第二步:HAP 构建
> hvigor BUILD SUCCESSFUL in 3 min 45 s 393 ms
hvigor exit=0
hap = 10/18/2026 14:03:57
⇒ HAP 构建成功 ✓
第三步:真机运行 / 页面渲染
✓ 「react-native-tooltip 鸿蒙自检台」 w=1248 h=63 x=36 y=144
✓ 「TTP 导入对象的 typeof = function」 w=1200 h=46 x=60 y=1142
✓ 「TTP action 示例文本:TPP 复制 / TPP 粘贴」 w=1200 h=46 x=60 y=1188
✓ 「TTP 结论:import 到达了?NativeModules.ToolTipMenu 是 undefined?」 w=1200 h=92 x=60 y=1403
⇒ 命中 4 / 4
⇒ 页面正常渲染、四处文本全中、无红屏、无 LogBox ✓
为什么能导入成功? 因为它那一行 import 并没有"抛错"的动作:
const ToolTipMenu = NativeModules.ToolTipMenu; // 取到 null,不抛
const RCTToolTipText = requireNativeComponent('RCTToolTipText'); // 只是登记名字,不抛
⇒ **两行都只是"取值"和"登记",没有立即校验 ⇒ 模块求值阶段不会失败 ✓
⇒ 失败被推迟到"真正调用"或"真正渲染"的时候 —— 这才是它的真实行为。
这条经验值得单独记:
"导入即失败"和"调用才失败"是两件事。 文档说前者,实测是后者。
对使用者来说这是个更糟的消息 —— 因为编译期/启动期看不出来,要等到用户长按才崩。
★★★★★ 二、NativeModules.ToolTipMenu 实测是 null —— 一个值得记住的细节
这是本轮最有意思的发现。
页面里的三条日志:
NOTE | NativeModules 是否可得 = true
NOTE | NativeModules 里有 'ToolTipMenu' 吗 = true ← in 运算返回 true!
NOTE | NativeModules.ToolTipMenu 的值 = null ← 但取出来是 null
NOTE | NativeModules 总键数(仅供参考) = 0 ← 而它是个空对象
NOTE | NativeModules 里含 'ToolTip' 的键 = (无) ← 遍历也遍历不到
这三条放在一起,说明了一个反直觉的事实:
| 观察 | 含义 |
|---|---|
Object.keys(NativeModules).length === 0 | 它没有任何自有键 |
'ToolTipMenu' in NativeModules === true | 但 in 判断返回真 |
NativeModules.ToolTipMenu === null | 而取值得到的是 null(不是 undefined) |
⇒ ⇒ 三者只有在 NativeModules 是个 Proxy(或用了 get 陷阱的对象)时才同时成立 ✓
这在实践上意味着两件事:
-
不能用
'xxx' in NativeModules判断"这个原生模块是否存在" —— 它永远返回true✗
正确做法:取出来判断是否null/undefined,或者检查typeof。 -
未注册的模块返回
null,不是undefined✓
所以if (!NativeModules.ToolTipMenu)这类判空能过,
但if (NativeModules.ToolTipMenu === undefined)会判错。
于是这个库在鸿蒙上的实际失败点是:
showMenu = () => {
ToolTipMenu.show(findNodeHandle(this.refs.toolTipText), …); // ← ToolTipMenu 是 null
}
⇒ 调用 showMenu() 会抛 TypeError: Cannot read property 'show' of null ✗
⇒ 而且它是在用户长按/点击之后才发生 ⇒ 启动时完全看不出来。
★★★ 三、它确实是原生库,而 spec.json 写错了
交付包里同时存在 JavaScript 层和 Objective-C 原生层:
ToolTip.ios.js 2701 B ← main 指向它
ToolTip.android.js 181 B ← 空 stub
ToolTipMenu/RCTToolTipText.h 336 B
ToolTipMenu/RCTToolTipText.m 2230 B ← 原生实现
ToolTipMenu/RCTToolTipTextManager.h 95 B
ToolTipMenu/RCTToolTipTextManager.m 337 B
ToolTipMenu/ToolTipMenu.h 123 B
ToolTipMenu/ToolTipMenu.m 1890 B ← 原生实现
ToolTipMenu.xcodeproj/project.pbxproj 14713 B
⇒ 共 5 个 .h/.m + 一个完整的 Xcode 工程 ⇒ 毫无疑问是个原生库 ✓
但 spec.json 里写着:
"native": false,
⇒ ⇒ 这个字段是错的 ✗ —— 它有原生代码、它 main 指向的 JS 依赖 iOS 原生模块。
⇒ 这个字段会误导读者以为"纯 JS,装上就能用"。
另外 ToolTip.android.js 也值得看一眼(181 B 全文):
'use strict';
var warning = require('warning');
var ToolTip = {
test: function() {
warning("Not yet implemented for Android.");
}
};
module.exports = ToolTip;
⇒ ⇒ 上游对 Android 的支持就是一个"打警告的空对象" ✓
⇒ 这说明上游本身只实现了 iOS,Android 从来没做过 ——
鸿蒙要支持它,等于要从零写一套原生 ToolTip 菜单,不是"适配"能解决的。
★★★ 四、即使原生模块存在,这库还有一处过不了的关
ToolTip.ios.js 的 render():
render() {
return (
<RCTToolTipText ref='toolTipText' onChange={…} onBlur={…}>
<TouchableHighlight {…this.getTouchableHighlightProps()}>
<View>{this.props.children}</View>
</TouchableHighlight>
</RCTToolTipText>
);
}
showMenu 里:
ToolTipMenu.show(findNodeHandle(this.refs.toolTipText), …)
⇒ ⇒ 它用的是 ref='toolTipText'(字符串 ref)+ this.refs.toolTipText ✗
字符串 ref 在 React 19 已被移除(这个能力从 React 16 起就已废弃,19 里彻底删掉)。
⇒ 所以即便你在鸿蒙上补出一个 ToolTipMenu 原生模块,
this.refs.toolTipText 仍然是 undefined ⇒ findNodeHandle(undefined) 也拿不到节点 ⇒ show 的参数是错的 ✗
⇒ ⇒ 也就是说:这个库要能用,需要动三处 ——
① 写一个鸿蒙原生 ToolTip 菜单;② 把 requireNativeComponent('RCTToolTipText') 换成 JS 实现或鸿蒙原生组件;③ 把字符串 ref 改成 createRef/useRef。
这已经超出"适配"的范畴,属于重写。
★★★★ 五、静态层的内容都是好的:契约 9 条全 PASS
虽然功能不可用,但能读到的那部分都如实反映源码,这部分判据全 PASS:
NOTE | import 是否成功 = true
NOTE | 默认导出 ToolTip 的 typeof = function
PASS | ★★★ typeof ToolTip 记录(class 应为 function)
NOTE | ToolTip.prototype.render = function
PASS | ★★★ 有 prototype.render(若是 class 组件)
PASS | ★★ PureComponent(isPureReactComponent === true)
NOTE | propTypes 的键数 = 5
NOTE | propTypes 的键 = actions, arrowDirection, longPress, onHide, onShow
PASS | ★★★ 5 个自有 propTypes(actions/arrowDirection/longPress/onHide/onShow)都在
NOTE | defaultProps 的键 = arrowDirection, onHide, onShow
PASS | ★★★ defaultProps.arrowDirection = 'down'
PASS | ★★★ defaultProps.onHide 是 function
PASS | ★★★ defaultProps.onShow 是 function
NOTE | 箭头字段里出现在 prototype 上的 = (无,符合预期)
PASS | ★★★ 7 个箭头字段【不在】prototype 上
PASS | ★★ Platform.OS === "harmony"
从这些能读出三件事 ✓:
ToolTip是 class,且是PureComponent(isPureReactComponent === true)propTypes只有 5 个自有键 —— 因为源码是...TouchableHighlight.propTypes展开,
而TouchableHighlight.propTypes在 React 19 里可能已不存在 ⇒ 展开出来是空 ✓
(实测propTypes键数 = 5,没有来自TouchableHighlight的键)- 7 个方法都是箭头类字段(
showMenu/hideMenu/getOptionTexts/getCallback/
getTouchableHighlightProps/handleToolTipTextChange/handleBlurToolTip),
都不在prototype上 ⇒ 只有render在prototype上 ✓
★★ 六、其余预检结论
| 预检项 | 结果 |
|---|---|
dependencies / peerDependencies | ✅ 都空(但 ToolTip.android.js 里 require('warning') ⇒ 那个文件未被加载) |
createClass | ✅ 精确命中 0、裸词 0 |
ViewPropTypes / Text.propTypes | ✅ 空(新写法,没有老 API) |
ListView / measure() / Platform.OS / componentWillReceiveProps | ✅ 全空 |
NativeModules 4 处 + requireNativeComponent 1 处 | ⚠️ 核心阻塞点 |
this.refs 1 处 | ⚠️ 字符串 ref,React 19 已移除 |
| 外部 import | prop-types / react / react-native / warning(warning 只在 android stub 里) |




七、接入与预检(要点)
# 能装、能打包、能构建 —— 但核心功能不可用
npm install "git+https://atomgit.com/oh-react-native/react-native-tooltip.git#5.2.0-ohos-1.0.0" --ignore-scripts
如果你想在鸿蒙上做 ToolTip,不要用这个库 —— 它需要重写三处(见第四节)。
建议改用纯 JS 的浮层方案(Modal / Portal + measureInWindow 定位)。
八、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | ToolTip.ios.js(2701 B,main)+ ToolTip.android.js(181 B 空 stub)+ 5 个 .m/.h + Xcode 工程 |
❌ spec.json 的 native: false 是错的 | 有 5 个 .m/.h + 完整 Xcode 工程 ⇒ 这是个原生库 |
| ❌ README 说「导入即失败」实测不成立 | 打包 / 构建 / 渲染都成功,失败被推迟到调用 showMenu() |
| ⚠️ 不支持的原因 | NativeModules.ToolTipMenu 是 null;requireNativeComponent('RCTToolTipText') 是 iOS 原生组件 |
| ⚠️ 字符串 ref | ref='toolTipText' + this.refs ⇒ React 19 已移除 |
spec.json | ⚠️ 另有 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 库代码零改动 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
⚠️ 但结论与基线无关 —— 该库依赖 iOS 原生模块,在任何非 iOS 平台上都不可用。
九、已知限制与本次验证边界
9.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★★★★★ 核心功能不可用 | NativeModules.ToolTipMenu 是 null ⇒ showMenu() 必抛 TypeError |
| ★★★★★ 但"导入不失败" | 打包 / 构建 / 页面渲染都成功 ⇒ 问题被推迟到"用户长按"才暴露 |
★★★★★ NativeModules 是 Proxy 兜底 | Object.keys() 是空、'x' in NativeModules 恒为 true、未注册模块返回 null(不是 undefined) |
| ★★★★ 它是原生库 | 5 个 .h/.m + Xcode 工程;spec.json 的 native: false 是错的 |
| ★★★★ 上游只实现了 iOS | ToolTip.android.js 是一个只打警告的空对象 |
| ★★★ 字符串 ref 已失效 | ref='toolTipText' + this.refs ⇒ React 19 移除 |
| ★★★ 要支持鸿蒙需重写三处 | ① 鸿蒙原生菜单 ② 替换 RCTToolTipText ③ 改字符串 ref |
★★ propTypes 只有 5 个自有键 | ...TouchableHighlight.propTypes 展开得到空(React 19 里已无此属性) |
9.2 本次验证的边界
- ★★★ 未渲染
<ToolTip>(刻意的 —— 它会挂未注册的RCTToolTipText,会把整页拖成红屏;
本轮的目标是先把"静态层可用、功能层不可用"这两件事分开证明); - ★★★ 未验证
showMenu()的实际抛错信息(只从ToolTipMenu === null推断会抛TypeError); - ★★
requireNativeComponent('RCTToolTipText')的返回值未打印(本轮没读它); - 未验证
onShow/onHide/longPress的行为; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测 iOS/Android 行为(无对应设备);
- 未做跨平台对照。
库代码未改动。
十、常见问题
Q1:它在鸿蒙上能用吗?
不能。 核心功能(长按弹出菜单)依赖 NativeModules.ToolTipMenu,
而实测它是 null ⇒ 调用 showMenu() 会抛 TypeError。
Q2:那我 import 它会立刻崩吗?
不会。 这也是本文最想说的一点 ——
打包成功(bundle exit=0)、HAP 构建成功(3 分 45 秒)、页面正常渲染。
失败被推迟到真正调用 showMenu() 的那一刻。
⇒ 所以不要在启动时"试一下就以为没问题",要看它有没有真的去调原生方法。
Q3:怎么判断一个库"有没有真的用到原生模块"?
三个动作:
// ① 看 main 指向的文件里有没有这些
NativeModules.xxx
requireNativeComponent('xxx')
NativeEventEmitter / TurboModuleRegistry
// ② 看包里有没有原生代码
*.m / *.h / *.java / *.kt / *.ets
// ③ 运行时探一下
Object.keys(NativeModules).length // 空
'ToolTipMenu' in NativeModules // ⚠️ 恒 true,别用这个判存在
NativeModules.ToolTipMenu // null ⇒ 未注册
Q4:'xxx' in NativeModules 为什么不能用来判断模块存在?
因为鸿蒙侧的 NativeModules 是个 Proxy 兜底对象:
Object.keys() 返回空数组、但 in 运算恒为 true、取值返回 null。
⇒ 要看"取出来的值",不要看 in。
Q5:为什么 propTypes 只有 5 个键?
源码是:
static propTypes = {
actions: …,
arrowDirection: …,
longPress: …,
onHide: …,
onShow: …,
...TouchableHighlight.propTypes, // ← 这个展开在 React 19 里是空
};
⇒ TouchableHighlight.propTypes 在 React 19 里已不存在 ⇒ 展开得到空 ⇒ 只剩 5 个自有键 ✓
Q6:如果我就是想在鸿蒙上做 ToolTip,怎么办?
不要改这个库(要动三处:写鸿蒙原生菜单、替换 RCTToolTipText、改字符串 ref —— 等于重写)。
用纯 JS 方案:Modal 或 Portal + measureInWindow 拿到锚点位置 + 绝对定位的浮层。
这条路线在鸿蒙上不需要任何原生模块。
Q7:ToolTip.android.js 里那个 test() 是什么?
它是上游对 Android 的"占位":
var ToolTip = {
test: function() { warning("Not yet implemented for Android."); }
};
module.exports = ToolTip;
⇒ ⇒ 上游从来没实现过 Android ⇒ 说明这个库的定位就是"iOS 专用插件" ✓
⇒ 鸿蒙支持它 = 从零写原生,不是适配能解决的。
小结
react-native-tooltip 是个iOS 专用的原生长按菜单插件(5 个 .m/.h + Xcode 工程)。
它在鸿蒙上不可用,但本文的实测过程推翻了交付包文档里的一个说法。
最值得记下来的是这几条:
-
★★★★★ 「导入即失败」实测不成立 —— 失败被推迟到了"调用时"。
实测:打包exit=0、HAP 构建BUILD SUCCESSFUL 3分45秒、页面正常渲染、4 处文本全中、无红屏 ✓
原因:NativeModules.ToolTipMenu(取到null)和requireNativeComponent('RCTToolTipText')(只登记名字)
都不在模块求值阶段抛错。
⇒ 这类"编译期看不出来、要等用户操作才崩"的库,比"直接 import 就报错"危险得多。 -
★★★★★
NativeModules是 Proxy 兜底 ——in恒真、未注册模块返回null。Object.keys(NativeModules).length → 0 'ToolTipMenu' in NativeModules → true ← 不能用它判断存在! NativeModules.ToolTipMenu → null ← 不是 undefined⇒ 判断原生模块是否存在,要看"取出来的值",不能看
in✓ -
★★★★★
spec.json的native: false与事实不符。
包里有 5 个.h/.m+ 完整 Xcode 工程,main指向的 JS 直接依赖 iOS 原生模块。
⇒native: false会让人误以为"纯 JS,装上就能用" —— 这是交付包的实质性缺陷。 -
★★★★ 上游只做了 iOS。
ToolTip.android.js是一个只打警告的空对象(181 B)⇒ 上游从未实现 Android。
⇒ 要支持鸿蒙得从零写原生菜单,不是"适配"。 -
★★★ 即使补上原生模块,还有第三道关:
ref='toolTipText'+this.refs(字符串 ref)
在 React 19 已被移除 ⇒ 拿不到节点 ⇒findNodeHandle参数是错的。
⇒ 总计要动三处,已属重写。 -
★★★ 静态层的契约判据依然值得做(9 条全 PASS):
class+PureComponent、5 个propTypes、defaultProps.arrowDirection='down'、
7 个箭头字段不在prototype上。
⇒ 这些证明"模块能被正确求值",正好与"功能不可用"形成对照 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-tooltip(上游 5.2.0 的鸿蒙交付包) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-tooltip |
| 适配 TAG | 5.2.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(但它内部依赖 iOS 原生模块) |
| 库类型 | iOS 原生长按菜单(非纯 JS) |
| 运行结论 | ❌ 核心功能不可用(NativeModules.ToolTipMenu 是 null) |
| 但静态层可用 | ✅ 打包 / 构建 / 页面渲染 / 契约读取全部成功 |
| 导出形态 | 只有默认导出 ToolTip(class,extends PureComponent) |
| 原生代码 | 5 个 .h/.m + ToolTipMenu.xcodeproj ⇒ spec.json 的 native: false 是错的 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/chirag04/react-native-tooltip |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = TooltipTestApp) |
npm install "git+https://atomgit.com/oh-react-native/react-native-tooltip.git#5.2.0-ohos-1.0.0" --ignore-scripts
# ⚠️ 能装、能打包、能构建 —— 但核心功能不可用(详见正文)
// ⚠️ 能 import,但 showMenu() 必然抛 TypeError
import ToolTip from 'react-native-tooltip';
// 读契约是好的
ToolTip.prototype.isPureReactComponent; // true
Object.keys(ToolTip.propTypes); // ['actions','arrowDirection','longPress','onHide','onShow']
ToolTip.defaultProps.arrowDirection; // 'down'
// 但真正用起来会挂:
// ToolTipMenu = NativeModules.ToolTipMenu === null
// ⇒ showMenu() 里 ToolTipMenu.show(...) 抛 TypeError
// 另外 ref='toolTipText' + this.refs 在 React 19 已移除
// ✅ 想在鸿蒙上做 ToolTip,请用纯 JS 方案:
// Modal / Portal + measureInWindow 定位 + 绝对定位浮层(不需要原生模块)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey TooltipTestApp
判据的复现方式
# ① 静态层(9 条全 PASS)
hdc shell "hilog -x | grep ttp-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ typeof ToolTip = function、PureComponent、5 个 propTypes、defaultProps 精确值
# ⇒ ★ NativeModules.ToolTipMenu = null(这是"功能不可用"的决定性证据)
# ⇒ Object.keys(NativeModules).length = 0 但 'ToolTipMenu' in NativeModules = true
# ② 打包与构建
npm run dev # ⇒ bundle exit=0
hvigorw … assembleHap # ⇒ BUILD SUCCESSFUL
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px = 39vp |
| 宿主依赖 | ✅ 无额外依赖(warning 只在未被加载的 android stub 里) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 45 秒 ⇒ 一次成功(打包 exit=0) |
| 验证规模 | 静态层 9/9 全 PASS;打包 + 构建 + 页面渲染全部成功;NativeModules.ToolTipMenu = null(决定性证据);LogBox 无通知;4 张截图互异;⚠️ 渲染 <ToolTip> 未验证(刻意避开) |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
⚠️ 但本库的结论与基线无关 —— 它依赖 iOS 原生模块,在任何非 iOS 平台上都不可用。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| React Native 鸿蒙官方社区(CPF-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview
更多推荐



所有评论(0)