本文记录把 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,鸿蒙侧不需要任何特殊处理
★ 替代实现保留了原 APIopen() / close() / toggle() 仍是原型方法,可用 ref 调
⚠️ onPress 的 this 语义有变化原库用 onPress.call(this),替代实现直接调用(React 19 下无必要)
⚠️ easing 映射表是白名单原库用 tween-functions 的名字;替代实现把常用名映射到 Easing.*,未覆盖的名字会回退到 linear

6.2 本次验证的边界

  1. 原库的渲染完全未验证(明知加载即崩,故意不触发其渲染);
  2. measure() 给 0 只在 RNOH 模拟器上实测,未做真机与其它平台对照;
  3. 动画的逐帧曲线未采集(只核对了起点 0、终点 90,以及 isVisible 状态);
  4. easing 的各种取值未逐项验证(只用了默认 linear);
  5. 替代实现与原库的"视觉一致性"未逐像素比对(高度值一致,但间距/边框等未逐项核);
  6. onPress 的 this 行为差异未实测(只是源码层面的说明);
  7. 未验证 TouchableHighlight 的 underlayColor / activeOpacity 实际效果;
  8. 未验证快速连点、展开过程中再次点击等边界;
  9. 未在 0.88 / RNOH 0.84.4 上复验(交付包声明的基线;理由见第五节);
  10. 只在一台模拟器上验证(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:改一行能修好吗?

不能。 至少要动四处:

  1. React.PropTypes.* ⇒ 删掉或换 prop-types 包
  2. React.createClass({...}) ⇒ 改写成 class(或函数组件)
  3. mixins: [tweenState.Mixin] ⇒ class 不支持 mixins,得自己实现(建议直接换成 Animated)
  4. 字符串 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、无原生代码,但在本轮基线(以及它自己声明的基线上)都不可用。

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

  1. ★★★★ 四种"已移除的写法"可以同时出现在一个文件里。
    React.createClass、React.PropTypes、字符串 ref + this.refs、mixins —— 任何一条都足以让加载失败 ⇒ 预扫必须逐项查全,不能只查一种。

  2. ★★★★ 崩溃点可以精确到字段,而且顺序有讲究。
    propTypes 是对象字面量,在 createClass(...) 被调用之前就求值完 ⇒ 崩在 React.PropTypes.number(第一个字段)上,
    createClass 根本没轮到执行 ⇒ 特征词 number 与源码逐字对应 ✓

  3. ★★★ require 返回 undefined 不等于"没问题" —— 异常被 guardedLoadModule 收进 LogBox 了。
    undefined 就是"加载失败"的信号,栈里的 guardedLoadModule 说明是模块求值期。

  4. ★★★★ 一个真实的环境发现:measure() 在 RNOH 上给不出高度。
    原库注释里那句 2015 年的老问题(facebook/react-native#953)在 RNOH 上依然成立,加 setTimeout 也没用。
    ⇒ 可靠解法:onLayout 上报自然高度 + 内容用绝对定位渲染在裁剪容器里(否则容器 0 高时内容也量不出)。
    对照数据:改前 contentHeight=0(展不开)→ 改后 90(精确到达) ✓

  5. ★★★ 依赖要用 file: 接入时,库自己的依赖不会自动装。 这个库声明了 react-tween-state@0.0.5(连带 tween-functions),必须显式 npm install 补上,否则连打包都过不去。

  6. ★★ 判据要逐条直写日志。 本轮 16 条判据全部以 PASS|描述|期望|实际 的独立行输出,
    采集端 grep 即可拿全证据 —— 不要把"证据能不能留下来"绑在页面的汇总面板上。

  7. ★ 基线要说清楚。 交付包声明 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
适配 TAG1.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 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
宿主依赖⚠️ 需补装 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐