CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/网络
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

网络

fetch/WebSocket。

30 分钟

网络请求

这一课你将能做到

  1. 用 fetch 和 axios 完成 GET / POST 请求,并说清楚 fetch 不抛 HTTP 错误这个坑
  2. 封装统一请求层:baseURL、超时、token 注入、响应剥壳、401 处理
  3. 实现标准的 loading / error / 空数据三态界面,完成图片上传和请求取消

为什么要学

没有网络的 App 只是本地玩具。登录、列表、下单、上传头像,全是网络请求。而且「你们项目怎么封装 axios」是面试必问题——这一课就是标准答案。裸写 fetch 也能跑,但项目里这么干,token 注入、错误处理、loading 管理会在几十个页面里复制粘贴,迟早乱套。

人话:每个页面裸写 fetch,就像每家每户自己去河边打水——迟早乱套。封装统一请求层,相当于给小区通自来水:家家拧开龙头就有水(自动带 token),停水了统一通知(统一错误处理),水表统一计费(统一日志)。

先建立图像

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((res) => res.json())
  .then((data) => console.log(data.title));

一次请求的三个阶段,对应界面的三种状态——这个「三态」模型会贯穿你所有的列表页:

发起请求 → loading = true  → 界面:转圈
请求成功 → setList(data)   → 界面:渲染数据(空数组还要显示「暂无数据」)
请求失败 → setError(true)  → 界面:错误提示 + 重试按钮

跟着做

步骤 1:fetch 基本用法与它的天坑

// GET
const res = await fetch('https://api.example.com/users');
const data = await res.json();

// POST JSON
const res2 = await fetch('https://api.example.com/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: 'tom', password: '123456' }),
});

天坑:fetch 遇到 404 / 500 不会抛错。它只在网络彻底断开时才 reject,HTTP 错误状态码照样走 then。所以用 fetch 必须自己判断:

const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);  // 不抛就等着把 404 当成功处理
const data = await res.json();

这一行判断就是大家宁愿用 axios 的核心原因之一——axios 对非 2xx 直接 reject。

步骤 2:封装 axios 实例(面试标准答案)

npx expo install axios @react-native-async-storage/async-storage
// utils/request.js
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

const request = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,   // 10 秒超时,弱网兜底
});

// 请求拦截器:每次请求自动注入 token
request.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 响应拦截器:统一剥壳 + 统一错误处理
request.interceptors.response.use(
  (res) => res.data,  // 业务层直接拿到 data,少写一层 .data
  (err) => {
    if (err.response?.status === 401) {
      // token 失效:清登录态、跳登录页(通过全局事件或 NavigationRef 通知外层)
    }
    return Promise.reject(err);
  }
);

export default request;

封装的价值全在拦截器:token 注入、401 跳转、错误提示、数据剥壳,全部收口在这一处,业务代码干干净净。

步骤 3:页面使用 + 三态界面完整代码

import { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator, Pressable, FlatList } from 'react-native';
import request from './utils/request';

export default function UserList() {
  const [list, setList] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  const load = async () => {
    setLoading(true);
    setError(false);
    try {
      const data = await request.get('/users');
      setList(data);
    } catch (e) {
      setError(true);
    } finally {
      setLoading(false);   // 成功失败都要关 loading
    }
  };

  useEffect(() => { load(); }, []);

  if (loading) return <ActivityIndicator style={{ flex: 1 }} size="large" />;
  if (error) return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>加载失败,请检查网络</Text>
      <Pressable onPress={load}>
        <Text style={{ color: '#007bff', marginTop: 12 }}>点我重试</Text>
      </Pressable>
    </View>
  );

  return (
    <FlatList
      data={list}
      keyExtractor={(item) => String(item.id)}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.name}</Text>}
      ListEmptyComponent={<Text style={{ textAlign: 'center', marginTop: 40 }}>暂无数据</Text>}
    />
  );
}

背下来这个结构,你以后写的每个列表页都是它:load 函数(try/catch/finally)+ 挂载时调用 + 三态渲染。

步骤 4:图片上传 FormData

配合第 9 课的 ImagePicker 选完图后上传。RN 的 FormData 接受一个特殊的文件描述对象:

const uploadAvatar = async (localUri) => {
  const formData = new FormData();
  formData.append('file', {
    uri: localUri,          // ImagePicker 返回的 file:// 路径
    name: 'avatar.jpg',     // 文件名
    type: 'image/jpeg',     // MIME 类型
  });

  await request.post('/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  });
};

三个字段 uri / name / type 一个不能少。注意不要手动拼 boundary,交给 axios 自动生成。

步骤 5:请求取消 AbortController

搜索联想场景:用户连续输入「re」「rea」「reac」,发出三个请求,如果响应乱序返回,旧结果会覆盖新结果。解法是新请求发起时取消上一个:

useEffect(() => {
  const controller = new AbortController();

  fetch(`https://api.example.com/search?q=${keyword}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setResult)
    .catch(e => { if (e.name !== 'AbortError') setError(true); });

  return () => controller.abort();  // keyword 变了或组件卸载,取消旧请求
}, [keyword]);

讲清楚要点

  1. fetch 不抛 HTTP 错误:只在断网时 reject,404/500 照样 resolve,必须判断 res.ok。axios 非 2xx 直接 reject,项目级一律用 axios。
  2. 拦截器的价值:token 注入、401 处理、数据剥壳收口一处。判断封装好坏的标准:业务页面里还看不看得到「token」这两个字。
  3. finally 里关 loading:漏了它,请求失败后界面永远转圈。
  4. 超时必设:axios 默认无限等待,弱网下用户体验灾难,timeout: 10000 是底线。
  5. RN 没有浏览器跨域限制:CORS 是浏览器安全策略,App 直连后端不受限。但 Android 9+ 默认禁明文 http,后端要上 https。
  6. localhost 在手机上不通:手机的 localhost 是手机自己。调电脑上的后端,用电脑局域网 IP(如 http://192.168.1.5:3000),并保证同一 Wi-Fi。

你可能会懵的地方

  • 安卓真机请求 http 接口直接失败? Android 9+ 默认禁止明文 http。临时方案在 app.json 配 "android": { "usesCleartextTraffic": true }(需重新构建),正经方案是后端上 https——上架时苹果对明文 http 也会卡审。

  • 401 拦截器里怎么跳登录页?拦截器拿不到 navigation 啊? 两种常规做法:创建全局 NavigationRef 暴露给非组件环境调用;或拦截器里发一个全局事件(如 eventBus.emit('logout')),App 根部监听后跳转。进阶做法是拦截器里先用 refreshToken 静默换新 token 并重放原请求。

  • token 过期怎么做到用户无感? 双 token 机制:响应拦截器捕获 401 后,用 refreshToken 调刷新接口,成功后重放原请求。注意并发控制——同时来 5 个 401 只能刷新一次,其余请求排队等新 token。

  • axios 和 fetch 到底用哪个? 项目级一律 axios:拦截器、超时、取消、自动 JSON 转换开箱即用。fetch 留给一次性脚本级调用。

  • GET 请求能带 body 吗? 规范上不建议,很多服务器会忽略。参数走 query:request.get('/users', { params: { page: 1 } })。

错法对照

错法问题正解
fetch 只 catch 不判断 res.ok404 被当成功处理检查 res.ok 或用 axios
每个页面手写 token 头漏一个就诡异 401请求拦截器统一注入
成功才 setLoading(false)失败时永远转圈放 finally
手机调 localhost连接失败用电脑局域网 IP
FormData 手动拼 boundary请求体损坏只写 multipart/form-data,边界自动生成
联想搜索不取消旧请求旧响应覆盖新结果AbortController 取消上一次
响应拦截器里忘 reject错误被吞,页面以为成功处理完 return Promise.reject(err)
上传文件少写 type 字段服务端解析失败uri / name / type 三件套写全

卡点急救

  1. 安卓 http 请求失败:后端上 https;临时方案 app.json 加 usesCleartextTraffic: true 后重新构建(改原生配置必须重新 build)。
  2. 请求发不出也没报错:确认手机与电脑同网段、防火墙放行后端端口、URL 用的是局域网 IP 而非 localhost。
  3. 401 拦截后没法跳页面:用全局 NavigationRef 或事件总线通知外层跳转,别试图在拦截器里 import 组件。
  4. 上传图片服务端收不到:检查三件套字段名是否与服务端约定一致(file / files / image),用抓包工具看实际请求体。

面试常问

  • 你们怎么封装 axios? 创建实例统一 baseURL/timeout;请求拦截器注入 token、按需打日志;响应拦截器剥 data、按业务 code 统一报错提示、401 走 refreshToken 静默刷新(并发排队只刷一次)或清登录态跳登录;最后导出语义化方法(getUser、login)而非让业务层裸调 request。

  • token 过期怎么处理? 响应拦截器捕获 401:有 refreshToken 则调刷新接口,成功后用新 token 重放原请求——关键点是并发请求只触发一次刷新(用一个进行中的 Promise 让其余请求排队等待),失败则清空凭证跳登录;没有 refreshToken 直接跳登录。

  • 如何做请求取消? AbortController:创建 controller,把 signal 传给请求(fetch 原生支持,axios 同样接受 signal),在组件卸载或新请求发起时 controller.abort()。取消产生的 AbortError 要在 catch 里甄别,别当成真实错误提示用户。

自测三问

  1. fetch 在什么情况下会 reject?404 会吗?
  2. axios 的两个拦截器各自解决什么重复劳动?
  3. 手机上为什么访问不到电脑的 localhost 接口?安卓访问 http 接口还有什么额外限制?

小结一句话

网络请求的关键不在「发出去」,而在封装:统一实例、拦截器注入 token、三态界面、超时与取消——做好这层,业务代码只剩一行 await。

延伸

  • Axios 官方文档
  • 下一课:本地存储——token、缓存和用户偏好该放哪里。
加载本节练习…
← 上一课State 与 JSX下一课 →本地存储