资讯动态

React Native for OpenHarmony实战:网络请求与高健壮性列表构建

发布时间:2026/10/16 1:46:18 来源:尧图企业网站定制
1. 写在前面三天的名单值得复盘的内容反而在最后训练营进行到第三天大家的状态明显不太一样了。前两天还在为环境搭建、依赖安装、Hello World跑通而兴奋到了Day 3讲师直接甩过来一个任务做一个能联网、能下拉刷新、能上拉加载、还要能扛住弱网和接口故障的真实业务列表页。也就是说今天的主线就是React Native for OpenHarmony实战里的两个硬骨头网络请求集成和高健壮性列表构建。我一开始觉得这有什么难的发个请求、渲染个列表在 Web 上写过几百遍了。但真正在鸿蒙生态设备上跑起来才发现网络层和列表层才是最能拉开工程水平差距的地方。真机上要处理的不只是“请求通了没”还有超时、断网、证书、分页并发、滚动性能、状态恢复、空态和错误态的组合这些细节任何一个没处理到位验收时就会被问住。这篇文章把我在训练营第三天的实操过程完整记录下来包括请求封装思路、错误分类模型、列表五态管理、分页边界保护以及我后来在真机上连续排查的几个典型问题。如果你正准备在鸿蒙设备上用 RN 做跨端页面这份记录可以直接当一份避坑清单来用。2. 页面拆解先想清楚“高健壮”到底指什么2.1 一个看似简单的列表藏着哪五层状态做任务之前讲师让我们先别急着写代码而是把“高健壮性”这个模糊的要求拆成可验证的条目。拆完之后我意识到我们日常说的“做一个列表页”其实至少包含五个状态首次加载中、下拉刷新中、上拉加载更多、空数据、错误状态。如果接口返回的数据本身有问题还要再加一个“数据异常”的判断。这五个状态不是互相独立的。最麻烦的是“错误状态”和“已有数据”同时存在的情况用户已经刷出了第一页下拉刷新时接口挂了这时候如果把列表清空再展示整页错误体验非常糟糕正确做法是保留原有数据仅给一个轻量提示。同理上拉加载更多失败时也要保留已加载的内容给用户一个“点击重试”或“继续滑动重试”的兜底入口。所以在真正动手之前我先把状态机画在了纸上什么操作触发什么状态、什么状态能转换到什么状态、哪些字段需要独立记录。这一步省掉了后来大量的返工。2.2 为什么网络层和列表层必须先于UI设计训练营Day 3这个任务特别强调“先封装、后页面”。原因是如果直接在每个页面里用fetch或axios发请求代码写起来是快但一旦后端调整了错误码规范、或者需要统一加鉴权头就得满项目找散落的请求代码改一处漏一处。更重要的是鸿蒙生态设备和普通手机相比网络环境更加多样可能走 Wi-Fi、可能走蜂窝也有可能临时断网、DNS 解析失败、TLS 握手被中断。这些错误在 Web 端可能不常见但在设备端非常普遍。统一封装网络层把错误进一步归类成“网络不可用、超时、HTTP错误、业务错误”四种页面只需要关心其中一种逻辑会清晰非常多。列表层同理。列表的加载、刷新、分页、空态、错误本质上是一个有限状态机应该集中管理而不是散落在组件的各个useState里。用多个useState也能实现但状态之间的组合关系会越来越难推理比如“refreshing 时能不能同时 loadingMore”“错误态能否保留旧数据”这类问题。后来我把状态的流转统一收敛到useReducer里一次只处理一个动作心智负担小了很多。2.3 任务定义某资讯信息流Demo的范围边界为了后面便于叙述我以训练营里我们小组做的“某资讯信息流Demo”为例。这个页面的逻辑很典型进入页面后请求第一页新闻列表支持下拉刷新、上拉分页每一页返回固定条数接口在部分场景下会返回错误码也会偶发超时。最终验收标准里明确写了三条弱网环境下不白屏、接口异常时用户知道该怎么办、列表滚动时内存稳定不暴涨。这个范围边界很重要。它决定了我们不需要为Demo实现搜索、缓存、离线包等更重的功能但需要把网络层、分页状态、异常恢复、列表性能这几块打磨好。明确边界之后每一阶段该写什么代码、该测试什么场景就非常清楚了。3. 网络请求集成的正确姿势封装与异常收敛3.1 axios实例配置与拦截器设计在 React Native for OpenHarmony 里网络请求可以基于fetch或axios来做。fetch是内置API依赖少但拦截器、统一错误处理、取消请求这些能力都需要自己实现。axios在 RN 生态里经过大量项目验证拦截器机制现成所以我选了axios做二次封装这样团队其他人接手时也更熟悉。基础封装我放在services/http.ts里核心是一个实例、两个拦截器、四个错误分类。实例负责统一baseURL、超时时间和默认请求头请求拦截器负责在每次请求发出前注入鉴权信息响应拦截器负责把后端不同返回结构统一成页面友好的 Promise。// services/http.ts import axios, { AxiosRequestConfig } from axios; interface ApiResultT { code: number; data: T; message?: string; } type AppErrorCode NETWORK_ERROR | TIMEOUT | HTTP_ERROR | BIZ_ERROR; class AppError extends Error { code: AppErrorCode; status?: number; data?: unknown; constructor(code: AppErrorCode, message: string, status?: number) { super(message); this.code code; this.status status; } } const http axios.create({ baseURL: https://api.example-service.com, timeout: 15000, headers: { Content-Type: application/json, }, }); http.interceptors.request.use((config) { const token getTokenFromStorage(); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); http.interceptors.response.use( (response) { const result response.data as ApiResultunknown; if (result typeof result.code number result.code ! 0) { throw new AppError(BIZ_ERROR, result.message || 业务处理失败, response.status); } return result typeof result.data ! undefined ? result.data : response.data; }, (error) { if (axios.isCancel(error)) { return Promise.reject(error); } if (error.code ECONNABORTED) { return Promise.reject(new AppError(TIMEOUT, 请求超时请稍后重试)); } if (!error.response) { return Promise.reject(new AppError(NETWORK_ERROR, 网络连接不可用)); } const status error.response.status; if (status 400 status 600) { return Promise.reject(new AppError(HTTP_ERROR, 服务暂时不可用${status}, status)); } return Promise.reject(error); } ); export function getT(url: string, config?: AxiosRequestConfig): PromiseT { return http.get(url, config) as PromiseT; } export function postT, B unknown(url: string, body: B, config?: AxiosRequestConfig): PromiseT { return http.post(url, body, config) as PromiseT; }这样封装之后页面里不会再出现response.data.data.list这种链路也不会出现if (error.response.status 500)的散装判断所有错误都通过AppError.code来识别职责非常清晰。3.2 错误分类模型网络错误、超时、HTTP错误、业务错误把错误收敛成四类不是为了好看而是为了对应不同的用户提示和重试策略。下面是我在训练营里整理过的一张对照表错误类型识别方式页面提示重试策略网络不可用无error.response且非取消、非超时“网络连接不可用请检查网络”手动重试不自动轮询请求超时error.code ECONNABORTED“请求超时请稍后重试”可自动重试一次再失败则手动HTTP错误状态码在 400 到 599 之间“服务暂时不可用xx”手动重试业务错误后端code ! 0使用后端message字段视业务规则决定这四类错误不是平均分配的。从真机测试的情况看设备端最常遇到的是网络不可用和超时后端换版本或网关抖动时会出现较多HTTP错误业务错误在参数不合法或登录态过期时出现。前两类建议配合“是否有旧数据”来决定UI表现后两类则需要更多的上下文信息。有一个细节值得强调业务错误不能简单用 HTTP 状态码判断。很多后端接口即使业务逻辑失败HTTP 状态仍然返回 200只是body里的code非零。如果不做这层归一化页面会把业务失败误判成成功列表就出现了一条“看似成功但实际没有数据”的空白状态。3.3 超时与取消请求的坑超时这个参数我在Web开发时经常直接写10秒但到了设备场景里需要重新考虑。鸿蒙真机首次建立TLS连接、DNS解析、网络切换后的重连耗时都可能明显高于模拟器。训练营里我们做了几次弱网模拟结论是业务接口的默认超时设置在10到20秒之间比较合理首屏可以短一点上传或导出类接口则要更长。这个值不要写死在组件里而是放在请求封装的配置里统一管理后续调优只改一处。真正让我踩坑的是“取消请求”。用户在下拉刷新之后立刻上拉加载更多或者快速切换页面返回上一个请求还在飞。如果不取消响应回来之后执行setState轻则产生无效渲染重则报“在已卸载组件上调用状态更新”的警告。使用AbortController或axios的CancelToken都可以但要注意取消的请求会走进响应拦截器的错误分支所以拦截器里必须加上axios.isCancel(error)的判断并且直接 reject不归入任何错误分类。const controller new AbortController(); const promise get(/v1/news/list, { params: { page: 1, pageSize: 20 }, signal: controller.signal, }); // 组件卸载或重复请求时 controller.abort();另一个容易被忽略的点是取消了请求不等于清空状态。列表已经展示的数据要保留只是不再等待这次请求的结果。我在实际操作中习惯把“取消”看成一种静默操作页面的 loading 状态和错误提示都不要因为取消而变化。4. 高健壮性列表从渲染到分页的完整状态方案4.1 FlatList在OpenHarmony上的适配情况网络层封装好之后下一步是列表页本身。训练营之前我一度担心 FlatList 在 React Native for OpenHarmony 上会不会不兼容实测下来核心API基本可用data、renderItem、keyExtractor、onEndReached、RefreshControl这些都没有问题。个别底层属性比如removeClippedSubviews在不同设备上的表现有差异这个放到后面的常见问题里单独讲。因此页面主结构选用FlatList是合理的。相比ScrollView一次性渲染所有子项FlatList是虚拟列表只会渲染可视区域附近的节点这对动辄几十上百条的资讯流非常关键。另一个好处是它原生支持下拉刷新和上拉加载的事件接入点状态管理可以很自然地挂在组件上。使用FlatList时有几个基本要求需要守住keyExtractor必须返回稳定且唯一的字符串直接使用接口返回的 id 并转成字符串是最稳妥的不要在renderItem内部定义内联函数或内联组件否则每次渲染都会重新创建组件实例造成不必要的重渲染。4.2 用useReducer管理列表五态列表状态我用useReducer集中管理。一个 state 对象里包含data、page、hasMore、initialLoading、refreshing、loadingMore、loadMoreError、error这些字段。每次请求开始、成功、失败都发送一个明确的 action由 reducer 集中决定哪些字段要变。type ListState { data: NewsItem[]; page: number; hasMore: boolean; initialLoading: boolean; refreshing: boolean; loadingMore: boolean; loadMoreError: boolean; error: Error | null; }; type ListAction | { type: FETCH_START; payload: { mode: refresh | loadmore } } | { type: FETCH_SUCCESS; payload: { list: NewsItem[]; page: number; hasMore: boolean } } | { type: FETCH_FAILURE; payload: { error: Error; mode: refresh | loadmore } };关于状态流转我定的规则是首屏加载时只转圈不显示任何列表内容。下拉刷新时保留旧列表顶部出现刷新指示器。上拉加载更多时保留旧列表底部出现加载指示器。首屏加载失败且没有旧数据时展示全屏错误提供“重新加载”按钮。下拉刷新失败但已有旧数据时不清空列表只做轻提示。上拉加载更多失败且已有旧数据时底部出现“点击重试”区域不清空列表。这几条规则看起来简单但直接决定了列表的体验下限。很多列表之所以“脆弱”就是因为刷新失败时把整屏数据清空了用户被迫再看一次loading和错误页。4.3 分页边界与并发保护分页是列表最容易出并发问题的地方。用户快速滑动时onEndReached在极短时间内可能被触发多次如果每次都发请求后端压力大页面也会出现重复数据。我的做法是给请求加一个requestingRef守卫请求进行中直接忽略后续触发同时用pageRef记录当前成功加载到的页码不依赖 state 里的 page 字段。export default function NewsListPage() { const [state, dispatch] useReducer(listReducer, initialState); const stateRef useRef(state); stateRef.current state; const pageRef useRef(0); const requestingRef useRef(false); const fetchList useCallback(async (mode: refresh | loadmore) { if (requestingRef.current) return; if (mode loadmore (!stateRef.current.hasMore || stateRef.current.data.length 0)) { return; } requestingRef.current true; const nextPage mode refresh ? 1 : pageRef.current 1; dispatch({ type: FETCH_START, payload: { mode } }); try { const result await get{ list: NewsItem[]; hasMore: boolean }(/v1/news/list, { params: { page: nextPage, pageSize: 20 }, }); pageRef.current nextPage; dispatch({ type: FETCH_SUCCESS, payload: { list: result.list, page: nextPage, hasMore: result.hasMore }, }); } catch (e) { dispatch({ type: FETCH_FAILURE, payload: { error: e as Error, mode } }); } finally { requestingRef.current false; } }, []); useEffect(() { fetchList(refresh); }, [fetchList]); const renderFooter () { if (state.loadingMore) { return ActivityIndicator style{styles.footer} sizesmall /; } if (state.loadMoreError) { return ( TouchableOpacity style{styles.footer} onPress{() fetchList(loadmore)} Text style{styles.footerText}加载失败点击重试/Text /TouchableOpacity ); } if (!state.hasMore state.data.length 0) { return Text style{styles.footerText}全部内容加载完成/Text; } return null; }; const renderEmpty () { if (state.initialLoading) { return ActivityIndicator style{styles.center} sizelarge /; } if (state.error) { return ( View style{styles.center} Text style{styles.errorText}{state.error.message}/Text TouchableOpacity style{styles.retryBtn} onPress{() fetchList(refresh)} Text style{styles.retryText}重新加载/Text /TouchableOpacity /View ); } return Text style{styles.center}暂无内容/Text; }; return ( FlatList style{styles.list} data{state.data} keyExtractor{(item) String(item.id)} renderItem{({ item }) NewsCard item{item} /} refreshControl{ RefreshControl refreshing{state.refreshing} onRefresh{() fetchList(refresh)} / } onEndReached{() fetchList(loadmore)} onEndReachedThreshold{0.35} ListEmptyComponent{renderEmpty()} ListFooterComponent{renderFooter()} initialNumToRender{10} maxToRenderPerBatch{10} windowSize{7} / ); }这里有几个细节值得展开。useCallback的依赖数组我留成了空数组这和很多教程里习惯写在依赖里的写法不太一样。原因是如果fetchList依赖了state.hasMore、state.data.length而useEffect(() { fetchList(refresh) }, [fetchList])又监听这个回调那么每次数据更新后回调引用都会变化首屏请求会被重复触发。用stateRef读取最新状态值既保证了请求逻辑能拿到最新判断又避免了重复请求。onEndReachedThreshold我设在0.35意思是可视区域底部进入内容高度最后35%时触发加载。阈值太小会导致用户滑到底还触发不了需要在屏幕底部反复上拉阈值太大会过早触发加载导致用户还没看完当前内容就开始请求下一页。0.3到0.5之间是一个比较稳的区间具体值还要根据屏幕尺寸和列表项高度微调。4.4 列表性能收尾分页和并发解决的是“请求正确性”性能则决定列表滑起来是否流畅。训练营验收时我们专门用“快速从第一屏滑到第30条”这个操作来压测看CPU和内存曲线。经验上有三个参数最值得调initialNumToRender首屏一次性渲染的条数。默认值偏保守建议根据屏幕高度和列表项高度估算能覆盖一屏多一点即可。maxToRenderPerBatch每次批量渲染的最大条数。设得太大首屏变慢设得太小快速滑动时会出现短暂空白。10到20比较常用。windowSize渲染窗口的高度倍数。数值越大视口外预渲染的节点越多滑动越顺但内存占用也越高。资讯流场景7左右通常能兼顾。图片处理是另一个大头。资讯流里如果每条都有一张封面图建议在接口层就返回不同尺寸的图片地址列表缩略图用低清版本点进详情再用高清图。如果后端暂时只给一张原图前端也要在图片组件上设置合适的resizeMode避免原图加载后无脑撑满列表项。5. 完整实操路径从接口联调到真机验证5.1 环境与项目结构本次实战基于 React Native for OpenHarmony 的现有开发链路项目初始化完成后页面代码放在src/pages/下网络封装在src/services/下公共组件在src/components/下。这样一个简单资讯流Demo的项目结构大致是src/ services/ http.ts types/ news.ts components/ NewsCard.tsx pages/ NewsListPage.tsx App.tsx在动手写页面之前先和后端约定好接口返回结构。我们使用的是{ code: number; data: { list: NewsItem[]; hasMore: boolean }; message: string }这套结构。hasMore字段非常关键它让客户端不用靠“返回条数是否等于 pageSize”来推断是否还有下一页能避免最后一页少两条数据时无法准确停止分页的问题。5.2 核心代码串联hearthttp.ts的代码在前面已经给出这里再补一段列表项的示意代码。列表项组件要尽量保持纯展示内部不要发请求、不要写复杂业务判断只负责把数据渲染成界面。// components/NewsCard.tsx import React, { memo } from react; import { Image, StyleSheet, Text, View } from react-native; import { NewsItem } from ../types/news; export const NewsCard memo(function NewsCard({ item }: { item: NewsItem }) { return ( View style{styles.card} Image source{{ uri: item.coverUrl }} style{styles.cover} resizeModecover / View style{styles.info} Text style{styles.title} numberOfLines{2}{item.title}/Text Text style{styles.summary} numberOfLines{2}{item.summary}/Text Text style{styles.time}{item.publishTime}/Text /View /View ); });给列表项包一层memo的作用是当列表因其他状态变化整体重渲染时props 未变化的列表项可以跳过 render。需要留意的是memo默认是浅比较如果item每次都是新对象比较依然不生效。所以列表项的 props 应该保持简单避免传整个store对象或每次执行都会生成新引用的内联函数。5.3 真机验证清单代码写完不等于任务完成。训练营最后一天留了半小时专门过验证清单我整理后大概是这样首屏进入显示loading接口返回后正确渲染第一页数据。下拉刷新顶部出现刷新指示器成功后列表更新。上拉加载更多滑到底部后自动请求下一页数据追加到列表末尾。空数据接口返回空数组时展示“暂无内容”空态。首屏失败进入页面时模拟断网不白屏展示错误页和重试按钮。刷新失败但已有数据下拉刷新断网列表数据不消失给出轻提示。加载更多失败翻到第二页断网底部出现点击重试。快速滑动从第一屏快速滑到第30条无明显卡顿、无死白屏。快速翻页连续触发上拉加载不出现重复请求、不出现重复数据。后台切回页面切到后台再返回列表数据不丢刷新状态合理。弱网通过调试工具模拟高延迟页面不无限loading超时后有明确提示。这份清单如果全部通过列表的“高健壮性”基本就立住了。很多团队只测“正常路径”忽略异常组合实际上验收时最容易挑出来的问题都集中在异常组合上。6. 常见问题与排查技巧实录6.1 模拟器或真机上的网络连接异常第一天联调时不少同学遇到同样的问题代码里请求地址写http://localhost:8080/api但真机上始终报网络不可用。原因是localhost指向的是设备自己而不是开发PC。在鸿蒙生态设备或模拟器上调试时需要把地址改成开发PC的局域网IP或者使用调试工具做端口转发。排查顺序建议是先确认设备与PC在同一局域网再确认PC防火墙放行了对应端口最后确认请求地址中没有写localhost。自己踩过这个坑之后我习惯把调试环境地址统一抽到环境配置文件里打包和联调分开避免在组件里手写IP。6.2 onEndReached疯狂触发的问题onEndReached被连续触发是非常经典的问题主要原因是首屏数据不满一屏列表高度不够FlatList会认为滚动区域一直在底部附近于是反复触发加载。另一个常见原因是page没有正确递增每次加载都请求第一页导致数据反复覆盖。我们的解决方案是双重保险第一在fetchList开头用requestingRef拦截并发请求第二把pageRef.current作为页码的唯一来源成功后才递增。这样即使onEndReached在短时间内触发多次也只有一个真实请求在跑。6.3 列表滚动卡顿与内存上涨核心表现是快速滑动时帧率下降长时间停留在列表页时内存持续上涨。第一个要检查的是图片加载尤其是高清原图直接进入列表项内存会瞬间暴涨。第二个检查点是渲染参数windowSize过大或maxToRenderPerBatch过大都会到造成不必要渲染。第三如果列表项高度固定可以设置getItemLayout让FlatList跳过动态测量滚动性能会提升非常明显。removeClippedSubviews这个属性在鸿蒙适配里要谨慎。它主要用于裁剪视口外组件以降低渲染负载苹果生态设备上收益明显但在一些鸿蒙真机上实测会出现列表项滚动时短暂空白或闪烁。我最终在资讯流Demo里选择不开启靠合理的数据分页和windowSize控制渲染节点数量。6.4 组件卸载后的异步回调页面快速返回时列表请求还没结束响应回来后执行dispatch或setState控制台会出现警告。这里的关键不是“可不可以调用状态更新”而是“应该不应该”。正确的做法是在请求层或组件里取消未完成的请求AbortController是比较标准的方案在useEffect的清理函数里调用controller.abort()即可。如果不方便给每个请求都挂 signal一个折中方案是在state更新前做一次“组件是否已卸载”的判断用一个 ref 标记卸载状态。但注意这只是兜底不是推荐做法最好的方式仍然是主动取消。7. 写在最后我把这次实战沉淀成了三个习惯训练营结束后我自己又花了两个晚上把资讯流Demo重构了一遍。三天的密集实战让我最受益的并不是某一段代码而是三个平时容易忽略的习惯。第一个习惯凡是列表先列状态机再写界面。把“什么操作产生什么状态、什么状态允许什么转换”写清楚后面所有 UI 和逻辑都会很顺。第二个习惯所有请求必须过统一封装错误必须归类。即使是一个三天小Demo也值得为网络层单独建一个文件。页面里永远不出现裸的fetch或散装的错误判断。第三个习惯真机验证清单要覆盖异常组合。只测正常路径远远不够弱网、断网、刷新失败、加载更多失败、快速滑动这些场景才是列表健壮性的试金石。每次写完列表页我会把上面的清单过一遍再宣称完成。React Native for OpenHarmony 这套链路还在快速演进组件和API的兼容情况会越来越好但网络层的鲁棒性和列表状态管理的扎实程度永远取决于写代码的人。希望这份记录能帮少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑