本文记录把 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✅ 都没有
mainindex.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 契约与死 APIReact 版本 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 替代实现 groupItems6 组输入输出 + 行数 + 每组不超限 + 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 是组内下标需要全局下标要自己换算
导出是 CommonJSmodule.exports = CollectionView,与 import 混用
零依赖不用补装任何包

7.2 本次验证的边界

  1. 没有真的去渲染库组件(明知会在 ListView 上抛错,属于"故意不触发";它的渲染行为完全未验证);
  2. require 返回 undefined 且不抛错的原因未彻底查穿(只给了最可能的解释);
  3. 替代实现的交互未验证(没有做点击/滚动/分页,也没验证 renderItem 的 key 处理);
  4. itemsPerRow 只验证了 1 / 3 / 5 三种;
  5. 大列表性能未验证(只用了 7 项);
  6. 列宽自适应未验证(替代实现用的是固定 40vp 格子);
  7. ScrollView 包裹与长列表滚动未验证;
  8. 只在一台模拟器上验证(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 上不可用。

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

  1. ★★ 它同时用了两个"早就被移除"的 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 预扫再决定要不要花时间接。

  2. ★ 有"推断"和"证据"要分开做。 我先静态查 typeof(推断:API 不在),再真的去调一次(证据:确实炸)。两步都做,结论才站得住。

  3. ★ 库不可用也能交付价值:把它唯一的纯逻辑抽出来验证、给出可用的替代实现。
    groupItems 已用 6 组输入(含空数组、itemsPerRow=1、整除、itemsPerRow 大于总数、恰好一行)验证;
    替代实现实测 3-3-1 行分布、列间距 138px 精确、末行居中。

  4. ★★ 一个新方法学发现:在 RNOH/Hermes 上,被 try/catch 捕获的 TypeError 仍会被上报到 LogBox 并以"Uncaught Error"整屏显示。
    我"故意调用死 API 取证"的实验因此把整屏盖住了(调用栈指到我调 runB() 的那一行)。⇒ 这类实验要放在单独的按钮/页面做,或者取证后重启页面,否则后续截图与布局核对全部失效。

  5. ★ 未解释清的就写清楚。 require 该库"不抛错却返回 undefined"这一点,我给了最可能的解释、并明确标注没有彻底查穿 —— 这比编一个动听的理由靠谱。


本篇用到的库

项内容
三方库react-native-grid-view(上游 0.4.1 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-grid-view
适配 TAG0.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 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(页面留白取 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

Logo

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

更多推荐