本文记录把 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 陷阱的对象)时才同时成立 ✓

这在实践上意味着两件事:

  1. 不能用 'xxx' in NativeModules 判断"这个原生模块是否存在" —— 它永远返回 true ✗
    正确做法:取出来判断是否 null/undefined,或者检查 typeof。

  2. 未注册的模块返回 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"

从这些能读出三件事 ✓:

  1. ToolTip 是 class,且是 PureComponent(isPureReactComponent === true)
  2. propTypes 只有 5 个自有键 —— 因为源码是 ...TouchableHighlight.propTypes 展开,
    而 TouchableHighlight.propTypes 在 React 19 里可能已不存在 ⇒ 展开出来是空 ✓
    (实测 propTypes 键数 = 5,没有来自 TouchableHighlight 的键)
  3. 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 已移除
外部 importprop-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 原生组件
⚠️ 字符串 refref='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 是错的
★★★★ 上游只实现了 iOSToolTip.android.js 是一个只打警告的空对象
★★★ 字符串 ref 已失效ref='toolTipText' + this.refs ⇒ React 19 移除
★★★ 要支持鸿蒙需重写三处① 鸿蒙原生菜单 ② 替换 RCTToolTipText ③ 改字符串 ref
★★ propTypes 只有 5 个自有键...TouchableHighlight.propTypes 展开得到空(React 19 里已无此属性)

9.2 本次验证的边界

  1. ★★★ 未渲染 <ToolTip>(刻意的 —— 它会挂未注册的 RCTToolTipText,会把整页拖成红屏;
    本轮的目标是先把"静态层可用、功能层不可用"这两件事分开证明);
  2. ★★★ 未验证 showMenu() 的实际抛错信息(只从 ToolTipMenu === null 推断会抛 TypeError);
  3. ★★ requireNativeComponent('RCTToolTipText') 的返回值未打印(本轮没读它);
  4. 未验证 onShow / onHide / longPress 的行为;
  5. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  6. 未测 iOS/Android 行为(无对应设备);
  7. 未做跨平台对照。

库代码未改动。


十、常见问题

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 工程)。
它在鸿蒙上不可用,但本文的实测过程推翻了交付包文档里的一个说法。

最值得记下来的是这几条:

  1. ★★★★★ 「导入即失败」实测不成立 —— 失败被推迟到了"调用时"。
    实测:打包 exit=0、HAP 构建 BUILD SUCCESSFUL 3分45秒、页面正常渲染、4 处文本全中、无红屏 ✓
    原因:NativeModules.ToolTipMenu(取到 null)和 requireNativeComponent('RCTToolTipText')(只登记名字)
    都不在模块求值阶段抛错。
    ⇒ 这类"编译期看不出来、要等用户操作才崩"的库,比"直接 import 就报错"危险得多。

  2. ★★★★★ NativeModules 是 Proxy 兜底 —— in 恒真、未注册模块返回 null。

    Object.keys(NativeModules).length        → 0
    'ToolTipMenu' in NativeModules           → true      ← 不能用它判断存在!
    NativeModules.ToolTipMenu                → null      ← 不是 undefined
    

    ⇒ 判断原生模块是否存在,要看"取出来的值",不能看 in ✓

  3. ★★★★★ spec.json 的 native: false 与事实不符。
    包里有 5 个 .h/.m + 完整 Xcode 工程,main 指向的 JS 直接依赖 iOS 原生模块。
    ⇒ native: false 会让人误以为"纯 JS,装上就能用" —— 这是交付包的实质性缺陷。

  4. ★★★★ 上游只做了 iOS。
    ToolTip.android.js 是一个只打警告的空对象(181 B)⇒ 上游从未实现 Android。
    ⇒ 要支持鸿蒙得从零写原生菜单,不是"适配"。

  5. ★★★ 即使补上原生模块,还有第三道关:ref='toolTipText' + this.refs(字符串 ref)
    在 React 19 已被移除 ⇒ 拿不到节点 ⇒ findNodeHandle 参数是错的。
    ⇒ 总计要动三处,已属重写。

  6. ★★★ 静态层的契约判据依然值得做(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
适配 TAG5.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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐