封装网络请求
1643字约5分钟
2024-11-11
项目中封装网络请求
以下列举常用的封装方式
- ajax请求
- axios请求 ...
ajax请求封装
1. 定义相关类型
// /src/types/api/index.ts
/**
* 为方法调用定义一个统一的配置接口
*/
export interface RequestOptions {
/**
* URL 查询参数 (主要用于 GET 请求)
*/
params?: Record<string, any>;
/**
* 请求体 (用于 POST, PUT, DELETE 请求)
*/
data?: any;
/**
* 自定义请求头
*/
headers?: Record<string, string>;
/**
* 进度回调
* @param progress - 进度百分比 (0-100)
*/
onProgress?: (progress: number) => void;
/**
* 请求超时时间 (单位: 毫秒)
*/
timeout?: number;
/**
* 用于取消请求的 AbortSignal
*/
signal?: AbortSignal;
}
/** 请求配置,不包含params和data */
export type RequestConfig = Omit<RequestOptions, 'params' | 'data' >2. 封装ajax请求
import { RequestOptions } from "@/types/api";
// 定义支持的 HTTP 方法
type Method = "GET" | "POST" | "PUT" | "DELETE";
/**
* 封装 XMLHttpRequest 的网络请求类
*/
export default class Request {
private static readonly baseURL: string = process.env.SDK_BASE_URL || "";
private static decoratorCache = new Map<string, Function>();
/**
* 类型守卫,用于判断一个对象是否是 RequestConfig 类型
* @param obj - 需要检查的对象
* @returns boolean
*/
private static isRequestConfig(obj: any): obj is RequestConfig {
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
return false;
}
// RequestConfig 的属性键
const configKeys: (keyof RequestConfig)[] = ["headers", "onProgress", "timeout", "signal"];
// 检查对象是否至少包含一个 RequestConfig 的键
return configKeys.some(key => key in obj);
}
private static ajax<T>(method: Method, url: string, options: RequestOptions = {}): Promise<T> {
return new Promise((resolve, reject) => {
const { params, data, headers, onProgress, timeout = 1000 * 60 * 5, signal } = options;
const xhr = new XMLHttpRequest();
// 如果请求已被取消,则直接拒绝
if (signal?.aborted) {
return reject(new DOMException("Request aborted", "AbortError"));
}
let fullURL: string;
// 检查 url 是否为绝对路径
if (/^(?:[a-z]+:)?\/\//i.test(url)) {
fullURL = url;
} else {
// 改进 URL 拼接,避免双斜杠或缺少斜杠的问题
fullURL =
(this.baseURL.endsWith("/") ? this.baseURL.slice(0, -1) : this.baseURL) +
(url.startsWith("/") ? url : "/" + url);
}
// 处理 URL 查询参数 (params)
if (params) {
const queryString = Object.keys(params)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join("&");
if (queryString) {
fullURL += (fullURL.includes("?") ? "&" : "?") + queryString;
}
}
xhr.open(method, fullURL, true);
// 设置超时时间
if (timeout) {
xhr.timeout = timeout;
}
// 设置请求头
if (headers) {
for (const key in headers) {
if (Object.prototype.hasOwnProperty.call(headers, key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
}
// 如果不是 FormData,并且是 POST/PUT/DELETE,且用户未提供 Content-Type,则设置默认值
const hasCustomContentType =
headers &&
(Object.prototype.hasOwnProperty.call(headers, "Content-Type") ||
Object.prototype.hasOwnProperty.call(headers, "content-type"));
if (
(method === "POST" || method === "PUT" || method === "DELETE") &&
data &&
!(data instanceof FormData) &&
!hasCustomContentType
) {
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
// 空响应体是有效的,直接 resolve
if (xhr.responseText === "") {
resolve("" as any);
return;
}
// 优先尝试解析 JSON,如果失败则直接返回文本
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(xhr.responseText as any);
}
} else {
// 提供更详细的错误信息
reject({
status: xhr.status,
statusText: xhr.statusText,
response: xhr.responseText,
});
}
};
// 监听请求错误
xhr.onerror = () => {
reject(new Error("Network Error"));
};
// 监听超时
xhr.ontimeout = () => {
reject(new Error(`Request timed out after ${timeout} ms`));
};
// 监听取消信号
const abortHandler = () => {
xhr.abort();
reject(new DOMException("Request aborted", "AbortError"));
};
if (signal) {
signal.addEventListener("abort", abortHandler);
}
// 清理逻辑,在请求完成、失败或中止时移除监听器
const cleanup = () => {
if (signal) {
signal.removeEventListener("abort", abortHandler);
}
};
xhr.addEventListener("loadend", cleanup);
// 监听进度 (包括上传和下载)
if (onProgress) {
// 上传进度
if (xhr.upload) {
xhr.upload.onprogress = (event: ProgressEvent) => {
if (event.lengthComputable) {
const progress = Math.floor((event.loaded / event.total) * 100);
onProgress(progress);
}
};
}
// 下载进度
xhr.onprogress = (event: ProgressEvent) => {
if (event.lengthComputable) {
const progress = Math.floor((event.loaded / event.total) * 100);
onProgress(progress);
}
};
}
// 发送请求
if (method === "GET") {
xhr.send();
} else {
if (data instanceof FormData) {
xhr.send(data);
} else if (data !== undefined && data !== null) {
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
}
});
}
/**
* 创建一个通用的HTTP方法装饰器工厂
* @param method - HTTP 方法 ('GET', 'POST', 'PUT', 'DELETE')
* @private
*/
private static createDecorator(method: Method) {
return (url: string) => {
const cacheKey = `${method}:${url}`;
if (this.decoratorCache.has(cacheKey)) {
return this.decoratorCache.get(cacheKey)!;
}
const decorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value; // 保存原始方法
descriptor.value = async function (...args: any[]) {
let payload: any;
let callTimeOptions: RequestConfig = {};
if (args.length === 1) {
const arg0 = args[0];
// 使用类型守卫来判断参数类型
if (Request.isRequestConfig(arg0)) {
callTimeOptions = arg0;
} else {
payload = arg0;
}
} else if (args.length >= 2) {
// 规则: 第一个参数是 payload, 第二个参数是 callTimeOptions
payload = args[0];
callTimeOptions = args[1] || {};
}
const finalOptions: RequestOptions = {
...callTimeOptions,
};
if (method === "GET" || method === "DELETE") {
finalOptions.params = { ...payload };
} else {
finalOptions.data = { ...payload };
}
const response = await Request.ajax<any>(method, url, finalOptions);
return await originalMethod.apply(this, [...args, response]);
};
};
this.decoratorCache.set(cacheKey, decorator);
return decorator;
};
}
public static get = Request.createDecorator("GET");
public static post = Request.createDecorator("POST");
public static put = Request.createDecorator("PUT");
public static delete = Request.createDecorator("DELETE");
}3.使用方式(案例)
// /src/api/user.ts
import { RequestConfig } from "@/types/api";
import Request from "@/utils/request";
export default class UserApi {
/**
* 获取一言。
* 此方法由 @Request.get 装饰器处理。
* `response` 参数将由装饰器在API调用成功后自动作为最后一个参数注入。
* @param params - (可选) 调用 API 时传入的参数。
* @param response - (由装饰器注入) API 响应。
* @returns 返回一个 Promise,其解析值为处理后的字符串。
*/
@Request.get("https://v2.xxapi.cn/api/yiyan")
public static async getImage(params?: ImageParams, response?: ImageResponse): Promise<string> {
if (response && typeof response === "object" && response.data) {
return response.data;
}
if (typeof response === "string") {
return response;
}
throw new Error("无效的API响应格式或未收到响应");
}
@Request.post("https://downloads.cursor.com")
public static async getVideo(data: any, config: RequestConfig, response?: string): Promise<string> {
return "";
}
}
// 任意文件 引入模块
// 示例用法:
(async () => {
try {
// data 为getImage方法逻辑处理后的数据
const data = await UserApi.getImage({ type: "hitokoto" });
console.log("获取到的数据:", data);
} catch (error) {
console.error("请求失败:", error);
}
})();4.注意
- 此方法,虽然TS使用较为方便,同样如果需要配置config,第一个参数体为必传,不然装饰器内部构造读取的内容会错误。
- 装饰器默认会
请求方式识别第一个参数为data还是params,如果需要自定义,请自行修改装饰器逻辑。 - 下列
axios请求也可采用同种方式,自行改造!
axios请求封装
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse, AxiosError, AxiosPromise, InternalAxiosRequestConfig } from "axios";
const { BASE_URL } = process.env;
const server = axios.create({
baseURL: BASE_URL,
timeout: 5000,
});
// 请求拦截器
server.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 处理config配置,如token等
return config;
},
(error: AxiosError) => {
console.log("请求出错:", error);
}
);
// 响应拦截器
server.interceptors.response.use(
(response: AxiosResponse) => {
// 处理不同返回结果的逻辑如:信息验证码错误、token过期等
const status = response.status;
if (status === 200) {
return Promise.resolve(response.data);
} else {
return Promise.reject(response.data);
}
},
(error: AxiosError) => {
console.log("响应出错:", error);
}
);
// 后续其他问题改造封装
type Methods = "GET" | "POST" | "PUT" | "DELETE";
function http(
method: Methods = "GET",
url: string,
data?: { [key: string]: any },
config: AxiosConfig = {
headers: {
"Content-Type": "application/json",
},
}
) {
const data_flag = method === "GET" ? true : false;
const request_config: AxiosRequestConfig = {
method,
url,
params: data_flag ? data : {},
data: data_flag ? {} : data,
...config,
};
return server(request_config);
}
type AxiosConfig = Omit<AxiosRequestConfig, "url" | "method" | "params" | "data">;
/** Axios 二次封装 */
export default class AxiosRequest {
static get(url: string, data: { [key: string]: any } = {}, config: AxiosConfig = {}): AxiosPromise<any> {
return http("GET", url, data, config);
}
static post(url: string, data: { [key: string]: any } = {}, config: AxiosConfig = {}): AxiosPromise<any> {
return http("POST", url, data, config);
}
static put(url: string, data: { [key: string]: any } = {}, config: AxiosConfig = {}): AxiosPromise<any> {
return http("PUT", url, data, config);
}
static delete(url: string, data: { [key: string]: any } = {}, config: AxiosConfig = {}): AxiosPromise<any> {
return http("DELETE", url, data, config);
}
}axios取消请求
根据以上封装做出取消请求的案例,实际情况自行甄别.
// AbortController 是全局终止控制器,其实就是一个实例签名
const controller = new AbortController();
AxiosRequest.get(
"/api/test",
{},
{
// 此处标识取消请求签名
signal: controller.signal,
}
);
// 在需要的地方直接调用,即可取消本次请求
controller.abort();