ts 简易封装 axios,统一 API

news2025/7/22 15:39:47

文章目录

    • 为什么要封装
    • 目标
    • 文件结构
    • 封装通用请求方法
    • 获得类型提示
    • http 方法
    • 文件上传
    • 使用示例
      • 实例化
      • post 请求
      • 类型提示
      • 文件上传
    • 总结
    • 完整代码:

为什么要封装

axios 本身已经很好用了,看似多次一举的封装则是为了让 axios 与项目解耦。
比如想要将网络请求换成 fetch,那么只需按之前暴露的 api 重新封装一下 fetch 即可,并不需要改动项目代码。

目标

  1. 统一请求API
  2. 使用接口数据时能有代码提示

文件结构

│  index.ts					# 实例化封装类实例
│
├─http
│      request.ts  			# 封装axios
│
└─modules
       login.ts				# 业务模块
       upload.ts

封装通用请求方法

先封装一个通用的方法 request,然后在此基础上封装出 http 方法:

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));
        });
    }
}

获得类型提示

我希望在使用请求方法时,可以得到后端接口请求参数的提示,并且希望在使用响应结果时,也能得到类型提示。

因此设计了三个泛型:

  1. TReqBodyData:请求体类型
  2. TResStructure:接口响应结构类型
    1. TResData:接口响应 data 字段数据类型

并提供了一个默认的响应结构。使用时可以根据需要改成项目中通用的接口规则。当然在具体方法上也支持自定义响应接口结构,以适应一些不符合通用接口规则的接口。

/** 默认接口返回结构 */
export interface ResStructure<TResData = any> {
    code: number;
    data: TResData;
    msg?: string;
}

http 方法

由 request 方法封装出 http 方法同名的 api。

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 个参数:

  1. axios config 对象
  2. 表单内容
    1. 文件对象
    2. 文件对象的表单字段名
    3. hash
    4. 文件名
    5. 更多的表单数据(可通过泛型 TOtherFormData 指定类型)
  3. 上传进度回调
  4. 取消上传的 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"
            }
        });
    }

使用示例

实例化

import HttpRequest from "./request";

/** 实例化 */
const httpRequest = new HttpRequest({
    baseURL: "http://localhost:8080",
    timeout: 10000
});

post 请求

/** 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);
}

类型提示

获得使用请求方法时的请求接口参数类型提示:

获得请求接口时的参数类型提示

获得接口默认响应结构的提示:

获得接口默认响应结构的提示

  • 如果个别方法响应结构特殊,则可传入第三个泛型,自定义当前方法的响应结构
// 响应结构
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 字段的提示:

获得接口响应中 data 字段的提示

文件上传

/**
 * 文件上传
 */

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);
}

总结

  1. 在通用请求方法 request 基础上封装了同名的 http 方法
  2. 使用泛型可获得请求参数和请求结果的类型提示
  3. 额外封装了文件上传的方法

完整代码:

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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1157088.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apache Doris (五十): Doris表结构变更-动态分区(2)

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录

4.数据库的基本操作

1.创建数据库 系统安装完成后会有部分默认数据库存在: 注意&#xff1a;mysql的语句每一个输入完后要有分号才能写下一个 这初始的四个库不要删除 其中:mysql数据库中存储用户访问权限。 创建自己的数据库命令如下: create database database_name&#xff08;数据库名字&am…

【教3妹学编辑-算法题】参加会议的最多员工数

2哥 :“我要飞的更高&#xff0c; 飞的更高哦……” 3妹&#xff1a;2哥唱的啥呀&#xff0c; 都跑调啦 2哥 :3妹&#xff0c;昨天神州十六返回舱成功着陆&#xff0c;载人飞行任务取得圆满成功。中国真是太厉害了。 3妹&#xff1a;是啊&#xff0c; 此生无悔入华夏~不跟你多说…

案例分析真题-软件架构风格

案例分析真题-软件架构风格 2009年真题 【问题1】 【问题2】 【问题3】 2010年真题 【问题1】 【问题2】 【问题3】 2010年真题 【问题1】 【问题2】 【问题3】 2010年真题 【问题1】 【问题2】 【问题3】 2012年真题 【问题1】 骚戴理解&#xff1a; 首先要知道过滤器-管道的…

阿里云服务器99元一年续费原价注意事项不能变配!

阿里云优惠活动上提供的99元服务器为云服务器ECS经济型e实例&#xff0c;2核2G配置、3M公网带宽和40G ESSD entry 系统盘&#xff0c;续费不涨价&#xff0c;但是一定要注意不能变更云服务器配置&#xff0c;否则续费就涨价&#xff01;阿腾云atengyun.com来详细说下阿里云99元…

酷开科技酷开系统,打造专属家庭云课堂,让学习变得更温馨、有趣

随着科技和时代的不断发展&#xff0c;家庭云课堂已经成为了一种新型的学习方式。酷开科技就通过自主研发的酷开系统凭借其强大的功能和海量的内容&#xff0c;为家庭消费者们打造了一个专属的学习平台。 不用再为孩子享受不到教育资源而感到遗憾&#xff0c;在这里酷开系统为孩…

当女朋友要求你用Python画一个粉粉的Hello Kitty的时候

先看效果图 完整代码 import math import turtle as t# 计算长度、角度 t1:画笔对象 r:半径 angle:扇形&#xff08;圆形&#xff09;的角度 def myarc(t1, r, angle):arc_length 2 * math.pi * r * angle / 360 # angle角度的扇形的弧长n int(arc_length / 3) 1 # 线段…

高性能消息中间件 - Kafka3.x(一)

文章目录 高性能消息中间件 - Kafka3.x&#xff08;一&#xff09;搭建Kafka3.2.1集群⭐Kafka集群机器规划创建3台虚拟机&#xff08;centos7系统&#xff09;必要的环境准备&#xff08;3台虚拟机都要执行如下操作&#xff09;⭐分别修改每个服务器的hosts文件&#xff08;将上…

【zip密码】如何取消zip压缩包的密码?

我们想要对压缩包进行加密&#xff0c;但是当我们不需要加密压缩包的时候&#xff0c;该如何取消zip压缩包密码呢&#xff1f;那么zip压缩包密码取消都有什么方法呢&#xff1f;今天将方法总结分享给大家。 最原始的方法&#xff0c;就是通过解压文件&#xff0c;将解压出的文…

HTTP和HTTPS本质区别——SSL证书

HTTP和HTTPS是两种广泛使用的协议&#xff0c;尽管它们看起来很相似&#xff0c;但是它们在网站数据传输的安全性上有着本质上的区别。 HTTP是明文传输协议&#xff0c;意味着通过HTTP发送的数据是未经加密的&#xff0c;容易受到拦截、窃听和篡改的风险。而HTTPS通过使用SSL或…

使用了Buzz库的HttpClient类来设置代理

php<?php//设置代理服务器//初始化Buzz库$browsernew Browser();//使用代理服务器$browser->setHttpClient(new HttpClient(array(proxy_host>$proxy_host,proxy_port>$proxy_port,)));//创建Crawler对象$crawler$browser->getCrawler();//访问www.jd.com$craw…

全志R128芯片如何解决打包时出现的ERROR: update_mbr failed?

打包出现&#xff1a;ERROR: update_mbr failed 249930 records in 249930 records out 6398208 bytes (6.4 MB) copied, 0.0271082 s, 236 MB/s ERROR: dl file rtos_riscv.fex size too large ERROR: filename rtos_riscv.fex ERROR: dl_file_size 1579 sector ERROR: part…

研发效能城市沙龙【11月12日】深圳站-《敏捷环境下的测试自动化实践指南》—陈晓鹏丨IDCF

IDCF社区研发效能城市沙龙是一个开放、共享的平台&#xff0c;我们欢迎每一位参加者积极分享自己的经验和见解&#xff0c;构建一个互联互通的技术社区。 随着这几年业务的快速变化诉求以及敏捷开发方法的流行&#xff0c;越来越多的组织都采用敏捷模式进行项目开发。而这种时…

Project#2: Extendible Hash Index

文章目录 准备Task #1-Read/Write Page GuardsBasicPageGuard/ReadPageGuard/WritePageGuardUpgradeWrappersTests Task #2-Extendible Hash Table PagesHash Table Header Pages**成员变量&#xff1a;****方法实现&#xff1a;** Hash Table Directory Pages**成员变量&#…

财务数字化转型的切入点是什么?_光点科技

随着科技的不断进步&#xff0c;数字化转型已经成为各个行业追求的目标&#xff0c;财务领域也不例外。那么&#xff0c;财务数字化转型的切入点在哪里呢&#xff1f;如何确保转型的成功进行&#xff1f; 数据整合与管理 财务数据的准确性与及时性是财务管理的基石。数字化转型…

1.认识下Docker

1.Docker为什么会火? Docker能火起来&#xff0c;不仅仅与它开源有关系&#xff0c;一定是解决了我们软件从开发到上线的痛点&#xff0c;要不然几乎不可能。那Docker到底解决了什么问题&#xff1f;下面这几点&#xff0c;相信我们程序员都深有感触: 1. 解决环境不一致的问…

pthread_attr_getstacksize 问题

最近公司里遇到一个线程栈大小的问题&#xff0c;借此机会刚好学习一下这个线程栈大小相关的函数。如果公司里用的还是比较老的代码的话&#xff0c;都是用的 pthread 库支持线程的&#xff0c;而不是 c11 里的线程类。主要有两个相关函数&#xff1a;pthread_attr_setstacksiz…

Android 10-11适配外部存储方案

Android Api 29 对文件和文件夹进行了重大更改。不允许使用外部存储&#xff0c;如下方法&#xff1a; Environment.getExternalStorageDirectory() /mnt/sdcard Environment.getExternalStoragePublicDirectory(“test”) /mnt/sdcard/test 只能使用内部存储 getExterna…

安防监控项目---web网页通过A9控制Zigbee终端节点的风扇

文章目录 前言一、zigbee的CGI接口二、请求线程和硬件控制三、现象展示总结 前言 书接上期&#xff0c;我们可以看一下前面的功能设计的部分&#xff0c;网页端的控制还有一个&#xff0c;那就是通过网页来控制zigbee上的风扇节点&#xff0c;这部分的工作量是相当大的&#x…

管理类联考——英语二——阅读篇——题材:心理

文章目录 2013年&#xff0c;Text 3——题材&#xff1a;心理细节题&#xff08;难&#xff09;细节题——排除法细节题细节题观点态度题 2015 年&#xff0c;Text 1——题材&#xff1a;心理细节题细节题推断题词义句意题细节题 2019 年&#xff0c;Text 1——题材&#xff1a…