本文记录把 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.js L7-L10:

import {
  StyleSheet, Dimensions,
  Text, View, TouchableWithoutFeedback, TextInput, ScrollView,
  BackHandler,
  ViewPropTypes,                              // ★ RN 0.62+ 已移除这个导出
} from 'react-native';

const StylePropType = ViewPropTypes.style;    // ★ L10,模块加载期求值 ⇒ undefined.style ⇒ TypeError

⇒ 库 import 即崩 ✗

修法:扩展已有的 polyfill

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

const PLACEHOLDER_PROP_TYPES = {style: {}};

// ① Text.propTypes
if (!Text.propTypes) Text.propTypes = {...PLACEHOLDER_PROP_TYPES};

// ② ★ ViewPropTypes 本身(RN 0.62+ 已从 react-native 的导出里移除)
const RN = 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):

static defaultProps = {
  posText: 'OK',
  onNegClick: function () { this.hide(); },        // ★
  onPosClick: function () { this.hide(); },        // ★
  onCancel:   function () { this.hide(); return true; },   // ★
};

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

const inst = 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):

export default class Dialog extends Component { … }
export class Alert   extends Dialog { … }
export class Confirm extends Dialog { … }
export class Prompt  extends Dialog { … }

实测:

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) {
    return null;                     // ★ 初始什么都不渲染
  }
  …
}

实测状态机:

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✅ 全空
Modal 4 处⚠️ 全是 modalStyle / styles.dialogModal 这类命名巧合,没有真的用 <Modal>(本月第四次同类误报)
唯一真坑ViewPropTypes.style(见第一节)
外部 import只有 prop-types / react / react-native ✓
交付包形态⚠️ 只有一个 index.js(7852 B,246 行),无 src/、无 .d.ts

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


七、接入与预检(要点)

# 装库(无 dependencies、无 peerDependencies)
npm install "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)

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


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

9.1 用这个库要知道的

事项说明
★★★★★ hide() 不存在,但三个默认回调都在调它必须自己传 onPosClick / onNegClick / onCancel,否则点 OK/取消/遮罩都会抛 undefined is not a function
★★★★★ 必须补 ViewPropTypesL10 ViewPropTypes.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(); return true; },

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

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

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

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

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

index.js L10 const 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.prototype instanceof Dialog;      // true
Confirm.prototype instanceof Dialog;    // true
Prompt.prototype instanceof Dialog;     // 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)
npm install "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(可选,但强烈建议)
import Dialog from 'react-native-modal-dialog';
if (typeof Dialog.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 -b com.rnoh084.demo -a EntryAbility --ps rnAppKey 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 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
宿主依赖✅ 无额外依赖;需引入扩展后的 polyfill
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 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

Logo

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

更多推荐