本文记录把react-native-action-button(可展开的悬浮操作按钮 FAB)适配到 HarmonyOS 的完整过程。
它做的事:右下角一个圆形按钮,点开后沿弧线/直线弹出若干操作项,每项带标题与图标。
结论:它在本环境里"import 就会崩",但崩溃点只有一行、而且只需一个 20 行的 polyfill 就能修好。
修好之后,29 项契约里 28 项 defaultProps 常量全部精确命中,两个操作项的标题正常渲染,ref也能调用它的reset()。
但有一处修不了的语义变化:它依赖的componentWillReceiveProps在 React 19 里已被移除 ⇒受控模式失效。
★★★★★ 一、它的崩溃点:一行Text.propTypes.style
ActionButton.jsL366(在模块顶层求值的propTypes对象里):
ActionButton.propTypes={…buttonTextStyle:Text.propTypes.style,// ★ L366…};而 RN 从 0.62 起就移除了Text.propTypes—— 我在 RN 0.84 上确证过:
node_modules/react-native/Libraries/Text/Text.js(27541 B)里 `propTypes` 命中 = 0 Libraries/Text/*.js 里 "propTypes =" 命中 = 0 RN index.js: get Text() { return require('./Libraries/Text/Text').default; }⇒Text.propTypes是undefined⇒undefined.style⇒ TypeError⇒库在 import 时直接崩✗
★★★★★ 修法:一个 20 行的 polyfill(不改库)
关键判断:这是一行【类型声明】,没有任何渲染逻辑用到它⇒只要让它能安全求值,库的其余部分完全不受影响:
// polyfills/text-prop-types.jsconst{Text}=require('react-native');if(!Text.propTypes){Text.propTypes={style:{}};// 只需要"存在且能取 .style"console.log('[polyfill] NOTE | 已为 RN 的 Text 补回 propTypes…');}执行的顺序极其关键—— 必须在"库被 import 之前"跑:
// index.jsimport'./polyfills/text-prop-types';// ★ 放在所有页面 import 之前(实测第 28 行)…importActionButtonTestAppfrom'./ActionButtonTestApp';// 第 76 行,它内部才 import 那个库ESM 的
import是按书写顺序执行的(声明会提升,但求值不会)
⇒ 所以这一行在前,就能保证库被 import 时Text.propTypes已经就位 ✓
实测(hilog 里的 polyfill 自己的日志 + 页面上的判据):
10-17 15:28:13.054 I A0beef/#RNOH_JS: __█__ [polyfill] NOTE | 已为 RN 的 Text 补回 propTypes(供依赖 Text.propTypes.style 的老库使用) PASS | ★★★ polyfill 生效:Text.propTypes 已存在 | 期望=object | 实际=object PASS | ★★★ polyfill 生效:Text.propTypes.style 已存在 | 期望=object | 实际=object PASS | ★ 默认导出 ActionButton 是 class(function) PASS | ★ ActionButtonItem 是 class(function)⇒⇒ 补上之后库正常加载、正常渲染✓✓
⚠️这是"兼容补丁",不是 RN 的正确用法。
判断它够不够用,关键看那个库是只在propTypes声明里用Text.propTypes,
还是在渲染路径上用 —— 前者一行补丁就够,后者就要单独评估。
★★★★ 二、修不了的那一处:componentWillReceiveProps已被 React 19 移除
ActionButton.jsL42:
componentWillReceiveProps(nextProps){if(nextProps.resetToken!==this.state.resetToken){if(nextProps.active===false&&this.state.active===true){…收起…}if(nextProps.active===true&&this.state.active===false){…展开…}this.setState({resetToken:nextProps.resetToken});}}⇒它负责"受控模式":外部用active+resetToken两个 prop 驱动按钮的展开/收起
⇒而 React 19 早已移除该生命周期⇒这段逻辑不会被调用✗
实测印证(它确实还挂在 prototype 上,但不会被执行):
NOTE | prototype 上的方法(filter 掉 constructor) = componentDidMount, componentWillUnmount, componentWillReceiveProps, getOrientation, getOffsetXY, getOverlayStyles, render, _renderMainButton, _renderButtonIcon, _renderActions, _renderTappableBackground, animateButton, reset NOTE | 实例 state 的键 = resetToken, active PASS | ★ 实例有 state.active PASS | ★★ ref 调用 reset() 不抛错⇒⇒ 结论:
- 受控模式(
active={true}驱动)在本环境失效⚠️ - 但"非受控模式"仍然可用✓ —— 点击触发内部的
animateButton,ref也能调reset()/animateButton()✓
用受控模式怎么办?两个办法:
- 用
ref手动驱动(inst.reset()/inst.animateButton()); - 给组件加
key,用重挂载代替"受控更新"(<ActionButton key={String(active)} active={active} />)——
这样active会作为初始值进入构造函数(this.anim = new Animated.Value(props.active ? 1 : 0))✓
★★★★ 三、28 项defaultProps常量逐个核对(其中一项是我数错了)
NOTE | defaultProps 的键数 = 28 NOTE | defaultProps 的键 = accessibilityLabel, accessible, active, activeOpacity, autoInactive, backdrop, backgroundTappable, bgColor, bgOpacity, buttonColor, buttonText, buttonTextStyle, degrees, fixNativeFeedbackRadius, nativeFeedbackRippleColor, offsetX, offsetY, onPress, onPressIn, onPressOn, outRangeScale, position, resetToken, size, spacing, testID, useNativeFeedback, verticalOrientation FAIL | ★★ defaultProps 恰好 29 项 | 期望=29 | 实际=28 ← ★ 我数错了 PASS | ★★ buttonText = "+" PASS | ★★ size = 56 PASS | ★ spacing = 20 PASS | ★ position = right PASS | ★ offsetX = 30 PASS | ★ offsetY = 30 PASS | ★ degrees = 45 PASS | ★ verticalOrientation = up PASS | ★ useNativeFeedback = true PASS | ★ activeOpacity = 0.85 PASS | ★ active = false PASS | ★ resetToken = null PASS | ★ backdrop = false PASS | ★ autoInactive = true PASS | ★ outRangeScale = 1 PASS | ★ bgColor = transparent PASS | ★ bgOpacity = 1⇒18 项取值全部精确命中✓✓
⇒但"恰好 29 项"那条 FAIL 是我自己数错了(实际 28 项)✗ ——如实记录,不改判据去迁就
ActionButtonItem的defaultProps(getter 形式):
NOTE | ActionButtonItem.defaultProps 的键 = active, activeOpacity, fixNativeFeedbackRadius, nativeFeedbackRippleColor, spaceBetween, useNativeFeedback★★★ 四、isAndroid = false在鸿蒙上是安全的
shared.js:
exportconstisAndroid=Platform.OS==="android";// 鸿蒙 ⇒ falseexportfunctiongetTouchableComponent(useNativeFeedback){if(useNativeFeedback===true&&isAndroid===true)returnTouchableNativeFeedback;returnTouchableOpacity;// ★ 鸿蒙走这里(安全)}exportfunctiontouchableBackground(color,fixRadius){if(isAndroid){…}// ★ 鸿蒙不进returnundefined;// ★ 安全}exportconstshadowStyle={shadowOpacity:.35,…,elevation:5};⇒elevation(Android 阴影)与shadow*(iOS 阴影)都是平台专属属性⇒鸿蒙上被忽略、不崩✓
⇒⇒ 这一处"平台判断"没有造成任何问题✓
★★★ 五、文本级判据:两个操作项的标题都渲染出来了
✓ AB 项一 w=1188 h=84 x=66 y=906 ✓ AB 项二 w=1188 h=84 x=66 y=906⇒ActionButtonItem的title确实渲染了✓✓
(ActionButton本体是绝对定位的浮层,按钮文本'+'在屏幕右下角,不在正文流里)
⇒LogBox:屏上无通知✓ —— 我原本预期会有Animated: 'useNativeDriver' was not specified警告
(因为它的Animated.spring(this.anim, {toValue: …})全都没传useNativeDriver),
但实测屏上没有出现⇒ 如实记录为"未观察到该警告" ✓
六、接入与预检(要点)
# 装库(file: 接入;它的 dependencies 只有 prop-types,宿主通常已有)npminstall"git+https://atomgit.com/oh-react-native/react-native-action-button.git#2.8.5-ohos-1.0.0"--ignore-scripts# ★ 然后必须在你入口文件的最前面加一行 polyfill(见第一节),否则 import 即崩七、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | ActionButton.js(11459 B)+ActionButtonItem.js(5414 B)+shared.js(1010 B)+index.d.ts⇒ 源码与类型都在 ✓ |
⚠️ 包内含example/ | 50 个文件里大半是 example 目录(Android/iOS 工程)⇒ 开发时可能有干扰,无害 |
spec.json | ⚠️缺upstreamCommit |
validation | ⚠️"build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0"+rnoh: "0.84.4"⇒两字段自相矛盾 |
| 备注 | 库代码零改动(我加的是自己的 polyfill,没改库) |
关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。
⚠️ 这个库有两处版本相关的问题(Text.propTypes、componentWillReceiveProps),换基线时必须重新评估这两条。
八、已知限制与本次验证边界
8.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★★★★★ 直接 import 会崩 | Text.propTypes.style(L366)在 RN ≥0.62 是undefined⇒必须加 polyfill |
★★★★ 受控模式(activeprop)失效 | componentWillReceiveProps被 React 19 移除 ⇒ 用ref或key重挂载代替 |
★★★★ 28 项defaultProps全是常量 | 适合做回归断言(buttonText '+'/size 56/spacing 20/position 'right'/offsetX 30…) |
★★★ref可用的公开方法 | reset()、animateButton()(reset实测不抛错) |
★★★isAndroid = false无害 | 走TouchableOpacity,elevation/shadow*被忽略 |
★★ 两个操作项的title正常渲染 | 文本级判据命中 |
★★ 未观察到useNativeDriver警告 | 它的Animated.spring都没传该参数,但屏上没出现警告(如实记录) |
★ 包里有example/目录 | 占了大半文件数 |
8.2 本次验证的边界
- ★★★ 没有验证"点击展开"的真实交互(FAB 的展开动画、弧线排布、
backdrop遮罩); - ★★★ 受控模式只"证伪"未"验证替代方案"(
ref调reset()/key重挂载都只是建议,未实测展开效果); - ★★ 未验证展开后的几何(
degrees: 45/spacing: 20/size: 56的弧线位置); - ★★ 未验证
renderIcon自定义图标; - ★★ 未验证
backdrop/backgroundTappable/autoInactive的实际行为; - ★★ 未验证
position='left'/verticalOrientation='down'; - 未验证
hideShadow/ 自定义shadowStyle在鸿蒙上的视觉表现; - 未验证多个
ActionButton并存的层级/zIndex; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
库代码未改动(只新增了我自己的 polyfill 文件)。
九、常见问题
Q1:一装就报Cannot read property 'style' of undefined?
因为ActionButton.jsL366 用了Text.propTypes.style,而RN ≥0.62 已移除Text.propTypes。
加 polyfill(见第一节):
// polyfills/text-prop-types.jsconst{Text}=require('react-native');if(!Text.propTypes)Text.propTypes={style:{}};并在入口文件最前面(所有页面 import 之前)import './polyfills/text-prop-types';
Q2:active={true}传进去没反应?
因为驱动它的是componentWillReceiveProps,React 19 已移除该生命周期。
两个替代方案:① 用ref调inst.reset()/inst.animateButton();② 用key重挂载(让active作为初始值进构造函数)。
Q3:它在 RN 0.84 上能跑吗?
加 polyfill 之后能跑:契约 28 项defaultProps(取值 18 项精确命中)、两个操作项标题渲染、ref调reset()不抛错、LogBox无通知。
但受控模式失效(见 Q2)。
Q4:Platform.OS === "android"的判断会有问题吗?
不会。鸿蒙上isAndroid = false⇒getTouchableComponent()返回TouchableOpacity、touchableBackground()返回undefined,都安全;elevation/shadow*这些平台专属属性会被忽略。
Q5:这种 polyfill 什么时候"够用"、什么时候"不够"?
看那个库是只在propTypes声明里用被移除的 API,还是在渲染路径上用。
前者(本库)一行补丁就够;后者补丁无效,需要改库或换库。
Q6:ref上有哪些可用方法?
实测prototype上有:componentDidMount、componentWillUnmount、componentWillReceiveProps(不会执行)、getOrientation、getOffsetXY、getOverlayStyles、render、_renderMainButton、_renderButtonIcon、_renderActions、_renderTappableBackground、animateButton、reset。
其中reset()实测可安全调用✓
小结
react-native-action-button是可展开的悬浮操作按钮,代码本身很干净(ListView/createClass/React.PropTypes/ViewPropTypes/字符串 ref/this.refs/mixins/getDefaultProps全部为空),
但它有两处与版本演进撞车的地方,一处能修、一处不能修。
最值得记下来的是这几条:
★★★★★ 一个"可修的 import 即崩":
Text.propTypes.style。
RN ≥0.62 移除了Text.propTypes⇒ 模块顶层的Text.propTypes.style⇒ TypeError。
但这是【一行类型声明】⇒ 一个 20 行的 polyfill(Text.propTypes = {style: {}})就能修 ✓,
而且是"不改库"的修法✓(对比:如果被移除的是整个列表实现,那就只能否决 —— 那种情况补丁无效)★★★★★ polyfill 的【执行顺序】是成败关键。
ESM 的import声明会提升、求值不会⇒ 把 polyfill 放在所有页面 import之前(实测第 28 行 / 页面在 76 行),hilog里能看到它自己的 NOTE,页面判据也确认Text.propTypes已是object✓★★★★ 一处修不了的:
componentWillReceiveProps被 React 19 移除 ⇒ 受控模式失效。
这不是"报错",而是逻辑静默不执行—— 更隐蔽。
⇒ 替代:ref手动驱动,或key重挂载(让active作为初始值进构造函数)。★★★★ 28 项
defaultProps逐个核对,18 项取值精确命中;但我把"恰好 29 项"断错了(实际 28)。
⇒如实记录、不改判据去迁就—— 契约型断言的价值就在于它会同时暴露"库变了"和"我数错了"✓★★★
Platform.OS === "android"这类判断在鸿蒙上是安全的:isAndroid = false⇒ 走TouchableOpacity、touchableBackground返回undefined;elevation/shadow*被忽略 ✓★★ 别把"预期会出现的警告"写成结论。
我本来预期有Animated: 'useNativeDriver' was not specified(它的Animated.spring确实都没传),
但屏上没出现⇒ 只能写"未观察到",不能写"没有" ✓★ 包里有
example/目录(50 个文件里大半是它)—— 排查时要记得"这些不是库本体" ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-action-button(上游2.8.5的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-action-button |
| 适配 TAG | 2.8.5-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 可展开的悬浮操作按钮(FAB) |
| 运行时依赖 | prop-types(宿主通常已有) |
| ⚠️ 必须的额外步骤 | 入口最前面加 polyfill 补Text.propTypes(否则 import 即崩) |
| 导出形态 | 默认导出ActionButton/ActionButtonItem |
| 许可证 | MIT |
| 上游仓库 | https://github.com/mastermoo/react-native-action-button |
| 宿主工程 | RNOH084Demo(测试页rnAppKey = ActionButtonTestApp) |
npminstall"git+https://atomgit.com/oh-react-native/react-native-action-button.git#2.8.5-ohos-1.0.0"--ignore-scripts// ① 先放这个文件:polyfills/text-prop-types.jsconst{Text}=require('react-native');if(!Text.propTypes)Text.propTypes={style:{}};// ② 入口文件最前面(在所有页面 import 之前)import'./polyfills/text-prop-types';import ActionButton from 'react-native-action-button'; import ActionButtonItem from 'react-native-action-button/ActionButtonItem'; // 非受控模式(推荐:点击自己展开/收起) <ActionButton buttonColor="#0b4f9e" position="right" offsetX={30} offsetY={90}> <ActionButtonItem title="拍照" onPress={() => {}} /> <ActionButtonItem title="相册" onPress={() => {}} /> </ActionButton> // 受控模式(active prop)在 React 19 上【失效】⇒ 改用 ref 或 key const ref = useRef(null); ref.current?.reset(); // 收起 // ⚠️ 不加 polyfill 会 import 即崩:Text.propTypes.style(L366) // ⚠️ componentWillReceiveProps 已被 React 19 移除 ⇒ 受控模式静默失效 // ⚠️ Animated.spring 都没传 useNativeDriver(实测屏上未出现警告) // ⚠️ 28 项 defaultProps:buttonText '+'、size 56、spacing 20、position 'right'、offsetX/Y 30、degrees 45# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey ActionButtonTestApp判据的复现方式
# ① polyfill 是否执行(hilog 里它自己会打一行 NOTE)hdc shell"hilog -x | grep polyfill"# ② 契约:28 项 defaultProps + Text.propTypes 是否已补hdc shell"hilog -x | grep abc-test"|Select-String'PASS|FAIL|SUMMARY|NOTE'# ③ 文本级:两个操作项的 titledevecocli ui layout|Select-String'AB 项'验证环境
| 项 | 版本 |
|---|---|
| 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 |
| 宿主依赖 | prop-types(已有)+必须加Text.propTypespolyfill |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap3 分 40 秒⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约判据 22 条(21 PASS + 1 条是我自己把 28 数成 29);polyfill 生效双证(hilog + 页面);两个操作项标题命中;ref调reset()不抛错;LogBox 无通知;4 张截图互异 |
⚠️基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
⚠️ 本库有两处版本相关问题(Text.propTypes/componentWillReceiveProps)⇒换基线时必须重新评估这两条。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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