2. 网络请求基础与Fetch API
在 React Native 中,网络请求是绝大多数应用与后端交互的核心通道。React Native 运行在 JavaScript 引擎之上,遵循 Web 标准,因此可以直接使用 fetch API 发起 HTTP 请求。与浏览器环境不同的是,React Native 的 fetch 由原生网络模块实现,底层走的是原生网络栈,因此它天然支持移动端的网络能力,但同时也带来一些与浏览器不同的行为差异,例如默认不携带 Cookie、没有同源策略限制、超时控制需要自行实现等。理解这些差异,是写出稳定网络层的前提。
fetch 的基本形态与 GET 请求
fetch 接收两个参数:请求地址和可选的配置对象,返回一个 Promise,该 Promise 在收到响应头后即进入 fulfilled 状态,解析为 Response 对象。这意味着 fetch 只在网络层面失败(如断网、DNS 解析失败)时才会 reject,而 HTTP 状态码为 404、500 时仍然会 resolve。这是初学者最容易踩的坑:必须手动检查 response.ok 或 response.status。
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json();
}
GET 请求的参数通常拼接到 URL 上。当参数较多或包含中文、特殊字符时,应使用 encodeURIComponent 逐个编码,或借助 URLSearchParams 构造查询串,避免出现非法字符导致请求失败。
POST 请求与请求头
POST 请求需要在配置对象中指定 method: 'POST',并通过 body 传递数据。发送 JSON 数据时,必须显式设置 Content-Type: application/json,否则服务端可能无法正确解析。React Native 不会自动为对象序列化,因此需要手动调用 JSON.stringify。
async function createUser(payload) {
const res = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`创建失败: ${res.status} ${text}`);
}
return res.json();
}
请求头是携带认证信息、内容协商、客户端标识的主要位置。常见的头字段包括 Authorization(令牌认证)、Accept(期望的响应类型)、Content-Type(请求体类型)以及自定义的业务头如 X-Client-Version。在 React Native 中,可以封装一个统一的请求函数,把公共头集中注入,避免每个调用点重复书写。
解析响应数据
Response 对象提供了多种读取方式:json() 解析 JSON、text() 读取纯文本、blob() 读取二进制。需要注意的是,这些方法都只能调用一次,因为响应体是流式读取的。如果先调用了 text(),再调用 json() 会抛出 "Body already used" 错误。因此,在封装时通常先判断 Content-Type,再决定用哪种方式解析。
解析 JSON 时,如果服务端返回的不是合法 JSON(例如返回了 HTML 错误页),json() 会抛出异常。稳妥的做法是先用 text() 取出原始字符串,再尝试 JSON.parse,这样在出错时能保留原始内容用于排查。
错误处理与超时控制
网络请求的错误来源可以分为三类:网络层错误(断网、超时、DNS 失败)、HTTP 状态错误(4xx、5xx)、业务错误(HTTP 200 但业务码表示失败)。前两类需要在请求层捕获,第三类通常由调用方根据返回体中的业务字段判断。一个健壮的请求函数应当把这三类错误区分开,并向上层抛出带有明确语义的错误对象。
React Native 的 fetch 没有内置超时参数,默认会一直等待。实现超时的常见方式是用 Promise.race 让请求与一个定时器赛跑,或者使用 AbortController 在超时后主动中止请求。后者更优,因为它能真正释放底层连接资源。
async function fetchWithTimeout(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('请求超时,请检查网络后重试');
}
throw err;
} finally {
clearTimeout(timer);
}
}
超时时长的设定需要结合业务场景:普通列表接口可设为 8~10 秒,上传大文件或弱网环境可放宽到 30 秒。超时后应给用户明确的反馈,并提供重试入口,而不是让界面一直停留在加载状态。
请求生命周期与错误分支
下面这张图展示了一次 fetch 请求从发起到结束的完整信号链,包括正常路径与三类错误分支的走向,帮助理解错误应该在哪个环节被捕获和处理。
从图中可以看到,一次请求会依次经过网络传输、响应头接收、状态码检查、响应体解析和业务码判断五个环节,每个环节都有对应的失败可能。把错误处理分散到各个环节,比在最后统一 catch 更容易定位问题。例如超时属于网络层,应抛出可识别的 AbortError;4xx 通常意味着请求参数或权限有问题,不应盲目重试;5xx 则适合做有限次数的退避重试。
封装建议与常见陷阱
在实际项目中,建议把 fetch 封装成统一的 request 函数,集中处理基础 URL、公共请求头、超时、错误归一化和日志。调用方只关心业务参数和返回数据,不再重复处理 HTTP 细节。封装时要注意几个常见陷阱:一是忘记检查 res.ok,把 404 当成成功;二是重复读取响应体导致 "Body already used";三是在组件卸载后仍然 setState,引发内存泄漏警告,这可以通过 AbortController 在卸载时中止请求来规避;四是把超时时间设得过短,在弱网环境下频繁误报失败。