本文记录把 react-native-action-button(可展开的悬浮操作按钮 FAB)适配到 HarmonyOS 的完整过程。

它做的事:右下角一个圆形按钮,点开后沿弧线/直线弹出若干操作项,每项带标题与图标。

结论:它在本环境里"import 就会崩",但崩溃点只有一行、而且只需一个 20 行的 polyfill 就能修好。
修好之后,29 项契约里 28 项 defaultProps 常量全部精确命中,两个操作项的标题正常渲染,ref 也能调用它的 reset()。
但有一处修不了的语义变化:它依赖的 componentWillReceiveProps 在 React 19 里已被移除 ⇒ 受控模式失效。

★★★★★ 一、它的崩溃点:一行 Text.propTypes.style

ActionButton.js L366(在 模块顶层 求值的 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.js
const {Text} = require('react-native');
if (!Text.propTypes) {
  Text.propTypes = {style: {}};      // 只需要"存在且能取 .style"
  console.log('[polyfill] NOTE | 已为 RN 的 Text 补回 propTypes…');
}

执行的顺序极其关键 —— 必须在"库被 import 之前"跑:

// index.js
import './polyfills/text-prop-types';    // ★ 放在所有页面 import 之前(实测第 28 行)
…
import ActionButtonTestApp from './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.js L42:

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() ✓

用受控模式怎么办? 两个办法:

  1. 用 ref 手动驱动(inst.reset() / inst.animateButton());
  2. 给组件加 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:

export const isAndroid = Platform.OS === "android";           // 鸿蒙 ⇒ false

export function getTouchableComponent(useNativeFeedback) {
  if (useNativeFeedback === true && isAndroid === true) return TouchableNativeFeedback;
  return TouchableOpacity;                                    // ★ 鸿蒙走这里(安全)
}
export function touchableBackground(color, fixRadius) {
  if (isAndroid) { … }                                        // ★ 鸿蒙不进
  return undefined;                                           // ★ 安全
}
export const shadowStyle = {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,宿主通常已有)
npm install "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,没改库)

关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
⚠️ 这个库有两处版本相关的问题(Text.propTypes、componentWillReceiveProps),换基线时必须重新评估这两条。


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

8.1 用这个库要知道的

事项说明
★★★★★ 直接 import 会崩Text.propTypes.style(L366)在 RN ≥0.62 是 undefined ⇒ 必须加 polyfill
★★★★ 受控模式(active prop)失效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 本次验证的边界

  1. ★★★ 没有验证"点击展开"的真实交互(FAB 的展开动画、弧线排布、backdrop 遮罩);
  2. ★★★ 受控模式只"证伪"未"验证替代方案"(ref 调 reset() / key 重挂载都只是建议,未实测展开效果);
  3. ★★ 未验证展开后的几何(degrees: 45 / spacing: 20 / size: 56 的弧线位置);
  4. ★★ 未验证 renderIcon 自定义图标;
  5. ★★ 未验证 backdrop / backgroundTappable / autoInactive 的实际行为;
  6. ★★ 未验证 position='left' / verticalOrientation='down';
  7. 未验证 hideShadow / 自定义 shadowStyle 在鸿蒙上的视觉表现;
  8. 未验证 多个 ActionButton 并存的层级/zIndex;
  9. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  10. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动(只新增了我自己的 polyfill 文件)。


九、常见问题

Q1:一装就报 Cannot read property 'style' of undefined?

因为 ActionButton.js L366 用了 Text.propTypes.style,而 RN ≥0.62 已移除 Text.propTypes。
加 polyfill(见第一节):

// polyfills/text-prop-types.js
const {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 全部为空),
但它有两处与版本演进撞车的地方,一处能修、一处不能修。

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

  1. ★★★★★ 一个"可修的 import 即崩":Text.propTypes.style。
    RN ≥0.62 移除了 Text.propTypes ⇒ 模块顶层的 Text.propTypes.style ⇒ TypeError。
    但这是【一行类型声明】 ⇒ 一个 20 行的 polyfill(Text.propTypes = {style: {}})就能修 ✓,
    而且是"不改库"的修法 ✓(对比:如果被移除的是整个列表实现,那就只能否决 —— 那种情况补丁无效)

  2. ★★★★★ polyfill 的【执行顺序】是成败关键。
    ESM 的 import 声明会提升、求值不会 ⇒ 把 polyfill 放在所有页面 import 之前(实测第 28 行 / 页面在 76 行),
    hilog 里能看到它自己的 NOTE,页面判据也确认 Text.propTypes 已是 object ✓

  3. ★★★★ 一处修不了的:componentWillReceiveProps 被 React 19 移除 ⇒ 受控模式失效。
    这不是"报错",而是逻辑静默不执行 —— 更隐蔽。
    ⇒ 替代:ref 手动驱动,或 key 重挂载(让 active 作为初始值进构造函数)。

  4. ★★★★ 28 项 defaultProps 逐个核对,18 项取值精确命中;但我把"恰好 29 项"断错了(实际 28)。
    ⇒ 如实记录、不改判据去迁就 —— 契约型断言的价值就在于它会同时暴露"库变了"和"我数错了" ✓

  5. ★★★ Platform.OS === "android" 这类判断在鸿蒙上是安全的:
    isAndroid = false ⇒ 走 TouchableOpacity、touchableBackground 返回 undefined;elevation/shadow* 被忽略 ✓

  6. ★★ 别把"预期会出现的警告"写成结论。
    我本来预期有 Animated: 'useNativeDriver' was not specified(它的 Animated.spring 确实都没传),
    但屏上没出现 ⇒ 只能写"未观察到",不能写"没有" ✓

  7. ★ 包里有 example/ 目录(50 个文件里大半是它)—— 排查时要记得"这些不是库本体" ✓


本篇用到的库

项内容
三方库react-native-action-button(上游 2.8.5 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-action-button
适配 TAG2.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)
npm install "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.js
const {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 -b com.rnoh084.demo -a EntryAbility --ps rnAppKey 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'
# ③ 文本级:两个操作项的 title
devecocli ui layout | Select-String 'AB 项'

验证环境

项版本
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
宿主依赖prop-types(已有)+ 必须加 Text.propTypes polyfill
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 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

Logo

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

更多推荐