React Native for OpenHarmony 实战:三方库 react-native-simple-table 的鸿蒙化适配指南
本文记录把 react-native-simple-table(极简表格)适配到 HarmonyOS 的完整过程。
它做的事:用"横向 ScrollView + 表头 + 纵向 ScrollView"拼出一个表格,
列由 columns(含 title / dataIndex / width)定义,行由 dataSource 提供。
结论:可用。而且这是这几轮里"代码量最小"的一个库 —— 只有 index.js(105 B)+ src/Table.js(3282 B / 137 行)两个源文件,零依赖。
判据结果是 12 PASS / 2 FAIL,而那 2 条 FAIL 是我自己的断言方法错(详见第五节):
propTypes.X.isRequired 在运行时根本不可检测 —— 它是"选了 isRequired 版本的 validator 函数",而这个函数上并没有 .isRequired 属性。
★★★★★ 一、预检:至今最"轻"的一个库
| 预检项 | 结果 |
|---|---|
老写法九项(createClass / React.PropTypes / ViewPropTypes / Text.propTypes / 字符串 ref / this.refs / mixins / componentWillReceiveProps / componentWillMount / getDefaultProps) | ✅✅ 全部为空 |
ListView.DataSource / <ListView | ✅ 空 |
measure() / onLayout / useNativeDriver / NativeModules / <Modal / Animated / FlatList | ✅ 全部为空 |
Platform.OS | ✅ 空(零平台分支) |
createClass | ✅ 精确命中 0、裸词也 0 |
dependencies / peerDependencies | ✅✅ 都是空 ⇒ 零依赖 |
| 外部 import | 只有 react + prop-types ✓ |
isRequired 4 处 | ⚠️ 全在 propTypes 里(L21/L22/L24/L27)⇒ 只是类型校验,不崩 ✓ |
ScrollView 5 处 | ✓(它就是用 ScrollView 拼的) |
scripts | 只有 test ⇒ 没有 prepare,不需要 --ignore-scripts |
| 规模 | 只有 index.js + src/Table.js(src/Table.js 3282 B / 137 行) |
index.js(105 B) | import Table from './src/Table' + export default Table ⇒ 只有默认导出 |
★★★★★ 二、Table 是 class —— 这次按 class 断言,一次全中
先看源码那一行(坑 327 的固定动作):
// src/Table.js L17
class Table extends Component {
// src/Table.js L137
export default Table
实测:
NOTE | 默认导出 Table 的 typeof = function
PASS | ★★★ Table 是 class(function)
NOTE | Table 有 prototype.render = function
PASS | ★★★ Table 有 prototype.render(它是 class 组件)
PASS | ★★ 入口没有具名的 Table 导出
⇒ ⇒ 这次是 class,所以断言 prototype.render 是 function ✓(另两种形态是 forwardRef 与函数组件,判据不同)
判别三档(现在每次都先查这个):
| 写法 | typeof | prototype.render | 判据 |
|---|---|---|---|
class X extends Component | function | function | 断言有 prototype.render |
const X = ({…}) => … | function | undefined | 断言语 没有 prototype.render |
X = React.forwardRef(…) | object | undefined | 断言 $$typeof === Symbol(react.forward_ref) |
★★★★★ 三、propTypes / defaultProps:清单比对 + 精确值全中
源码:
class Table extends Component {
static propTypes = {
columns: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
dataIndex: PropTypes.string.isRequired,
width: PropTypes.number,
})).isRequired,
columnWidth: PropTypes.number,
height: PropTypes.number,
dataSource: PropTypes.array.isRequired,
renderCell: PropTypes.func,
};
static defaultProps = {
columns: [],
dataSource: [],
columnWidth: DEFAULT_COLUMN_WIDTH, // = 60
height: DEFAULT_HEIGHT, // = 240
renderCell: undefined,
};
…
}
实测(清单比对 + 精确值):
NOTE | propTypes 的键数 = 5
NOTE | propTypes 的键 = columnWidth, columns, dataSource, height, renderCell
PASS | ★★★ propTypes 清单 = [columns, columnWidth, dataSource, height, renderCell]
NOTE | defaultProps 的键数 = 5
NOTE | defaultProps 的键 = columnWidth, columns, dataSource, height, renderCell
PASS | ★★★ defaultProps 清单 = [columns, columnWidth, dataSource, height, renderCell]
NOTE | defaultProps.columnWidth = 60
PASS | ★★★ defaultProps.columnWidth = 60(DEFAULT_COLUMN_WIDTH)
NOTE | defaultProps.height = 240
PASS | ★★★ defaultProps.height = 240(DEFAULT_HEIGHT)
PASS | ★★ defaultProps.columns 是空数组
PASS | ★★ defaultProps.dataSource 是空数组
PASS | ★★ defaultProps.renderCell = undefined
⇒ ⇒ defaultProps 8 条全 PASS(清单一致 + 60 / 240 / 两个空数组 / undefined)✓✓
⇒ 60 与 240 是从源码常量 DEFAULT_COLUMN_WIDTH / DEFAULT_HEIGHT 抄的,不是猜的 ✓
★★★★★ 四、4 个方法全在 prototype 上(含 3 个 _ 前缀私有方法)
源码里这些方法都是普通方法写法(_xxx() {}):
_renderCell(cellData, col) { … }
_renderHeader() { … }
_renderRow(rowData, index) { … }
render() { … }
实测:
NOTE | prototype 上缺失的方法 = (无)
PASS | ★★★ 4 个方法都在 prototype 上(含 3 个 _ 前缀私有方法)
⇒ ⇒ 这次是"原型方法"档(与"箭头类字段只在实例上"那一档形成对照)✓
★★★★★ 五、2 条 FAIL 是我的断言方法错:propTypes.X.isRequired 不可运行时检测
我写了两条断言:
check('★★ propTypes.dataSource 是 isRequired', true, !!T.propTypes?.dataSource?.isRequired);
check('★★ propTypes.columns 是 isRequired', true, !!T.propTypes?.columns?.isRequired);
实测:
NOTE | propTypes.dataSource 是否 isRequired = false
FAIL | ★★ propTypes.dataSource 是 isRequired | 期望=true | 实际=false
FAIL | ★★ propTypes.columns 是 isRequired | 期望=true | 实际=false
但源码里明明写了 .isRequired(L24 / L27)。于是我单独验证了 prop-types 本身的机制:
string.isRequired 的 typeof = function
string.isRequired.isRequired = undefined
string.isRequired 有没有 isRequired 属性 = false
String(P.string) = function () { [native code] }
String(P.string.isRequired) = function () { [native code] }
⇒ ⇒ PropTypes.X.isRequired 是一个"validator 函数"(isRequired 版本的校验器),
这个函数上并没有 .isRequired 这个属性 ✗
⇒ ⇒ 所以 !!propTypes.dataSource.isRequired 永远是 false —— 我的断言在原理上就不可能通过 ✓
这意味着:
| 想验证的东西 | 可行的做法 | 不可行的做法 |
|---|---|---|
| “这个 prop 是必填” | 读源码(唯一可靠)、或缺参时看 LogBox 是否警告 | ❌ !!propTypes.X.isRequired |
| “这个 prop 有校验器” | typeof propTypes.X === 'function' | ❌(两种版本都是 function,区分不了) |
⇒ ⇒ 教训:isRequired 是"在声明时选用了 isRequired 版本的 validator",
一旦拿到 propTypes.X,就无法再判断它当初是不是 isRequired 版本** ——
因为 String() 两边都是 function () { [native code] },也没有任何可读标记。
⇒ ⇒ 这是本轮最有价值的技术发现,也是我这一轮唯一的两条 FAIL(都是同一个原因)。
★★★★★ 六、渲染证据:7 处文本命中 + 行高间隔一致
页面里放了两个表格:
- E1:3 列 × 3 行,列宽 120 / 80 / 100,
height={300} - E2:2 列 × 2 行,不传
columnWidth与height⇒ 用默认的 60 / 240
布局 dump 实测(E1):
✓ 「STB 名称」 w=171 h=49 x=141 y=1220
✓ 「STB 数量」 w=171 h=49 x=441 y=1220
✓ 「STB 备注」 w=171 h=49 x=711 y=1220
✓ 「苹果」 w=85 h=49 x=184 y=1303
✓ 「香蕉」 w=85 h=49 x=184 y=1381
✓ 「樱桃」 w=85 h=49 x=184 y=1459
✓ 「红富士」 w=127 h=49 x=733 y=1303
⇒ 命中 7 / 9
从这些数字能读出三件事 ✓✓:
| 观察 | 结论 |
|---|---|
三列表头的 y 都是 1220、x = 141 / 441 / 711 | 表头横向排列在同一个行上 ✓(x 递增、y 相同 ⇒ 是"一行三格"而不是"三行") |
三行数据的 y = 1303 / 1381 / 1459 | y 间隔恰好 78(1381−1303=78、1459−1381=78)⇒ 行高一致、逐行排布无重叠 ✓ |
所有 h 都是 49 | 单元格高度统一 ✓ |
数据格 x=184 与表头 x=141 不同 | 表头与数据是两套 View(headerItem 与 cell,两者 minHeight 分别是 30 与 25)⇒ 内容居中位置略有差异 ✓ |
⇒ ⇒ 这比"文本存在"强得多:它证明表格是"表头一行 + 数据多行"的正确结构,而不是"一堆文本堆在一起" ✓
⚠️ 「STB 键」/「STB 值」(E2 的表头)没在首屏 dump 里命中 —— 它们在页面更下方,需要滚动。
本轮的采样点在第一屏,所以记"未观察到(在更下方)"。
★★ 七、其余预检结论
| 预检项 | 结果 |
|---|---|
| 交付包内容 | index.js(105 B)+ src/Table.js(3282 B / 137 行)+ .idea/(IDE 配置,无害)+ screenshots/screenshot.png(151 KB) |
| 样式精确值 | headerItem.minHeight=30 / cell.minHeight=25 / row.backgroundColor='#fbfbfb' / borderRightColor='#dfdfdf' |
ScrollView 用法 | 外层 horizontal={true} + 内层纵向 ⇒ 双向滚动 |
| 许可证 | Apache-2.0(不是 MIT) |




八、接入与预检(要点)
# 零依赖:dependencies 与 peerDependencies 都空;也没有 prepare 脚本
npm install "git+https://atomgit.com/oh-react-native/react-native-simple-table.git#0.0.3-ohos-1.0.0" --ignore-scripts
九、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | index.js(105 B)+ src/Table.js(3282 B),只有 2 个源文件 |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 库代码零改动 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
本库零依赖、零平台分支、零原生代码、useNativeDriver 零命中 ⇒ 结论跨基线成立 ✓
十、已知限制与本次验证边界
10.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★★ Table 是 class | typeof 是 function、prototype.render 是 function;4 个方法(_renderCell/_renderHeader/_renderRow/render)全在 prototype 上 |
★★★★★ defaultProps 5 项、值精确 | columns: [] / dataSource: [] / columnWidth: 60 / height: 240 / renderCell: undefined |
★★★★★ propTypes.X.isRequired 不可运行时检测 | PropTypes.X.isRequired 是一个 validator 函数,它本身没有 .isRequired 属性 ⇒ !!propTypes.X.isRequired 永远是 false |
| ★★★★ 零依赖、只有 2 个源文件 | index.js + src/Table.js |
| ★★★★ 表结构 | 外层 ScrollView horizontal + 表头一行 + 内层纵向 ScrollView;列宽取 col.width || columnWidth || 60 |
| ★★★ 列宽优先级 | col.width > columnWidth > DEFAULT_COLUMN_WIDTH(60) |
| ★★★ 行高 | 由 cell.minHeight = 25 与文字行高决定(实测 h=49、行间隔 78) |
★★ height prop 控制内容区高度 | 默认 240,写到 contentContainerStyle 上 |
| ★★ 许可证是 Apache-2.0 | 与多数三方库(MIT)不同 |
★ 不需要 --ignore-scripts | 它没有 prepare 脚本 |
10.2 本次验证的边界
- ★★★ 未验证横向滚动(E1 三列总宽 300vp < 屏宽,无需横滚;更宽的表未测);
- ★★★ 未验证
renderCell自定义单元格(只用了默认渲染); - ★★ 未验证
heightprop 的实际生效(E1 传了 300、E2 用默认 240,但只从文本坐标看,没量容器高度); - ★★ 未验证
columns[].width与columnWidth的优先级(虽然源码写法明确,但没做三组对照实测); - ★★ E2 的表头(「STB 键」/「STB 值」)未在首屏 dump 命中(在页面更下方);
- 未验证 空
dataSource/ 空columns的渲染; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
库代码未改动。
十一、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 零依赖、零平台分支、零原生代码;Table 是 class(prototype.render 存在)、
defaultProps 的 60 / 240 实测精确;表格的三列表头在同一行、三行数据的 y 间隔恰好 78;LogBox 无通知。
Q2:怎么判断某个 prop 是必填(isRequired)?
只能读源码。运行时检测不出来:
// ❌ 这永远是 false
!!PropTypes.string.isRequired.isRequired // undefined ⇒ false
// ❌ 这也区分不了
typeof PropTypes.string.isRequired // 'function'(与 PropTypes.string 相同)
String(PropTypes.string.isRequired) // 'function () { [native code] }'
⇒ isRequired 是"在声明时选用了 isRequired 版本的 validator",一旦拿到 propTypes.X,
就无法再判断它当初是不是 isRequired 版本。
⇒ 可行的替代:缺参时看 LogBox 是否出现 “Failed prop type” 警告。
Q3:列宽怎么定?
优先级是 columns[i].width > columnWidth(prop)> DEFAULT_COLUMN_WIDTH = 60:
<Table
columns={[{title: '名称', dataIndex: 'name', width: 120}, // ← 最高优先
{title: '数量', dataIndex: 'count'}]} // ← 用 columnWidth 或 60
columnWidth={80} // ← 次优先
dataSource={DATA}
/>
Q4:表格高度怎么定?
height prop(默认 240),写到 contentContainerStyle 上:
<Table columns={…} dataSource={…} height={300} />
Q5:没有 renderCell 时单元格怎么渲染?
走内部 _renderCell:<View style={[styles.cell, {width}]}><Text>{cellData}</Text></View> ⇒ 数据直接当文本渲染。
要自定义就传 renderCell={(cellData, col) => <YourCell/>}。
Q6:为什么表头和数据格的 x 不一样?
因为它们是两套 View:表头 headerItem(minHeight: 30)与数据格 cell(minHeight: 25),
两者都 alignItems: 'center',但父容器不同 ⇒ 文字居中位置略有差异。
实测表头 x=141、数据格 x=184(同一列)。
Q7:它用 ListView 吗?
不用 —— 它只用 ScrollView(外层横向 + 内层纵向)。预检里 ListView.DataSource 与 <ListView 都是 0 命中 ✓
小结
react-native-simple-table 是极简表格(横向 ScrollView + 表头 + 纵向 ScrollView),
只有 2 个源文件、零依赖、零平台分支。
最值得记下来的是这几条:
-
★★★★★
propTypes.X.isRequired在运行时【不可检测】—— 这是本轮最重要的发现。
PropTypes.string.isRequired是一个 validator 函数,它身上没有.isRequired属性:typeof PropTypes.string.isRequired → 'function' PropTypes.string.isRequired.isRequired → undefined 'isRequired' in PropTypes.string.isRequired → false String(PropTypes.string.isRequired) → 'function () { [native code] }'⇒ 所以
!!propTypes.dataSource.isRequired永远是false⇒ 我那 2 条 FAIL 在原理上就不可能通过 ✗
⇒ 要验证"必填"只能读源码,或用"缺参看 LogBox 警告"。 -
★★★★★
Table是 class,所以断言prototype.render—— 一次全中。
源码 L17class Table extends Component⇒typeof是function且prototype.render是 function ✓
⇒ 这次与另两档(forwardRef是 object、函数组件prototype是 undefined)正好构成第三档对照 ✓ -
★★★★★
defaultProps的清单 + 精确值全中:
5 项键名清单比对通过;columnWidth = 60、height = 240(都来自源码常量DEFAULT_COLUMN_WIDTH/DEFAULT_HEIGHT)、
columns/dataSource是空数组、renderCell是undefined✓ -
★★★★ 渲染证据靠坐标而不是"存在性":
三列表头y都是 1220、x= 141/441/711 ⇒ 证明"横向一行三格";
三行数据y= 1303/1381/1459,间隔恰好 78 ⇒ 证明"逐行排布、行高一致" ✓✓
⇒ 比"文本都在"强得多。 -
★★★ 极简库的适配成本最低:2 个源文件、零依赖、零平台分支、
连prepare脚本都没有 ⇒ 预检全部为空、构建一次成功 ✓ -
★★
isRequired那两条 FAIL 也说明了另一个道理:
“源码里明明写了” ≠ “运行时能读到” —— 写断言前要确认那个信息在运行时是否还保留 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-simple-table(上游 0.0.3 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-simple-table |
| 适配 TAG | 0.0.3-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 极简表格(横向 ScrollView + 表头 + 纵向 ScrollView) |
| 运行时依赖 | ✅✅ dependencies / peerDependencies 都为空 |
| 导出形态 | 只有默认导出 Table(class 组件,prototype.render 存在) |
| 默认值 | columnWidth: 60 / height: 240 / columns: [] / dataSource: [] / renderCell: undefined |
| ⚠️ 注意 | propTypes.X.isRequired 不可运行时检测(它是 validator 函数,没有 .isRequired 属性) |
| 许可证 | Apache-2.0 |
| 上游仓库 | https://github.com/Jeepeng/react-native-simple-table |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = SimpleTableTestApp) |
npm install "git+https://atomgit.com/oh-react-native/react-native-simple-table.git#0.0.3-ohos-1.0.0" --ignore-scripts
# 零依赖、无 prepare 脚本
import Table from 'react-native-simple-table';
const columns = [
{title: '名称', dataIndex: 'name', width: 120}, // ← width 优先
{title: '数量', dataIndex: 'count', width: 80},
{title: '备注', dataIndex: 'note', width: 100},
];
const dataSource = [
{name: '苹果', count: 12, note: '红富士'},
{name: '香蕉', count: 8, note: '进口'},
];
// ① 自定义列宽 + 自定义高度
<Table columns={columns} dataSource={dataSource} height={300} />
// ② 用默认列宽(60)与默认高度(240)
<Table
columns={[{title: '键', dataIndex: 'k'}, {title: '值', dataIndex: 'v'}]}
dataSource={[{k: 'A', v: 1}]}
/>
// ③ 自定义单元格
<Table
columns={columns}
dataSource={dataSource}
renderCell={(cellData, col) => (
<View style={{width: col.width || 60}}><Text>{String(cellData)}</Text></View>
)}
/>
// ⚠️ Table 是 class(typeof 是 function 且 prototype.render 是 function)
// ⚠️ 列宽优先级:col.width > columnWidth prop > DEFAULT_COLUMN_WIDTH(60)
// ⚠️ height 默认 240;写到 contentContainerStyle
// ⚠️ propTypes.X.isRequired 不可运行时检测(它是 validator 函数,没有 .isRequired 属性)
// ⚠️ 不用 ListView,只用 ScrollView(外层 horizontal + 内层纵向)
// ⚠️ 许可证是 Apache-2.0
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SimpleTableTestApp
判据的复现方式
# ① 契约(12 PASS / 2 FAIL,FAIL 是 isRequired 不可检测)
hdc shell "hilog -x | grep stb-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ Table 是 class + prototype.render 是 function
# ⇒ propTypes/defaultProps 清单一致;columnWidth=60 / height=240
# ② 文本与坐标(表头同行、数据行间隔 78)
devecocli ui layout | Select-String 'STB 名称|STB 数量|STB 备注|苹果|香蕉|樱桃'
# ⇒ 表头 y=1220、x=141/441/711;数据 y=1303/1381/1459(间隔 78)
验证环境
| 项 | 版本 |
|---|---|
| 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 分 57 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 12 PASS / 2 FAIL(2 条 FAIL 是 isRequired 不可检测,原理上不可能通过);defaultProps 清单 + 精确值 60/240 全中;4 个原型方法全在;文本 7/9 命中 + 行 y 间隔 78;LogBox 无通知;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)