在 OpenHarmony 上跑 React Native,大家最容易一脚踩进去的坑,就是网络请求。很多从 Android/iOS 转过来的同学,以为直接写 fetch 就完事,结果真机一跑,要么请求超时,要么证书校验不过,要么中文乱码,最难受的是日志里的 Network request failed 根本看不出原因。这篇文章我把 Fetch API 的完整用法、OpenHarmony 上的适配细节、还有我调过的几个典型问题都梳理一遍。主要面向两类人:一类是想把现有 React Native 应用迁移到 OpenHarmony 的开发者,另一类是在 OpenHarmony 上从零搭应用、卡在接口联调阶段的同学。读完后你至少能搞定常规 GET/POST 请求、超时取消、文件上传,以及排掉 90% 的网络报错。如果你是刚接触 RN 的小白,代码可以直接抄;如果你是中后台项目负责人,这里面的边界条件和设计取舍也应该能给你一些参考。
1. 为什么在 OpenHarmony 上写 Fetch 请求要先理解这套网络栈
1.1 从 fetch 到系统网络栈:请求到底走了哪条路
很多人在浏览器里写多了 fetch,会下意识认为 fetch 就是一行 JS 的事,发起一个 HTTP 请求然后把 Promise 接住。但在 React Native 里,情况要复杂一些。JS 侧调用的 fetch 并不会在 JavaScript 引擎里完成整个网络传输,而是通过 RN 的 Native 层桥接到当前操作系统的网络能力。在 OpenHarmony 上,这个底层网络能力就是系统提供的 HTTP 客户端,相当于把请求交给了一个原生网络栈去执行。
这意味着什么?意味着你写的 fetch 代码只是“请求的描述”,真正去建连、发数据、读响应、处理证书的是原生模块。所以影响请求成败的因素从 JS 代码延伸到了系统权限、网络策略、证书库、甚至设备的网络连接状态。这也是为什么很多人把同一段 fetch 代码从浏览器搬到 RN 后,突然开始各种报错。浏览器默认允许跨域、默认帮你管理 Cookie、默认信任一堆公开 CA 证书,但 OpenHarmony 的 App 沙箱环境不会自动帮你做这些事。
理解这条链路后,你遇到问题就知道去哪查了。如果错误是 “Unable to resolve host”,大概率是 DNS 或网络连通性;如果是 “SSL handshake aborted”,大概率是证书或 TLS 版本问题;如果是 “Network request failed” 且没有更细的堆栈,那就得去系统网络日志和抓包工具里看。切莫一上来就改代码重试,那样只是在碰运气。
1.2 为什么不直接全用原生 HTTP 能力
有人会问:既然底层都是原生网络栈,那我干脆在 OpenHarmony 里直接用原生 HTTP 接口请求不就好了,为什么还要包一层 React Native 的 Fetch?这就要回到 RN 的定位。一个跨端框架最大的价值是让大家用一套业务代码覆盖多端,而不是每端都用各自的方式写一遍。如果网络层在每端单独实现,接口参数、错误处理、日志上报、公共 Header 注入这些逻辑都得重复维护,团队很快会陷入碎片化。
Fetch API 是 React Native 对外提供的标准抽象,它把多端底层的差异尽量“抹平”。你在 JS 层只需要关心 method、headers、body 这类语义化字段,剩下的连接池、重定向、TLS 握手这些交给底层。OpenHarmony 适配层已经把大部分逻辑接好了,多数情况下 Fetch 的写法与其他端保持一致。当然,完全抹平不现实,总会有一些小差异,这正是后面几章要专门讲适配细节的原因。
但我仍然建议默认优先使用 Fetch 封装网络层,而不是在业务里直接调用原生 HTTP 模块。原因很简单:团队协作时,JS 层的代码所有人都会读,而原生模块只有少数人能维护。当网络层出现问题时,统一入口能让你拿到一套标准日志和错误结构,排查效率会高很多。
1.3 Fetch 与 XMLHttpRequest 怎么分工
React Native 里除了 fetch,还有一套 XMLHttpRequest 实现。在 OpenHarmony 适配层中,XMLHttpRequest 也已经被接到底层网络能力上。两者各有擅长:fetch 语法简洁、Promise 友好,适合绝大多数业务接口;XMLHttpRequest 支持 upload progress 和 download progress 事件,适合上传下载场景。
我在实际项目里的选择很简单:普通业务请求一律走 fetch,上传下载需要进度条时走 XMLHttpRequest,或者用 fetch 拿到 arrayBuffer 后再结合原生文件能力处理。如果你没有非常强的进度需求,直接用 fetch 也没问题,只是要注意 fetch 的标准类型里 Response.body 是一个可读流,在 RN 环境和 OpenHarmony 上对流式读取的支持稳定性需要实测,不能想当然。
所以,下面这套封装会以 fetch 为核心实现。当你理解了 fetch 的各种边界问题后,再看 XMLHttpRequest 补丁场景会容易得多。
2. 写 Fetch 请求前,先把环境和权限铺平
2.1 OpenHarmony 工程里的网络权限配置
在 OpenHarmony 应用里,应用默认不持有网络访问权限。这一点和浏览器完全不一样。你在 JS 里写了优雅的 fetch,结果设备上请求直接失败,首先要检查的就是工程有没有声明ohos.permission.INTERNET。
以标准工程为例,需要修改entry/src/main/module.json5,在requestPermissions数组里添加权限声明。下面是一个最小可用的结构:
{ "module": { "name": "entry", "type": "entry", "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }如果你用的是 RN for OpenHarmony 的脚手架工程,有时模板已经默认写了这个权限。但只要改过模块信息、重建过工程或者把代码拷贝到另一个项目里,就得重新确认。缺权限的报错非常容易被误判:JS 层抛 Network request failed,后端日志里却完全没有请求记录,同时设备系统日志可能出现 net permission denied 之类提示。
一个小经验:我每次新建调试工程后的第一件事,不是先写 UI,而是先写一个“最小 fetch 探针”。在页面加载时请求一个健康检查接口,确保基础网络通路是通的,再开始做业务。这样能避免把网络配置问题混进业务逻辑里排查。
2.2 明文 HTTP 与 HTTPS 的选型
OpenHarmony 对 HTTP 明文流量有默认安全限制。开发阶段很多人喜欢直接请求http://192.168.x.x:8080这种内网地址,但到了真机上很容易被策略挡住。我不建议为了联调方便把工程的网络安全校验全局放开,因为那会把生产环境也暴露在风险里。
首选方案是让后端直接提供 HTTPS 接口,证书使用正规 CA 签发。一个小型 Demo 可能觉得没必要上 HTTPS,但一旦你要长期维护这个项目,HTTPS 能帮你少踩很多坑:明文流量限制、运营商注入、中间人抓包导致的调试困惑,都能避开。
如果确实只能在联调环境使用 HTTP,先查阅你所用版本的 RN 适配层是否提供了明文流量开关,再按官方文档配置专属调试配置。配置时尽量只对测试域名开放,不要把开关直接应用到 release 包。这里有个容易忽略的地方:同一个工程里的模拟器可能默认允许某些本地地址,真机环境却严格得多。所以不要拿模拟器的结果推断真机行为。
2.3 联调环境的连通性准备
很多从浏览器开发转到真机联调的人,会被“地址”这个概念搞晕。在浏览器里访问localhost指向你自己的电脑,但在真机上运行 App 时,localhost指向的是手机本身。如果你的后端跑在电脑上,那么在手机上的请求应该写电脑在局域网里的 IP,比如http://192.168.1.100:3000,而不是http://localhost:3000。
这里有一个很实用的排查顺序:先确认手机和电脑在同一个局域网,再在手机浏览器里访问同一个接口地址,最后再回到 App 里测。如果手机浏览器能打开而 App 不行,说明是你 App 的网络权限或证书配置有问题;如果手机浏览器也打不开,那就是网络路径本身的问题,别急着查代码。
另外要注意防火墙。电脑防火墙有时会拦截来自手机的网络请求,导致 fetch 一直超时。Windows 上尤其常见。临时放行对应端口,或者直接让后端换到已放行的端口,很快就能定位到问题。
3. 一套完整可复用的 Fetch 请求封装
3.1 从 GET 到 POST:先把基础调用写对
先说最基础的 GET 请求。很多入门教程只写fetch(url),但真实业务里你通常需要设置 header、处理状态码、解析 JSON。一个基础调用是这样:
const response = await fetch('https://api.example.com/user/123', { method: 'GET', headers: { Accept: 'application/json', Authorization: 'Bearer your-token', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json();POST 请求则需要多关注 body 的类型。如果请求体是 JSON,必须序列化,并且设置Content-Type: application/json。忘记设置 Content-Type 是后端报参数解析失败的最大元凶之一。
const response = await fetch('https://api.example.com/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ account: 'demo', password: 'xxxxxx' }), }); const data = await response.json();GET 请求带查询参数时,不要直接把中文拼进 URL。先用encodeURIComponent对每个参数做编码,再拼进 query string。否则你会在服务端日志里看到一堆乱码,严重的甚至直接被网关拒绝。
3.2 用 AbortController 补上超时与取消能力
fetch 和原生 XMLHttpRequest 不一样,它不带默认超时。如果网络一直不返回,请求可能长时间挂在那边,用户界面也会一直处于等待中。所以在 React Native 里实现超时,最标准的方式是结合AbortController和定时器。
const controller = new AbortController(); const timer = setTimeout(() => { controller.abort(); }, 10000); try { const response = await fetch('https://api.example.com/slow-api', { method: 'GET', signal: controller.signal, }); const data = await response.json(); // 业务处理 } catch (error) { if (error.name === 'AbortError') { console.warn('请求超时,已取消'); } else { console.error('请求失败', error); } } finally { clearTimeout(timer); }在封装层里,我会把 timeout 作为可选参数,默认给 10 秒。业务方如果知道某个接口需要更长的时间,再单独覆盖。注意AbortError和普通Error的区分,不要把所有抛错都当网络异常处理。
另外,不要以为用户已经离开了页面,请求就自动停了。fetch 请求一旦发出,如果没有主动 abort,它会继续在后台跑。因此在列表页、详情页这类频繁进出的页面里,卸载组件时记得触发 abort,否则会产生一堆无效的竞态响应,导致页面显示旧数据。
3.3 统一响应处理与异常分类
真实项目里,如果每个页面都自己写if (!response.ok)和try/catch,代码很快会烂掉。我建议在封装层做统一处理,把“网络错误”“HTTP 状态码错误”“业务错误”三类分开。
一个比较稳定的 request wrapper 长这样:
const request = async (path, options = {}) => { const { timeout = 10000, params, method = 'GET', ...restOptions } = options; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); let finalUrl = path; if (params) { const query = Object.keys(params) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join('&'); finalUrl = `${path}?${query}`; } try { const response = await fetch(finalUrl, { method, signal: controller.signal, ...restOptions, }); const text = await response.text(); let body = null; if (text) { try { body = JSON.parse(text); } catch (error) { body = text; } } if (!response.ok) { const error = new Error(`请求失败(${response.status})`); error.status = response.status; error.body = body; throw error; } return body; } catch (error) { if (error.name === 'AbortError') { const timeoutError = new Error('请求超时'); timeoutError.name = 'TimeoutError'; throw timeoutError; } throw error; } finally { clearTimeout(timer); } };这段代码有几个关键设计:先取文本再解析 JSON,这样即使响应不是 JSON 也不会把原始信息丢掉;非 2xx 状态码会被包装成带 status 和 body 的 Error;超时会被改写成更语义化的TimeoutError。业务调用方就可以这样写:
try { const data = await request('/user/profile', { method: 'GET', headers: { Authorization: `Bearer ${token}` }, }); } catch (error) { if (error.name === 'TimeoutError') { // 提示用户稍后重试 } else if (error.status) { // 根据 status 处理 } else { // 网络不可达 } }统一异常结构之后,后续接上报系统、错误弹窗、自动重试都会省很多事。
3.4 文件上传用 FormData,别转 base64
OpenHarmony 上的 RN 应用经常会遇到图片上传。新手常犯的错是把图片文件转成 base64 字符串塞进 JSON,服务端收到后还要解码,性能和体积都不划算。正确的做法是用 FormData 提交文件。
React Native 的 FormData 文件对象有三个核心字段:uri、name、type。示例:
const form = new FormData(); form.append('file', { uri: fileUri, name: 'photo.jpg', type: 'image/jpeg', }); form.append('scene', 'avatar'); await request('/upload', { method: 'POST', body: form, });这里要特别注意:上传 FormData 时不要手动设置Content-Type: application/json,也不应该手动设置Content-Type: multipart/form-data。因为 RN 底层会自动生成带 boundary 的 multipart 内容,手动设置会破坏请求体格式,导致后端解析不到文件。
关于 fileUri,在 OpenHarmony 上一定要使用 App 能访问到的本地路径。如果文件来自相册或者临时目录,先确认这个路径在沙箱内可读,否则底层网络模块读不到文件,请求会静默失败或者报路径异常。大文件上传时尽量避免一次性加载进内存,先用真机测试内存占用。如果遇到上传大文件内存暴涨,可以改用原生文件流方式上传,不要硬扛。
3.5 下载场景的落盘思路
fetch 的响应结果在浏览器里可以直接触发下载,但 React Native 没有这个能力。在 OpenHarmony 上,你需要拿到响应体的二进制数据,再写入应用的沙箱文件目录。
const response = await fetch('https://api.example.com/files/report.pdf', { method: 'GET', }); if (!response.ok) { throw new Error(`下载失败 ${response.status}`); } const buffer = await response.arrayBuffer(); // 通过文件模块把 buffer 写入指定沙箱路径RN 自带能力里没有直接写文件的 API,所以这里通常需要借助原生模块封装或者第三方库。整体思路是:fetch 负责请求网络资源,拿到二进制数据后交给文件模块写盘。注意下载大文件时,arrayBuffer()会把整个文件加载进内存,所以超大文件建议在原生侧做流式下载,不要在 JS 层一把梭。下载进度同样需要 XMLHttpRequest 或原生能力,fetch 给不了实时进度。
4. OpenHarmony 特有的网络适配细节
4.1 沙箱路径与本地地址的坑
OpenHarmony 的应用有严格沙箱机制,文件系统访问、网络访问都受权限控制。很多开发者在做文件上传和下载时,习惯把文件路径直接写成 PC 上的路径或者相对路径,这在浏览器里也许没问题,但在 RN for OpenHarmony 上就会出现不可读的问题。
当你从一个模块拿到文件 uri 时,先打印出来看一眼,确认是完整可访问的绝对路径,而不是content://这种需要额外协议解析的路径。如果原生返回的是类似file:///data/storage/...的格式,RN 的 FormData 通常可以直接处理;如果返回的是应用内部私有路径,则要确认网络模块是否有权限读取该文件。总之,路径不对是上传和下载场景里最隐蔽的坑,因为报错往往不是立即出现的。
4.2 证书校验与内网 HTTPS
HTTPS 证书校验是 OpenHarmony 上比较容易让人崩溃的地方。由于 fetch 最终走系统网络栈,证书的信任判断依赖系统证书库。你自己用工具生成的自签名证书,或者公司内网私有 CA 签发的证书,默认情况下很可能被拒。
常见报错表现为SSL handshake aborted、Certificate verify failed等。如果你只是为了测试,不要试图在 JS 层忽略证书错误,因为 RN for OpenHarmony 的 fetch 通常不提供这种开关。最稳妥的方法是为测试域名配置合法的证书。如果公司有统一的内网 CA,就把 CA 证书安装到测试设备上,或者让后端网关代理到公网合法证书。这不是为了安全洁癖,而是因为你有 90% 的概率绕不过去,绕过去了也会给生产环境埋雷。
4.3 Cookie 和登录态处理
在浏览器里,fetch 请求会自动带上当前域名的 Cookie,登录态管理似乎天然不用操心。但在 React Native 里没有页面上下文,Cookie 不会自动持久化。在 OpenHarmony 的适配层,系统网络栈可能维护了自己的 Cookie 管理器,但 JS 侧的 fetch 不一定每次都读写同一份 Cookie。
所以我对登录态的建议非常简单:不要依赖 Cookie。登录接口返回 token 或 session id 时,把它显式存到本地存储里,之后每个请求在 header 中手动带上。常见的做法是用Authorization: Bearer <token>。如果后端一定要读取 Cookie,那你需要从登录响应头里把Set-Cookie提取出来,存好并手动放到后续请求中。这个过程比较繁琐,而且要小心处理过期时间。能改成 header 鉴权就尽量改。
4.4 并发上限与连接复用
操作系统对单个应用的网络并发请求数通常是有限制的,OpenHarmony 同理。如果你的业务里出现大量并发请求,比如一次性拉取几十个头像、列表页并发加载多个模块,就要在封装层做并发控制,而不是把请求一次性全部抛出去。
一个简单的并发控制思路是写一个异步信号量,控制同一时间最多发起 N 个请求。我曾经在联调时遇到过一个问题:页面首屏同时发 20 多个请求,结果后面的请求全部超时。一开始以为是接口性能问题,后来逐个请求排查,才发现是并发数已经触顶。限制并发到 5 到 8 个之后,问题立刻消失。
连接复用方面,保持同一个域名下多个请求共享连接,能显著减少 TLS 握手开销。但这更多依赖底层网络栈的行为,JS 层能做的只是:尽量使用相同的 baseUrl 前缀,不要在 URL 里频繁塞随机参数避免缓存击穿,不要每次请求都重建一个与域名无关的连接名。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
下面这个表格是我在 OpenHarmony 上联调 React Native 网络请求时经常会遇到的报错,按出现频率排了个序。表格里的处理方式基本可以直接套用。
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| Network request failed,没有更细堆栈 | 网络权限缺失、域名解析失败、底层网络异常 | 先检查ohos.permission.INTERNET,再确认域名能解析 |
| 请求一直挂起直到超时 | 网络不可达、防火墙拦截、后端未响应 | 从真机浏览器访问同地址,确认网络路径 |
| SSL handshake aborted | 自签名证书、证书过期、TLS 版本不匹配 | 使用合法证书或安装测试 CA |
| JSON 解析报错 | 响应不是 JSON 或编码不对 | 先response.text()看原始内容 |
| 上传文件服务端收不到文件 | 手动设置了 Content-Type 或 uri 不可读 | 确认 fileUri 完整、不手动设置 multipart 的 Content-Type |
| 中文乱码 | 服务端响应编码与解析不一致 | 统一 UTF-8 编码,必要时用 base64 过渡 |
| 并发请求大量失败 | 超出系统并发限制或连接池打满 | 封装层做并发控制,降低同时请求数量 |
这个速查表不能完全覆盖所有场景,但能帮你快速定位 80% 的问题。剩下 20% 的疑难杂症,多半需要结合系统日志和抓包工具一起看。
5.2 一次联调“连不上内网”的排查路径
有一次我在联调一个 RN for OpenHarmony 项目,模拟器里跑得好好的,换到真机上就报连接失败。第一反应是后端地址写错,检查后发现是局域网 IP,理论上没问题。接着用真机浏览器访问该 IP 的接口,发现浏览器也打不开。这才意识到根本不是 App 的问题,而是电脑防火墙把手机过来的请求拦了。
类似这样的经验告诉我,联调网络问题一定要一层一层剥离变量。第一步,在能访问网络的浏览器里请求接口;第二步,在手机浏览器里请求;第三步,在 App 里请求。哪一步失败就停留在哪一步排查。如果 App 失败而浏览器成功,再去查代码、权限和证书;如果两者都失败,先解决网络环境问题。
还有一次遇到的现象是:同一个接口,POST 请求正常,GET 请求却必现失败。最初以为是后端路由问题,后来抓包发现 GET 的 URL 里带了一个中文 query 参数,没有编码。后端收到的是乱码,返回了 400。把参数改成encodeURIComponent后恢复。这种小问题最隐蔽,因为后端日志里的乱码往往看起来像“请求包不对”。
5.3 请求重试与缓存优化
网络请求在弱网环境下不可避免会失败。对于 GET 接口,可以设计一个带退避时间的重试机制。但重试不是所有接口都适合,POST 这类非幂等请求要谨慎,除非后端确认接口支持幂等,否则重试可能导致重复下单、重复支付这类严重事故。
一个最简单的指数退避重试封装:
async function requestWithRetry(path, options = {}, retries = 2) { for (let attempt = 0; attempt <= retries; attempt++) { try { return await request(path, options); } catch (error) { if (attempt === retries) { throw error; } const delay = Math.pow(2, attempt) * 500; await new Promise((resolve) => setTimeout(resolve, delay)); } } }重试期间要考虑用户体感。如果第一次重试要等 5 秒,页面最好有加载态或提示,否则用户可能以为卡死了又多点了一次按钮,反而制造重复请求。
缓存方面,我习惯只对 GET 请求做一层很轻的本地缓存。进入页面时先渲染缓存数据,再去请求最新数据,等拿到新数据后更新界面和缓存。这样做离线场景下体验会好很多,但缓存数据要标明时间和来源,不要让用户错把旧数据当实时数据。写入缓存前也要注意数据结构大小,别把几 MB 的大响应直接塞进本地存储,那会造成严重的读写卡顿。
最后再分享一点我的个人习惯:我会在 request 封装里保留一个原始日志开关,方便上线后按需开启。别把日志默认全打开,也别完全关掉。网络请求的细节非常多,遇到问题能立刻看到 method、url、耗时、status,比反复翻抓包工具高效得多。如果你按这套结构把 Fetch 封装好,OpenHarmony 上的跨端网络层基本就稳了一大半。后续如果还想深入,可以继续聊上传下载进度、断点续传和请求队列治理,这里就不再展开了。