本文记录把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✓ |
isRequired4 处 | ⚠️ 全在propTypes里(L21/L22/L24/L27)⇒ 只是类型校验,不崩 ✓ |
ScrollView5 处 | ✓(它就是用 ScrollView 拼的) |
scripts | 只有test⇒ 没有prepare,不需要--ignore-scripts |
| 规模 | 只有index.js+src/Table.js(src/Table.js3282 B / 137 行) |
index.js(105 B) | import Table from './src/Table'+export default Table⇒只有默认导出 |
★★★★★ 二、Table是 class —— 这次按 class 断言,一次全中
先看源码那一行(坑 327 的固定动作):
// src/Table.js L17classTableextendsComponent{// src/Table.js L137exportdefaultTable实测:
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:清单比对 + 精确值全中
源码:
classTableextendsComponent{staticpropTypes={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,};staticdefaultProps={columns:[],dataSource:[],columnWidth:DEFAULT_COLUMN_WIDTH,// = 60height:DEFAULT_HEIGHT,// = 240renderCell: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⇒⇒defaultProps8 条全 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 脚本npminstall"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"⇒两字段自相矛盾 |
| 备注 | 库代码零改动 |
关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。
本库零依赖、零平台分支、零原生代码、useNativeDriver零命中⇒结论跨基线成立✓
十、已知限制与本次验证边界
10.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★★Table是 class | typeof是function、prototype.render是 function;4 个方法(_renderCell/_renderHeader/_renderRow/render)全在prototype上 |
★★★★★defaultProps5 项、值精确 | 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) |
★★heightprop 控制内容区高度 | 默认 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// ❌ 这也区分不了typeofPropTypes.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:表格高度怎么定?
heightprop(默认 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) |
npminstall"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-bcom.rnoh084.demo-aEntryAbility--psrnAppKey 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-openharmony0.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 |
| 本次构建 | assembleHap3 分 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