☰
React Native for OpenHarmony实战:网络请求与列表性能优化指南
2026/10/14 4:22:35 网站建设 项目流程

1. 为什么Day 3先拿“网络请求”和“列表”开刀

1.1 跨平台落地绕不开的两块硬骨头

Day 3 的训练营课程表出来的时候,我扫了一眼标题:React Native for OpenHarmony 实战,网络请求集成与高健壮性列表构建。老实说,看到这个选题我反而松了一口气。因为前两天的内容都在讲环境搭建、工程模板、组件映射,属于“让项目先跑起来”的阶段,而 Day 3 开始真正切入业务开发里最常用、也最容易翻车的两条链路——网络和列表。绝大多数内容型应用,打开首页之后要做的事情无非就是两件:拉数据、渲染列表。这两件事只要有一条不稳,用户的第一体验就崩了。

先说网络请求。任何一个真实的业务应用,都不可能像 Demo 那样把数据写死在代码里。登录、首页、个人中心、消息推送,全部依赖网络层把数据搬回来。RNOH 环境下,JS 侧的代码虽然长得像 React Native,但底层的网络栈并不完全等同于传统 RN 环境,很多以前不需要关心的代理、证书、DNS 解析问题,在开源鸿蒙生态下都会冒出来。再说列表。列表是内容应用的基本盘,但同时也是性能问题的重灾区。数据一多,滚动掉帧、内存上涨、白屏闪烁就全都来了。所以训练营把这两个点放在同一天,思路其实是清楚的:先解决数据怎么进来,再解决数据怎么稳住。

这一篇内容,既是 Day 3 当天我跟着讲师节奏做的笔记,也是课后我回到工位又重新复现了一遍的完整记录。如果你正打算把 RNOH 用到实际项目中,或者已经碰到列表白屏、请求失败这类问题,可以直接照着这里的步骤和参数去调整,能省掉不少自己排查的时间。

1.2 RNOH网络栈与渲染栈的特殊之处

先讲一个容易误导人的地方。很多人一听说“React Native for OpenHarmony”,就觉得代码里的fetch和原来的 React Native 一模一样,直接搬过来用就行。这句话对了一半。RNOH 提供了对 JS 侧 API 的兼容实现,像fetch、XMLHttpRequest这些基础能力确实能用,但底层的实现路径已经换了。常规 RN 环境里,网络请求走的是各自平台的网络库,而 RNOH 在开源鸿蒙环境里,请求最终会落到系统网络能力上,中间隔着一层 ArkTS 侧的适配逻辑。这层适配做得好不好,直接影响请求的成功率和调试的复杂度。

列表的渲染链路同样有差异。RNOH 的组件最终会映射到 ArkUI 的组件树里,FlatList这类长列表组件背后依赖的是原生侧的可视区域回收机制。理解这一点很重要,因为这意味着你在 JS 侧做的每一项优化,最终都要看原生侧买不买账。比如removeClippedSubviews这个属性,在传统 RN 里是有效的裁剪手段,但在 RNOH 环境下,如果底层对应组件不支持对应的裁剪逻辑,设置了也起不到预期效果。所以 Day 3 的课程里,讲师反复强调一句话:先搞清楚这条链路底下是谁在干活,再谈优化。

2. 基础环境与请求封装:先让网络层稳稳站住

2.1 初始化RNOH工程并配置网络权限

课程开始前,我重新建了一个干净的工程,打算把网络请求从头到尾走通一遍。建议你也这么做,别在原来的练习项目里改,因为环境变量和依赖版本一旦混了,排查起来非常痛苦。

工程创建过程不算复杂。先用 IDE 新建一个支持 RNOH 的空工程,然后在工程目录里通过包管理命令安装 RNOH 相关依赖。这一步有几点需要注意:依赖版本要跟工程模板匹配,如果之前装过其他版本的 RN 核心库,最好先清理再装,避免出现 JS 引擎加载失败或者模块找不到的问题。

网络权限是这里最容易漏的坑。开源鸿蒙应用默认不会授予网络访问权限,必须在module.json5里显式声明ohos.permission.INTERNET。我把这个权限的声明放在最前面,因为后边所有联调工作都建立在网络通的基础上。声明完成之后,先写一个最小请求用例,确认基本连通性。当时我写了一个最简单的fetch请求,拿到 200 状态码之后才继续往下封装。

另外,如果你用的是模拟器,还要确认模拟器的网络模式。我第一天测试时用的是 NAT 模式,访问公网正常,但访问局域网内的测试服务时,发现根本连不上。后来把网络模式改成桥接模式才好。这个问题在训练营里不止一个人踩到,值得提前记下来。

2.2 统一request封装:拦截器、超时与取消

工程跑通之后,第一件事不是急着写业务接口,而是做一层统一的请求封装。RNOH 的 JS 侧支持fetch,也支持axios这类库。直接用fetch当然可以,但我更推荐在fetch之上做一层薄封装,把超时、错误码、取消逻辑统一收敛起来。原因很简单:业务代码里如果到处散落着fetch,等到要统一加请求头、统一处理 token 过期时,你会后悔的。

我当天的封装结构大致是这样的:

const request = async (url, options = {}) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), options.timeout || 10000); try { const res = await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return await res.json(); } finally { clearTimeout(timer); } };

别看这段代码简单,它解决了我后续联调中绝大多数问题。超时是必须有的,RNOH 环境下如果服务器无响应,请求会长时间挂起,用户看到一个永远转圈的页面,体验非常差。统一加Content-Type头也很重要,因为开源鸿蒙环境里某些框架版本对缺失请求头的情况处理得比较宽松,但服务端不一定宽松,少了这个头,后端接口可能直接返回 415 或者解析失败。

错误码的处理我建议单独抽一个模块。当天我遇到一个情况:服务端返回了 200,但响应体里的业务状态码表示 token 过期。这类不算 HTTP 层错误的问题,如果不在封装层统一拦截,业务代码会写得很啰嗦。我的做法是在封装函数里对响应体做个结构约定,如果code不为 0,就抛一个携带业务码的异常对象,由上层统一处理。

取消请求的能力也要留好。用户在列表页快速下拉刷新时,旧请求还没返回,新请求已经发出去了,这时候旧请求的结果不能再覆盖列表数据。用AbortController在最外层控制,组装参数时给每个请求一个独立的 controller,就能在竞态场景下主动把旧请求 Cancel 掉。后文列表部分会展开讲这个场景。

2.3 请求头、Cookie和TLS证书的现场处理

基础请求通了之后,紧接着就是各种真实环境下的细节问题。第一个是自定义请求头。RNOH 的应用打包发布后,不能依赖浏览器环境自动携带 Cookie,业务上如果需要登录态,最好显式做 token 传递,并由服务端允许自定义头。当时我们测试用的后端服务要求请求头里带Authorization,我直接在封装层插入了头部字段,这样业务代码里就不用操心这件事了。

第二个是 TLS 证书问题。开发阶段我们连的是测试环境的 HTTPS 接口,证书是自签名的,结果请求直接报证书校验失败。这个问题在传统 RN 环境里可以通过配置忽略证书错误来临时绕过,但在 RNOH 里,我发现这个绕过逻辑并不总是生效。当时我不敢用那种全局忽略证书的办法,太危险,尤其是训练营环境里大家用的是同一个测试服务,如果为了图省事关掉证书校验,后续排查其他问题时会多出一层干扰。最终的处理方式是在测试环境用 HTTP 接口跑通逻辑,等正式环境再用合规的 HTTPS 证书。这个决策帮我省了很多事,也保证了真实发布时的安全性。

第三个是代理和 DNS。如果你的开发机通过代理访问外网,模拟器里的应用也要跟着配置代理。RNOH 的模拟器网络配置和常规 Android 模拟器不完全一样,我当时花了一些时间才找到入口。比较稳妥的办法是抓包时把代理地址配置到当前网络的代理设置里,然后在请求日志里确认是否真正走了代理。DNS 方面,如果测试服务用的是局域网 IP,建议直接写 IP,而不要用 hostname。倒不是说 hostname 不能用,而是在助记设备上解析顺序可能受到系统配置影响,直接写 IP 可以减少一类不稳定因素。

3. 高健壮性列表构建:不止是“能滑”更反对“扛得住”

3.1 健壮性列表到底检验哪些场景

网络上拉数据之后,就该处理列表了。我见过不少同学把列表健壮性理解成“不崩溃、能滑动”,其实这个标准太低了。所谓高健壮性,至少得覆盖下面这些场景:弱网环境下请求超时,列表不能卡在加载态;断网之后恢复网络,用户下拉刷新要能正常触发;分页加载时重复点击到底部,不能出现重复数据;接口返回的字段类型和预期不一致,页面不能直接白屏;大量数据快速滚动时,内存不能肉眼可见地膨胀。

这些场景说起来简单,但真要在代码里全部覆盖,需要一套完整的列表状态机配合。我当天构建的列表组件,内部维护了四个状态:初始化加载、下拉刷新、上拉加载更多、错误显示。每个状态都要有对应的 UI 表现,并且状态之间要能正确切换。

以空列表为例。很多人只处理了“有数据”和“加载中”两种状态,结果接口正常返回但是 data 为空数组时,页面直接渲染一个空白区域,用户一脸懵。正确做法是检测到空数据时展示一个空状态视图,告诉用户当前没有内容,并且提供一个刷新入口。当时讲师随口说了一句:列表健壮性的本质,其实是把“正常情况”以外的那 80% 分支都补齐。这句话我记下来之后,回头检查自己写的代码,发现确实漏了很多边界分支。

3.2 FlatList参数调优的取舍

RNOH 环境下,FlatList仍然是长列表的首选组件,但它的默认参数在性能表现上并不理想。Day 3 课程里专门用了一个下午做参数调优,我把实际测试结果整理成了一张表。

参数默认值调整值实测效果
initialNumToRender1012首屏渲染速度略降,但减少了滑动时的空白感
windowSize217减少可视区外的渲染数量,滚动更流畅
removeClippedSubviewsfalsetrue大幅降低内存占用,但需确认底层裁剪正常
maxToRenderPerBatch106降低单批渲染压力,减少卡顿峰值
keyExtractor默认显式设置避免重复 key 导致渲染异常

这里要特别说一下removeClippedSubviews。这个参数在传统 RN 中可以把滚出屏幕的视图裁剪掉,但在 RNOH 里,它的实际效果依赖底层 ArkUI 组件的实现。我当天在模拟器上开启后发现滚动变流畅了,但列表偶尔会出现整块空白,必须轻轻滚动一下才能恢复。后来把参数调成Platform.OS === 'android'条件下才开启解决了一部分,因为真实设备上的内存压力比模拟器大,开启后收益明显。如果你要直接抄作业,建议先在目标设备上测试一整天,再决定要不要开。

getItemLayout如果能提供,就一定要提供。当列表项高度固定时,这个属性可以让FlatList跳过动态测量,直接计算滚动位置。我测试的列表卡片高度是固定的,设置getItemLayout之后,快速跳转到列表底部从“能看到短暂白屏”变成“几乎没有感知”,提升非常明显。

3.3 分页刷新与请求竞态的状态机设计

分页刷新是列表健壮性的核心难点。最常见的 bug 是两个请求同时运行,旧请求先返回,结果把新请求的数据覆盖掉了。这个现象在弱网环境下特别明显。我在模拟项目X里复现过一次:用户快速下拉触发了刷新,紧接着列表又滑到底部触发了加载更多。刷新请求返回的是第一页数据,但它的响应比加载更多请求晚到,最后列表被第一页数据覆盖,用户发现翻到底部还是那些旧的条目,但其实加载更多请求已经成功了。

解决竞态的思路有两个。一个是请求序号控制,每一次请求都带一个自增 ID,响应返回时只接受当前最新 ID 对应的结果。这个方案实现简单,但代码要写得小心。另一个是直接让刷新和加载更多互斥,刷新时如果发现有加载更多请求在跑,先取消它;反过来也一样。我最终用的是第二种方案,加一个简单的请求锁:

let isLoadingMore = false; let isRefreshing = false;

状态机设计上,我习惯用一个loadState枚举来标识列表状态。它的取值包括idle、loading、refreshing、loadingMore、error、empty。UI 层根据这个枚举决定展示骨架屏、加载指示器、空视图还是错误提示。单独设置error状态的好处是,接口失败时不用把整个页面的布局推倒重来,只需要在列表顶部弹出一个可点击重试的横幅即可。

还有一个小细节:onEndReached触发阈值。默认情况下,列表滚动接近底部一点就会触发加载更多,如果用户快速滑动,可能一次性触发多次。我在实际项目里把onEndReachedThreshold设为 0.3,并且在onEndReached里用if (loadState !== 'idle') return做了拦截,这样即使触发多次,也只发起一次请求。

3.4 渲染层优化:从renderItem到数据去重

列表的数据源,我建议在进入FlatList之前先做一次预处理。最典型的问题是数据去重。分页接口在网络抖动时容易返回重复数据,比如用户滑到底部触发加载更多,超时后重试,服务端可能把同一批数据又返回一次。如果直接塞进data数组,轻则重复渲染,重则因为重复 key 触发 React 的渲染警告,导致整个列表更新异常。

去重的核心依据是列表项的唯一标识。我的做法是维护一个Set来记录已渲染数据的 ID,新接口返回的数据先过滤一遍,只把不在Set里的数据追加进去。必须要注意的是,keyExtractor返回的 key 要稳定,不能使用数组索引。数组索引做 key 的坏处是:当你删除中间一条数据后,后面的数据 index 会乱掉,React 无法准确匹配新旧节点,可能引发整个列表的非预期重渲染。

渲染函数本身也有讲究。renderItem不能写成内联函数,否则父组件每次刷新都会创建新的函数引用,导致所有列表项都重新渲染。我习惯把renderItem抽到组件外部,或者用useCallback包裹起来,让它在依赖不变的情况下保持引用稳定。列表项内部的子组件不要过度嵌套,尽量拍平结构,减少组件层级可以降低渲染成本。

还有个容易被忽略的优化:图片加载。列表里的封面图如果每个 item 都独立请求资源,在快速滚动时会大量触发网络请求。合理的做法是复用已有的图片缓存,或者在列表滚动停止之后再进行图片的预加载。RNOH 环境下图片缓存的行为跟原生组件强相关,我测试下来,至少在首次加载后,第二次滚动到同一张图时明显变快了,说明缓存是生效的。如果你发现图片加载不稳定,可以先检查图片组件是否设置了合适的缓存策略,再去看网络层。

4. 实战复现:模拟项目X的联调链路与踩坑记录

4.1 复现环境搭建与测试方法

前面写的是理论知识,这一节来说说我当天实际复现的过程。为了让问题更容易暴露,我没有直接连公司现有的业务接口,而是基于模拟项目X搭建了一个本地测试服务。模拟项目X是一个很像内容社区的应用,首页是一个信息流列表,列表里的每条数据包含文字、封面图和发布时间,后端接口支持分页、排序以及可选的延迟参数。我把延迟参数设置成 3 秒,用来模拟弱网环境。

环境搭建分三层:第一层是本地 HTTP 服务,提供/list接口和一个静态图片资源目录;第二层是 RNOH 应用工程,网络封装和列表组件都写在这里;第三层是用抓包代理工具和日志工具做观测。抓包工具我建议在联调前就配置好,不然出错时只能靠猜。日志方面,我打开了 hilog 的控制台输出,把 JS 侧的异常和 ArkTS 侧的网络日志分开看。两种日志混在一起的时候问题难定位,分开过滤会清晰很多。

4.2 三个典型问题及其排查过程

第一个问题是列表整页白屏。我带着 3 秒延迟启动应用,首页先进入 skeleton 加载状态,等接口返回后开始渲染列表。结果某次测试时,接口返回的数据里有一条记录的title字段是空的,但后端文档里写的最多是“可能为空”,我前端代码直接用了item.title.toUpperCase()之类的操作,导致 JS 侧抛异常,整个列表渲染失败,界面变成白屏。定位过程其实很直接:打开日志,看到 JS 侧红色报错堆栈,指向 Title 组件里的一行方法调用。这个 bug 的教训是:接口数据的结构永远不要假设 100% 符合预期,进入渲染之前做一个字段规整,给缺失字段提供默认值,或者直接在渲染层做条件判断。

第二个问题是分页竞态导致的旧数据覆盖。我故意把请求延迟设置成不等长,让第一页请求比第二页慢 1 秒。操作方式是进入列表后立刻上拉触发加载更多,然后马上从底部下拉触发刷新。结果页面出现了“刷新已经显示成功,但列表内容却是第二页数据”的错乱现象。定位时我先看日志,发现刷新请求的响应晚于加载更多请求返回,而我们代码里没有做竞态保护,旧的刷新数据直接 setState 覆盖了列表。修复方式就是前文提到的那套状态机互斥逻辑,改成之后问题消失。

第三个问题是滚动掉帧和内存增长。在模拟器上,我把列表数据加到 500 条,快速滑动,明显感觉帧率不稳,切换多任务再回来,内存数字肉眼可见地涨。这个问题的根因并不复杂:列表项内部有一张 100KB 左右的封面图,滚动过程中图片不断被加载和释放,加上removeClippedSubviews未开启,滚出去的 item 没有被及时清理。我的处理是开启裁剪、给图片加缓存策略、降低单次渲染批次,最终在测试设备上重新跑了 10 分钟,帧率稳定,内存曲线也平缓了。

4.3 调参后的性能对比与结论

我在复现过程中保留了调整前后的数据,做成了一张简单的对比表,方便后续直接参考。

指标优化前优化后
首屏渲染耗时1.8s(含请求)1.6s(含请求)
快速滑动帧率约 20fps,偶发卡顿稳定在 50fps 以上
500 条数据内存增量约 120MB约 60MB
分页竞态问题必现未复现
空字段异常白屏必现未复现

数据说明不了全部,但至少能证明一个方向:RNOH 的列表性能不是不能优化,而是需要精确地找到底层对应机制。很多人在默认参数下跑一遍,觉得卡,就认为是 RNOH 不行,这个判断多少有点草率。我在实际调参过程中,大部分收益来自windowSize、initialNumToRender和removeClippedSubviews这三个参数的组合,而且它们互相影响,不能单独只调一个。

性能对比只能作为一个参考,因为不同设备、不同数据结构的表现在数值上会差异很大。关键是把这个排查链路记住:先看是不是请求层的问题,再看是不是渲染层的问题,最后才考虑调底层参数。顺序反过来的话,你会陷入不断试参数的泥潭里。

5. 训练营之外:给后续学员的几点补充建议

5.1 调试工具与日志过滤的快速上手

训练营结束之后,我又花了两天时间把当天踩过的坑重新整理了一遍,发现如果一开始就掌握正确的调试方法,会省下不少时间。首先,日志过滤一定要会用。RNOH 场景下,JS 侧日志和 ArkTS 侧日志的格式不一样,我习惯用关键字去过滤。比如要看网络请求,可以通过http关键字过滤;要看 JS 异常,直接搜JS或Exception。把两条通道分开看,比在一个大日志里捞针高效得多。

抓包工具我建议在联调阶段全程开着。它不仅能看请求是否发出,还能看到响应体的实际内容。很多时候后端接口返回的数据格式和前端预期并不一致,比如字段名大小写不同,类型从 string 变成了 number,这些错误通过日志很难发现,但抓包工具里一眼就能看出来。我当天遇到的一个“列表莫名白屏”问题,最终就是通过抓包发现接口返回的字段名和后端文档不一致导致的。

5.2 建议的验收清单(弱网、断网、快速切换等领域)

训练营的实战环节,讲师最后给了一张验收清单,我照着跑了一遍,觉得非常适合日常开发时用。这里分享给你,作为你自己的列表和网络模块的测试兜底:

  • 在 3G/4G 限速环境下启动应用,确认列表能进入正确状态;
  • 页面加载过程中断开网络,确认出现错误提示而不是无限加载;
  • 断网后恢复网络,点击错误提示上的重试按钮,确认列表能恢复;
  • 快速下拉刷新并同时上拉加载更多,确认最终列表数据一致且不重复;
  • 连续快速点击到底部加载更多,确认不会出现重复请求;
  • 切换应用到后台再回来,确认列表滚动位置和加载状态保持正常;
  • 列表项内部包含异常数据(空字符串、null、类型不符),确认页面不白屏。

这些场景不一定全都能在开发环境复现,但至少要在心里有数。网络层的健壮性和列表层的健壮性一样,靠的不是一次写对,而是靠反复在边界条件下测试补漏。我自己的习惯是每次改动完网络封装或者列表组件,都把这张清单跑一遍,跑完再提交代码,能省下不少线上事故。

如果你正准备做 RNOH 方向的跨平台开发,Day 3 的课程内容算是一块很扎实的敲门砖。把网络请求这层封装好,再把列表的状态机理顺,绝大部分内容型页面的体验基本就能稳住。要是你在实操中也遇到了列表白屏、请求竞态或者掉帧问题,不妨按着这篇文章里的思路先排查一轮,大概率会有收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询