多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

React Native鸿蒙适配实战:待办列表组件从白屏到流畅运行

React Native鸿蒙适配实战:待办列表组件从白屏到流畅运行 最近把一个 React Native 的待办事项列表组件完整跑到了鸿蒙设备上整个过程比预想中曲折不少但收获也很大。待办事项列表看起来是入门级 demo实际上它是移动端高频交互场景的一个典型缩影既要有增删改查这种基础数据操作又要处理状态管理还要把优先级这种抽象概念用视觉方式表达出来顺带还要在不同平台上保持一致的交互体验。这篇就围绕我在鸿蒙环境下实现这个组件的完整过程把方案选型、核心实现、踩坑记录和对高频交互的理解一次说清楚。如果你正在做 React Native 跨平台开发或者准备把已有 RN 项目适配到鸿蒙这篇文章应该能帮你省掉不少弯路。1. 为什么拿待办事项开刀高频场景的复合能力拆解1.1 移动端高频交互场景的“四件套”待办事项列表这个场景几乎每台手机上都有用户一天可能要打开十几次。它的核心操作无外乎四类新建一条待办、查看列表内容、把待办标记为完成或删除、偶尔还要编辑一下内容。看起来枯燥但这四个操作恰好覆盖了移动端高频交互场景的绝大部分特征。我习惯把这四件事叫作“高频交互四件套”新增是写入操作列表渲染是读取操作状态切换是更新操作删除是移除操作。一个前端页面无论多复杂落到用户手指上高频操作翻来覆去就是这几样。这也是为什么很多组件库、框架文档都拿 todo list 当例子——不是因为它简单而是因为它把核心问题暴露得很彻底。而且待办场景还有一个特殊之处用户对它的响应速度极其敏感。你新建一条待办如果点了没反应或者列表插入后闪了一下才稳定用户立刻就会觉得这个应用“不跟手”。高频场景对性能的要求不是 benchmark 上的数字而是手指头传来的那种“即时感”。1.2 复合能力拆开看CRUD、状态管理、优先级可视化各承担什么标题里提到“增删改查状态管理优先级可视化”这个复合能力这三块分别解决不同层面的问题。增删改查是数据层能力决定了组件能不能完成最基本的业务闭环。数据从哪来、新增的数据如何插入列表、编辑后如何同步、删除后如何更新界面这是 CRUD 要回答的问题。在鸿蒙和 iOS 双端都要跑的情况下CRUD 逻辑必须做到平台无关否则每端各写一套状态就失控了。状态管理是组件层能力解决的是“多个 UI 组件之间如何共享和同步数据”。比如列表页和详情页都要看到同一条待办的标题输入框里敲了半天的草稿切走再切回来还要保留这些都需要状态管理来兜底。说实话单机版的待办组件不用状态管理也能写但一旦加上“编辑中标记”“筛选条件”“排序方式”这些界面状态状态变多了之后没有统一管理就是灾难。优先级可视化是表现层能力把一条待办的紧急程度、重要程度用颜色、图标、排序位置、标签这些视觉元素表达出来。它属于“接口简单、实现复杂”的一类需求数据模型很好设计一个 priority 字段就完了但把三五个优先级映射成用户一眼能看懂的信息这一步很考验细节。1.3 为什么要专门聊鸿蒙这个目标平台鸿蒙是这次实现里最大的变量。React Native 在 iOS 和 Android 上已经跑了很多年生态成熟但到了鸿蒙这里事情就变了底层渲染引擎要对接鸿蒙的 ArkUI 能力原生模块要重新找鸿蒙实现构建链路由原来的 Metro Gradle/Xcode 变成了鸿蒙的 DevEco Studio 和 hvigor。更现实的问题是很多跑在 iOS/Android 上没问题的原生依赖在鸿蒙上根本没有对应版本。所以“跨平台”这件事在鸿蒙环境下特别有意思它不是简单地把代码编译一遍就行而是要重新审视每一个依赖、每一处原生调用是不是真的被鸿蒙社区支持。这也是我把这个项目当作一次检验的原因——它能逼你把 RN 跨端的底层机制都想明白。2. 技术选型与架构设计从 RN 到鸿蒙的三层取舍2.1 为什么还要继续用 React Native而不是直接上 ArkTS这个问题在立项的时候一定会被问。既然都做鸿蒙了鸿蒙原生开发用 ArkTS 加 ArkUI 它不香吗为什么非要在鸿蒙上套一层 React Native我的答案分三层。第一层是代码复用。团队如果已经有 React Native 的代码库直接跑鸿蒙意味着业务代码不用重写Hooks、组件、状态管理逻辑全部复用节省的是以人月计的工作量。第二层是生态桥接。RN 社区有大量现成的轮子比如 UI 组件、动画库、图表库鸿蒙原生生态还在成长期能用 RN 生态补齐是很大优势。第三层是团队技能延续。React 技术栈的同学上手 RN 鸿蒙开发的成本远低于从头学 ArkTS 加 ArkUI。但注意我这么说不是否定 ArkTS。如果这是一个纯鸿蒙单端产品、团队没有 RN 存量那直接 ArkTS 开发完全合理性能还会更好。这次选 RN 是因为手头已经有一套跨端代码鸿蒙只是新增的第三个平台复用是最高优先级。2.2 鸿蒙适配的两条主流路径react-native-harmony 与社区 fork目前把 RN 跑到鸿蒙上主流路径是围绕 OpenHarmony 社区维护的 react-native-harmony 体系来做的。这套东西的核心是一个运行时适配层把 React Native 的 C 核心与鸿蒙的 ArkUI 组件衔接起来让 RN 的 JS 组件能映射成鸿蒙原生组件去渲染。具体到工程接入常规做法是在鸿蒙工程里引入对应的 Harmony 运行时模块配置好 RN 实例的加载入口。大致依赖长这样{ react-native: 0.72.x, react-native-oh/react-native-harmony: ^0.72.x, react-native-harmony-cli: ^0.0.x }这里有个很容易踩的坑react-native-harmony 对 RN 版本有严格的配套要求不是随便哪一版 RN 都能跑。我见过不少人在这一步栽跟头RN 升到最新版结果 Harmony 适配没跟上只能灰溜溜回退版本。建议用 LTS 偏保守的 RN 版本比如 0.72 或 0.73这两个版本在鸿蒙社区的适配覆盖比较全。另一条路径是直接用开源社区的 fork 版本有些团队基于特定 RN 版本做了定制改造支持了更多鸿蒙特性。这类 fork 的优点是功能新缺点是跟进维护要自己负责。我的建议很直接非必要不用 fork跟着官方社区维护的版本走遇到问题还能在 GitHub 上找到 issue 和修复记录。2.3 状态管理选型Zustand 还是 Redux Toolkit待办组件这种规模状态管理用不着重量级方案。Redux Toolkit 功能全但模板代码多一个 todo 组件要写 action、reducer、slice光心智负担就够喝一壶了。Zustand 这种轻量方案对我来说是现阶段跨端开发更顺手的选择。原因有三一是 API 简单一个 create 函数搞定全局 store不需要 Provider 包裹二是对 TypeScript 支持友好类型推导比 Redux 丝滑不少三是对鸿蒙这类新平台的适配成本低它是纯 JS 实现不依赖任何原生模块只要 RN 能跑Zustand 就能跑。实际选型对比可以看这个表方案模板代码量学习曲线鸿蒙兼容风险适合场景Redux Toolkit多陡低纯 JS大型应用、复杂状态流Zustand少平缓低纯 JS中小组件、快速迭代MobX中中低纯 JS偏好响应式编程的团队Recoil中中低纯 JS细粒度原子状态在鸿蒙这种新平台上我倾向于选“纯 JS 依赖”因为每引入一个原生依赖就多一个“鸿蒙有没有适配”的未知数。3. 核心实现增删改查 状态管理 优先级可视化的落地细节3.1 数据类型与 Store 设计数据类型是第一步。我设计的待办项包含这些字段export type PriorityLevel high | medium | low | none; export interface TodoItem { id: string; title: string; description?: string; completed: boolean; priority: PriorityLevel; createdAt: number; completedAt?: number; }id 用时间戳加随机数生成确保快速创建时不会重复。createdAt 和 completedAt 用时间戳保存方便后续做排序和统计。priority 先定义成联合类型而不是 number主要原因是代码可读性更好传值不容易出错。Store 用 Zustand 实现核心结构如下import { create } from zustand; interface TodoStore { todos: TodoItem[]; filter: all | active | completed; sortBy: priority | createdAt | completed; addTodo: (title: string, priority: PriorityLevel) void; toggleTodo: (id: string) void; removeTodo: (id: string) void; updateTodo: (id: string, patches: PartialTodoItem) void; setFilter: (filter: TodoStore[filter]) void; setSortBy: (sortBy: TodoStore[sortBy]) void; }这里我把数据状态和界面状态filter、sortBy放在同一个 store 里因为它们在交互上强耦合用户切换“只看高优先级”的同时列表要立刻重新过滤和排序分开管反而要多写同步逻辑。3.2 增删改查的交互实现与边界处理新增操作有个很反直觉的细节输入框应该聚焦后自动弹出软键盘用户敲完直接点回车或点“新增”按钮这一条要立刻插到列表顶部。如果新增后列表定位不重置用户在列表底部操作时新条目出现在看不见的位置体验直接崩掉。新增核心逻辑addTodo: (title, priority) { const trimmed title.trim(); if (!trimmed) return; set((state) ({ todos: [ { id: ${Date.now()}_${Math.random().toString(36).slice(2, 8)}, title: trimmed, completed: false, priority, createdAt: Date.now(), }, ...state.todos, ], })); },标题的去空格校验很重要不然用户敲了一串空格也能建空白待办后面清垃圾数据的时候会怀疑人生。每次新增把新条目 unshift 到数组头部这是 todo 类场景的默认交互。删除操作我加了一个小的滑动删除手势这是移动端待办列表的高频操作方式之一。但手势和列表滚动会有冲突需要设置好触发阈值滑动超过一定距离再触发删除否则列表滚起来时稍微横滑一点就误删那就变成灾难了。编辑操作我放在了待办详情弹层里做点击列表项弹出底部弹层里面改标题、描述、优先级。这样列表本身保持干净编辑能力也没有缺席。弹层内维护一套本地草稿状态点保存才一次性提交到 store点关闭直接丢弃草稿——这个设计可以有效避免在键盘弹起时实时同步造成的输入卡顿。3.3 优先级可视化从数据到视觉的完整映射链路优先级可视化是最能体现“看着简单做起来琐碎”的部分。我分了三个层次来落地。第一层是优先级到颜色的映射。高优先级用红色系中优先级用橙色系低优先级用绿色系无优先级用灰色。这套语义是跨平台通识Red 代表紧急和危险Green 代表安全不用解释用户就能猜个大概。在 RN 里我用一个纯函数做映射const priorityColor: RecordPriorityLevel, string { high: #E5484D, medium: #F76B15, low: #30A46C, none: #8E8E93, };第二层是排序规则。我提供“按优先级排序”的选项规则是按权重排序high 权重 3、medium 权重 2、low 权重 1、none 权重 0同时完成状态的待办沉底。实现上用权重映射加 stable sort。第三层是辅助视觉元素。除了颜色每条待办左侧增加一条 4px 宽的颜色竖条颜色跟随优先级。这一步看似冗余但对色弱用户极其重要——如果只靠圆点颜色区分优先级红绿分辨困难的人就看不出差别了。加上竖条之后表单感更强列表的视觉节奏也更清楚。标题加粗、描述用次级文字颜色这些都是让信息层级一眼可扫的常用手段。3.4 组件树划分与列表渲染组件结构上我分成三层屏幕容器组件、列表组件、单项组件。单项组件用 React.memo 包裹依赖 props 的浅比较来减少重渲染。const TodoItemView React.memo(function TodoItemView({ item, onToggle, onRemove }) { return ( View style{styles.itemContainer} View style{[styles.priorityBar, { backgroundColor: priorityColor[item.priority] }]} / Text style{[styles.itemTitle, item.completed styles.itemCompleted]} {item.title} /Text {/* 完成勾选、删除按钮 */} /View ); });React.memo 配合不可变数据在列表类组件里效果特别明显只有某一条 todo 的引用变化时对应单项才重新渲染其他项全部跳过 diff。Zustand 默认返回新引用配合起来很顺。列表用 FlatList 渲染这里的一些性能配置细节我会放到后面的优化章节单独聊。4. 鸿蒙适配与启动白屏的踩坑实战4.1 启动白屏第一个遇到的拦路虎标题里的热搜词“react native 启动白屏”我这次算是亲身体验了。在鸿蒙上跑 RN 应用第一次在 DevEco Studio 里启动模拟器屏幕上白茫茫一片Logcat 里刷了一堆报错。这个问题的本质是 JS Bundle 没能正确加载到原生运行时里。排查路径是固定的先看 Metro 服务有没有跑起来再看 JS Bundle 的加载地址对不对最后看原生侧有没有注册正确的组件入口。我在鸿蒙工程里遇到的情况是原生入口配置里没有正确调用加载 Harmony 运行时的方法导致应用启动后找不到 JS 模块。补上这一步后白屏立刻消失// 鸿蒙工程 EntryAbility 里初始化 RN 实例 import { RNAbility } from react-native-oh/react-native-harmony; export default class EntryAbility extends RNAbility { // 指定加载的 JS Bundle 名称 onWindowStageCreate(windowStage) { const instance this.getRNInstance(); // 确保 bundle 加载路径指向 Metro instance.loadBundle(index); // 对应 index.js 入口 } }这里有个很实用的经验遇到白屏先别慌着改代码先在日志里确认三个状态——原生层有没有成功初始化、JS 引擎有没有收到 Bundle、React 组件有没有完成首次挂载。三层都确认了问题就定位了八成。4.2 鸿蒙平台的 API 兼容性排查清单RN 代码从 iOS/Android 搬到鸿蒙最大的隐性成本是 API 兼容性。我整理了一份排查清单每次适配新模块都按这个过一遍。检查项常见问题解决策略原生模块依赖依赖的第三方原生库没有鸿蒙实现优先替换为纯 JS 实现查找社区鸿蒙适配版本系统 APIStatusBar、DeviceInfo 等 API 行为不一致封装统一工具层分平台处理差异字体与文本渲染字体加载路径不一致文字显示异常使用系统内置字体避免依赖 iOS 特有字体滚动容器ScrollView/FlatList 在鸿蒙上滚动阻尼差异设置统一的 decelerationRate 和 overScrollMode弹窗层级Modal 在某些鸿蒙版本上的层级问题改用 Absolute 布局封装的弹层组件这里要特别提醒鸿蒙的 ArkUI 有自己的渲染生命周期RN 的组件挂载卸载和鸿蒙原生的页面生命周期不是一一对应的。我在适配时发现页面从后台切回前台部分 RN 组件的状态没有自动刷新需要在鸿蒙侧重写对应的页面监听事件再手动触发 RN 层的刷新。这类问题不跑真机根本发现不了所以模拟器只能做基础验证最终一定要上真机测。4.3 包体积和构建链路鸿蒙特有的工程改动RN 代码跑鸿蒙构建链路要从 Metro 直接打包切到 DevEco Studio 的 hvigor 构建。这意味着整个开发工作流要调整RN 的启动脚本要支持同时拉起 Metro 和鸿蒙的调试工程。我在工程里用了 npm script 串联整个流程scripts: { start:harmony: react-native start, build:harmony: react-native-harmony build, run:harmony: npm run start:harmony npm run build:harmony }第一次在鸿蒙工程里跑构建的时候由于包名、moduleName 之类的不一致折腾了很久。建议新建项目时就把 moduleName 统一好比如都用 index后续构建能少踩不少坑。另外鸿蒙的构建产物是 hap 包签名证书配置和 Apple/Android 的签名体系完全不同需要到 AppGallery Connect 申请对应的调试证书这一步要提前走流程不然开发到一半可能被签名卡住没法上真机。5. 高频交互下的细节打磨与性能优化5.1 列表渲染优化的三板斧待办列表是高频率操作的列表优化重点在 FlatList 的渲染配置。我直接用了这几板斧实际滚动和插入的顺滑度提升非常明显。第一板斧是 windowSize 调小。默认值是 21表示渲染当前屏幕前后共 21 个屏幕高度的内容。列表项很简单时不需要渲染这么多我调到了 7减少了很多无谓的渲染。第二板斧是 maxToRenderPerBatch 和 initialNumToRender 配合。一条待办组件渲染成本不高这两个值不用太激进默认值 10 对我来说够用。如果待办项卡片里再放图片、富文本就要把 initialNumToRender 调小先把首屏快速画出来再慢慢补充渲染。第三板斧是 getItemLayout。待办项高度如果不固定很难用这个优化但如果做了动态高度可以测量后缓存偏移量。实际做下来我固定了单项高度然后实现了 getItemLayout列表滚动时跳转非常稳没有高度跳动。const ITEM_HEIGHT 64; const getItemLayout (_data, index) ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, });这里要顺带提一个场景问题待办项内容长度不同固定高度必然会导致文字截断。我的处理是标题最多一行超出省略描述不展示详情进弹层看。这样用固定高度换列表性能业务上是可接受的。5.2 键盘弹起、输入聚焦与手势冲突输入框是待办列表的高频交互入口这里有两个经典问题几乎谁都躲不过。第一个是键盘遮挡。新增输入框在列表底部时软键盘一弹起来就把输入框盖住了。RN 的 KeyboardAvoidingView 是跨端最常用的方案但在鸿蒙上对键盘高度估算偶尔不准。我的实测方案是给新增输入框区域一个 minHeight再配合 KeyboardAvoidingView 的 behavior 设为 padding在鸿蒙上用的效果最稳定。第二个是手势冲突。列表支持左滑删除输入框区域支持水平滑动光标选择文字两者在边缘地带容易互相抢手势。解决方式是在输入框聚焦状态下禁用列表的滑动删除手势失焦后再恢复。这种“上下文感知的手势开关”在移动端交互里是不可忽视的细节。5.3 本地持久化让待办数据抗得住重启待办组件如果刷新就丢数据那和记事本就没区别了。数据持久化这块我用的是 AsyncStorage鸿蒙上需要确认当前使用的版本是否有对应适配。import AsyncStorage from react-native-async-storage/async-storage; const STORAGE_KEY todo_list_data_v1; export async function loadTodos(): PromiseTodoItem[] { try { const raw await AsyncStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch (e) { // 解析失败时返回空列表避免白屏 return []; } } export async function saveTodos(todos: TodoItem[]) { try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); } catch (e) { // 存储失败要静默处理不让用户感受到错误 } }存储策略上有两个取向。一个是每操作一次就写一次存储最简单但频繁 IO另一个是 debounce 合并写入比如操作停止后 500ms 再存。我实际选的是后者数据量小且存储频率可控。加载数据后我会做一个数据结构的版本校验字段以后字段升级时能做迁移不至于直接解析失败。5.4 边缘状态空列表、加载态、错误态高频组件最容易忽略的是空和错的状态。待办全部清空后直接展示空列表会显得很突兀我做了个简单的空态视图一行文案加一个按钮“暂无待办点此新建”。首次加载从 AsyncStorage 读数据时会有几百毫秒的空窗期这个期间展示一个轻量加载指示器避免用户看到一闪而过的空白。错误态我分两层处理。读取存储失败时静默返回空数组写入失败时在全局加一条 Toast 提示“数据保存失败请检查存储空间”。高频交互场景里错误提示要尽量轻不能让用户从头阅读一篇“错误报告”。6. 常见问题与排查技巧实录6.1 问题速查表这次开发过程里攒了不少问题我挑几个典型的整理成表格每个都踩过坑对应解决思路也是实测有效的。问题现象排查思路实测解决方案鸿蒙启动白屏看原生日志确认 Bundle 是否加载检查 EntryAbility 是否正确加载 RN 实例并指向 index.js列表操作后闪烁或跳位检查 key 是否稳定唯一用 id 做 key不用 index固定单项高度中文输入时回车触发两次新增检查键盘事件在输入法直接回车时的行为新增逻辑里加标志位提交后立即重置输入框值为空左滑删除与列表滚动冲突判断手势方向与距离阈值设置触发阈值 60px超过才触发删除数据重启后丢失确认持久化写入是否完成增加 debounce 存储并在应用进入后台时强制 flush弹层键盘顶起后按钮错位KeyboardAvoidingView 高度估算不准确弹层内改用 minHeight flexGrow 布局自适应高优先级颜色在深色模式下看不清颜色在深色背景对比度不足色值改用亮色系列表背景避免纯黑鸿蒙真机 debug 时 Metro 连不上检查真机与电脑网络连通使用同一局域网adb 反向端口转发6.2 独家排查技巧分享最后说几个快速定位问题的技巧。第一个技巧是“打印优先级”。不管问题多诡异先分清是 JS 层还是原生层。JS 层的逻辑问题用 console.log 配合 Metro 日志看输出原生层的渲染和生命周期问题去 DevEco Studio 的 Logcat 里找线索。两层日志区域不同别混在一起翻。第二个技巧是“二分注释法”。当某个界面在鸿蒙上渲染异常时把组件树从下往上逐个注释每注释一层跑一次很快就能定位是哪一层出了问题。这个办法土但非常高效。第三个技巧是“真机优先”。鸿蒙的模拟器性能再好很多交互细节、手势冲突、键盘弹出问题模拟器都模拟不出来的。开发到第三天我就养成了习惯改完一个交互细节立刻上真机划两下比看半天代码都管用。还有个细节鸿蒙机的硬件返回手势、侧滑返回和 RN 页面路由的返回手势需要单独适配否则会出现“返回键把整个应用退出”而不是“返回上一页”的情况。这个在 iOS 上不需要处理因为 iOS 侧滑返回是 Navigation 栈统一管的ArkUI 的返回手势和 Router 的 back 需要确认联动。7. 最后的实操心得与调试建议项目做完后复盘我最大的体会是跨平台开发的核心不是“一套代码处处跑”而是“每一层差异都有人负责”。React Native 本身帮你抹平了很多差异但在鸿蒙这个新平台上总有一些边界需要自己填。做这类项目我建议从一开始就把各平台的差异点搜集到一个文档里每次适配都往里补充项目做完了这就是团队最宝贵的资产。调试流程上我强烈建议配置“模拟器 真机”双跑道。Metro 起来之后模拟器里快速验证组件逻辑真机上验证交互手势、键盘、系统返回这些模拟器测不出来的东西。这两个跑道相互补充能覆盖绝大多数问题场景。如果这个小组件后续要继续扩展可以考虑的方向是把本地数据换成 SQLite 或更结构化的存储增加待办提醒和通知能力以及接入账号体系实现多端同步。但有一点我要提醒每增加一个能力都要重新评估它在鸿蒙端的原生依赖情况这一步前置了后面会省掉很多返工。待办事项列表组件这个项目麻雀虽小五脏俱全。把增删改查、状态管理、优先级可视化这三件事在 RN 鸿蒙环境下真正跑通你收获的不只是一个组件而是对整个跨端技术栈在新平台上的一次深度体检。我这次做下来对 React Native 的底层渲染逻辑、鸿蒙的构建机制、移动端高频交互的细节都有了更具体的认知。希望这篇文章能给你省点时间少走点我已经走过的弯路。
返回列表