
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 startMetro 启动后不要关另开一个终端执行鸿蒙侧的编译命令。别问我为什么要两个终端一起开因为我第一次只开了 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] useStateUser[]([]); 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 sizelarge /; } 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?: Recordstring, unknown) request.getT(url, { params }).then((res) res.data), post: T(url: string, body?: unknown) request.postT(url, body).then((res) res.data), put: T(url: string, body?: unknown) request.putT(url, body).then((res) res.data), delete: T(url: string, params?: Recordstring, unknown) request.deleteT(url, { params }).then((res) res.data), };这样业务层写起来就是const list await http.getUser[](/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: () PromiseT, retries 3 ): PromiseT { 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 实现界面出现但图片加载不出来图片域名不在白名单把图片域名加入安全域名或改成 HTTPS401 弹窗重复出现每个页面都做了错误提示收敛到 axios 响应拦截器统一处理这张表不是标准答案但覆盖了鸿蒙跨端网络请求里 80% 的“莫名其妙”问题。这些问题的共同点值得再说一次别急着怀疑 JS 代码先检查宿主环境放没放行。6.2 我在实操中积累的几个小习惯第一个习惯是所有请求地址单独放一个配置文件里按环境区分 dev、test、prod不要直接在组件里写 URL。鸿蒙真机调试时换 IP 实在太频繁了每次全身搜索替换太容易漏。第二个习惯是在切环境时顺手看一眼请求日志。如果你的抓包工具只能看到 HTTPS 但看不到明文说明不是抓包功能问题而是请求根本没发出往权限和安全配置方向查。第三个习惯是给每个接口都设计一个最小可用 mock 数据。鸿蒙环境搭建本身就有门槛如果后端接口还不稳定前端调试会卡在两层问题上有了 mock 至少能分清是平台问题还是接口问题。我后来甚至把 mock 开关做到了请求层里面切环境时只需要改一个布尔值。