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 请求从发起到结束的完整信号链,包括正常路径与三类错误分支的走向,帮助理解错误应该在哪个环节被捕获和处理。

fetch 请求生命周期与错误分支 发起 fetch url + options 网络传输 DNS / TCP / TLS 收到响应头 Promise resolve 网络层错误 → reject 断网 / 超时 / DNS 失败 检查 res.ok status 2xx ? HTTP 错误 4xx / 5xx 解析响应体 json() / text() ok = true 解析失败 非法 JSON 业务码判断 code === 0 ? 返回数据 交给调用方 业务错误 → 提示用户 / 重试 try / catch 捕获 AbortError 单独处理

从图中可以看到,一次请求会依次经过网络传输、响应头接收、状态码检查、响应体解析和业务码判断五个环节,每个环节都有对应的失败可能。把错误处理分散到各个环节,比在最后统一 catch 更容易定位问题。例如超时属于网络层,应抛出可识别的 AbortError;4xx 通常意味着请求参数或权限有问题,不应盲目重试;5xx 则适合做有限次数的退避重试。

封装建议与常见陷阱

在实际项目中,建议把 fetch 封装成统一的 request 函数,集中处理基础 URL、公共请求头、超时、错误归一化和日志。调用方只关心业务参数和返回数据,不再重复处理 HTTP 细节。封装时要注意几个常见陷阱:一是忘记检查 res.ok,把 404 当成成功;二是重复读取响应体导致 "Body already used";三是在组件卸载后仍然 setState,引发内存泄漏警告,这可以通过 AbortController 在卸载时中止请求来规避;四是把超时时间设得过短,在弱网环境下频繁误报失败。