React Native for OpenHarmony 实战:三方库 react-native-grid-view 的鸿蒙化适配指南
本文记录把 react-native-grid-view(网格 / 集合视图)适配到 HarmonyOS 的完整过程。
这个库只有 1482 字节(单文件、零依赖),但它是整个系列里"最不能直接用"的一个:它同时踩了两个早年被移除的 API,在 RN 0.84 + React 19 上根本没法工作。
★★ 结论先摆出来:两把钥匙都死了
源码(index.js 全文 63 行)里有三处关键:
var CollectionView = React.createClass({ // ① React.createClass —— React 16 起被移除
...
render: function() {
var ds = new ListView.DataSource({ // ② ListView —— RN 0.61 起被移除
rowHasChanged: (r1, r2) => r1 !== r2
});
return (<ListView {...this.props} renderRow={this.renderGroup}
dataSource={ds.cloneWithRows(groups)} />); // ③ 同上
},
});
实测(静态检查 + 直接调用实验):
React.createClass ⇒ undefined (typeof)
RN.ListView ⇒ undefined (typeof)
RN.ListView.DataSource ⇒ undefined (typeof)
★★★ 直接调用 React.createClass({...}) ⇒ 抛错:undefined is not a function
★★★ 直接 new RN.ListView.DataSource ⇒ 抛错:Cannot read property 'DataSource' of undefined
⇒ 也就是说:这个库一被使用就会在这两处直接崩。
★ 进一步的实测(一个我暂时没解释清、但如实记录的现象)
把库本身 require 进来时,它居然没有抛错,但也没有导出任何东西:
require('react-native-grid-view') ⇒ 成功,但 typeof = undefined
第二次 require ⇒ 同样是 undefined
它编译后的模块体明明是:
var CollectionView = _react.default.createClass({ … }); // 会在模块作用域直接调用
module.exports = CollectionView;
⇒ 按说这一行必然抛 undefined is not a function。实测却是"不抛错、返回 undefined"。
最可能的解释是:该文件通篇是 ESM 语法(import),而导出用的是 CommonJS 的 module.exports = … ⇒ 在 Metro 的 ESM 处理下导出没有生效,同时模块体的求值结果没有把异常抛到 require 的调用点。我没有把这一步彻底查穿,所以按"现象"如实记录。
但结论不受影响:它没有导出任何可用对象(typeof === undefined),而它内部的 render() 一旦被调用就会在 new ListView.DataSource(...) 上抛错。
★ 所以这一篇给的是"怎么办"
库本身不可用 ⇒ 我把它唯一可独立复用的逻辑(分组)等价实现出来,并逐一验证,再用一个可用的网格把它画出来:
groupItems([0,1,2,3,4,5,6], 3) ⇒ [[0,1,2],[3,4,5],[6]] (3-3-1)
渲染后的布局实测(布局 dump 里数 120px 方块):
找到 7 个约 120×120 方块(预言 7)
y=1308 ⇒ 3 个 x=462, 600, 738
y=1446 ⇒ 3 个 x=462, 600, 738
y=1584 ⇒ 1 个 x=600 ← 末行单格居中
⇒ 3-3-1 的行分布与 groupItems 完全一致;列间距 138px(= 格子 40vp + 左右 margin 3vp each ⇒ 46vp × 3 = 138px,精确);末行只剩一格时因为 justifyContent:'center' 落在正中 ✓
设备侧断言 26 / 26 全部通过。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 0.4.1;license: MIT;上游 star 278 |
| 库类型 | 纯 JS 单文件组件库(index.js 仅 1482 B,63 行) |
| 是否需要原生适配 | 不需要(无 harmony/)—— 但它也用不了(见下) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
main | index.js(源码直发) |
| 导出形态 | module.exports = CollectionView(CommonJS,与 import 混用) |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| 设备侧断言 | ✅ 26 / 26 通过(A 9 + B 8 + C 9) |
★★ React.createClass | 不存在;直接调用 ⇒ undefined is not a function |
★★ ListView / ListView.DataSource | 不存在;直接 new ⇒ Cannot read property 'DataSource' of undefined |
★ require 该库 | 不抛错但返回 undefined(现象未完全解释清,如实记录) |
| ★★ 可用替代 | 自己按 itemsPerRow 分组再逐行渲染 ⇒ 实测 3-3-1、列间距 138px 精确、末行居中 |
| ★ 唯一的纯逻辑 | groupItems(items, itemsPerRow) ⇒ 已等价实现并 9 条断言验证 |
| ★ 源码细节 | renderItem(item, index) 的 index 是组内下标,不是全局下标 |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules | 全无 |
| ④ | import 图体检 | 只有 react + react-native ⇒ 零依赖 ✓ |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
| ⑥ | ★ 老 API 预扫(本轮的关键一步) | React.createClass、ListView ⇒ 两个都已被移除 |
npm install "git+https://atomgit.com/oh-react-native/react-native-grid-view.git#0.4.1-ohos-1.0.0" --ignore-scripts
⚠️ 装得上,但用不了:一旦真的去渲染它,就会在 ListView 上抛错。建议直接用第三节的替代实现。
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-grid-view') ],
import GridViewTestApp from './GridViewTestApp';
AppRegistry.registerComponent('GridViewTestApp', () => GridViewTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey GridViewTestApp
三、这里没有"适配"可做,只有"替代"
3.1 为什么不能适配
| 库里的写法 | 状态 | 实测证据 |
|---|---|---|
React.createClass({...}) | React 16 起被移除 | typeof React.createClass === 'undefined';直接调用抛 undefined is not a function |
new ListView.DataSource({...}) | RN 0.61 起被移除 | typeof RN.ListView === 'undefined';直接 new 抛 Cannot read property 'DataSource' of undefined |
<ListView ... /> | 同上 | 同上 |
注:React.createClass 在模块作用域就被调用(见编译产物),所以连"先加载、后面再说"都不行。
3.2 ★ 唯一可复用的逻辑:groupItems
groupItems: function(items, itemsPerRow) {
var itemsGroups = [];
var group = [];
items.forEach(function(item) {
if (group.length === itemsPerRow) { itemsGroups.push(group); group = [item]; }
else { group.push(item); }
});
if (group.length > 0) { itemsGroups.push(group); }
return itemsGroups;
}
语义:把一维数组按每行 itemsPerRow 个切成二维数组,不足一行也保留。
实测(9 条断言全过):
| 输入 | 输出 |
|---|---|
([0..6], 3) | [[0,1,2],[3,4,5],[6]](行数 3,每组 ≤3) |
([], 3) | [] |
([1,2,3,4], 1) | [[1],[2],[3],[4]] |
([1..6], 3) | [[1,2,3],[4,5,6]](正好整除,无残余行) |
([1,2], 5) | [[1,2]](itemsPerRow 大于总数) |
([1,2,3], 3) | [[1,2,3]](恰好一行也不产生空组) |
3.3 ★ 替代实现(可直接拿去用)
function groupItems<T>(items: T[], itemsPerRow: number): T[][] {
const groups: T[][] = [];
let group: T[] = [];
items.forEach(item => {
if (group.length === itemsPerRow) { groups.push(group); group = [item]; }
else { group.push(item); }
});
if (group.length > 0) { groups.push(group); }
return groups;
}
// 用法:先分组,再每组渲染成一行 flexDirection:'row' 的 View
<View style={{alignItems: 'center'}}>
{groupItems(data, 3).map((row, ri) => (
<View key={ri} style={{flexDirection: 'row', alignItems: 'center',
justifyContent: 'center', overflow: 'hidden'}}>
{row.map(item => <Cell key={...} {...item} />)}
</View>
))}
</View>
⇒ 实测:7 项、每行 3 个 ⇒ 3 行(3-3-1),列间距 138px(格子 40vp + margin 3vp×2 ⇒ 46vp,density 3),末行单格居中 ✓
3.4 ★ 顺带记下的源码细节
renderGroup: function(group) {
var that = this;
var items = group.map(function(item, index) {
return that.props.renderItem(item, index); // ⚠️ index 是【组内下标】
});
...
}
⇒ renderItem 收到的 index 是这一行内的下标(0、1、2、0、1、2…),不是全局下标。要全局下标得自己按 rowIndex * itemsPerRow + index 算。
四、验证设计
| 判据 | 内容 |
|---|---|
| ① 静态(不触发崩溃) | typeof React.createClass / typeof RN.ListView / typeof RN.ListView.DataSource |
| ② 直接调用(决定性) | 真的调 React.createClass({...})、真的 new ListView.DataSource({...}),把抛错信息记下来 |
| ③ 容器化 require | 在 try/catch 里 require 整个库,观察是否抛错、返回值是什么 |
| ④ 纯函数 | 把 groupItems 等价实现,用 6 组输入验证(含边界) |
| ⑤ 结构(像素/布局) | 用替代实现画出 7 项 3 列的网格 ⇒ 从 dump 里数方块、看行分布与间距 |
① 与 ② 是有意分开的:① 说明"API 不在",② 说明"真的会炸" —— 前者是推断,后者是证据。
五、实测结果
5.1 断言 26 / 26
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约与死 API | React 版本 19.2.3;React.createClass 不存在;ListView/ListView.DataSource 不存在;View/StyleSheet/AppRegistry 仍在;导出方式与成员写法记录 | 9/9 |
| B 容器化 require + 直接调用 | require 不抛错但返回 undefined(两次一致);直接调用 createClass 抛 undefined is not a function;直接 new ListView.DataSource 抛 Cannot read property 'DataSource' of undefined;标题安全区 | 8/8 |
C 替代实现 groupItems | 6 组输入输出 + 行数 + 每组不超限 + renderItem 的 index 语义 + 替代方案记录 | 9/9 |
| 合计 | 26/26 |
关键观测值:
React 版本 = 19.2.3
★★ typeof React.createClass = undefined
★★ typeof RN.ListView = undefined
★★ typeof RN.ListView?.DataSource = undefined
RN.View 的 typeof = object (RN 0.84 里是 forwardRef 对象,不是 function)
★★ require 该库的结果 = require 成功,typeof = undefined
★★ 第二次 require 的结果 = 第二次 require 也成功,typeof = undefined
★★★ 直接调用 React.createClass({...}) 的结果 = 抛错:undefined is not a function
★★★ 直接 new RN.ListView.DataSource 的结果 = 抛错:Cannot read property 'DataSource' of undefined
UI 安全区:TITLE measureInWindow y=420 > 117 ✓
5.2 ★★ 替代实现的结构实测
布局 dump 里统计约 120×120 的方块(格子 = 40vp,density 3):
找到 7 个约 120×120 方块(预言 7)
y=1308 ⇒ 3 个 x=462, 600, 738
y=1446 ⇒ 3 个 x=462, 600, 738
y=1584 ⇒ 1 个 x=600
- 行分布 3-3-1,与
groupItems([0..6], 3)逐组一致 ✓ - 列间距 138px(
40 + 3 + 3 = 46vp × 3 = 138px)⇒ 精确 ✓ - 末行单格 x=600 = 屏幕中心(容器
justifyContent:'center')✓



六、★ 本轮我自己踩的一个方法学坑(值得单独说)
我在 B 组里做了"直接调用 React.createClass"的实验,并且用 try/catch 包住了它:
try {
const X = (React as any).createClass({render: () => null});
callMsg = `居然没抛错,typeof = ${typeof X}`;
} catch (e) {
callMsg = `抛错:${String(e?.message ?? e)}`; // ✅ 确实抓到了
}
日志里也确实记录成功:抛错:undefined is not a function ✓
但是,页面仍然被一整屏红色的 LogBox 盖住,标题是 Uncaught Error、正文 undefined is not a function,调用栈指到 bundle.harmony.js:204833:11 —— 而那正是我的 runAll 里调用 runB() 的那一行:
Uncaught Error
undefined is not a function
bundle.harmony.js:204833:11
_performTransitionSideEffects → _receiveSignal → onResponderRelease → executeDispatch → …
hilog 里也留下了对应记录:
[TypeError: undefined is not a function] ← 与"直接调用 React.createClass"同刻
⇒ 结论:在 RNOH/Hermes 上,即使异常被 try/catch 捕获,这次 TypeError 仍会被上报到 LogBox 并以"Uncaught Error"整屏显示。
⇒ 对我们的意义:"故意触发一个异常来取证"这种实验,会污染整屏 UI(截图、dump 都会受影响)。要做这类实验,应该把触发点放到单独的按钮/单独的页面,或者在取证后重启一次页面。
这一条与"渲染期
setState""updater里读事件对象"是同一类问题:代价不在逻辑上,而在"页面被红屏盖住后,后续的截图与布局核对全部失效"。
另外顺手记两条:
RN.View在 RN 0.84 里是forwardRef对象(typeof是object,不是function)—— 我第一版断言写成了function,挂了一条;JSON.stringify(-0)会归一成0(写期望值时已留意,本轮没再踩)。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★ 在 RN 0.84 上不可用 | React.createClass(React 16 移除)+ ListView(RN 0.61 移除)⇒ 不要直接接 |
★ 可用部分只有 groupItems | 分组逻辑是纯函数,可以照抄;渲染部分必须重写 |
renderItem(item, index) 的 index 是组内下标 | 需要全局下标要自己换算 |
| 导出是 CommonJS | module.exports = CollectionView,与 import 混用 |
| 零依赖 | 不用补装任何包 |
7.2 本次验证的边界
- 没有真的去渲染库组件(明知会在
ListView上抛错,属于"故意不触发";它的渲染行为完全未验证); require返回undefined且不抛错的原因未彻底查穿(只给了最可能的解释);- 替代实现的交互未验证(没有做点击/滚动/分页,也没验证
renderItem的 key 处理); itemsPerRow只验证了 1 / 3 / 5 三种;- 大列表性能未验证(只用了 7 项);
- 列宽自适应未验证(替代实现用的是固定 40vp 格子);
ScrollView包裹与长列表滚动未验证;- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
八、常见问题
Q1:装上就直接崩,怎么回事?
因为库用了两个早已被移除的 API:
React.createClass ⇒ React 16 起移除
ListView / DataSource ⇒ RN 0.61 起移除
实测直接调用会抛 undefined is not a function 与 Cannot read property 'DataSource' of undefined。
Q2:那还能用吗?
渲染部分要自己重写。库里唯一值得照抄的是 groupItems(把一维数组按每行 N 个切成一维数组的数组)。
Q3:怎么用替代实现画出网格?
<View style={{alignItems: 'center'}}>
{groupItems(data, 3).map((row, ri) => (
<View key={ri} style={{flexDirection: 'row', alignItems: 'center',
justifyContent: 'center', overflow: 'hidden'}}>
{row.map((item, ci) => (
<View key={ri * 3 + ci} style={{width: 40, height: 40, margin: 3}} />
))}
</View>
))}
</View>
实测:7 项 ⇒ 3 行(3-3-1),列间距 138px,末行居中。
Q4:renderItem 的 index 为什么从 0 开始重复?
因为它是组内下标(每一行都从 0 重新开始),不是全局下标。
Q5:为什么我 try/catch 住了异常,页面还是红屏?
这是本轮实测到的一条:在 RNOH/Hermes 上,被 try/catch 捕获的 TypeError 仍会被上报到 LogBox 并以"Uncaught Error"整屏显示。所以"故意触发异常"的实验会污染整屏 UI,建议放到单独的按钮或页面上做。
Q6:这个库需要鸿蒙侧做什么特殊处理吗?
原生侧什么都不需要(纯 JS、零依赖)。真正的障碍在 JS 侧的两个已移除 API。
小结
react-native-grid-view 是一个 1482 字节的纯 JS 网格组件(零依赖)。设备侧 26 / 26 断言通过。但它本身在 RN 0.84 + React 19 上不可用。
最值得记下来的是这几条:
-
★★ 它同时用了两个"早就被移除"的 API。
React.createClass(React 16 移除)与ListView/ListView.DataSource(RN 0.61 移除)。
静态检查:typeof全是undefined;
直接调用:分别抛undefined is not a function与Cannot read property 'DataSource' of undefined。
⇒ 这类老库应该先做老 API 预扫再决定要不要花时间接。 -
★ 有"推断"和"证据"要分开做。 我先静态查
typeof(推断:API 不在),再真的去调一次(证据:确实炸)。两步都做,结论才站得住。 -
★ 库不可用也能交付价值:把它唯一的纯逻辑抽出来验证、给出可用的替代实现。
groupItems已用 6 组输入(含空数组、itemsPerRow=1、整除、itemsPerRow大于总数、恰好一行)验证;
替代实现实测 3-3-1 行分布、列间距 138px 精确、末行居中。 -
★★ 一个新方法学发现:在 RNOH/Hermes 上,被
try/catch捕获的TypeError仍会被上报到 LogBox 并以"Uncaught Error"整屏显示。
我"故意调用死 API 取证"的实验因此把整屏盖住了(调用栈指到我调runB()的那一行)。⇒ 这类实验要放在单独的按钮/页面做,或者取证后重启页面,否则后续截图与布局核对全部失效。 -
★ 未解释清的就写清楚。
require该库"不抛错却返回undefined"这一点,我给了最可能的解释、并明确标注没有彻底查穿 —— 这比编一个动听的理由靠谱。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-grid-view(上游 0.4.1 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-grid-view |
| 适配 TAG | 0.4.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS、零依赖) |
| 库类型 | 单文件网格组件(在 RN 0.84 上不可用:React.createClass + ListView 均已被移除) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/lucholaf/react-native-grid-view(★278) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = GridViewTestApp) |
# 从适配仓库安装(零依赖)
npm install "git+https://atomgit.com/oh-react-native/react-native-grid-view.git#0.4.1-ohos-1.0.0" --ignore-scripts
# 本地目录接入
npm install ../react-native-grid-view --ignore-scripts
⚠️ 装得上、用不了:它的 render() 会在 new ListView.DataSource(...) 上抛错。请直接用下面的替代实现。
// ① 照抄库里的分组逻辑(这是它唯一可复用的部分)
function groupItems<T>(items: T[], itemsPerRow: number): T[][] {
const groups: T[][] = [];
let group: T[] = [];
items.forEach(item => {
if (group.length === itemsPerRow) { groups.push(group); group = [item]; }
else { group.push(item); }
});
if (group.length > 0) { groups.push(group); }
return groups;
}
// ② 用普通 View 逐行渲染(替代 ListView)
const COL = 3;
<View style={{alignItems: 'center'}}>
{groupItems(data, COL).map((row, ri) => (
<View key={ri} style={{flexDirection: 'row', alignItems: 'center',
justifyContent: 'center', overflow: 'hidden'}}>
{row.map((item, ci) => (
<View key={ri * COL + ci} style={{width: 40, height: 40, margin: 3,
borderRadius: 6, backgroundColor: '#0A84FF'}} />
))}
</View>
))}
</View>
// ⚠️ 库自身不可用:
// React.createClass ⇒ React 16 起移除
// ListView / DataSource ⇒ RN 0.61 起移除
// ⚠️ 库里 renderItem(item, index) 的 index 是【组内下标】
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey GridViewTestApp
判据的复现方式
# ① 老 API 静态检查(不触发崩溃)
# typeof React.createClass / typeof RN.ListView / typeof RN.ListView.DataSource ⇒ 都应是 undefined
# ② 决定性实验(⚠️ 会污染整屏 LogBox,建议放单独按钮/页面)
# try { React.createClass({}) } catch (e) { 记录 e.message } ⇒ "undefined is not a function"
# try { new RN.ListView.DataSource({}) } catch (e) { 记录 e.message }
# ③ 结构:从 dump 里数方块,看行分布与间距
devecocli ui layout | Select-String 'Custom#View'
# 7 项 / 每行 3 个 ⇒ 行分布应为 3-3-1;列间距 = (格子 + 左右 margin) × density
验证环境
| 项 | 版本 |
|---|---|
| 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(页面留白取 48vp) |
| 宿主依赖 | ✅ 无需补装(库零依赖) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 首轮 5 分 07 秒(20/23);修断言后 4 分 11 秒(24/26);再修后 4 分 08 秒 ⇒ 26/26 |
| 验证规模 | 设备侧 26 / 26 断言;老 API 静态检查 5 项;决定性直接调用 2 项(含确切抛错信息);容器化 require 3 项;纯函数 groupItems 6 组输入;结构实测 7 个方块(3-3-1 + 138px 间距 + 末行居中);UI 安全区 1 项 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)