Artikelverzeichnis
Warum Kapselung
Axios selbst ist bereits sehr einfach zu bedienen und die scheinbar mehrfache Kapselung soll Axios vom Projekt entkoppeln.
Wenn Sie beispielsweise Netzwerkanforderungen durch Abruf ersetzen möchten, müssen Sie den Abruf nur gemäß der zuvor bereitgestellten API neu kapseln und den Projektcode nicht ändern.
Ziel
- Einheitliche Anforderungs-API
- Bei der Verwendung von Schnittstellendaten gibt es Code-Eingabeaufforderungen
Dateistruktur
│ index.ts # 实例化封装类实例
│
├─http
│ request.ts # 封装axios
│
└─modules
login.ts # 业务模块
upload.ts
Kapseln Sie gängige Anforderungsmethoden
Kapseln Sie zunächst eine allgemeine Methodenanforderung und kapseln Sie dann die darauf basierende http-Methode:
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));
});
}
}
Holen Sie sich Typhinweise
Ich hoffe, dass ich bei Verwendung der Anforderungsmethode Hinweise auf die Anforderungsparameter der Backend-Schnittstelle erhalten kann, und ich hoffe, dass ich bei Verwendung der Antwortergebnisse auch Typhinweise erhalten kann.
Daher wurden drei Generika entwickelt:
- TReqBodyData: Anforderungstexttyp
- TResStructure: Typ der Schnittstellenantwortstruktur
- TResData: Datentyp des Schnittstellenantwortdatenfelds
Und stellt eine Standardantwortstruktur bereit. Wenn Sie es verwenden, können Sie es nach Bedarf in die allgemeinen Schnittstellenregeln im Projekt ändern. Natürlich unterstützen bestimmte Methoden auch benutzerdefinierte Antwortschnittstellenstrukturen, um sie an einige Schnittstellen anzupassen, die nicht den allgemeinen Schnittstellenregeln entsprechen.
/** 默认接口返回结构 */
export interface ResStructure<TResData = any> {
code: number;
data: TResData;
msg?: string;
}
http-Methode
Die Anforderungsmethode kapselt die API mit demselben Namen wie die http-Methode.
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" });
}
...
Datei-Upload
Beim Hochladen von Dateien werden im Allgemeinen Formdaten verwendet, wir können sie aber auch einfach kapseln.
Die Methode uploadFile empfängt 4 Parameter:
- axiosconfig-Objekt
- Inhalt bilden
- Dateiobjekt
- Der Formularfeldname des Dateiobjekts
- Hash
- Dateiname
- Weitere Formulardaten (
TOtherFormData
Typen können durch Generika angegeben werden)
- Fortschrittsrückruf hochladen
- Hochladen abbrechen
signal
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"
}
});
}
Anwendungsbeispiel
Instanziieren
import HttpRequest from "./request";
/** 实例化 */
const httpRequest = new HttpRequest({
baseURL: "http://localhost:8080",
timeout: 10000
});
Post-Anfrage
/** 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
});
}
/** 发起请求 */
async function handleClickPost() {
const res = await postReq({
user: "ikun", age: 18 });
console.log(res);
}
Geben Sie Hinweise ein
Erhalten Sie Hinweise zum Parametertyp der Anforderungsschnittstelle, wenn Sie Anforderungsmethoden verwenden:
Erhalten Sie einen Hinweis zur Standardantwortstruktur der Schnittstelle:
- Wenn die Antwortstruktur einer einzelnen Methode speziell ist, können Sie ein drittes generisches Element übergeben, um die Antwortstruktur der aktuellen Methode anzupassen.
// 响应结构
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
});
}
Benutzerdefinierte Schnittstellenantwortstruktur der aktuellen Methode:
Erhalten Sie Hinweise zum Datenfeld in der Schnittstellenantwort:
Datei-Upload
/**
* 文件上传
*/
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
);
}
// 发起请求
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);
}
Zusammenfassen
- Basierend auf der allgemeinen Anforderungsmethodenanforderung wird die gleichnamige http-Methode gekapselt
- Verwenden Sie Generika, um Typhinweise für Anforderungsparameter und Anforderungsergebnisse zu erhalten
- Zusätzliche Kapselung von Datei-Upload-Methoden
Vollständiger Code:
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;