ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React Native鸿蒙跨平台开发:环境搭建与手写表格组件实战

React Native鸿蒙跨平台开发:环境搭建与手写表格组件实战 说实话我最初对“React Native 跑鸿蒙”这件事是持观望态度的。直到我把一个带 FlatList、网络请求和基础组件的 RN 工程真正装进 HarmonyOS 真机才发现这条路已经比想象中成熟很多。这篇是这个系列的第一篇不聊虚的直接带零基础读者把 React Native 鸿蒙跨平台开发环境跑起来然后手写一个可复用的基础表格组件。为什么第一篇就写表格因为表格是后台管理类应用最逃不掉的东西它同时涉及布局、列表、数据结构设计和交互作为入门第一个组件性价比非常高。这篇文章会完整走一遍RN 与鸿蒙的适配原理、环境搭建、表格组件的数据模型设计、核心代码实现、真机编译部署以及最后单独聊一聊“启动白屏”这个每个人都绕不开的问题。1. 为什么选 React Native 而不直接学 ArkTSRNOH 的适配原理1.1 React Native 跨平台的真正含义很多人对跨平台开发有个误解以为 RN 的 JS 组件会被“翻译”成平台的 UI 代码。实际上不是这样。RN 做的事情是你用 JS 写组件树经过 React 的调和机制计算出一个虚拟 UI 树再由 Yoga 布局引擎算出每个组件的位置和大小最后把这份布局数据交给原生渲染层由原生组件真正画出来。换句话讲RN 的跨平台能力不在于“把代码翻译成别的语言”而在于它在每个平台都有一套“渲染后端”。Android 的渲染后端负责把布局喂给 Android ViewiOS 的渲染后端负责喂给 UIView。你的 JS 代码基本不用动需要换的只是这套渲染后端。1.2 RNOH 给鸿蒙补上的那一块拼图React Native 官方支持 Android 和 iOS鸿蒙不在其中。RNOHReact Native for OpenHarmony一个由社区主导的鸿蒙适配项目做的事情就是给 RN 增加一套“鸿蒙渲染后端”——把 Yoga 布局结果映射到 ArkUI 的组件树上去。我做一个比较粗浅但很有用的类比你的 JS 代码是一份中文剧本Android 和 iOS 是两支演员队伍RNOH 是第三支队伍他们拿到同一份剧本用鸿蒙原生组件把戏演出来。所以你写业务代码的时候基本感知不到底层是哪套 UI 系统只要 RN 组件本身在鸿蒙上做了适配你就能直接用。这也是我说“现阶段 RN 鸿蒙方案已经能用于真机”的原因。对于 View、Text、FlatList、ScrollView 这类最常用的基础组件RNOH 的覆盖已经很完整跑通业务主流程没有问题。而且它同时支持老的架构和新的 Fabric 架构等于说社区后续迭代的路也铺好了。1.3 哪些场景适合这套组合先泼一盆冷水如果你要做一个深度依赖鸿蒙特色的应用比如要用到分布式能力、系统级卡片、元服务那 RN 不合适老老实实学 ArkTS 才是正路。反过来如果你的团队已经有成熟的 RN 技术栈或者你手里有一套跨平台业务代码想以尽可能低的成本覆盖鸿蒙用户那 RNOH 是一个性价比非常高的选择。你可能只需要补齐鸿蒙壳工程、处理几个平台差异的小细节业务代码就能直接跑起来。这篇文章后面所有内容都是建立在“业务代码复用鸿蒙只是多一个编译目标”这个大前提下。2. 开跑之前的环境准备六个软件缺一不可顺序还会踩坑2.1 先确认你的电脑装了什么零基础入门最容易栽的地方不是代码而是环境。我按自己的实践顺序列一张清单你可以直接截图对照软件版本要求作用Node.js18 或更高运行 npm、Metro 打包器JDK17鸿蒙原生工程编译需要DevEco Studio5.0 及以上鸿蒙原生 IDE用来编译 hap 包HarmonyOS SDKAPI 12 或更高系统接口与模拟器镜像ohpmDevEco 自带鸿蒙侧的原生依赖管理鸿蒙设备HarmonyOS NEXT 真机或模拟器运行目标我更推荐真机这里有个容易忽略的点RN 工程的 JS 依赖用 npm 管理而鸿蒙原生工程里还有一套 ohpm 依赖。很多人装完 npm 包就直接去构建结果原生侧编译报错就是因为 ohpm 依赖没装。2.2 初始化一个带鸿蒙壳的 RN 工程环境装好之后真正的初始化流程我用过一次就记住了。核心思路是先用 RN 官方脚手架创建标准工程再通过 RNOH 的脚手架在工程里生成一个独立的鸿蒙原生壳子工程。# 1. 创建标准 RN 工程 npx react-nativelatest init RnHarmonyTable # 2. 进入工程目录 cd RnHarmonyTable # 3. 通过 RNOH 脚手架生成鸿蒙壳工程 npx react-native-oh-tpl/react-native-harmonylatest init第三步执行完之后工程根目录下会出现一个harmony目录这就是要用 DevEco Studio 打开的原生工程。RNOH 的版本迭代比较快如果命令有调整以它官方 README 为准但总体思路不变你的 JS 业务代码留在 RN 工程里鸿蒙壳工程只是为了编译和运行。我在这一步卡过很久的地方是顺手把harmony目录用 DevEco Studio 打开后IDE 一直在同步 SDK界面看起来像卡死了。实际上它是在后台拉取鸿蒙 SDK 组件网速慢的时候等个十几分钟都很正常。别急着关看右下角进度条。2.3 签名配置一个绕不开的环节鸿蒙真机安装 hap 包必须有签名这和 Android 的 APK 签名是一个道理。零基础读者到这里最容易懵因为 DevEco Studio 的签名配置藏在工程设置里。最简单的做法是使用自动签名在 DevEco Studio 里登录你的华为账号打开工程的File - Project Structure - Signing Configs勾选自动签名IDE 会自动生成调试证书和 Profile。如果没有华为账号或者不想登录也可以走本地签名但步骤会繁琐很多对第一篇来说不划算。我的建议很简单先买不了吃亏注册一个账号用自动签名把第一条链路跑通再研究签名细节。3. 拆解表格组件的设计数据模型、列宽策略与渲染分工3.1 为什么第一版不直接引第三方表格库你可能会问表格组件不是现成的吗react-native-table-component这类库在普通 RN 项目里确实好用但在鸿蒙适配场景下第三方库对 RNOH 的支持程度不一而且封装库会隐藏掉太多布局和渲染细节。对零基础入门来说自己手写一个基础表格能把“列宽怎么分配”“表头怎么和表体对齐”“列表怎么滚动”这些底层逻辑彻底搞明白后面再遇到任何表格需求都能自己改比黑盒调用靠谱得多。3.2 表格的三个核心数据模型一个表格组件说到底只做三件事定义列、接收数据、渲染单元格。列的抽象可以设计成这样export interface ColumnT { key: string; // 对应数据对象里的字段名 title: string; // 表头显示的文字 width: number; // 列宽单位是像素 align?: left | center | right; // 文字对齐方式 render?: (item: T, index: number) React.ReactNode; // 自定义单元格 }ColumnT用了泛型这样表格组件就能适配任意数据对象。比如你有一组用户数据里面有name、age、city字段那就定义三列key分别填name、age、city。数据的抽象就简单了就是T[]一个泛型数组。组件内部拿到columns和data之后先算出表格总宽度tableWidth columns.reduce((sum, col) sum col.width, 0)然后严格按这个宽度渲染表头和数据行。3.3 列宽用固定像素而不是 flex 比例你可能觉得“列宽应该用 flex 比例更灵活”。这个想法没错但实测下来在表头和表体分开渲染的场景里flex 比例会导致一个很烦的问题两段独立的布局对同一组比例做计算时四舍五入会产生 0.5 到 1 像素的偏差表头单元格和表体单元格就会对不齐细看特别明显。固定像素宽度的方案则完全绕开了这个问题表头每个单元格设width: col.width表体每个单元格也设同样的值再保证整行宽度等于tableWidth两边就能精确对齐。等以后需要自适应屏幕宽度时再做一层宽度换算也不迟第一版求稳就好。3.4 表头用 View表体用 FlatList表格的渲染分工很清晰表头是固定不动的用一行View渲染columns即可表体是数据列表必须用FlatList渲染而不是把全部数据塞进ScrollView里 map 一遍。原因也很简单FlatList自带虚拟化只渲染当前屏幕附近的行。100 条数据时两种写法看不出差别1000 条数据时FlatList依然流畅ScrollView已经卡到掉帧。对鸿蒙设备也一样RNOH 对FlatList的适配做得比较成熟性能瓶颈主要在你的 item 渲染逻辑上而不是列表本身。4. 完整代码实现一个约190行的泛型基础表格组件4.1 先看完整源码我建议你把下面这份代码直接复制到项目的BasicTable.tsx里边看边跑然后再读后面的逐段讲解。import React, { useCallback, useMemo } from react; import { View, Text, FlatList, StyleSheet, } from react-native; export interface ColumnT { key: string; title: string; width: number; align?: left | center | right; render?: (item: T, index: number) React.ReactNode; } interface BasicTablePropsT { columns: ColumnT[]; data: T[]; rowHeight?: number; headerHeight?: number; } function BasicTableT({ columns, data, rowHeight 44, headerHeight 40, }: BasicTablePropsT) { const tableWidth useMemo( () columns.reduce((sum, col) sum col.width, 0), [columns], ); const renderHeader useCallback(() { return ( View style{[styles.headerRow, { width: tableWidth, height: headerHeight }]} {columns.map((col) ( View key{col.key} style{[styles.headerCell, { width: col.width }]} Text style{styles.headerText} numberOfLines{1} {col.title} /Text /View ))} /View ); }, [columns, tableWidth, headerHeight]); const renderRow useCallback( ({ item, index }: { item: T; index: number }) { return ( View style{[styles.row, { width: tableWidth, height: rowHeight }]} {columns.map((col) ( View key{col.key} style{[ styles.cell, { width: col.width }, col.align left styles.cellLeft, col.align right styles.cellRight, ]} {col.render ? ( col.render(item, index) ) : ( Text style{styles.cellText} numberOfLines{1} {String((item as Recordstring, unknown)[col.key] ?? )} /Text )} /View ))} /View ); }, [columns, rowHeight, tableWidth], ); const getItemLayout useCallback( (_data: T[] | null, index: number) ({ length: rowHeight, offset: rowHeight * index, index, }), [rowHeight], ); return ( View style{styles.container} {renderHeader()} FlatList data{data} renderItem{renderRow} keyExtractor{(item, index) String((item as Recordstring, unknown)?.[id] ?? index) } getItemLayout{getItemLayout} showsVerticalScrollIndicator{false} ListEmptyComponent{ View style{[styles.empty, { width: tableWidth }]} Text style{styles.emptyText}暂无数据/Text /View } / /View ); } const styles StyleSheet.create({ container: { flex: 1, backgroundColor: #fff, }, headerRow: { flexDirection: row, backgroundColor: #f5f6fa, borderBottomWidth: 1, borderBottomColor: #e5e7eb, }, headerCell: { justifyContent: center, paddingHorizontal: 8, }, headerText: { fontSize: 14, fontWeight: 600, color: #333, }, row: { flexDirection: row, borderBottomWidth: 0.5, borderBottomColor: #eee, }, cell: { justifyContent: center, alignItems: center, paddingHorizontal: 8, }, cellLeft: { alignItems: flex-start, }, cellRight: { alignItems: flex-end, }, cellText: { fontSize: 13, color: #444, }, empty: { alignItems: center, paddingVertical: 40, }, emptyText: { color: #999, fontSize: 13, }, }); export default BasicTable;4.2 逐段拆解几个关键设计tableWidth的用途非常核心。表头那一行和表体每一行都依赖同一个宽度这就在源头保证了表头和表体永远对齐。renderHeader是纯函数式的表头渲染。它遍历columns为每一列生成一个宽度精确等于col.width的单元格。我特意给表头文字加了numberOfLines{1}避免列名过长时把表头撑高破坏布局。renderRow是表格的数据行渲染。这里有个细节默认文本渲染用的是(item as Recordstring, unknown)[col.key]把你的数据对象当成字典取字段。如果你的数据字段不存在会回退成空字符串不会直接抛错这对动态数据场景很重要。render自定义函数的出现是这套表格组件的灵魂。比如你要在“得分”列显示一个带颜色的数字或者要在“操作”列放一个按钮只需要在列定义里写render组件就会优先使用你的自定义渲染而不是默认文本。4.3 在 App 里把它跑起来写完组件在App.tsx里写一段示例数据就能看到效果import React from react; import BasicTable, { Column } from ./BasicTable; type User { id: string; name: string; age: number; city: string; score: number; }; const users: User[] [ { id: 1, name: 张明, age: 28, city: 上海, score: 88 }, { id: 2, name: 李思, age: 24, city: 北京, score: 72 }, { id: 3, name: 王强, age: 31, city: 广州, score: 55 }, { id: 4, name: 赵雅, age: 26, city: 深圳, score: 91 }, ]; const columns: ColumnUser[] [ { key: name, title: 姓名, width: 100 }, { key: age, title: 年龄, width: 70, align: center }, { key: city, title: 城市, width: 120 }, { key: score, title: 得分, width: 90, align: right, render: (item) ( Text style{{ color: item.score 60 ? #16a34a : #dc2626 }} {item.score} /Text ), }, ]; export default function App() { return BasicTable columns{columns} data{users} /; }这个示例刻意覆盖了三种单元格场景普通文本列、居中对齐列、自定义渲染列。跑起来之后你会在屏幕上方看到灰色表头下方是四行数据最后一列的 55 分会显示成红色其他分数是绿色。就这个小交互已经足够说明表格组件的核心机制。5. 从 DevEco 到手机编译、签名、安装与常见报错5.1 一次完整的构建链路是怎么走的代码写完之后你面对的是两套工程的协作JS 侧工程负责业务代码鸿蒙壳工程负责把 JS 代码和原生渲染环境打包成一个可安装的 hap 包。在 DevEco Studio 里打开harmony目录之后构建链路大致是DevEco 会先读取工程里配置的 JS bundle 入口Debug 模式下它不会把 JS 打进去而是让 App 启动后去连接 Metro 开发服务器Release 模式下你需要先在前端工程里执行 bundle 命令把 JS 代码打包成一份index.harmony.bundle再由 DevEco 一起编译进 hap。所以第一次验证功能我建议直接用 Debug 模式跑这样改完 JS 代码CtrlS 之后 Metro 会自动增量编译真机上马上能看到效果开发效率高很多。5.2 真机运行的三个细节点真机调试比模拟器靠谱。模拟器上的网络端口转发偶尔会有延迟真机只要和电脑在同一局域网Metro 连接就很稳定。第一开启鸿蒙设备的开发者模式。路径大致是“设置 - 关于设备 - 连续点击版本号”开启后回到设置里出现“开发者选项”打开里面的 USB 调试。如果用的是无线调试确保手机和电脑连同一个 Wi-Fi在 DevEco Studio 的设备列表里选择无线连接方式即可。第二Metro 地址配置。鸿蒙设备没有adb reverse这种等价命令所以 Debug 模式下 App 需要知道你的电脑在局域网里的 IP。你可以先启动 Metro再在 App 的调试设置里把 dev server 地址改成http://你的电脑IP:8081。这个地址不对启动白屏是大概率事件。第三真机首次安装 hap 时系统会弹提示询问是否允许安装。别慌正常点允许。如果在旧版本系统上还要求开启“允许安装未知来源应用”一起打开就行。5.3 我实际踩过的三个报错环境类报错的共性就是“别硬猜先看日志”。我记录最典型的三类第一个是签名报错编译到一半提示证书或 profile 不匹配。解决方式就是回到Project Structure - Signing Configs重新勾选自动签名确保当前设备已经加到你的测试设备列表里。第二个是 ohpm 依赖缺失。报错信息里往往出现native module not found或类型找不到。先检查harmony工程的oh-package.json5文件确认依赖是否完整然后在 DevEco 的 Terminal 里执行ohpm install第三个是 Metro 连接失败。App 启动后一直白屏DevEco 日志里能刷出类似connection refused的信息。按顺序做三件事确认 Metro 窗口有没有启动、确认手机和电脑是不是同一网络、确认 dev server 地址填的是不是电脑 IP。6. 启动白屏的五种原因与排查顺序6.1 先分清是哪种白屏“启动白屏”这个词太笼统了。在 RN 鸿蒙的组合里白屏至少要分两种。第一种是原生壳已经起来了但 JS 还没加载完成。屏幕上可能短暂出现空白过一两秒才渲染出内容这种情况下 Metro 在编译的话属于正常现象。第二种是原生壳起来了JS 也加载了但页面始终没有渲染这才是真正需要排查的问题。我自己遇到过几次白色页面长时间不消失最长一次卡了五分钟最后发现是 Metro 服务挂了App 在后台反复重连却连不上界面就一直是白的。6.2 白屏原因排查顺序表建议按下面的顺序排查不要东一榔头西一棒子。排查顺序检查项判断方法1Metro 是否启动电脑终端有没有显示 “Waiting on” 和打包日志2设备与电脑网络手机能否通过浏览器访问http://电脑IP:80813dev server 地址App 里配置的 Metro host 是否为当前电脑 IP4release 模式 bundle是否提前执行了 bundle 命令bundle 是否在指定目录5原生日志DevEco 的 Logcat 里过滤ReactNativeJS查看 JS 报错这个顺序的逻辑是先确认基础服务在线再确认网络通路再确认入口配置最后才去翻具体报错。其中的第 4 点特别容易坑新手。如果你用 Release 模式安装但没有在 JS 工程里执行 bundle 打包那 App 启动时根本找不到 JS 代码自然会白屏。命令行大致是这样的npx react-native bundle --platform harmony \\ --entry-file index.js \\ --bundle-output ./harmony/entry/src/main/resources/rawfile/index.harmony.bundle \\ --assets-dest ./harmony/entry/src/main/resources/rawfile6.3 表格组件的下一站优化方向表格组件跑通之后后面的路就很清晰了。排序功能可以加在列定义上表头点击时切换升序降序这就要在Column上扩展一个sortable属性。分页功能可以用FlatList的onEndReached实现上拉加载。横向滚动需要给整个表格套一层横向ScrollView让表头和表体同步滚动实现列固定。自定义单元格目前已经支持后续可以扩展出标签、进度条、按钮等组件。作为系列第一篇先把基础表格用起来再说。我个人的建议是不要急着把这些功能全部塞进第一版先跑通完整链路等你亲眼看着自己的 RN 代码在鸿蒙真机上渲染出来信心起来了后面每一步都顺。我在实际开发中还有个很个人的体会跨平台开发最容易让人沮丧的不是写业务代码而是环境链路太长、报错太隐晦。所以这个系列每一篇都会把“怎么跑起来”放在“怎么写得更好”前面。你先跑起来再优化这是最稳的路。
返回列表