本文记录把 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 与函数组件,判据不同)

判别三档(现在每次都先查这个):

写法typeofprototype.render判据
class X extends Componentfunctionfunction断言有 prototype.render
const X = ({…}) => …functionundefined断言语 没有 prototype.render
X = React.forwardRef(…)objectundefined断言 $$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 / 1459y 间隔恰好 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 是 classtypeof 是 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 本次验证的边界

  1. ★★★ 未验证横向滚动(E1 三列总宽 300vp < 屏宽,无需横滚;更宽的表未测);
  2. ★★★ 未验证 renderCell 自定义单元格(只用了默认渲染);
  3. ★★ 未验证 height prop 的实际生效(E1 传了 300、E2 用默认 240,但只从文本坐标看,没量容器高度);
  4. ★★ 未验证 columns[].width 与 columnWidth 的优先级(虽然源码写法明确,但没做三组对照实测);
  5. ★★ E2 的表头(「STB 键」/「STB 值」)未在首屏 dump 命中(在页面更下方);
  6. 未验证 空 dataSource / 空 columns 的渲染;
  7. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  8. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动。


十一、常见问题

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 个源文件、零依赖、零平台分支。

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

  1. ★★★★★ 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 警告"。

  2. ★★★★★ Table 是 class,所以断言 prototype.render —— 一次全中。
    源码 L17 class Table extends Component ⇒ typeof 是 function 且 prototype.render 是 function ✓
    ⇒ 这次与另两档(forwardRef 是 object、函数组件 prototype 是 undefined)正好构成第三档对照 ✓

  3. ★★★★★ defaultProps 的清单 + 精确值全中:
    5 项键名清单比对通过;columnWidth = 60、height = 240(都来自源码常量 DEFAULT_COLUMN_WIDTH / DEFAULT_HEIGHT)、
    columns/dataSource 是空数组、renderCell 是 undefined ✓

  4. ★★★★ 渲染证据靠坐标而不是"存在性":
    三列表头 y 都是 1220、x = 141/441/711 ⇒ 证明"横向一行三格";
    三行数据 y = 1303/1381/1459,间隔恰好 78 ⇒ 证明"逐行排布、行高一致" ✓✓
    ⇒ 比"文本都在"强得多。

  5. ★★★ 极简库的适配成本最低:2 个源文件、零依赖、零平台分支、
    连 prepare 脚本都没有 ⇒ 预检全部为空、构建一次成功 ✓

  6. ★★ isRequired 那两条 FAIL 也说明了另一个道理:
    “源码里明明写了” ≠ “运行时能读到” —— 写断言前要确认那个信息在运行时是否还保留 ✓


本篇用到的库

项内容
三方库react-native-simple-table(上游 0.0.3 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-simple-table
适配 TAG0.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 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
宿主依赖✅ 无需任何额外依赖(零依赖)
宿主 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐