React Native for OpenHarmony 实战:三方库 react-native-virtualized-view 的鸿蒙化适配指南
本文记录把 react-native-virtualized-view 适配到 HarmonyOS 的完整过程。
这个库极小(全部实现只有 448 B),做的事也只有一件:让你能在"可滚动的容器"里安全地放 FlatList,不再报那条嵌套警告。
结论:完全可用,而且是少见的"能用行为级 A/B 对照证明其价值"的库 ——
我把"原生 ScrollView 会报警告"和"换成这个库就不报警告"放在同一页面的两个相位里,用 hilog 时间戳把因果定死了。
★★★★★ 一、它到底做了什么:448 B 的全部实现
src/ScrollView/index.tsx(也就 16 行):
const ScrollViewComponent = ({children, ...props}) => (
<FlatList
{...props}
data={[]} // 空数据
keyExtractor={(_e, i) => 'dom' + i.toString()}
ListEmptyComponent={null}
renderItem={null}
ListHeaderComponent={() => <>{children}</>} // ★ 所有 children 塞进 ListHeaderComponent
/>
);
核心思路一句话:用一个"空的 FlatList"去冒充 ScrollView,把所有 children 放进它的 ListHeaderComponent。
为什么这样就不报警告了? 因为 RN 的那条警告针对的是
“VirtualizedList 被嵌套在 plain ScrollView 里” ——
而这里外层已经不是 plain ScrollView,而是一个 VirtualizedList(FlatList 本身就是 VirtualizedList 的封装)⇒ 警告条件不成立 ✓
★★★★★ 二、行为级 A/B 对照:用 hilog 时间戳把因果定死
这是本轮最有价值的部分。我没有笼统地说"换了这个库就好了",而是做了一次隔离实验:
页面结构(同一页里两个容器):
| 实例 | 外层容器 | 内容 |
|---|---|---|
| E1 | 原生 ScrollView(react-native) | 两个内层 FlatList(每个 3 项) |
| E2 | 本库 ScrollView(react-native-virtualized-view) | 两个内层 FlatList(每个 3 项) |
相位设计(隔离因果):
- P0(t≈3.5s 起):只挂 E1 ⇒ 观察警告是否出现(基线)
- P1(t≈10.5s 起):再挂 E2 ⇒ 观察警告计数是否不再增加
实测(hilog 是最可靠的证据):
hilog 里 VirtualizedLists 警告总数 = 1
10-17 15:19:20.530 9594 9765 E A0beef/#RNOH_JS:
__█__ VirtualizedLists should never be nested inside plain ScrollViews with the same orientation bec…
时间戳对时:
| 时刻 | 相位 | 说明 |
|---|---|---|
| 15:19:20 | P0(只挂 E1 原生版) | ★ 警告出现在这一刻 |
| 15:19:30 | P1(已追加 E2 本库版) | hilog 总数仍是 1 ⇒ 没有新增 ✓ |
⇒ ⇒ 因果清楚:警告来自原生 ScrollView;换成本库的 ScrollView 后不再产生新警告 ✓✓
⇒ 这比"我换了库、好像不报了"强得多 —— 有时间戳、有基线、有增量对照
同时的文本级印证(FlatList 的项有没有真的渲染出来):
P0(只 E1):E1 标记 = 6 E2 标记 = 0
P1(加 E2):E1 标记 = 6 E2 标记 = 4
⇒ E1 的 6 个标记恒定(2 个内层 FlatList × 3 项 = 6,精确)✓✓
⇒ E2 的标记从 0 变为 4 ⇒ 内容确实渲染了(还有 2 个在屏外未被布局 dump 列出 ✓)
★★★ 三、一个方法论发现:屏上 LogBox 节点数 ≠ 警告次数
我同时还统计了"屏上 LogBox 里出现 VirtualizedLists 文本的节点数",结果和 hilog 对不上:
P0:屏上 LogBox 里的警告节点数 = 1
P1:屏上 LogBox 里的警告节点数 = 2 ← 看起来像"多了一条"
而 hilog 里的警告总数 = 1 ← 实际只有一条
⇒ ⇒ 结论:屏上 LogBox 的"节点计数"不等于"警告次数"(一条通知可能对应多个文本节点,也可能是 LogBox 的分页指示在变)✓
⇒ ⇒ 判"某条警告出现了几次",要以 hilog / 日志为准,不要数 LogBox 的节点 ✓✓
(而且 LogBox 过一会儿会被 dismiss,屏上就什么都查不到了 —— 见最后一步"屏上无 LogBox")
★★ 四、契约与预检(全 PASS)
NOTE | 具名导出清单 = ScrollView
PASS | ★ 具名导出恰好 1 个(ScrollView)
PASS | ★★ 导出名是 ScrollView
PASS | ★★ ScrollView 是函数组件
NOTE | 默认导出(mod.default) = undefined
PASS | ★★ 没有默认导出(只能用 {} 具名导入)
PASS | ★ 是函数组件(无 prototype.render)
NOTE | react-native 的 FlatList 可用 = function
PASS | ★ react-native 的 FlatList 可用(本库依赖它)
SUMMARY | 合计 7/7 通过,失败 0
⇒ export { ScrollView } ⇒ 具名导出,import {ScrollView} from 'react-native-virtualized-view' 必须带花括号 ✓
(写成 import ScrollView from … 会拿到 undefined ⇒ 渲染时报 Element type is invalid —— 这是一类很常见的坑)
预检九项(全部干净):
| 预检项 | 结果 |
|---|---|
dependencies | ✅ 空 |
peerDependencies | ✅ 只有 react: * / react-native: * ⇒ 无原生 peer |
| 第三方 import | ✅ 只有 react + react-native |
ListView(RN 已移除) | ✅ 空 |
isRequired / propTypes | ✅ 空 |
| 老写法九项 | ✅✅ 全部为空 |
Platform.OS / measure() / onLayout / useNativeDriver / NativeModules | ✅ 全空 |
⚠️ 两个要注意的点:
scripts.prepare = "bob build"⇒npm install会跑它 ⇒ 安装时必须带--ignore-scripts(否则可能因为缺少react-native-builder-bob而失败);- star 只有 27 —— 是个小库(但功能确实成立,README 也说得很清楚:它就是为解决那条警告而生)。




★★★ 五、它适合什么、不适合什么
| 场景 | 是否适合 |
|---|---|
ScrollView 里要放 FlatList/SectionList | ✅ 正是为此而生 |
| 外层需要长列表虚拟化(成千上万行) | ❌ 不适合 —— 它把所有 children 塞进 ListHeaderComponent,等于一次性渲染全部内容 |
需要 ScrollView 的 refreshControl / stickyHeaderIndices | ⚠️ 要看 FlatList 是否支持同名 prop(refreshControl 支持;stickyHeaderIndices 不支持) |
| 只是想要一个能滚的容器,内部没有虚拟列表 | ⚠️ 用原生 ScrollView 更直接(不会有多余的虚拟化开销) |
⇒ 一句话:它是一剂"针对嵌套警告的对症药",不是"更好的 ScrollView" ✓
⇒ 代价必须清楚:ListHeaderComponent 里的内容不会虚拟化 ⇒ 内容很多时性能等同于普通 ScrollView ✓
六、接入与预检(要点)
# ★ 必须带 --ignore-scripts:它的 scripts.prepare 是 "bob build"
npm install "git+https://atomgit.com/oh-react-native/react-native-virtualized-view.git#1.0.0-ohos-1.0.0" --ignore-scripts
七、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | src/(3 个文件,共不到 700 B)+ lib/commonjs + lib/module + lib/typescript ⇒ 完整 ✓(27 文件) |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 实现零改动(本轮未修改库任何文件) |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
这个库无已移除写法、无平台分支、无原生依赖 ⇒ 结论跨基线成立 ✓
八、已知限制与本次验证边界
8.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★★ 它只是"空 FlatList 冒充 ScrollView" | data={[]} + renderItem={null} + ListHeaderComponent={() => children} |
| ★★★★★ 内容不会虚拟化 | 所有 children 都在 ListHeaderComponent 里 ⇒ 一次性渲染;内容多时性能等同普通 ScrollView |
| ★★★★ 具名导出,无默认导出 | import {ScrollView} from 'react-native-virtualized-view'(必须带花括号) |
★★★ scripts.prepare = bob build | 安装必须 --ignore-scripts |
★★ 不支持的 ScrollView props | 例如 stickyHeaderIndices(FlatList 没有对应能力) |
| ★ star 只有 27 | 小库;但它的 README 与实现完全一致,功能成立 |
8.2 本次验证的边界
- ★★★ 只验证了"嵌套警告不再产生"这一个行为,没验证其它
ScrollView语义(滚动位置、onScroll回调、contentOffset等); - ★★★ 未验证长内容下的性能(本次每个
FlatList只有 3 项); - ★★ 未验证
refreshControl/onEndReached等透传 prop 的行为; - ★★ 未验证
horizontal方向(警告只在"同方向"嵌套时出现); - 未验证
SectionList嵌套场景; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
未改动库代码。
九、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 契约 7/7 通过;FlatList 正常渲染(E1 的 6 个标记、E2 的标记都出现了);
A/B 对照证明:原生 ScrollView + 内层 FlatList 会产生 VirtualizedLists should never be nested… 警告(hilog 时间戳 15:19:20,P0 阶段),换成本库的 ScrollView 后不再新增警告 ✓
Q2:为什么 import ScrollView from 'react-native-virtualized-view' 拿到 undefined?
因为它是具名导出(export {ScrollView}),没有默认导出(实测 mod.default === undefined)。
必须写 import {ScrollView} from 'react-native-virtualized-view'。
写错时的报错是 Element type is invalid … got: undefined,而这个错误信息不会告诉你"导入方式错了" ⇒ 记住这条。
Q3:装了之后 npm install 报错?
它的 scripts.prepare 是 bob build,安装时会执行。加 --ignore-scripts 即可。
Q4:它能替代 ScrollView 吗?
不能当作"更好的 ScrollView"用。 它把 children 全放进 ListHeaderComponent,内容不会虚拟化。
只在"ScrollView 里确实要放 FlatList"时才用它;否则直接用原生 ScrollView 更合适。
Q5:为什么 LogBox 上数出来 2 条,hilog 里只有 1 条?
屏上 LogBox 的节点数不等于警告次数(一条通知可能对应多个文本节点,分页指示也会变)。
判"警告出现几次"以 hilog/日志为准 ✓
Q6:怎么确认警告到底是谁产生的?
像我这样隔离:先用原生 ScrollView 复现(拿到基线与时间戳),再追加本库版本,看总数是否增加。
两步之间留足相位间隔,日志时间戳就能对上 ✓
小结
react-native-virtualized-view 是一个 448 B 的"对症药":用空 FlatList 冒充 ScrollView,
把 children 放进 ListHeaderComponent,从而消除"虚拟列表嵌套在 plain ScrollView"的警告。
零依赖、零原生 peer、老写法九项全空、具名导出、无默认导出。
最值得记下来的是这几条:
-
★★★★★ 用"隔离 + 追加"的方式做 A/B,而不是笼统地说"换了库就好了"。
P0 只挂原生版 ⇒ hilog 在 15:19:20 出现 1 条警告;
P1 追加本库版 ⇒ hilog 总数仍是 1 ⇒ 不再新增 ✓✓
⇒ 有时间戳、有基线、有增量 —— 这才叫证明。 -
★★★★★ 屏上 LogBox 节点数 ≠ 警告次数。
屏上从 1 变 2,而 hilog 只有 1 条 ⇒ 数 LogBox 会被误导 ✓
⇒ 判"警告几次"一律看 hilog/日志。 -
★★★★ 极小的库,判据反而更硬。
448 B 的源码一次读完 ⇒ 能精确知道它"就是用空FlatList冒充ScrollView" ⇒
于是判据可以直指核心(警告是否新增),而不是泛泛地"跑起来了没崩" ✓ -
★★★ 具名导出 / 无默认导出,是一类隐蔽的坑。
写错导入方式只会得到Element type is invalid … got: undefined,报错不会提示"你导入方式错了" ⇒
预检阶段就该把"导出形态"断言清楚(本次实测mod.default === undefined)✓ -
★★★ 一定要说清"它的代价":
内容不虚拟化 ⇒ 它是"治嵌套警告的药",不是"更好的ScrollView" ✓ -
★★
scripts.prepare会阻塞安装 ⇒--ignore-scripts是这类库的标准姿势 ✓ -
★ star 只有 27,但功能成立 ——
star 低不等于不能用;关键看"它声称解决的问题是否真的解决" ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-virtualized-view(上游 1.0.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-virtualized-view |
| 适配 TAG | 1.0.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | ✅✅ 都不需要 |
| 库类型 | 容器(空 FlatList 冒充 ScrollView,解决嵌套警告) |
| 运行时依赖 | ✅ dependencies 为空;peer 只有 react / react-native |
| 导出形态 | ⚠️ 具名导出 ScrollView,无默认导出 |
scripts.prepare | ⚠️ bob build ⇒ 安装需 --ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/hoaphantn7604/react-native-virtualized-view(★27,pushed 2024-01) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = VirtualizedViewTestApp) |
# ★ 必须 --ignore-scripts(它的 prepare 是 bob build)
npm install "git+https://atomgit.com/oh-react-native/react-native-virtualized-view.git#1.0.0-ohos-1.0.0" --ignore-scripts
// ★ 具名导出(没有默认导出)
import {ScrollView} from 'react-native-virtualized-view';
import {FlatList} from 'react-native';
// 用法:外层用它,内层照常放 FlatList
<ScrollView>
<FlatList data={a} renderItem={…} />
<FlatList data={b} renderItem={…} />
</ScrollView>
// 它等价于(全部实现):
// <FlatList data={[]} renderItem={null} ListHeaderComponent={() => <>{children}</>} />
// ⚠️ 具名导出,写成 import ScrollView from … 会拿到 undefined ⇒ Element type is invalid
// ⚠️ children 不会虚拟化(都在 ListHeaderComponent 里)⇒ 内容多时性能等同普通 ScrollView
// ⚠️ 安装必须带 --ignore-scripts(prepare = bob build)
// ⚠️ stickyHeaderIndices 这类 ScrollView 专属 prop 不可用
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey VirtualizedViewTestApp
判据的复现方式
# ① 契约:具名导出 / 无默认导出
hdc shell "hilog -x | grep vvv-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ② ★ 核心 A/B:数 hilog 里的警告(不要数 LogBox 节点)
hdc shell "hilog -x | grep -c VirtualizedLists"
# ⇒ P0(只挂原生 ScrollView 版)时为 1;P1(追加本库版)后仍为 1
# ③ 文本级:内层 FlatList 的项确实渲染
devecocli ui layout | Select-String 'VVV 列表项'
# ⇒ P0 时只有 E1 的标记;P1 时 E2 的标记也出现
验证环境
| 项 | 版本 |
|---|---|
| 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 |
| 宿主依赖 | ✅ 无需任何额外依赖 |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 39 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约 7/7 全 PASS;hilog 时间戳级 A/B 对照(警告只在 P0 出现 1 次、P1 不新增);文本级印证(E1 标记恒 6、E2 标记 0→4);4 张截图互异 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库无任何已移除写法、无平台分支、无原生依赖 ⇒ 结论跨基线成立。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)