请求封装实践
封装一个通用的网络请求工具,提高开发效率,统一管理接口调用。
概述
在企业级前端项目中,网络请求是核心功能之一。合理的请求封装可以:
- 统一管理:集中处理请求配置、拦截器、错误处理等
- 提高效率:减少重复代码,提供一致的 API 调用方式
- 便于维护:统一的错误处理和日志记录
- 功能增强:添加缓存、重试、取消等高级功能
核心特性
| 特性 | 说明 | 优势 |
|---|---|---|
| 统一配置 | baseURL、timeout、headers 等 | 避免重复配置 |
| 拦截器 | 请求/响应拦截 | 统一添加 token、处理响应 |
| 错误处理 | 自定义错误类型 | 友好的错误提示 |
| 请求缓存 | GET 请求缓存 | 减少网络请求 |
| 请求重试 | 失败自动重试 | 提高请求成功率 |
| 请求取消 | AbortController | 避免无效请求 |
架构设计
code
┌─────────────────────────────────────────────────┐
│ HttpClient │
├─────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 配置管理 │ │ 拦截器链 │ │
│ │ - baseURL │ │ - 请求拦截 │ │
│ │ - timeout │ │ - 响应拦截 │ │
│ │ - headers │ └──────────────────┘ │
│ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 核心请求方法 │ │
│ │ request | get | post | put | delete │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 错误处理 │ │ 高级功能 │ │
│ │ - HttpError │ │ - 缓存 │ │
│ │ - 超时处理 │ │ - 重试 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘一、基础封装
1.1 配置参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
baseURL | string | '' | 请求基础路径,会自动拼接到所有请求 URL 前 |
timeout | number | 10000 | 请求超时时间(毫秒) |
headers | object | {} | 默认请求头,所有请求都会携带 |
1.2 核心实现
javascript
class HttpClient {
/**
* 创建 HTTP 客户端实例
* @param {Object} config - 配置选项
* @param {string} [config.baseURL=''] - 基础 URL
* @param {number} [config.timeout=10000] - 超时时间(ms)
* @param {Object} [config.headers={}] - 默认请求头
*/
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 10000;
this.headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
...config.headers
};
}
/**
* 发起请求
* @param {Object} config - 请求配置
* @param {string} config.url - 请求地址
* @param {string} [config.method='GET'] - 请求方法
* @param {Object} [options={}] - 其他选项
*/
delete(url, options = {}) {
return this.request({ url, method: 'DELETE', ...options });
}
}1.3 使用示例
javascript
// 创建实例
const http = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'X-Custom-Header': 'value'
}
});
// GET 请求 - 获取用户列表
const users = await http.get('/users', { page: 1, size: 10 });
// 实际请求: https://api.example.com/users?page=1&size=10
// POST 请求 - 创建用户
const newUser = await http.post('/users', {
name: 'Alice',
email: 'alice@example.com'
});
// DELETE 请求 - 删除用户
await http.delete('/users/123');
// 自定义请求头
const data = await http.get('/protected', null, {
headers: {
'Authorization': 'Bearer token123'
}
});二、拦截器机制
拦截器可以在请求发送前或响应返回后进行统一处理,是请求封装的核心功能。
2.1 拦截器工作流程
code
请求发起 → 请求拦截器 → 发送请求 → 接收响应 → 响应拦截器 → 返回数据
↓ ↓
添加 token 统一处理错误
修改配置 转换数据格式2.2 完整实现
javascript
class HttpClient {
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 10000;
this.headers = config.headers || {};
// 拦截器队列
this.interceptors = {
request: [], // 请求拦截器数组
response: [], // 响应拦截器数组
};
}
// 注册请求拦截器
useRequestInterceptor(onFulfilled, onRejected) {
this.interceptors.request.push({ onFulfilled, onRejected });
return this;
}
// 注册响应拦截器
useResponseInterceptor(onFulfilled, onRejected) {
this.interceptors.response.push({ onFulfilled, onRejected });
return this;
}
// 核心请求方法
async request({ url, method = 'GET', data, params, headers = {} }) {
let config = {
url: this.baseURL + url,
method,
data,
params,
headers: { ...this.headers, ...headers }
};
// 执行请求拦截器(可修改配置)
for (const { onFulfilled } of this.interceptors.request) {
config = await onFulfilled(config);
}
// 构建请求 URL
let fullUrl = config.url;
if (config.params) {
const query = new URLSearchParams(config.params).toString();
fullUrl += (fullUrl.includes('?') ? '&' : '?') + query;
}
// 发起请求
const response = await fetch(fullUrl, {
method: config.method,
headers: config.headers,
body: config.data ? JSON.stringify(config.data) : undefined
});
// 处理响应
let result = response.ok ? await response.json() : null;
// 执行响应拦截器
for (const { onFulfilled } of this.interceptors.response) {
result = await onFulfilled({ response, data: result });
}
return result;
}
post(url, data, options) {
return this.request({ url, method: 'POST', data, ...options });
}
}2.3 拦截器应用场景
场景一:自动添加认证 Token
javascript
const http = new HttpClient({ baseURL: '/api' });
// 请求拦截器:自动添加 token
http.useRequestInterceptor(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
(error) => {
// 请求配置错误的处理
console.error('请求配置错误:', error);
return Promise.reject(error);
}
);场景二:统一错误处理
javascript
// 响应拦截器:统一处理响应
http.useResponseInterceptor(
(response) => {
// 业务状态码判断
if (response.code !== 0) {
throw new Error(response.message || '请求失败');
}
// 返回业务数据
return response.data;
},
(error) => {
// 统一错误提示
if (error.message.includes('401')) {
// Token 过期,跳转登录
window.location.href = '/login';
} else if (error.message.includes('超时')) {
alert('网络请求超时,请稍后重试');
} else {
alert(error.message || '网络错误');
}
return Promise.reject(error);
}
);场景三:请求日志记录
javascript
// 请求拦截:记录请求开始
http.useRequestInterceptor((config) => {
console.log(`[请求开始] ${config.method} ${config.url}`, config);
config.startTime = Date.now();
return config;
});
// 响应拦截:记录请求完成
http.useResponseInterceptor(
(response) => {
const duration = Date.now() - response.config.startTime;
console.log(`[请求成功] 耗时 ${duration}ms`, response);
return response;
},
(error) => {
console.error('[请求失败]', error);
return Promise.reject(error);
}
);2.4 完整使用示例
javascript
// 创建实例
const http = new HttpClient({
baseURL: 'https://api.example.com',
timeout: 10000,
});
// 添加请求拦截器
http.useRequestInterceptor((config) => {
// 添加 token
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`
};
}
return config;
});
try {
const users = await http.get('/users', { page: 1 });
console.log('用户列表:', users);
} catch (error) {
console.error('获取失败:', error);
}三、错误处理
3.1 错误类型说明
| 错误类型 | 场景 | 处理方式 |
|---|---|---|
HttpError | HTTP 状态码错误 | 根据状态码提示用户 |
TimeoutError | 请求超时 | 提示超时或自动重试 |
NetworkError | 网络断开 | 提示检查网络连接 |
BusinessError | 业务逻辑错误 | 显示后端返回的错误信息 |
3.2 自定义错误类
javascript
/**
* HTTP 错误类
*/
class HttpError extends Error {
/**
* @param {string} message - 错误消息
* @param {number} status - HTTP 状态码
* @param {Object} data - 响应数据
*/
constructor(message, status, data = {}) {
super(message);
this.name = 'HttpError';
this.status = status;
this.data = data;
}
}
/**
* 业务错误类
*/
class BusinessError extends Error {
constructor(message, code, data = {}) {
super(message);
this.name = 'BusinessError';
this.code = code;
this.data = data;
}
}3.3 统一错误处理
javascript
class HttpClient {
// ... 其他代码
/**
* 发送请求(带完整错误处理)
*/
async request(options) {
try {
const response = await this._fetch(options);
return response;
} catch (error) {
// 转换为自定义错误
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
if (error.response) {
throw new HttpError(error.message, error.response.status, error.response.data);
}
throw new NetworkError(error.message);
}
}
// 带超时控制的内部请求
async _fetch(options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(options.url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
}3.4 错误处理最佳实践
javascript
// 创建 HTTP 实例
const http = new HttpClient({
baseURL: '/api',
timeout: 10000,
});
// 使用示例
async function fetchUserData(userId) {
try {
const data = await http.get(`/users/${userId}`);
return data;
} catch (error) {
// 根据错误类型分别处理
if (error instanceof HttpError) {
if (error.status === 401) {
// 未授权,跳转登录
redirectToLogin();
} else if (error.status === 404) {
showToast('用户不存在');
}
} else if (error instanceof TimeoutError) {
showToast('请求超时,请重试');
} else {
showToast('网络异常,请检查连接');
}
throw error;
}
}
// 使用
fetchUserData(123)
.then(data => console.log('用户数据:', data))
.catch(() => console.log('获取用户数据失败'));3.5 全局错误处理
javascript
// 添加响应拦截器统一处理错误
http.useResponseInterceptor(
(response) => response,
(error) => {
// 记录错误日志
logError(error);
// 根据环境处理
if (process.env.NODE_ENV === 'development') {
console.error('[HTTP Error]', error);
}
// 用户提示
if (error instanceof HttpError) {
showErrorMessage(`请求失败(${error.status})`);
}
// 上报错误监控平台
reportErrorToMonitor(error);
}
);
// 错误上报函数
function reportErrorToMonitor(error) {
// 使用 sendBeacon 发送错误信息
navigator.sendBeacon('/api/errors', JSON.stringify({
message: error.message,
name: error.name,
stack: error.stack,
url: window.location.href,
timestamp: Date.now(),
}));
}四、请求缓存
请求缓存可以减少重复的网络请求,提升应用性能和用户体验。
4.1 缓存策略
code
┌─────────────┐
│ 发起请求 │
└──────┬──────┘
│
▼
┌──────────────┐ 是 ┌──────────┐
│ 缓存是否存在 │────────> │ 返回缓存 │
└──────┬───────┘ └──────────┘
│ 否
▼
┌──────────────┐ 是 ┌──────────────┐
│ 缓存是否过期 │────────> │ 重新请求更新 │
└──────┬───────┘ └──────────────┘
│ 否
▼
┌──────────────┐
│ 返回缓存数据 │
└──────────────┘4.2 内存缓存实现
javascript
/**
* 缓存项
*/
class CacheItem {
constructor(data, ttl) {
this.data = data;
this.timestamp = Date.now();
this.ttl = ttl; // 过期时间(毫秒)
}
// 检查是否过期
isExpired(entry) {
return Date.now() - entry.timestamp > entry.ttl;
}
// 读取缓存
get(key) {
const entry = this.cache.get(key);
if (!entry) return null;
if (this.isExpired(entry)) {
this.cache.delete(key);
return null;
}
return entry.data;
}
// 写入缓存
set(key, data, ttl = this.cacheConfig.ttl) {
// 超过容量时删除最旧缓存
if (this.cache.size >= this.cacheConfig.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, { data, timestamp: Date.now(), ttl });
}
// 带缓存和超时的请求
async _fetchWithCache(url, params, options) {
const cacheKey = url + JSON.stringify(params || {});
if (options.cache !== false) {
const cached = this.get(cacheKey);
if (cached) return cached;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
if (options.cache !== false) {
this.set(cacheKey, data, options.cacheTTL);
}
clearTimeout(timeoutId);
return data;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
}4.3 使用示例
javascript
// 创建带缓存的实例
const http = new HttpClientWithCache({
baseURL: '/api',
cacheConfig: {
enabled: true,
ttl: 3 * 60 * 1000, // 3分钟
maxSize: 50,
}
});
// 使用缓存(默认启用)
const users1 = await http.get('/users', { page: 1 });
// 发送请求并缓存
const users2 = await http.get('/users', { page: 1 });
// 从缓存返回(3分钟内)
// 禁用缓存
const freshData = await http.get('/users', { page: 1 }, {
cache: false
});
// 自定义缓存时间
const longCache = await http.get('/config', null, {
cacheTTL: 30 * 60 * 1000 // 30分钟
});
// 手动清除缓存
http.clearCache('/users'); // 清除包含 '/users' 的缓存
http.clearCache(); // 清除所有缓存4.4 高级缓存策略
javascript
/**
* 支持多种缓存策略的 HTTP 客户端
*/
class AdvancedHttpClient extends HttpClientWithCache {
constructor(config) {
super(config);
// 缓存策略
this.cacheStrategy = {
// 仅网络请求
'network-only': async (url, params, options) => {
return this.request({ url, method: 'GET', params, ...options });
},
// 优先缓存,未命中再请求
'cache-first': async (url, params, options) => {
const key = url + JSON.stringify(params || {});
const cached = this.get(key);
if (cached) return cached;
const data = await this.request({ url, method: 'GET', params, ...options });
this.set(key, data);
return data;
},
// 先缓存,后台重新验证更新
'stale-while-revalidate': async (url, params, options) => {
const key = url + JSON.stringify(params || {});
const cached = this.get(key);
if (cached) {
this.request({ url, method: 'GET', params, ...options })
.then(fresh => this.set(key, fresh));
return cached;
}
const data = await this.request({ url, method: 'GET', params, ...options });
this.set(key, data);
return data;
},
// 先网络,失败用缓存
'network-first': async (url, params, options) => {
try {
const data = await this.request({ url, method: 'GET', params, ...options });
this.set(url + JSON.stringify(params || {}), data);
return data;
} catch (error) {
const key = url + JSON.stringify(params || {});
const cached = this.get(key);
if (cached) return cached;
throw error;
}
}
};
this.getWithStrategy = async (strategy, url, params, options) => {
const fn = this.cacheStrategy[strategy];
if (!fn) throw new Error(`未知缓存策略: ${strategy}`);
return fn(url, params, options);
};
}
}
// 先缓存,后台更新(适合实时性要求不高的场景)
const config = await http.getWithStrategy('stale-while-revalidate', '/config');
// 先网络,失败用缓存(适合离线优先的应用)
const data = await http.getWithStrategy('network-first', '/users', { page: 1 });五、请求重试
网络请求可能因暂时性问题失败,自动重试机制可以提高请求成功率。
5.1 重试策略
code
第1次请求 ─失败─> 延迟1秒 ─> 第2次请求 ─失败─> 延迟2秒 ─> 第3次请求 ─失败─> 抛出错误
│ │
成功 成功
│ │
▼ ▼
返回结果 返回结果5.2 基础重试实现
javascript
/**
* 带重试的请求函数
* @param {Function} requestFn - 请求函数
* @param {Object} options - 重试选项
* @returns {Promise}
*/
async function requestWithRetry(requestFn, options = {}) {
const {
retries = 3, // 最大重试次数
delay = 1000, // 基础延迟时间
backoff = 'linear', // 退避策略:linear | exponential
maxDelay = 30000, // 最大延迟时间
retryable = () => true // 哪些错误需要重试
} = options;
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;
// 是否需要重试
if (attempt >= retries || !retryable(error)) break;
// 计算延迟时间
let delayTime;
if (backoff === 'exponential') {
delayTime = Math.min(delay * Math.pow(2, attempt), maxDelay);
} else {
delayTime = delay * (attempt + 1);
}
// 添加随机抖动,避免雪崩
delayTime += Math.random() * 100;
console.log(`请求失败,${delayTime}ms 后重试(第 ${attempt + 1} 次)`);
await new Promise(resolve => setTimeout(resolve, delayTime));
}
}
throw lastError;
}5.3 集成到 HttpClient
javascript
class HttpClient {
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 10000;
this.headers = config.headers || {};
// 重试配置
this.retryConfig = {
enabled: true,
retries: 3,
delay: 1000,
backoff: 'exponential',
maxDelay: 30000
};
}
// 带重试和超时的请求
async request(url, options = {}) {
const retry = options.retry ?? this.retryConfig;
if (!retry.enabled || retry.retries === 0) {
return this._fetch(url, options);
}
return requestWithRetry(() => this._fetch(url, options), {
retries: retry.retries,
delay: retry.delay,
backoff: retry.backoff,
maxDelay: retry.maxDelay
});
}
// 带超时的基础请求
async _fetch(url, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
if (!response.ok) {
throw new HttpError(`HTTP ${response.status}`, response.status);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
}5.4 使用示例
javascript
// 创建实例
const http = new HttpClient({
baseURL: '/api',
retry: {
enabled: true,
retries: 3,
delay: 1000,
backoff: 'exponential',
}
});
// 自动重试(使用默认配置)
const retried = await http.get('/flaky-api', null, {});
// 自定义重试次数
const custom = await http.get('/important', null, {
retry: { retries: 5, delay: 2000 }
});
// 禁用重试
const noRetry = await http.get('/critical', null, {
retry: { enabled: false }
});5.5 高级重试场景
javascript
/**
* 熔断器模式
* 当错误率达到阈值时,快速失败,避免无效请求
*/
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000;
this.failures = 0;
this.state = 'CLOSED'; // CLOSED | OPEN | HALF_OPEN
this.nextAttempt = Date.now();
}
// 是否允许发起请求
allowRequest() {
if (this.state === 'CLOSED') return true;
if (this.state === 'OPEN') {
// 达到重置时间,进入半开状态
if (Date.now() >= this.nextAttempt) {
this.state = 'HALF_OPEN';
return true;
}
return false; // 快速失败
}
// HALF_OPEN 状态
return true;
}
// 执行请求(带熔断保护)
async execute(fn) {
if (!this.allowRequest()) {
throw new Error('熔断器打开,请求被拒绝');
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
// 成功:重置计数
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
// 失败:累积并判断是否打开熔断器
onFailure() {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}
// 使用
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 60000 });
try {
const data = await breaker.execute(() => http.get('/api/data'));
console.log('请求成功:', data);
} catch (error) {
console.error('请求失败:', error.message);
}六、完整实现
6.1 生产级 HttpClient
javascript
/**
* 自定义错误类
*/
class HttpError extends Error {
constructor(message, status, data = {}) {
super(message);
this.name = 'HttpError';
this.status = status;
this.data = data;
this.timestamp = Date.now();
}
}
/**
* 生产级 HTTP 客户端
* 集成:拦截器、缓存、重试、超时、错误处理
*/
class HttpClient {
constructor(config = {}) {
this.baseURL = config.baseURL || '';
this.timeout = config.timeout || 10000;
this.headers = config.headers || {};
this.retryConfig = config.retry || { enabled: false };
this.interceptors = { request: [], response: [] };
this.cache = new Map();
}
// 注册拦截器
useRequestInterceptor(handler) {
this.interceptors.request.push(handler);
return this;
}
useResponseInterceptor(handler) {
this.interceptors.response.push(handler);
return this;
}
// 核心请求方法
async request({ url, method = 'GET', data, params, headers = {}, options = {} }) {
let config = {
url: this.baseURL + url,
method,
data,
params,
headers: { ...this.headers, ...headers }
};
// 请求拦截器
for (const handler of this.interceptors.request) {
config = await handler(config) || config;
}
// 构建 URL
let fullUrl = config.url;
if (config.params) {
const query = new URLSearchParams(config.params).toString();
fullUrl += (fullUrl.includes('?') ? '&' : '?') + query;
}
// 发送请求(带重试)
const fetchFn = () => fetch(fullUrl, {
method: config.method,
headers: config.headers,
body: config.data ? JSON.stringify(config.data) : undefined
}).then(async (response) => {
if (!response.ok) {
throw new HttpError(`HTTP ${response.status}`, response.status);
}
return response.json();
});
let result = this.retryConfig.enabled
? await requestWithRetry(fetchFn, this.retryConfig)
: await fetchFn();
// 响应拦截器
for (const handler of this.interceptors.response) {
result = await handler(result) || result;
}
return result;
}
get(url, params, options) {
return this.request({ url, method: 'GET', params, options });
}
post(url, data, options) {
return this.request({ url, method: 'POST', data, options });
}
delete(url, options) {
return this.request({ url, method: 'DELETE', options });
}
}
// 导出实例
export default HttpClient;6.2 使用示例
javascript
// 创建实例
const http = new HttpClient({
baseURL: '/api',
timeout: 10000,
headers: {
'X-App-Version': '1.0.0',
},
retry: {
enabled: true,
retries: 3,
delay: 1000,
backoff: 'exponential',
maxDelay: 30000
}
});
// 添加拦截器
http.useRequestInterceptor((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
http.useResponseInterceptor((response) => {
// 统一处理业务码
if (response && response.code !== 0) {
throw new Error(response.message || '业务错误');
}
return response;
});
// 使用
const users = await http.get('/users', { page: 1 });
// 自定义重试
const data = await http.get('/data', null, { retries: 5, retryDelay: 2000 });
// 清除缓存
http.clearCache('/users');七、API 参考
7.1 构造函数参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
baseURL | string | '' | 请求基础路径 |
timeout | number | 10000 | 请求超时时间(毫秒) |
headers | object | {} | 默认请求头 |
retry.enabled | boolean | true | 是否启用重试 |
retry.retries | number | 3 | 最大重试次数 |
retry.delay | number | 1000 | 重试延迟(毫秒) |
retry.backoff | string | 'exponential' | 退避策略 |
cache.enabled | boolean | false | 是否启用缓存 |
cache.ttl | number | 300000 | 缓存过期时间(毫秒) |
cache.maxSize | number | 100 | 最大缓存数量 |
7.2 实例方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
request(options) | Object | Promise | 发送请求 |
get(url, params, options) | string, Object, Object | Promise | GET 请求 |
post(url, data, options) | string, Object, Object | Promise | POST 请求 |
put(url, data, options) | string, Object, Object | Promise | PUT 请求 |
patch(url, data, options) | string, Object, Object | Promise | PATCH 请求 |
delete(url, options) | string, Object | Promise | DELETE 请求 |
useRequestInterceptor(onFulfilled, onRejected) | Function, Function | HttpClient | 添加请求拦截器 |
useResponseInterceptor(onFulfilled, onRejected) | Function, Function | HttpClient | 添加响应拦截器 |
clearCache(pattern) | string | void | 清除缓存 |
7.3 请求选项
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | - | 请求路径(必填) |
method | string | 'GET' | 请求方法 |
data | Object | - | 请求体数据 |
params | Object | - | URL 查询参数 |
headers | Object | - | 自定义请求头 |
timeout | number | - | 本次请求超时时间 |
retries | number | - | 本次请求重试次数 |
retryDelay | number | - | 本次请求重试延迟 |
cache | boolean | - | 是否启用缓存 |
cacheTTL | number | - | 本次请求缓存时间 |
八、最佳实践
8.1 实例管理
javascript
// ✅ 推荐:单例模式
// api/http.js
const http = new HttpClient({ baseURL: '/api' });
export default http;
// 使用
import http from '@/api/http';
// ❌ 不推荐:每次都创建新实例
const http = new HttpClient({ baseURL: '/api' });
const data = await http.get('/users');8.2 错误处理
javascript
// ✅ 推荐:统一错误处理 + 局部处理
http.useResponseInterceptor(
(res) => res,
(error) => {
// 全局错误处理
if (error.status === 401) {
// 跳转登录
}
return Promise.reject(error);
}
);
// 局部处理
try {
const data = await http.get('/users');
} catch (error) {
// 局部特殊处理
console.error('获取用户失败:', error);
}
// ✅ 推荐:错误类型判断
if (error instanceof HttpError) {
// HTTP 错误
} else if (error.name === 'TypeError') {
// 网络错误
}8.3 缓存使用
javascript
// ✅ 推荐:合理使用缓存
// 静态数据、配置数据使用长缓存
const config = await http.get('/config', null, { cacheTTL: 30 * 60 * 1000 });
// 动态数据禁用缓存或使用短缓存
const notifications = await http.get('/notifications', null, {
cache: false
});
// 数据更新后清除相关缓存
await http.post('/users', userData);
http.clearCache('/users'); // 清除用户相关缓存8.4 取消请求
javascript
// ✅ 推荐:避免重复请求
let pendingRequest = null;
async function searchUsers(query) {
// 取消之前的请求
if (pendingRequest) {
pendingRequest.abort();
}
const controller = new AbortController();
pendingRequest = controller;
try {
const result = await http.get('/users', { q: query }, {
signal: controller.signal,
});
pendingRequest = null;
return result;
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
throw error;
}
}
}九、常见问题
Q1: 如何处理文件上传?
javascript
class HttpClient {
/**
* 上传文件
*/
async upload(url, file, options = {}) {
const formData = new FormData();
formData.append('file', file);
// 添加其他字段
if (options.data) {
Object.entries(options.data).forEach(([key, value]) => {
formData.append(key, value);
});
}
// 使用 XHR 实现上传进度
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
options.onProgress?.(percent);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.open('POST', this.baseURL + url);
xhr.send(formData);
});
}
}
// 使用
const file = document.querySelector('#file').files[0];
await http.upload('/upload', file, {
data: { userId: 123 },
onProgress: (percent) => console.log(`上传进度: ${percent}%`),
});Q2: 如何实现请求进度监听?
javascript
/**
* 带进度的请求
*/
async requestWithProgress(url, data, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
onProgress?.(percent);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.open('POST', this.baseURL + url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
});
}Q3: 如何处理并发请求?
javascript
// 并发请求
const [users, posts, comments] = await Promise.all([
http.get('/users'),
http.get('/posts'),
http.get('/comments'),
]);
// 限制并发数量
async function limitConcurrency(tasks, limit = 3) {
const results = new Array(tasks.length);
const executing = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const taskIndex = index++;
try {
results[taskIndex] = await tasks[taskIndex]();
} catch (error) {
results[taskIndex] = { error };
}
}
}
// 启动 limit 个并发 worker
const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(workers);
return results;
}
// 使用
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5'];
const results = await limitConcurrency(
urls.map(url => () => http.get(url)),
2 // 最多同时 2 个请求
);Q4: 如何实现请求防抖?
javascript
/**
* 防抖请求
*/
function debounceRequest(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) {
clearTimeout(timer);
}
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
fn.apply(this, args)
.then(resolve)
.catch(reject);
}, delay);
});
};
}
// 使用
const searchUsers = debounceRequest(async (query) => {
return http.get('/users', { q: query });
}, 500);
// 输入时自动搜索(防抖)
input.addEventListener('input', (e) => {
searchUsers(e.target.value);
});💡 提示:请求封装是前端工程化的基础,合理的封装可以大幅提升开发效率和代码质量。建议根据项目实际需求选择合适的功能模块。
💡 提示:封装请求工具可以统一管理接口,提高代码可维护性。