Appearance
第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 axios12.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 对比
| 特性 | axios | fetch 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 发送请求
- 跨域问题解决方案
- 加载状态和错误处理
- 不同请求库的对比
在实际项目中,合理的网络请求管理可以提高代码的可维护性和用户体验。建议根据项目的具体需求选择合适的请求库和封装方式。
