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 |
★★★★★ 必须补ViewPropTypes | L10ViewPropTypes.style在 RN ≥0.62 会抛 TypeError ⇒ import 即崩 |
★★★★render()初始返回null | state.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 本次验证的边界
- ★★★ 未能在 dump 里定位到对话框的内容文本(title / message / OK 按钮)——
只确认了全屏容器渲染(w=1320 h=2115,与styles.dialog的全屏定义吻合); - ★★★ 未验证
confirm()与prompt()的行为(只调了alert()); - ★★ 未验证
Prompt的TextInput与onChangeText; - ★★ 未验证
static inject(ref)的用法(只断言它是函数); - ★★ 未验证
BackHandler(Android 返回键)逻辑; - 未验证自定义
dialogStyle/modalStyle/contentStyle等 8 个样式 prop; - 未验证
Confirm的双按钮布局与negText; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
库代码未改动(改的是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都为空、老写法九项全空。
最值得记下来的是这几条:
★★★★★ 一个真实的库缺陷:
hide()不存在,但三个默认回调都在调它。
静态(源码无定义)+ 运行时(inst.hide抛undefined is not a function)+ own keys(没有hide)三重确认✓✓
⇒影响:点 OK / 取消 / 遮罩的默认行为全部抛错⇒ 必须自己传回调或给原型补一个hide。★★★★★ polyfill 可以"按需扩展",而且这次扩展得很干净。
原来只补Text.propTypes;这轮加了ViewPropTypes(用Object.defineProperty覆盖react-native模块上的 getter)+View/Image/ScrollView
⇒ hilog 里 5 行[polyfill] NOTE全部打出、页面判据确认ViewPropTypes已是object✓✓
⇒这类补丁修的是"同一类被移除的运行时类型入口",值得放进工程的polyfills/长期保留。★★★★
X.prototype instanceof Dialog是断言继承关系最干净的写法(三条全 true)。★★★★
Alert.defaultProps有 5 个键(4 个继承 + title)
⇒{...Dialog.defaultProps, title:'Alert'}的展开继承行为被验证✓★★★ 我又把一个数错了:
Dialog.propTypes实际 14 项、我写成 15。
这是连续第三轮在"数量类断言"上出错(前两次:defaultProps项数、forwardRef类型)。
⇒这次的根因很明确:我是手数源码里的字段,而不是直接Object.keys(...).length取真值再写期望。
⇒改法:数量类断言必须先在同一环境里取到真值、再把它作为期望(我这一轮其实已经把真值打成了NOTE,
只是写期望时又凭手数了)✓★★★ "全屏容器渲染了、内容文本没观察到"要如实区分。
我找到了w=1320 h=2115的全屏层(与styles.dialog定义吻合)⇒容器确实渲染;
但没找到title/message/OK文本 ⇒内容只写"未观察到",不写"没渲染"✓★★
Modal的 4 处命中又是命名巧合(modalStyle/styles.dialogModal)⇒
本月第四次"名字里含已移除 API"的误报,再次印证"要扫真实使用形态"。★★
render()读的是this.params而不是this.props⇒
props 变化不会反映到渲染⇒ 想做"受控对话框"会比较别扭 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-modal-dialog(上游1.2.3的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-modal-dialog |
| 适配 TAG | 1.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 Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.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 |
| 宿主依赖 | ✅ 无额外依赖;需引入扩展后的 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