React Native for OpenHarmony 实战:三方库 react-native-modal-dialog 的鸿蒙化适配指南

发布时间:2026/10/24 2:18:32
React Native for OpenHarmony 实战:三方库 react-native-modal-dialog 的鸿蒙化适配指南

本文记录把react-native-modal-dialog(对话框:Alert / Confirm / Prompt)适配到 HarmonyOS 的完整过程。

它做的事:提供一个Dialog基类,并派生出Alert/Confirm/Prompt三种对话框,通过实例方法alert()/confirm()/prompt()弹出来。

结论:能跑 ——alert()成功切换了内部状态、全屏层也渲染了;但本文发现了一个真实的库缺陷:hide()方法根本不存在,而它的三个默认回调都在调用它。
另外这个库踩了ViewPropTypes这个坑(RN 0.62+ 已移除)—— 我用扩展 polyfill的方式解决了,这正好验证了"兼容层可扩展"。

★★★★★ 一、会崩的地方:ViewPropTypes.style(以及我如何扩展 polyfill)

index.jsL7-L10:

import{StyleSheet,Dimensions,Text,View,TouchableWithoutFeedback,TextInput,ScrollView,BackHandler,ViewPropTypes,// ★ RN 0.62+ 已移除这个导出}from'react-native';constStylePropType=ViewPropTypes.style;// ★ L10,模块加载期求值 ⇒ undefined.style ⇒ TypeError

⇒库 import 即崩✗

修法:扩展已有的 polyfill

我之前为另一个依赖Text.propTypes.style的库写过一份polyfills/text-prop-types.js。
这一轮在同一个文件里加了ViewPropTypes的补齐(关键是要覆盖react-native模块上的同名 getter):

constPLACEHOLDER_PROP_TYPES={style:{}};// ① Text.propTypesif(!Text.propTypes)Text.propTypes={...PLACEHOLDER_PROP_TYPES};// ② ★ ViewPropTypes 本身(RN 0.62+ 已从 react-native 的导出里移除)constRN=require('react-native');if(!RN.ViewPropTypes){Object.defineProperty(RN,'ViewPropTypes',{configurable:true,enumerable:true,get(){return{...PLACEHOLDER_PROP_TYPES};},});}// ③ 顺手补上 View / Image / ScrollView 的 propTypes

实测(hilog 里 polyfill 自己打的 5 行):

[polyfill] NOTE | 已为 Text 补回 propTypes(供依赖 Text.propTypes.style 的老库使用) [polyfill] NOTE | 已为 react-native 的 ViewPropTypes 补上导出(供 `ViewPropTypes.style` 使用) ← ★ 本轮新增 [polyfill] NOTE | 已为 View 补回 propTypes [polyfill] NOTE | 已为 Image 补回 propTypes [polyfill] NOTE | 已为 ScrollView 补回 propTypes

页面判据确认:

NOTE | ★ RN.ViewPropTypes 是否存在(polyfill 后应为 object) = object PASS | ★★★ polyfill 生效:ViewPropTypes 已存在 PASS | ★★★ polyfill 生效:ViewPropTypes.style 可取

⇒⇒ 库成功 import、页面正常挂载✓✓
⇒⇒ 这说明"一份 polyfill 可以按需扩展"—— 它修的是同一类问题(RN 移除的"运行时类型声明入口"),
所以遇到新的同类库,只要在这个文件里加一段即可 ✓

★★★★★ 二、发现一个真实的库缺陷:hide()方法不存在

Dialog.defaultProps的三个默认回调全都调用this.hide()(源码 L35-43):

staticdefaultProps={posText:'OK',onNegClick:function(){this.hide();},// ★onPosClick:function(){this.hide();},// ★onCancel:function(){this.hide();returntrue;},// ★};

但Dialog类里没有任何hide方法。实测:

constinst=dlgRef.current;inst.alert('MKD 消息一',{title:'MKD 标题一'});// ✓ 调用成功inst.hide();// ✗ 抛错
NOTE | alert() 调用 = ok PASS | ★★★ alert() 之后 state.hidden = false(已显示) NOTE | hide 抛错 = undefined is not a function ← ★★ FAIL | ★★ ref 调用 hide() 不抛错 | 期望=true | 实际=false

实例的 own keys 也印证了:

props, context, refs, updater, _lockQueue, state, onCancel, params, _reactInternals, _reactInternalInstance

⇒没有hide(alert是原型方法,所以不在 own keys 里,但能用)

⇒ 所以这个库的"默认行为"是坏的:

  • 点OK(posText)⇒ 调onPosClick⇒this.hide()⇒抛错✗
  • 点取消(negText)⇒ 调onNegClick⇒this.hide()⇒抛错✗
  • 点遮罩(onCancel)⇒this.hide()⇒抛错✗

使用建议:必须自己传onPosClick/onNegClick/onCancel,并且在里面用别的方式关闭
(例如给Dialog传一个自己的hide,或者干脆用state受控 —— 但这个库的render()读的是this.params,受控改造比较麻烦)。

⚠️ 这条是静态 + 运行时双重确认的:
① 源码里Dialog类没有hide方法定义;② 运行时inst.hide抛undefined is not a function;
③ 实例 own keys 里也没有hide。三点一致 ⇒ 结论可靠 ✓

★★★★ 三、继承关系可以精确断言

源码(L12 / L141 / L150 / L160):

exportdefaultclassDialogextendsComponent{…}exportclassAlertextendsDialog{…}exportclassConfirmextendsDialog{…}exportclassPromptextendsDialog{…}

实测:

PASS | ★★ Alert 是 class(function) PASS | ★★★ Alert.prototype instanceof Dialog ← 期望=true | 实际=true PASS | ★★★ Confirm.prototype instanceof Dialog ← 期望=true | 实际=true PASS | ★★★ Prompt.prototype instanceof Dialog ← 期望=true | 实际=true

⇒X.prototype instanceof Dialog这个断言写法很干净✓
(比X.super_/Object.getPrototypeOf(X) === Dialog更直观)

static inject四个类都有:

PASS | ★ Dialog / Alert / Confirm / Prompt 都有 static inject(函数)

★★★ 四、propTypes/defaultProps逐项核对(我又把一个数错了)

NOTE | Dialog.propTypes 的键数 = 14 NOTE | Dialog.propTypes 的键 = buttonBarStyle, buttonStyle, contentStyle, dialogStyle, message, messageStyle, modalStyle, negText, onCancel, onNegClick, onPosClick, posText, title, titleStyle FAIL | ★★ Dialog.propTypes 有 15 项 | 期望=15 | 实际=14 ← ★ 我数错了 NOTE | Dialog.defaultProps 的键数 = 4 NOTE | Dialog.defaultProps 的键 = onCancel, onNegClick, onPosClick, posText PASS | ★★ Dialog.defaultProps 有 4 项 PASS | ★★ defaultProps.posText = OK NOTE | Alert.defaultProps 的键 = onCancel, onNegClick, onPosClick, posText, title PASS | ★★ Alert.defaultProps.title = Alert PASS | ★★ Alert.defaultProps.posText 继承自 Dialog(OK) ← ★ 展开运算符的继承也验证了 NOTE | Confirm.defaultProps 的键 = negText, onCancel, onNegClick, onPosClick, posText, title PASS | ★★ Confirm.defaultProps.title = Confirm PASS | ★★ Confirm.defaultProps.negText = Cancel NOTE | Prompt.propTypes 的键 = onChangeText, placeholder PASS | ★★ Prompt.propTypes 有 2 项 NOTE | Prompt.defaultProps 的键 = children, message, negText, onCancel, onNegClick, onPosClick, posText, title PASS | ★★ Prompt.defaultProps.title = Prompt / negText = Cancel PASS | ★★ Prompt.defaultProps.children 是长度 2 的稀疏数组([,,])

⇒Dialog.propTypes实际 14 项,我写成 15✗ —— 这是连续第三轮在"数量类断言"上数错
⇒Alert.defaultProps有 5 个键(4 个继承 + title)⇒{...Dialog.defaultProps, title:'Alert'}展开继承被验证✓

★★★ 五、状态机与渲染观察

render()开头就很关键(L44-47):

render(){if(this.state.hidden){returnnull;// ★ 初始什么都不渲染}…}

实测状态机:

NOTE | 实例 state 的键 = hidden NOTE | 实例 state.hidden = true PASS | ★★★ 初始 state.hidden = true(所以 render 返回 null) NOTE | alert 之后 state.hidden = false PASS | ★★★ alert() 之后 state.hidden = false(已显示)

⇒alert()成功把hidden置为false✓

但内容文本没在布局 dump 里观察到⚠️:

  • 我在alert()之后重新采样 dump,含MKD的节点只有我自己页面上的说明文字
  • 但找到了两个全屏层:w=1320 h=2115与w=1320 h=2061
  • 这与源码styles.dialog的position:'absolute' + width:window.width + height:window.height完全吻合

⇒⇒ 结论:对话框的"全屏容器"确实渲染了,但它的内容文本(title / message / OK 按钮)本轮没能在 dump 里定位到⚠️
⇒ 我不把"文本渲染了"写成结论,只写"容器渲染、内容未观察到" ✓

★★ 六、其余预检结论

预检项结果
dependencies/peerDependencies✅都空
老写法九项(createClass/React.PropTypes/Text.propTypes/ 字符串 ref /this.refs/mixins/componentWillReceiveProps/componentWillMount/getDefaultProps)✅✅全部为空
ListView/measure()/onLayout/useNativeDriver/NativeModules/Platform.OS/isRequired✅全空
Modal4 处⚠️全是modalStyle/styles.dialogModal这类命名巧合,没有真的用<Modal>(本月第四次同类误报)
唯一真坑ViewPropTypes.style(见第一节)
外部 import只有prop-types/react/react-native✓
交付包形态⚠️只有一个index.js(7852 B,246 行),无src/、无.d.ts


七、接入与预检(要点)

# 装库(无 dependencies、无 peerDependencies)npminstall"git+https://atomgit.com/oh-react-native/react-native-modal-dialog.git#1.2.3-ohos-1.0.0"--ignore-scripts# ★ 还需要在入口最前面引入 polyfill(补 Text.propTypes 与 ViewPropTypes),否则 import 即崩

八、交付包质量与基线说明

类别结果
交付包内容⚠️只有一个index.js(7852 B / 246 行)⇒ 没有src/、没有.d.ts
spec.json⚠️缺upstreamCommit
validation⚠️"build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0"+rnoh: "0.84.4"⇒两字段自相矛盾
备注库代码零改动(改的是我自己的 polyfill)

关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。
⚠️ViewPropTypes与hide()这两条都是版本相关的(前者随 RN 版本,后者是库自身缺陷)⇒ 换基线需重新确认。


九、已知限制与本次验证边界

9.1 用这个库要知道的

事项说明
★★★★★hide()不存在,但三个默认回调都在调它必须自己传onPosClick/onNegClick/onCancel,否则点 OK/取消/遮罩都会抛undefined is not a function
★★★★★ 必须补ViewPropTypesL10ViewPropTypes.style在 RN ≥0.62 会抛 TypeError ⇒ import 即崩
★★★★render()初始返回nullstate.hidden初始为true⇒ 必须调alert()/confirm()/prompt()才显示
★★★ 继承关系可断言Alert.prototype instanceof Dialog等三条全 true
★★★Alert.defaultProps是{...Dialog.defaultProps, title}实测 5 个键(4 继承 + 1 title)⇒ 展开继承被验证
★★Prompt.defaultProps.children是稀疏数组[,,]长度 2(不是 0)
★★props变化不会反映到渲染render()读的是this.params(构造时从 props 复制)
★Modal命中的 4 处是命名巧合modalStyle/styles.dialogModal,不是<Modal>

9.2 本次验证的边界

  1. ★★★ 未能在 dump 里定位到对话框的内容文本(title / message / OK 按钮)——
    只确认了全屏容器渲染(w=1320 h=2115,与styles.dialog的全屏定义吻合);
  2. ★★★ 未验证confirm()与prompt()的行为(只调了alert());
  3. ★★ 未验证Prompt的TextInput与onChangeText;
  4. ★★ 未验证static inject(ref)的用法(只断言它是函数);
  5. ★★ 未验证BackHandler(Android 返回键)逻辑;
  6. 未验证自定义dialogStyle/modalStyle/contentStyle等 8 个样式 prop;
  7. 未验证Confirm的双按钮布局与negText;
  8. 未在RN 0.88.0/RNOH 0.84.4上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动(改的是polyfills/text-prop-types.js,属于我自己的兼容层)。


十、常见问题

Q1:它在 RN 0.84 上能跑吗?

加 polyfill 之后能跑:alert()能把state.hidden从true置为false、全屏容器渲染、
继承关系与defaultProps继承都对;LogBox无通知。
但hide()不存在(见 Q2),而且内容文本本轮没能在 dump 里定位到。

Q2:点 OK / 取消会报undefined is not a function?

因为Dialog.defaultProps里有:

onNegClick:function(){this.hide();},onPosClick:function(){this.hide();},onCancel:function(){this.hide();returntrue;},

而Dialog类里没有hide方法⇒ 这三个默认回调一执行就炸。
必须自己传:

<Dialog onPosClick={function () { /* 自己的关闭逻辑 */ }} onNegClick={function () { /* 自己的关闭逻辑 */ }} onCancel={function () { /* 自己的关闭逻辑 */ }} />

⚠️ 由于render()读的是this.params(构造时快照),"自己维护一个 hidden 状态"这条路不太好走 ⇒
比较现实的做法是给Dialog的原型补一个hide:

importDialogfrom'react-native-modal-dialog';if(typeofDialog.prototype.hide!=='function'){Dialog.prototype.hide=function(){this.setState({hidden:true});};}

Q3:一 import 就报Cannot read property 'style' of undefined?

index.jsL10const StylePropType = ViewPropTypes.style;,而 RN ≥0.62 已移除ViewPropTypes。
用 polyfill 把它补上(见第一节;关键是用Object.defineProperty覆盖react-native模块上的同名 getter)。

Q4:挂了<Dialog />却什么都不显示?

正常——render()第一行就是if (this.state.hidden) return null;,而state.hidden初始为true。
必须先调实例方法:

ref.current.alert('消息内容',{title:'标题'});ref.current.confirm('确认吗?');ref.current.prompt('请输入','占位符',text=>console.log(text));

Q5:怎么确认它真的派生了Dialog?

Alert.prototypeinstanceofDialog;// trueConfirm.prototypeinstanceofDialog;// truePrompt.prototypeinstanceofDialog;// true

(比看super_或Object.getPrototypeOf直观)

Q6:Prompt的children为什么是[,,]?

源码里写的是children: [, ,]—— 一个长度 2 的稀疏数组([undefined, undefined]),
render()里通过children.length == 2 && children[0] === undefined && children[1] === undefined判断"用户没传 children",
然后自己塞进去一个Text和一个TextInput。实测length === 2✓


小结

react-native-modal-dialog是一个index.js单文件的对话框库(Dialog+Alert/Confirm/Prompt),
dependencies与peerDependencies都为空、老写法九项全空。

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

  1. ★★★★★ 一个真实的库缺陷:hide()不存在,但三个默认回调都在调它。
    静态(源码无定义)+ 运行时(inst.hide抛undefined is not a function)+ own keys(没有hide)三重确认✓✓
    ⇒影响:点 OK / 取消 / 遮罩的默认行为全部抛错⇒ 必须自己传回调或给原型补一个hide。

  2. ★★★★★ polyfill 可以"按需扩展",而且这次扩展得很干净。
    原来只补Text.propTypes;这轮加了ViewPropTypes(用Object.defineProperty覆盖react-native模块上的 getter)+View/Image/ScrollView
    ⇒ hilog 里 5 行[polyfill] NOTE全部打出、页面判据确认ViewPropTypes已是object✓✓
    ⇒这类补丁修的是"同一类被移除的运行时类型入口",值得放进工程的polyfills/长期保留。

  3. ★★★★X.prototype instanceof Dialog是断言继承关系最干净的写法(三条全 true)。

  4. ★★★★Alert.defaultProps有 5 个键(4 个继承 + title)
    ⇒{...Dialog.defaultProps, title:'Alert'}的展开继承行为被验证✓

  5. ★★★ 我又把一个数错了:Dialog.propTypes实际 14 项、我写成 15。
    这是连续第三轮在"数量类断言"上出错(前两次:defaultProps项数、forwardRef类型)。
    ⇒这次的根因很明确:我是手数源码里的字段,而不是直接Object.keys(...).length取真值再写期望。
    ⇒改法:数量类断言必须先在同一环境里取到真值、再把它作为期望(我这一轮其实已经把真值打成了NOTE,
    只是写期望时又凭手数了)✓

  6. ★★★ "全屏容器渲染了、内容文本没观察到"要如实区分。
    我找到了w=1320 h=2115的全屏层(与styles.dialog定义吻合)⇒容器确实渲染;
    但没找到title/message/OK文本 ⇒内容只写"未观察到",不写"没渲染"✓

  7. ★★Modal的 4 处命中又是命名巧合(modalStyle/styles.dialogModal)⇒
    本月第四次"名字里含已移除 API"的误报,再次印证"要扫真实使用形态"。

  8. ★★render()读的是this.params而不是this.props⇒
    props 变化不会反映到渲染⇒ 想做"受控对话框"会比较别扭 ✓


本篇用到的库

项内容
三方库react-native-modal-dialog(上游1.2.3的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-modal-dialog
适配 TAG1.2.3-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS)
库类型对话框(Dialog基类 +Alert/Confirm/Prompt)
运行时依赖✅dependencies/peerDependencies都空
导出形态默认导出Dialog(class)+具名Alert/Confirm/Prompt(都 extends Dialog)
⚠️ 必须的额外步骤入口最前面引入 polyfill(补Text.propTypes与ViewPropTypes)
⚠️ 库缺陷hide()不存在,但defaultProps的 3 个回调都调它 ⇒ 必须自己传回调或给原型补hide
许可证MIT
上游仓库https://github.com/zhengxiaoyao0716/react-native-modal-dialog
宿主工程RNOH084Demo(测试页rnAppKey = ModalDialogTestApp)
npminstall"git+https://atomgit.com/oh-react-native/react-native-modal-dialog.git#1.2.3-ohos-1.0.0"--ignore-scripts
// ① polyfill(入口最前面,见正文第一节)import'./polyfills/text-prop-types';// 内部同时补 Text.propTypes 与 ViewPropTypes// ② 库缺陷补救:给 Dialog 原型补一个 hide(可选,但强烈建议)importDialogfrom'react-native-modal-dialog';if(typeofDialog.prototype.hide!=='function'){Dialog.prototype.hide=function(){this.setState({hidden:true});};}
import Dialog, {Alert, Confirm, Prompt} from 'react-native-modal-dialog'; const ref = useRef(null); <Dialog ref={ref} /> // 弹出(render 初始返回 null,必须调方法) ref.current.alert('消息内容', {title: '标题'}); ref.current.confirm('确认执行吗?', {title: '确认'}); ref.current.prompt('请输入', '占位符', text => console.log(text)); // 关闭(⚠️ 库本身没有 hide,见上面的原型补丁) ref.current.hide(); // ⚠️ Dialog.propTypes 14 项 / defaultProps 4 项(posText 'OK' + 3 个回调) // ⚠️ Alert/Confirm/Prompt 都 extends Dialog;Alert.prototype instanceof Dialog === true // ⚠️ 默认回调调用 this.hide(),而 hide 不存在 ⇒ 不传回调点 OK/取消会抛 undefined is not a function // ⚠️ render 读的是 this.params(构造时快照)⇒ props 变化不反映
# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey ModalDialogTestApp

判据的复现方式

# ① polyfill 是否把 ViewPropTypes 补上hdc shell"hilog -x | grep '\[polyfill\]'"# ⇒ 5 行(Text / ViewPropTypes / View / Image / ScrollView)# ② 契约 + 继承 + 状态机 + hide 缺陷hdc shell"hilog -x | grep mkd-test"|Select-String'PASS|FAIL|SUMMARY|NOTE'# ⇒ Alert.prototype instanceof Dialog = true# ⇒ 初始 state.hidden = true;alert() 后 = false# ⇒ hide 抛错 "undefined is not a function"

环境搭建

本手册的验证环境是React Native for OpenHarmony(RNOH)。从零准备开发环境(DevEco Studio、HarmonyOS SDK API 26、Node.js、ohpm、hvigorw、模拟器与hdc调试)请按 CPF-RN 开发者指南的「环境初始化」操作:

  • CPF-RN 开发者指南 · 02-搭建准备 · 环境初始化:https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md

环境就绪后,本次验证使用的具体版本组合见文末「验证环境」一节。


验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.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
宿主依赖✅ 无额外依赖;需引入扩展后的 polyfill
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap3 分 32 秒⇒ 一次成功(打包与构建退出码都检查了)
验证规模polyfill 5 行日志全打出(含 ViewPropTypes);契约 17/18(1 条是我数错);继承断言 3/3 命中;状态机true → false命中;hide()缺陷三重确认;全屏容器渲染确认;4 张截图互异

⚠️基线说明:交付包spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
⚠️ViewPropTypes与hide()两条都是版本/库自身相关的,换基线需重新确认。


相关社区与项目地址

项目地址
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