☰
鸿蒙上React Native网络请求实战:从fetch到axios封装
2026/10/23 5:17:59 网站建设 项目流程

1. 项目拆解:先搞清楚跨端网络请求的完整链路

把 React Native 跑在鸿蒙设备上,听着像两个世界强行糅合,但实际动手做完网络请求实战后,我发现这套跨平台方案比想象中顺。它的核心价值不是让你少写几行代码,而是让团队里已经存在的 React Native 业务代码,能以尽可能低的成本延伸到鸿蒙生态。这个项目的最初目标,就是把一个普通的 React Native 应用在鸿蒙环境下跑通,并实现一条带权限、带超时、带错误处理的网络请求链路。

适合谁看?第一类是 React Native 工程师,想知道鸿蒙平台和 Android/iOS 到底差在哪;第二类是鸿蒙原生应用开发者,想评估跨端方案到底靠不靠谱;第三类是刚入门,连 fetch 和 axios 都还没用明白的人。下面给的代码不是“运行一下就好”的 demo,而是可以直接拿进项目里改改就能用的封装。

先把结论放前面:鸿蒙上的 React Native 网络请求,JS 层代码和 Android/iOS 没有本质区别,真正花精力的是工程配置、权限声明、明文流量策略和原生网络栈差异。用一句话概括就是:难点不在“怎么写请求”,而在“为什么我的请求还没发出去就被系统拦了”。

1.1 React Native 如何在鸿蒙上工作

React Native 本身不直接操作系统 API,它靠宿主容器提供 JavaScript 运行环境和原生能力桥。鸿蒙版 React Native 的本质,就是把 iOS/Android 那套原生 UI 桥和网络模块,换成鸿蒙的 UI 组件和网络客户端。JS 业务层仍然只认识 fetch、XMLHttpRequest、WebSocket,底层真正干活的已经变成了鸿蒙系统自己的网络框架。

理解这一点非常重要。你在 JS 里写fetch('https://xxx'),不代表一定走的是浏览器那套网络栈,它是被桥接到宿主的网络 API。所以在 Android 上能通过的请求,在鸿蒙上可能因为权限、证书信任策略、HTTP 明文限制等原因被拦。这看起来像 React Native 的 bug,其实更像是一套新的“系统级规则”。

1.2 网络请求在跨端链路中的位置

一次完整的请求要经过这么几层:界面组件发起调用,JS 层通过桥接模块把请求参数传给原生,原生网络客户端真正发出请求,拿到响应后再回传 JS。任何一层出问题,表现都是“请求失败”或者“网络异常”,但排查路径完全不同。

我这次做项目时,习惯性把问题定位到“接口写错了”或者“后端没启动”,结果查到最后是权限没加。所以建议你也把排查思路切成两段:先确认 JS 层能正常拿到 bundle、能正常弹 UI,把宿主层和业务层分清;再排查请求本身。这样定位问题会快很多。

2. 环境搭建:鸿蒙跑 React Native 之前要准备什么

2.1 鸿蒙工程与 React Native 工程怎么对接

别以为在原有 React Native 项目里直接 npm run 就能跑出鸿蒙包。我拿到的方案,是让脚手架先生成一个鸿蒙原生工程目录,再和 React Native 的 JS bundle 绑定。从目录结构看,鸿蒙工程是一个独立的原生宿主,里面放的是编译后的 bundle 资源和 React Native 运行时。

所以环境准备分成两步:第一步把 Node 环境和 React Native 依赖装好,保证 Metro 能正常启动;第二步用鸿蒙开发工具打开鸿蒙工程目录,编译到真机或模拟器。两者之间通过一个调试端口连接,手机加载的 JS bundle 来自 Metro,而不是打包进 app 的那份。

npm install npm start

Metro 启动后不要关,另开一个终端执行鸿蒙侧的编译命令。别问我为什么要两个终端一起开,因为我第一次只开了 Metro,然后一直等 app 里的内容刷新,等了一个小时才意识到编译根本没跑。

这里容易踩的第一个坑是版本匹配。React Native 的版本、鸿蒙 SDK 版本、脚手架版本三者不匹配时,报错信息不会直接告诉你不兼容,而是编到一半莫名其妙失败,甚至编译成功但运行起来白屏。所以稳妥做法是先照着脚手架的文档锁版本,而不是用“最新版”。

2.2 网络调试前的三个基础检查

环境能跑起来只代表 UI 能显示,不代表网络通了。我列了一个固定检查清单,每次换设备、换电脑、换项目都会过一遍。

检查项判断标准说明
INTERNET 权限module.json5 中已声明没有权限时 fetch 会直接报 Network request failed
设备网络真机与电脑在同一局域网加载 bundle 和请求接口都需要链路连通
后端地址不能用 localhost真机上 localhost 指向手机自己,要改成局域网 IP 或线上域名

这套检查看起来基础,但我在团队里见过太多人漏掉第二条。电脑连着公司无线,手机连着同一个无线,结果公司无线开了 AP 隔离,设备之间互相不可见,Metro 那边报各种连接超时。后来把手机热点打开、电脑连手机热点才解决。

3. 网络请求核心实现:从 fetch 到 axios 的完整封装

3.1 权限声明:没有 INTERNET 权限一切白搭

鸿蒙应用默认没有网络权限,第一步是在entry/src/main/module.json5里补上ohos.permission.INTERNET。直接看代码:

{ "module": { "name": "entry", "type": "entry", "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }

注意这个文件的位置,权限是加在 entry 模块的requestPermissions数组里,不是全局配置里。我做过一个蠢事,把权限加在了项目根目录的某个配置文件里,IDE 不报任何错,但运行起来请求照样失败。

这段配置如果是手动创建的项目,很容易被漏掉;如果用的是脚手架初始化,通常会带好,但保险起见还是要检查。判断方法很简单:用鸿蒙开发工具打开 app,执行一个任意网络请求,如果控制台抛出Network request failed或者net::ERR_NAME_NOT_RESOLVED,多半就是这里的问题。

3.2 实际请求代码:fetch 版本

权限和网络环境就绪后,JS 层写法和普通 React Native 一模一样。下面这个例子请求一个用户列表,同时处理 loading 和 error 状态:

import { useState, useEffect } from 'react'; import { View, Text, FlatList, ActivityIndicator } from 'react-native'; type User = { id: number; name: string; email: string; }; export default function UserList() { const [data, setData] = useState<User[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { loadUsers(); }, []); const loadUsers = async () => { try { const response = await fetch('https://api.example.com/users', { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const json = (await response.json()) as User[]; setData(json); } catch (e) { setError(e instanceof Error ? e.message : '请求失败'); } finally { setLoading(false); } }; if (loading) { return <ActivityIndicator size="large" />; } if (error) { return <Text>{error}</Text>; } return ( <FlatList data={data} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => <Text>{item.name}</Text>} /> ); }

这段代码里最容易忽略的是response.ok判断。很多人直接用await fetch(url)就算请求成功,结果后端返回 500 时,前端拿到的还是一坨错误数据。网络请求不是“能收到响应”就代表成功,还要看 HTTP 状态码。

3.3 axios 封装与拦截器

项目里需求多起来后,用原生 fetch 会写很多重复代码,token 携带、统一超时、错误处理每处都要复制一遍。所以我把请求层切到了 axios,倒不是说 fetch 不能做,只是 axios 的拦截器机制更适合统一管理。这里先把依赖装上:

npm install axios

装完依赖后需要创建一个基础实例。第一次做的时候我直接复制了网上的一份配置,baseURL 写死、timeout 也只给了 10 秒,结果弱网下页面频繁报错。所以这里我把 baseURL、timeout 和默认 headers 都写在实例里,尤其注意 timeout 别拍脑袋,先给 15 秒跑几轮再往下压。代码如下:

import axios from 'axios'; export const request = axios.create({ baseURL: 'https://api.example.com', timeout: 15000, headers: { 'Content-Type': 'application/json', }, });

请求拦截器是统一处理 token 的地方。代码写起来很直接,但有一个细节:token 过期时不要在这里弹错误页,因为很多接口会同时报 401,弹窗会满天飞。下面是常用的写法:

request.interceptors.request.use( (config) => { const token = getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) );

响应拦截器是收敛错误处理的核心。我第一次做的时候,在每个页面 catch 里重复判断 error.response.status,后来全部搬到拦截器,维护成本低很多,404、500 这类错误再也不会漏处理。下面是 401、403、500 场景的常见写法:

request.interceptors.response.use( (response) => response.data, (error) => { if (error.response) { switch (error.response.status) { case 401: // 清除登录态,跳转登录页 break; case 403: // 提示权限不足 break; case 500: // 提示服务器异常 break; } } else if (error.code === 'ECONNABORTED') { // 超时 } return Promise.reject(error); } );

拦截器里最值得注意的就是 401 处理。我之前在几个页面分别写“如果 401 就弹提示”,结果一个接口失败时连弹三个 toast。现在统一在拦截器里做,UI 层只关心业务数据。

3.4 统一请求层

封装到这一步,业务组件里就不该直接操作 axios 了。我习惯再包一层函数,把 get/post 变成带类型提示的方法:

export const http = { get: <T>(url: string, params?: Record<string, unknown>) => request.get<T>(url, { params }).then((res) => res.data), post: <T>(url: string, body?: unknown) => request.post<T>(url, body).then((res) => res.data), put: <T>(url: string, body?: unknown) => request.put<T>(url, body).then((res) => res.data), delete: <T>(url: string, params?: Record<string, unknown>) => request.delete<T>(url, { params }).then((res) => res.data), };

这样业务层写起来就是const list = await http.get<User[]>('/users'),清爽很多。如果后端不是标准 JSON 结构,统一在.then前面加一层 adapter,把{ code, data, message }拆开,避免每个页面都做一次 data 解包。

4. 鸿蒙适配里最容易被坑的四个点

4.1 明文 HTTP:不是加了权限就万事大吉

如果接口走的是http://而不是https://,就算INTERNET权限加好了,请求也可能在原生层被拦下来。这和 Android 9 开始默认禁止明文流量是同一个思路,鸿蒙这边也有类似的安全策略,只是配置入口和 Android 不一样。

我在本地调试时用的接口是http://192.168.1.100:8080,在 iOS 模拟器里一切正常,到鸿蒙真机上所有请求全部静默失败。折腾到最后,发现是明文流量没有放行。处理方式是去鸿蒙工程里找网络安全配置,把当前调试用的域名或 IP 加进白名单。

不同 IDE 版本或者 SDK 版本生成的配置文件名可能有差异,如果你不确定,直接在工程目录里搜cleartext、networkSecurityConfig、明文三个关键词,比翻文档快。有一个提醒必须写出来:能用 HTTPS 就不要依赖明文放行,上线包更不要全局放开,一旦被安全扫描发现,整个上架流程都会很被动。

4.2 真机 localhost 永远连不上

在真机上调试最经典的错误,是把接口地址写成http://localhost:8080。这个地址在手机里指向手机自己,不指向你的电脑。正确做法是先用ipconfig或ifconfig查到电脑在局域网里的 IP,再把它填进请求 baseURL。要注意 Wi-Fi 网段变了,IP 也要跟着换。

还有另一种情况,Metro 能连上,但业务接口连不上。这是因为 Metro 的端口和业务接口端口不一样,防火墙可能只放行了其中一个。检查时别只看一个端口,要把 Metro 端口和 API 端口都测一遍,最简单的办法是在电脑上用浏览器访问那个接口地址,如果电脑都打不开,问题就不在 app 侧。

4.3 证书校验与自签名证书

鸿蒙对 HTTPS 证书的信任策略和 Android/iOS 不完全一样。如果后端用的是自签名证书,或者内网签发的私有 CA 证书,在 React Native 的 JS 层没有特别简洁的办法直接绕过,因为请求底层走的是鸿蒙原生网络客户端,不是浏览器内核。

我建议只把“临时跳过证书校验”放在 debug 包或者内部测试包里,正式包绝对不要这么干。最稳妥的做法还是让后端换一张受信任的正式证书,省去后面所有平台的适配问题。真要处理自签名证书,优先在鸿蒙原生侧配置信任证书,而不是在网上找一段“关闭证书验证”的代码硬贴。

4.4 某些 npm 库在鸿蒙上不兼容

网络请求本身用 axios 是没问题的,它是纯 JS 实现。但很多增强类库,比如文件下载、WebSocket 封装、图片缓存加速,底层都依赖 Android/iOS 的原生模块,鸿蒙版本不一定实现。如果某个库在 iOS/Android 上正常,鸿蒙上报NativeModule: xxx is null,问题往往不是代码写错,而是那个原生模块根本不存在。

遇到这种情况,先看库的作者是否提供了鸿蒙适配版本,没有的话就退回到 React Native 自带的能力。还是那句话:跨端方案的价值是让 JS 业务层复用,原生能力层始终要跟着平台走,不能指望所有 npm 包都能一键跑通。

5. 网络请求的性能与体验优化

5.1 超时与重试

网络请求最怕的不是慢,而是“一直转菊花”。axios 实例里的timeout只负责终止请求,不负责让用户体验更好。在慢网络场景下,合理的做法是给超时时间一个分级:登录接口 10 秒,列表接口 8 秒,上传接口 30 秒。全项目用一个超时时间的后果是,轻量接口迟迟不失败,用户以为卡死了。

重试要克制。只对幂等 GET 请求做自动重试,POST 请求重复提交会造成脏数据,这个坑我踩过一次,下单接口在弱网下自动重试了三次,结果生成了两笔重复订单。下面是一个简单的重试封装:

const retryRequest = async <T>( fn: () => Promise<T>, retries = 3 ): Promise<T> => { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { if (i === retries - 1) { throw error; } await new Promise((resolve) => setTimeout(resolve, 500 * (i + 1))); } } throw new Error('unreachable'); };

重试间隔用线性递增就好,1 次失败等 500 毫秒,2 次失败等 1000 毫秒。如果服务端已经过载,立刻重试只会加重压力,线性递增比固定间隔更容易避开瞬时抖动,也比指数退避容易实现。

5.2 请求取消与页面竞态

列表页经常有个问题:组件已经卸载或者筛选条件变了,旧的请求才返回,导致界面渲染一份过期的数据。React Native 里可以用 AbortController 配合 fetch 做取消,axios 对应的是 signal 参数。写法不算复杂,下面这个例子同时处理了组件卸载时取消请求:

const controller = new AbortController(); useEffect(() => { fetch('/users', { signal: controller.signal }) .then((res) => res.json()) .catch((error) => { if (error.name !== 'AbortError') { // 真正处理错误 } }); return () => controller.abort(); }, []);

设计上要养成一个习惯:请求发起时记录一个递增的标识,响应返回时先判断标识是不是最新,不是就直接丢弃。这在搜索框场景特别有用,用户打了三个字,触发了三次请求,只有最后一次的结果应该被展示,但网络返回顺序不一定是按发起顺序来的。

5.3 缓存与弱网优化

网络请求不只是“发出去、收回来”,缓存策略会直接影响体验。服务端返回响应头里的ETag或Last-Modified时,客户端可以配合发条件请求,只要数据没变,服务端返回 304,响应体很小,速度自然快。

对于列表这类实时性要求不高的数据,还可以在本地做一层内存缓存,下次进入页面先用旧数据渲染,后台再拉新数据,这就是常说的“先展示后刷新”。这个策略在弱网环境下体验提升非常明显,代价是代码里要处理缓存失效,不要把用户上次的数据一直留在那里。

6. 常见问题速查表与实操心得

6.1 常见问题速查表

把我在这个项目里遇到过的典型问题整理成一张速查表。我通常把它放在团队文档开头,谁遇到“怪问题”先查表,比重新从零排查快得多。表格如下:

现象可能原因解决建议
fetch 报 Network request failed权限缺失、域名不通、明文流量被拦检查 module.json5 权限、网络配置、域名可访问性
真机上 localhost 连不上请求地址指向手机自身改为电脑局域网 IP 或线上域名
接口偶尔超时后端慢、单次超时时间太短分级设置 timeout,幂等请求加一次重试
HTTPS 报证书错误自签名证书或私有 CA 不被信任原生侧配置信任证书,debug 包再临时处理
iOS/Android 正常,鸿蒙失败某个原生模块未适配看日志找 NativeModule,替换为纯 JS 实现
界面出现但图片加载不出来图片域名不在白名单把图片域名加入安全域名或改成 HTTPS
401 弹窗重复出现每个页面都做了错误提示收敛到 axios 响应拦截器统一处理

这张表不是标准答案,但覆盖了鸿蒙跨端网络请求里 80% 的“莫名其妙”问题。这些问题的共同点值得再说一次:别急着怀疑 JS 代码,先检查宿主环境放没放行。

6.2 我在实操中积累的几个小习惯

第一个习惯是所有请求地址单独放一个配置文件里,按环境区分 dev、test、prod,不要直接在组件里写 URL。鸿蒙真机调试时换 IP 实在太频繁了,每次全身搜索替换太容易漏。

第二个习惯是在切环境时顺手看一眼请求日志。如果你的抓包工具只能看到 HTTPS 但看不到明文,说明不是抓包功能问题,而是请求根本没发出,往权限和安全配置方向查。

第三个习惯是给每个接口都设计一个最小可用 mock 数据。鸿蒙环境搭建本身就有门槛,如果后端接口还不稳定,前端调试会卡在两层问题上,有了 mock 至少能分清是平台问题还是接口问题。我后来甚至把 mock 开关做到了请求层里面,切环境时只需要改一个布尔值。

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

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

立即咨询