React Native for OpenHarmony 实战:三方库 react-native-accordion 的鸿蒙化适配指南
本文记录把 react-native-accordion(手风琴/折叠面板)适配到 HarmonyOS 的完整过程。
它做的事很小:一个可点击的头部 + 一段能展开/收起的内容区,展开时高度从 0 动画到内容的实际高度。
结论先说:这个库在 React 19 上【加载即崩】,而且不是改一行能修的。 原因不是依赖、不是原生代码,而是三种早已被移除的写法同时出现在同一个 89 行的文件里:
import React, { PropTypes } from 'react'; // ① React.PropTypes 已被移除(连具名导入一起失效)
var Accordion = React.createClass({ // ② React.createClass 已被移除
mixins: [tweenState.Mixin], // ③ mixins 在 class 里不存在
propTypes: {
activeOpacity: React.PropTypes.number, // ① 又一处(对象字面量,先于 createClass 求值)
content: React.PropTypes.element.isRequired,
…
},
getDefaultProps() { … }, // createClass 独有的生命周期
getInitialState() { … }, // 同上
toggle() { … this.tweenState('height', {…}) }, // mixin 注入的方法
render() {
return (
<View>
<TouchableHighlight ref="AccordionHeader" … /> {/* ④ 字符串 ref */}
<View ref="AccordionContentWrapper" …>
<View ref="AccordionContent"> {/* ④ 又两处 */}
四种问题、任何一条都足以让模块加载失败。下面把证据、以及一份可用的现代写法替代实现都记录下来。
★★★★ 一、加载即崩的取证(容器化 require)
页面上放一个按钮,按下后在 try/catch 里 require('react-native-accordion'):
require 不抛错(返回 undefined) ← 它抛了,只是没抛到你手上
同一时刻 LogBox 整屏红屏:
Uncaught Error
Cannot read property 'number' of undefined ← ★ 特征词是 number
anonymous
loadModuleImplementation
guardedLoadModule
metroRequire
因果链可以精确到字段:
propTypes是对象字面量,它的成员会在React.createClass(...)被调用之前就求值;- 其中第一个字段就是
activeOpacity: React.PropTypes.number; - 而
React.PropTypes在 React 19 上已经是undefined⇒undefined.number⇒ 抛错。
⇒ 所以崩溃发生在 React.PropTypes 上,React.createClass 根本没轮到执行 ✓
⇒ 特征词 number 与源码逐字对应 ✓;栈里的 guardedLoadModule 说明是模块求值期 ✓
顺带记一条通用经验:Metro 的
guardedLoadModule会把模块求值期的异常报给 LogBox,而require()返回undefined而不抛错 ⇒undefined就是"这个模块加载失败了"的信号,不要以为"没报错就是好的"。

★★★ 二、"已移除的写法"这份清单又长了
这个库把几种不同年代的写法混在了一起:
| 写法 | 状态 | 典型报错 |
|---|---|---|
React.createClass({...}) | React 16 起弃用、后被移除 | 加载即崩 |
React.PropTypes.* | React 15.5 起移出到 prop-types 包 | Cannot read property 'number' of undefined |
字符串 ref="x" + this.refs.x | 早已弃用、后被移除 | 取不到 ref / 抛错 |
mixins: [...] + this.tweenState() | class 组件不支持 mixins | 方法不存在 |
⇒ 这类"加载即崩"的写法并不孤例,而且四种可以在同一个文件里共存 ⇒ 预扫时必须逐项查、不能只查一种。
★★★★ 三、一份可用的替代实现(实测 16/16 全通过)
原库的对外行为其实很简单,用现代写法重写不到 100 行:class 组件 + Animated,丢掉 createClass、mixins、字符串 ref 和一个 2015 年的依赖。
3.1 关键改写点
| 原写法 | 替代写法 |
|---|---|
React.createClass({...}) | class Accordion extends React.Component |
getDefaultProps() | static defaultProps = {…} |
getInitialState() | 构造函数里的 this.state = {…} |
mixins: [tweenState.Mixin] | 一个 Animated.Value(高度动画) |
this.tweenState('height', {…}) | Animated.timing(this.heightAnim, {toValue, duration, easing, useNativeDriver: false}).start() |
this.getTweeningValue('height') | this.heightAnim(直接作为 style.height) |
ref="AccordionContent" + this.refs.x | 回调 ref(ref={r => (this.contentRef = r)}) |
React.PropTypes.* | 删掉(或换 prop-types 包) |
3.2 ★★★★ 一个真实的环境发现:measure() 在 RNOH 上给不出高度
原库量内容高度用的是:
_getContentHeight() {
if (this.refs.AccordionContent) {
this.refs.AccordionContent.measure((ox, oy, width, height, px, py) => { … });
}
}
componentDidMount() {
// 原库注释:without setTimeout, measure returns 0 for every value.
// See https://github.com/facebook/react-native/issues/953
setTimeout(this._getContentHeight);
}
我照做了(含 setTimeout),实测仍然拿不到高度:
A1.state = {"isVisible":false,"contentHeight":0} ← contentHeight 恒为 0
⇒ 后果:内容高度是 0 ⇒ 展开动画的目标值是 0 ⇒ 永远展不开(连 expanded 初始展开的实例也展不开)✗
改成 onLayout 上报自然高度后就正常了,但要注意一个先有鸡还是先有蛋的问题:
- 如果把内容直接放在
height: 0的裁剪容器里 ⇒ 内容自己也是 0 高 ⇒onLayout报出 0 ✗ - 解法:把内容用绝对定位渲染在裁剪容器里 ⇒ 内容始终按自然高度布局 ⇒
onLayout才能报出真高度 ✓
<Animated.View style={{height: this.heightAnim, overflow: 'hidden'}}>
<View
style={{position: 'absolute', left: 0, right: 0, top: 0}} {/* ★ 关键 */}
onLayout={e => this._onContentLayout(e.nativeEvent.layout.height)}>
{this.props.content}
</View>
</Animated.View>
改前改后的对照数据:
contentHeight | 展开动画值 | expanded 实例 | |
|---|---|---|---|
用 measure() | 0 ✗ | 0 ✗ | 展不开 ✗ |
用 onLayout + 绝对定位 | 90 ✓ | 90(精确到达)✓ | 正常展开 ✓ |
⇒ 原库注释里那个 2015 年的老问题(measure 全返回 0),在 RNOH 上依然成立;而 onLayout 是可靠的替代。
3.3 实测结果:16 / 16 全通过
每条判据各自一行日志(PASS/FAIL 明确),采集端直接 grep 就能拿全证据:
PASS | 主标题窗口 y(vp) ≥ 39(状态栏 117px=39vp) | 实际=48
PASS | 替代实现是 class(prototype.open 存在)
PASS | prototype.close 存在 / prototype.toggle 存在 / prototype.render 存在
PASS | defaultProps.expanded = false
PASS | defaultProps.animationDuration = 300
PASS | defaultProps.easing = linear
PASS | defaultProps.underlayColor = #000
PASS | A1 初始 isVisible = false(expanded 未传)
PASS | A1 初始高度动画值为 0
PASS | A1 已量到内容高度(>0) ← contentHeight = 90
PASS | A1 open 后 isVisible = true
PASS | A1 动画值已到达内容高度 ← 动画当前值 = 90,精确到达
PASS | A1 close 后 isVisible = false
PASS | A1 动画值已回到 0
SUMMARY | 合计 16/16 通过,失败 0
几何层面双向验证(布局 dump):
A1 区(收起) :只有 MARK + 头部 ⇒ 内容不可见 ✓
A2 区(expanded):MARK + 头部 + "A2 初始就展开的内容"(h=66)⇒ 内容可见 ✓



四、接入与预检
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ / ios/ / android/ | 都没有 |
| ③ | 原生痕迹预扫 | 无原生源文件、无 NativeModules ⇒ 纯 JS ✓ |
| ④ | import 图体检 | react + react-native + react-tween-state ⇒ ⚠️ 一个真实的运行时依赖 |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
| ⑥ | ★ 老 API 预扫 | 命中 React.createClass、React.PropTypes(含从 react 具名导入)、字符串 ref、mixins |
| ⑦ | 平台分支预扫 | ✅ 无(一处 Platform.OS 都没有) |
⚠️ 依赖陷阱(务必注意):库的 dependencies 里声明了 react-tween-state: 0.0.5,而
用 file:(本地目录)方式接入时,npm 不会自动安装库自己的依赖 ⇒ 必须显式补装:
npm install react-tween-state@0.0.5 --ignore-scripts
它自身还会带一个传递依赖 tween-functions(实测装上的是 1.2.0)✓
(react-tween-state 本身不含已移除的 API ✓ —— 问题在库自己身上)
装完之后的真实状态:能装、能打包、但一 require 就崩 ✓
npm install "git+https://atomgit.com/oh-react-native/react-native-accordion.git#1.0.1-ohos-1.0.0" --ignore-scripts
# 或本地目录
npm install ../react-native-accordion --ignore-scripts
五、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 逐字节一致 | 5 / 6(.eslintignore、.npmignore、index.js、README.md、src/index.js) |
| 不同 | 1 个:package.json |
| 缺少 | 0 |
| 多出 | 6 个(.gitignore、两个 README.OpenHarmony*、代码检查报告、spec.json、契约测试) |
- ✅ 实现零改动(
src/index.js与上游 npm 包逐字节一致); - ⚠️
spec.json缺upstreamCommit,而 npm 的gitHead有值(1cc6b8362a34d4fa0d0ec10383d0f6eed0c792a9)⇒ 属应修项; - ⚠️
validation: "build-pass-device-pending"⇒ 交付方设备侧未完成; - ⚠️ 交付包未修改任何实现 ⇒ 它同样在 React 19 上加载即崩(本轮的取证就是针对交付包本身做的)。
★ 关于基线(必须说明的一点)
这个交付包的 spec.json 声明的是 reactNative: "0.88.0" + rnoh: "0.84.4"(verification: evidence/batch50/…),
而本次验证是在 RN 0.84.1 + RNOH 0.84.3 的宿主上完成的 —— 两者不是同一条基线。
为什么这不影响结论:崩溃的四个原因(React.createClass、React.PropTypes、字符串 ref、mixins)
在新版本上同样是移除状态,它们与 RN 的小版本无关;取证拿到的是模块求值期抛 Cannot read property 'number' of undefined,
这是 React 侧的行为,不随 RN 版本变化。
⇒ 所以本轮的判定(该交付包不可用,需按第三节重写)是跨基线成立的。
六、已知限制与本次验证边界
6.1 用这个库/看这份交付包要知道的
| 事项 | 说明 |
|---|---|
| ★ 在 React 19 上不可用 | 四种已移除的写法同时存在,加载即崩 |
★★ measure() 在 RNOH 上取不到高度 | 原库靠它量内容高度 ⇒ 即便改了前三个问题,也还展不开 |
★ 必须改用 onLayout + 绝对定位 | 见 3.2 的对照数据 |
| ★ 依赖要显式补装 | react-tween-state@0.0.5(file: 接入不自动装) |
| ★ 无平台分支、无原生代码 | 纯 JS,鸿蒙侧不需要任何特殊处理 |
| ★ 替代实现保留了原 API | open() / close() / toggle() 仍是原型方法,可用 ref 调 |
⚠️ onPress 的 this 语义有变化 | 原库用 onPress.call(this),替代实现直接调用(React 19 下无必要) |
⚠️ easing 映射表是白名单 | 原库用 tween-functions 的名字;替代实现把常用名映射到 Easing.*,未覆盖的名字会回退到 linear |
6.2 本次验证的边界
- 原库的渲染完全未验证(明知加载即崩,故意不触发其渲染);
measure()给 0 只在 RNOH 模拟器上实测,未做真机与其它平台对照;- 动画的逐帧曲线未采集(只核对了起点 0、终点 90,以及
isVisible状态); easing的各种取值未逐项验证(只用了默认linear);- 替代实现与原库的"视觉一致性"未逐像素比对(高度值一致,但间距/边框等未逐项核);
onPress的this行为差异未实测(只是源码层面的说明);- 未验证
TouchableHighlight的underlayColor/activeOpacity实际效果; - 未验证快速连点、展开过程中再次点击等边界;
- 未在 0.88 / RNOH 0.84.4 上复验(交付包声明的基线;理由见第五节);
- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机。
未改动库代码(交付包的 src/index.js 与上游逐字节一致,我只在旁边新增了自己的替代实现)。
七、常见问题
Q1:为什么一 require 就崩?到底卡在哪一句?
卡在对象字面量的求值上,而不是 createClass 上:
var Accordion = React.createClass({
propTypes: {
activeOpacity: React.PropTypes.number, // ← 先算这个
React.PropTypes 在 React 19 上是 undefined ⇒ undefined.number ⇒
Cannot read property 'number' of undefined ⇒ 栈停在 loadModuleImplementation → guardedLoadModule。
Q2:我 require 它没报错?
它抛了,只是没抛到你手上。 Metro 的 guardedLoadModule 把异常报给 LogBox,而 require() 返回 undefined。
⇒ undefined 就是"加载失败"的信号。
Q3:改一行能修好吗?
不能。 至少要动四处:
React.PropTypes.*⇒ 删掉或换prop-types包React.createClass({...})⇒ 改写成 class(或函数组件)mixins: [tweenState.Mixin]⇒ class 不支持 mixins,得自己实现(建议直接换成Animated)- 字符串
ref="x"+this.refs.x⇒ 换成回调 ref
而且改完这四处还会遇到第五个坑:measure() 取不到高度(见 Q4)。
Q4:为什么我重写成 class 之后还是展不开?
因为内容高度是 0 ⇒ 动画目标是 0。原库用 measure() 量高度,而实测 measure() 在 RNOH 上回调里一直是 0(原库注释里那句 “without setTimeout, measure returns 0” 的老问题依然成立,加 setTimeout 也没用)。
⇒ 改用 onLayout,并让内容绝对定位在裁剪容器里(否则容器高度为 0 时内容也量不出真高度)。
Q5:那我还需要用 react-tween-state 吗?
不需要。 它的作用就是给 this 注入 tweenState() / getTweeningValue() 两个方法(配合 mixins)。
改用 Animated 之后,连这个 2015 年的依赖(以及它的 tween-functions 子依赖)都可以不要了。
Q6:expanded 是干嘛的?
初始是否展开。替代实现里由构造函数读取:
this.state = {isVisible: !!props.expanded, contentHeight: 0};
实测 expanded 实例的内容在 dump 里可见(h=66),未传的实例内容不可见 ✓
Q7:open() / close() / toggle() 还能用吗?
能 —— 替代实现把它们保留为原型方法(AccordionReplacement.prototype.open 等),可以 ref 调用。
这比"模拟点击"更稳,判据不依赖点击定位。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、无原生代码、无平台分支 —— 障碍全在 JS 侧(四种已移除的写法 + measure() 量不到高度)。
小结
react-native-accordion 是 89 行的手风琴组件,纯 JS、无原生代码,但在本轮基线(以及它自己声明的基线上)都不可用。
最值得记下来的是这几条:
-
★★★★ 四种"已移除的写法"可以同时出现在一个文件里。
React.createClass、React.PropTypes、字符串ref+this.refs、mixins—— 任何一条都足以让加载失败 ⇒ 预扫必须逐项查全,不能只查一种。 -
★★★★ 崩溃点可以精确到字段,而且顺序有讲究。
propTypes是对象字面量,在createClass(...)被调用之前就求值完 ⇒ 崩在React.PropTypes.number(第一个字段)上,
createClass根本没轮到执行 ⇒ 特征词number与源码逐字对应 ✓ -
★★★
require返回undefined不等于"没问题" —— 异常被guardedLoadModule收进 LogBox 了。
undefined就是"加载失败"的信号,栈里的guardedLoadModule说明是模块求值期。 -
★★★★ 一个真实的环境发现:
measure()在 RNOH 上给不出高度。
原库注释里那句 2015 年的老问题(facebook/react-native#953)在 RNOH 上依然成立,加setTimeout也没用。
⇒ 可靠解法:onLayout上报自然高度 + 内容用绝对定位渲染在裁剪容器里(否则容器 0 高时内容也量不出)。
对照数据:改前contentHeight=0(展不开)→ 改后90(精确到达) ✓ -
★★★ 依赖要用
file:接入时,库自己的依赖不会自动装。 这个库声明了react-tween-state@0.0.5(连带tween-functions),必须显式npm install补上,否则连打包都过不去。 -
★★ 判据要逐条直写日志。 本轮 16 条判据全部以
PASS|描述|期望|实际的独立行输出,
采集端grep即可拿全证据 —— 不要把"证据能不能留下来"绑在页面的汇总面板上。 -
★ 基线要说清楚。 交付包声明
RN 0.88.0 + RNOH 0.84.4,而验证在RN 0.84.1 + RNOH 0.84.3上完成;
但四种已移除写法与 RN 小版本无关 ⇒ 结论跨基线成立 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-accordion(上游 1.0.1 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-accordion |
| 适配 TAG | 1.0.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 手风琴 / 折叠面板(在 React 19 上不可用) |
| 运行时依赖 | ⚠️ react-tween-state@0.0.5(连带 tween-functions@1.2.0)⇒ file: 接入需手动补装 |
prepare / postinstall | ✅ 都没有 |
| 许可证 | ISC |
| 上游仓库 | https://github.com/naoufal/react-native-accordion(★435,pushed 2018) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = AccordionTestApp) |
# 从适配仓库安装(⚠️ 必须补装它自己的运行时依赖)
npm install "git+https://atomgit.com/oh-react-native/react-native-accordion.git#1.0.1-ohos-1.0.0" --ignore-scripts
npm install react-tween-state@0.0.5 --ignore-scripts # ★ file:/本地目录接入不会自动装
// ★ 原库在 React 19 上加载即崩,下面是【替代实现】的用法(AccordionReplacement.tsx)
import Accordion from './AccordionReplacement';
const ref = useRef(null);
<Accordion
ref={ref}
expanded={false} // 初始是否展开(实测:expanded 实例内容可见)
animationDuration={300}
easing="linear" // 白名单映射到 Easing.*
underlayColor="#0b4f9e"
onPress={() => console.log('头部被点')}
header={<Text style={{color: '#fff'}}>▶ 头部(点击可切换)</Text>}
content={
<View>
<Text>内容第一行</Text>
<Text>内容第二行</Text>
</View>
}
/>
// 编程控制(原型方法,可用 ref 调)
// ref.current.open(); ref.current.close(); ref.current.toggle();
// ⚠️ 原库的四处已移除写法:React.createClass / React.PropTypes / 字符串 ref / mixins
// ⚠️ 原库用 measure() 量内容高度 ⇒ RNOH 上取到的恒为 0(展不开)
// 替代实现改为 onLayout + 内容绝对定位(见正文 3.2)
// ⚠️ Animated 驱动高度是布局属性 ⇒ useNativeDriver 必须为 false
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey AccordionTestApp
判据的复现方式
# ① 加载崩溃取证(受控 require,放在截图之后再按 —— 会整屏红屏)
hdc shell "uitest uiInput click <x> <y>"
hdc shell "hilog -x | grep accordion-test" | Select-String 'PROBE'
# ⇒ require 不抛错(返回 undefined)
devecocli ui layout | Select-String 'logbox_message_contents_text|logbox_stack_frame_text'
# ⇒ Cannot read property 'number' of undefined
# ⇒ anonymous → loadModuleImplementation → guardedLoadModule → metroRequire
# ② 替代实现的判据:逐条 PASS/FAIL 日志(见正文 3.3)
hdc shell "hilog -x | grep accordion-test" | Select-String 'PASS|FAIL|SUMMARY'
# ③ 几何:收起/展开的内容可见性
devecocli ui layout | Select-String 'MARK-A1|MARK-A2|初始就展开的内容'
# ④ 内容高度:优先看 contentHeight(改 onLayout 前后是 0 → 90)
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.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 |
| 宿主依赖 | ⚠️ 需补装 react-tween-state@0.0.5(连带 tween-functions@1.2.0) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 4 分 05 秒(含替代实现的两次构建:先 12/16,修好 onLayout 后 16/16) |
| 验证规模 | 容器化 require 取证 1 次(LogBox + 调用栈);替代实现 16 条判据全通;几何双向验证 2 组(收起/展开);内容高度对照 2 组(0 → 90);4 张截图互异;交付包逐字节 diff 5/6 一致 |
⚠️ 基线说明:本交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4,本次验证在RN 0.84.1 + RNOH 0.84.3上完成;
因四种已移除写法与 RN 小版本无关,结论跨基线成立(详见第五节)。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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
更多推荐



所有评论(0)