文章目录
为什么要封装
axios 本身已经很好用了,看似多次一举的封装则是为了让 axios 与项目解耦。
比如想要将网络请求换成 fetch,那么只需按之前暴露的 api 重新封装一下 fetch 即可,并不需要改动项目代码。
目标
- 统一请求API
- 使用接口数据时能有代码提示
文件结构
bash
│ index.ts # 实例化封装类实例
│
├─http
│ request.ts # 封装axios
│
└─modules
login.ts # 业务模块
upload.ts
封装通用请求方法
先封装一个通用的方法 request,然后在此基础上封装出 http 方法:
typescript
class HttpRequest {
private readonly instance: AxiosInstance;
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config);
}
request<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return new Promise<TResStructure>((resolve, reject) => {
this.instance
.request<any, AxiosResponse<TResStructure>>(config)
.then(res => {
// 返回接口数据
resolve(res?.data);
})
.catch(err => reject(err));
});
}
}
获得类型提示
我希望在使用请求方法时,可以得到后端接口请求参数的提示,并且希望在使用响应结果时,也能得到类型提示。
因此设计了三个泛型:
- TReqBodyData:请求体类型
- TResStructure:接口响应结构类型
- TResData:接口响应 data 字段数据类型
并提供了一个默认的响应结构。使用时可以根据需要改成项目中通用的接口规则。当然在具体方法上也支持自定义响应接口结构,以适应一些不符合通用接口规则的接口。
typescript
/** 默认接口返回结构 */
export interface ResStructure<TResData = any> {
code: number;
data: TResData;
msg?: string;
}
http 方法
由 request 方法封装出 http 方法同名的 api。
typescript
get<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config?: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "GET" });
}
post<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "POST" });
}
...
文件上传
文件上传一般使用 formdata,我们也可以简易封装一下。
uploadFile 方法接收 4 个参数:
- axios config 对象
- 表单内容
- 文件对象
- 文件对象的表单字段名
- hash
- 文件名
- 更多的表单数据(可通过泛型
TOtherFormData
指定类型)
- 上传进度回调
- 取消上传的
signal
typescript
export interface UploadFileParams<TOtherFormData = Record<string, any>> {
file: File | Blob; // 文件对象
fileHash?: string; // hash
filename?: string; // 文件名
filed?: string; // formdata 中文件对象的字段
formData?: TOtherFormData; // 文件其他的参数(对象 key-value 将作为表单数据)
}
/**
* 文件上传
* @param {AxiosRequestConfig} config axios 请求配置对象
* @param {UploadFileParams} params 待上传文件及其一些参数
* @param {(event: AxiosProgressEvent) => void} uploadProgress 上传进度的回调函数
* @param {AbortSignal}cancelSignal 取消axios请求的 signal
* @returns
*/
uploadFile<TOtherFormData>(
config: AxiosRequestConfig,
params: UploadFileParams<TOtherFormData>,
uploadProgress?: (event: AxiosProgressEvent) => void,
cancelSignal?: AbortSignal
) {
const formData = new window.FormData();
// 设置默认文件表单字段为 file
const customFilename = params.filed ?? "file";
// 是否指定文件名,没有就用文件本来的名字
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
formData.append("filename", params.filename);
} else {
formData.append(customFilename, params.file);
}
// 添加文件 hash
if (params.fileHash) {
formData.append("fileHash", params.fileHash);
}
// 是否有文件的额外信息补充进表单
if (params.formData) {
Object.keys(params.formData).forEach(key => {
const value = params.formData![key as keyof TOtherFormData];
if (Array.isArray(value)) {
value.forEach(item => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, value as any);
});
}
return this.instance.request({
...config,
method: "POST",
timeout: 60 * 60 * 1000, // 60分钟
data: formData,
onUploadProgress: uploadProgress,
signal: cancelSignal,
headers: {
"Content-type": "multipart/form-data;charset=UTF-8"
}
});
}
使用示例
实例化
typescript
import HttpRequest from "./request";
/** 实例化 */
const httpRequest = new HttpRequest({
baseURL: "http://localhost:8080",
timeout: 10000
});
post 请求
typescript
/** post 请求 */
// 定义请求体类型
interface ReqBodyData {
user: string;
age: number;
}
// 定义接口响应中 data 字段的类型
interface ResDataPost {
token: string;
}
export function postReq(data: ReqBodyData) {
return httpRequest.post<ReqBodyData, ResDataPost>({
url: "/__api/mock/post_test",
data: data
});
}
typescript
/** 发起请求 */
async function handleClickPost() {
const res = await postReq({ user: "ikun", age: 18 });
console.log(res);
}
类型提示
获得使用请求方法时的请求接口参数类型提示:
获得接口默认响应结构的提示:
- 如果个别方法响应结构特殊,则可传入第三个泛型,自定义当前方法的响应结构
typescript
// 响应结构
interface ResStructure {
code: number;
list: string[];
type: string;
time: number;
}
function postReq(data: ReqBodyData) {
return httpRequest.post<ReqBodyData, any, ResStructure>({
url: "/__api/mock/post_test",
data: data
});
}
当前方法自定义接口响应结构:
获得接口响应中 data 字段的提示:
文件上传
typescript
/**
* 文件上传
*/
interface OtherFormData {
fileSize: number;
}
function uploadFileReq(
fileInfo: UploadFileParams<OtherFormData>,
onUploadProgress?: (event: AxiosProgressEvent) => void,
signal?: AbortSignal
) {
return httpRequest.uploadFile<OtherFormData>(
{
baseURL: import.meta.env.VITE_APP_UPLOAD_BASE_URL,
url: "/upload"
},
fileInfo,
onUploadProgress,
signal
);
}
typescript
// 发起请求
const controller = new AbortController();
async function handleClickUploadFile() {
const file = new File(["hello"], "hello.txt", { type: "text/plain" });
const res = await uploadFileReq(
{ file, fileHash: "xxxx", filename: "hello.txt", formData: { fileSize: 1024 } },
event => console.log(event.loaded),
controller.signal
);
console.log(res);
}
总结
- 在通用请求方法 request 基础上封装了同名的 http 方法
- 使用泛型可获得请求参数和请求结果的类型提示
- 额外封装了文件上传的方法
完整代码:
typescript
import axios, { AxiosInstance, AxiosProgressEvent, AxiosRequestConfig, AxiosResponse } from "axios";
export interface UploadFileParams<TOtherFormData = Record<string, any>> {
file: File | Blob;
fileHash?: string;
filename?: string;
filed?: string;
formData?: TOtherFormData; // 文件其他的参数(对象 key-value 将作为表单数据)
}
/** 默认接口返回结构 */
export interface ResStructure<TResData = any> {
code: number;
data: TResData;
msg?: string;
}
/**
* A wrapper class for making HTTP requests using Axios.
* @class HttpRequest
* @example
* // Usage example:
* const httpRequest = new HttpRequest({baseURL: 'http://localhost:8888'});
* httpRequest.get<TReqBodyData, TResData, TResStructure>({ url: '/users/1' })
* .then(response => {
* console.log(response.name); // logs the name of the user
* })
* .catch(error => {
* console.error(error);
* });
*
* @property {AxiosInstance} instance - The Axios instance used for making requests.
*/
class HttpRequest {
private readonly instance: AxiosInstance;
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config);
}
/**
* Sends a request and returns a Promise that resolves with the response data.
* @template TReqBodyData - The type of the request body.
* @template TResData - The type of the `data` field in the `{code, data}` response structure.
* @template TResStructure - The type of the response structure. The default is `{code, data, msg}`.
* @param {AxiosRequestConfig} [config] - The custom configuration for the request.
* @returns {Promise<TResStructure>} - A Promise that resolves with the response data.
* @throws {Error} - If the request fails.
*
* @example
* // Sends a GET request and expects a response with a JSON object.
* const response = await request<any, {name: string}>({
* method: 'GET',
* url: '/users/1',
* });
* console.log(response.name); // logs the name of the user
*/
request<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return new Promise<TResStructure>((resolve, reject) => {
this.instance
.request<any, AxiosResponse<TResStructure>>(config)
.then(res => {
// 返回接口数据
resolve(res?.data);
})
.catch(err => reject(err));
});
}
/**
* 发送 GET 请求
* @template TReqBodyData 请求体数据类型
* @template TResData 接口响应 data 字段数据类型
* @template TResStructure 接口响应结构,默认为 {code, data, msg}
* @param {AxiosRequestConfig} config 请求配置
* @returns {Promise} 接口响应结果
*/
get<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config?: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "GET" });
}
/**
* 发送 post 请求
* @template TReqBodyData 请求体数据类型
* @template TResData 接口响应 data 字段数据类型
* @template TResStructure 接口响应结构,默认为 {code, data, msg}
* @param {AxiosRequestConfig} config 请求配置
* @returns {Promise} 接口响应结果
*/
post<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "POST" });
}
patch<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "PATCH" });
}
delete<TReqBodyData, TResData, TResStructure = ResStructure<TResData>>(
config?: AxiosRequestConfig<TReqBodyData>
): Promise<TResStructure> {
return this.request({ ...config, method: "DELETE" });
}
/**
* 获取当前 axios 实例
*/
getInstance(): AxiosInstance {
return this.instance;
}
/**
* 文件上传
* @param {AxiosRequestConfig} config axios 请求配置对象
* @param {UploadFileParams} params 待上传文件及其一些参数
* @param {(event: AxiosProgressEvent) => void} uploadProgress 上传进度的回调函数
* @param {AbortSignal}cancelSignal 取消axios请求的 signal
* @returns
*/
uploadFile<TOtherFormData = any>(
config: AxiosRequestConfig,
params: UploadFileParams<TOtherFormData>,
uploadProgress?: (event: AxiosProgressEvent) => void,
cancelSignal?: AbortSignal
) {
const formData = new window.FormData();
// 设置默认文件表单字段为 file
const customFilename = params.filed || "file";
// 是否指定文件名,没有就用文件本来的名字
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
formData.append("filename", params.filename);
} else {
formData.append(customFilename, params.file);
}
// 添加文件 hash
if (params.fileHash) {
formData.append("fileHash", params.fileHash);
}
// 是否有文件的额外信息补充进表单
if (params.formData) {
Object.keys(params.formData).forEach(key => {
const value = params.formData![key as keyof TOtherFormData];
if (Array.isArray(value)) {
value.forEach(item => {
// 对象属性值为数组时,表单字段加一个[]
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, value as any);
});
}
return this.instance.request({
...config,
method: "POST",
timeout: 60 * 60 * 1000, // 60分钟
data: formData,
onUploadProgress: uploadProgress,
signal: cancelSignal,
headers: {
"Content-type": "multipart/form-data;charset=UTF-8"
}
});
}
}
export default HttpRequest;