ts kapselt einfach Axios und vereinheitlicht die API

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

  1. Einheitliche Anforderungs-API
  2. 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:

  1. TReqBodyData: Anforderungstexttyp
  2. TResStructure: Typ der Schnittstellenantwortstruktur
    1. 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:

  1. axiosconfig-Objekt
  2. Inhalt bilden
    1. Dateiobjekt
    2. Der Formularfeldname des Dateiobjekts
    3. Hash
    4. Dateiname
    5. Weitere Formulardaten ( TOtherFormDataTypen können durch Generika angegeben werden)
  3. Fortschrittsrückruf hochladen
  4. Hochladen abbrechensignal
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 Parametertyphinweise, wenn Sie die Schnittstelle anfordern

Erhalten Sie einen Hinweis zur Standardantwortstruktur der Schnittstelle:

Erhalten Sie einen Hinweis zur Standardantwortstruktur einer 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:

Benutzerdefinierte Antwortstruktur

Erhalten Sie Hinweise zum Datenfeld in der Schnittstellenantwort:

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

  1. Basierend auf der allgemeinen Anforderungsmethodenanforderung wird die gleichnamige http-Methode gekapselt
  2. Verwenden Sie Generika, um Typhinweise für Anforderungsparameter und Anforderungsergebnisse zu erhalten
  3. 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;

Guess you like

Origin blog.csdn.net/qq_43220213/article/details/134153027