Skip to content

第12章:网络请求(实战核心)

在 React 项目中,网络请求是与后端 API 交互的核心环节,也是实战开发中必不可少的技能。本章将详细介绍如何在 React 中处理网络请求,包括库的选择、请求封装、错误处理等关键内容。

12.1 axios 安装与基础使用(GET、POST 请求)

12.1.1 安装 axios

首先,我们需要安装 axios 库:

bash
# 使用 npm
npm install axios

# 使用 yarn
yarn add axios

# 使用 pnpm
pnpm add axios

12.1.2 基础 GET 请求

jsx
import axios from 'axios';
import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/users');
        setUsers(response.data);
      } catch (err) {
        setError('获取用户列表失败');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>{error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

12.1.3 基础 POST 请求

jsx
import axios from 'axios';
import { useState } from 'react';

function CreatePost() {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    
    try {
      const response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
        title,
        body,
        userId: 1
      });
      setSuccess(true);
      console.log('创建成功:', response.data);
    } catch (err) {
      setError('创建失败');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      {success && <div>创建成功!</div>}
      {error && <div>{error}</div>}
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="标题"
        />
        <textarea
          value={body}
          onChange={(e) => setBody(e.target.value)}
          placeholder="内容"
        />
        <button type="submit" disabled={loading}>
          {loading ? '提交中...' : '提交'}
        </button>
      </form>
    </div>
  );
}

export default CreatePost;

12.2 请求封装(统一管理接口,实战必备)

在实际项目中,我们通常会封装 axios 实例,以便统一管理接口配置、拦截器等。

12.2.1 创建请求实例(配置基础路径、超时时间)

js
// src/api/request.js
import axios from 'axios';

// 创建 axios 实例
const request = axios.create({
  baseURL: 'https://api.example.com', // 基础路径
  timeout: 10000, // 超时时间 10s
  headers: {
    'Content-Type': 'application/json'
  }
});

export default request;

12.2.2 请求拦截器(添加token、请求头)

js
// src/api/request.js
import axios from 'axios';

const request = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    // 从本地存储获取 token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default request;

12.2.3 响应拦截器(统一错误处理、数据解析)

js
// src/api/request.js
import axios from 'axios';

const request = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    // 直接返回响应数据
    return response.data;
  },
  (error) => {
    // 统一错误处理
    if (error.response) {
      // 服务器返回错误状态码
      switch (error.response.status) {
        case 401:
          // 未授权,跳转到登录页
          window.location.href = '/login';
          break;
        case 403:
          console.error('没有权限访问');
          break;
        case 404:
          console.error('请求的资源不存在');
          break;
        case 500:
          console.error('服务器内部错误');
          break;
        default:
          console.error('请求失败');
      }
    } else if (error.request) {
      // 请求已发出但没有收到响应
      console.error('网络错误,无法连接到服务器');
    } else {
      // 请求配置出错
      console.error('请求配置错误:', error.message);
    }
    return Promise.reject(error);
  }
);

export default request;

12.2.4 接口统一管理

js
// src/api/api.js
import request from './request';

// 用户相关接口
export const userApi = {
  // 获取用户列表
  getUsers: (params) => request.get('/users', { params }),
  // 获取用户详情
  getUserById: (id) => request.get(`/users/${id}`),
  // 创建用户
  createUser: (data) => request.post('/users', data),
  // 更新用户
  updateUser: (id, data) => request.put(`/users/${id}`, data),
  // 删除用户
  deleteUser: (id) => request.delete(`/users/${id}`)
};

// 文章相关接口
export const postApi = {
  // 获取文章列表
  getPosts: (params) => request.get('/posts', { params }),
  // 获取文章详情
  getPostById: (id) => request.get(`/posts/${id}`),
  // 创建文章
  createPost: (data) => request.post('/posts', data),
  // 更新文章
  updatePost: (id, data) => request.put(`/posts/${id}`, data),
  // 删除文章
  deletePost: (id) => request.delete(`/posts/${id}`)
};

12.3 结合 useEffect 发送请求(避免重复请求)

在 React 中,我们通常使用 useEffect 钩子来发送网络请求,但需要注意避免重复请求。

12.3.1 基本用法

jsx
import { useState, useEffect } from 'react';
import { userApi } from '../api/api';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const data = await userApi.getUsers();
        setUsers(data);
      } catch (err) {
        setError('获取用户列表失败');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []); // 空依赖数组,只在组件挂载时执行一次

  // 渲染逻辑...
}

12.3.2 带依赖项的请求

当请求需要根据某些状态或 props 变化而重新发送时,我们可以在依赖数组中添加这些变量:

jsx
import { useState, useEffect } from 'react';
import { postApi } from '../api/api';

function PostList({ userId }) {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        setLoading(true);
        const data = await postApi.getPosts({ userId });
        setPosts(data);
      } catch (err) {
        setError('获取文章列表失败');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, [userId]); // 当 userId 变化时重新发送请求

  // 渲染逻辑...
}

12.3.3 取消请求

当组件卸载或依赖项变化时,我们应该取消未完成的请求,以避免内存泄漏:

jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 创建取消令牌
    const source = axios.CancelToken.source();

    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://jsonplaceholder.typicode.com/users', {
          cancelToken: source.token
        });
        setUsers(response.data);
      } catch (err) {
        if (axios.isCancel(err)) {
          console.log('请求已取消');
        } else {
          setError('获取用户列表失败');
          console.error(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();

    // 清理函数
    return () => {
      source.cancel('组件卸载,取消请求');
    };
  }, []);

  // 渲染逻辑...
}

12.4 跨域问题解决方案(Proxy 代理、CORS)

在开发过程中,我们经常会遇到跨域问题。以下是两种常见的解决方案:

12.4.1 开发环境 Proxy 代理

package.json 文件中配置代理:

json
// package.json
{
  "name": "my-react-app",
  "proxy": "https://api.example.com"
}

这样,当我们发送请求到 /api/users 时,会被代理到 https://api.example.com/api/users

对于 Vite 项目,在 vite.config.js 中配置:

js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});

12.4.2 生产环境 CORS 配置

在生产环境中,需要在后端服务器配置 CORS(跨域资源共享):

以 Express 为例:

js
// 安装 cors 包
// npm install cors

const express = require('express');
const cors = require('cors');
const app = express();

// 允许所有跨域请求
app.use(cors());

// 或者配置特定的域名
app.use(cors({
  origin: 'https://your-frontend-domain.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// 路由和其他配置...

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

12.5 加载状态、错误状态处理(用户体验优化)

良好的加载状态和错误处理可以提升用户体验:

jsx
import { useState, useEffect } from 'react';
import { userApi } from '../api/api';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null); // 重置错误状态
        const data = await userApi.getUsers();
        setUsers(data);
      } catch (err) {
        setError('获取用户列表失败,请稍后重试');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) {
    return (
      <div className="loading-container">
        <div className="loading-spinner"></div>
        <p>加载中...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="error-container">
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>重试</button>
      </div>
    );
  }

  return (
    <div className="user-list">
      <h2>用户列表</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            <h3>{user.name}</h3>
            <p>{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

12.6 常用请求库备选(fetch API、axios 对比)

除了 axios,我们还可以使用浏览器内置的 fetch API 或其他请求库。

12.6.1 fetch API 基本使用

jsx
import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        
        if (!response.ok) {
          throw new Error('网络请求失败');
        }
        
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError('获取用户列表失败');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  // 渲染逻辑...
}

12.6.2 axios vs fetch API 对比

特性axiosfetch API
浏览器支持需要引入现代浏览器内置
自动转换 JSON支持需要手动调用 response.json()
请求拦截器支持不支持
响应拦截器支持不支持
取消请求支持支持(AbortController)
超时设置支持支持(AbortController)
错误处理状态码非 2xx 时会 reject只在网络错误时 reject
并发请求支持(axios.all)支持(Promise.all)

12.6.3 其他请求库

  • Ky: 基于 fetch API 的现代化 HTTP 客户端,API 设计简洁
  • SuperAgent: 功能丰富的 HTTP 客户端,支持链式调用
  • node-fetch: 在 Node.js 环境中使用 fetch API

小结

本章介绍了 React 中网络请求的核心知识,包括:

  • axios 的安装和基础使用
  • 请求封装和拦截器配置
  • 结合 useEffect 发送请求
  • 跨域问题解决方案
  • 加载状态和错误处理
  • 不同请求库的对比

在实际项目中,合理的网络请求管理可以提高代码的可维护性和用户体验。建议根据项目的具体需求选择合适的请求库和封装方式。

© 2026 编程马·菜鸟教程 版权所有