【axios封装】万字长文,TypeScript实战,封装一个axios - 基础封装篇

目录

  • 前言
  • 版本
  • 环境变量配置
    • 引入的类型
      • 1、AxiosIntance: axios实例类型
      • 2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型
      • 3、AxiosRequestConfig: 请求体配置参数类型
      • 4、AxiosError: 错误对象类型
      • 5、AxiosResponse: 完整原始响应体类型
  • 目标效果
  • 开始封装
    • 骨架
    • 在拦截器封装之前
    • 全局请求拦截器
    • 全局响应拦截器
    • requst封装
    • CRUD
    • upload
  • 完整代码:
    • 类型文件
    • code配置
    • 封装的axios
  • 使用
  • 源码地址

前言

axios 是一个流行的网络请求库,简单易用。但实际上,我们开发时候经常会出于不同的需求对它进行各种程度的封装。

最近在制作自己的脚手架时,写了一个Vue3+ts+Vite项目模板,其中使用TypeScript对axios的基础请求功能进行了简单的封装,在这里梳理一下思路,也留作一个记录,为后续其他功能封装做准备。

希望这篇文章能够帮助到刚学习axios和ts的小伙伴们。同时,若文中存在一些错误或者设计不合理的地方,也欢迎大家指正。

版本

  • axios1.6.2
  • TypeScript5.3.2

环境变量配置

一般我们会使用环境变量来统一管理一些数据,比如网络请求的 baseURL 。这个项目模板中,我将文件上传的接口地址、token的key也配置在了环境变量里。

.env.development

# .env.production 和这个一样
# the APP baseURL
VITE_APP_BASE_URL = 'your_base_url'# the token key
VITE_APP_TOKEN_KEY = 'your_token_key'# the upload url
VITE_UPLOAD_URL = 'your_upload_url'# app title
VITE_APP_TITLE = 'liushi_template'

环境变量类型声明文件 env.d.ts

/// <reference types="vite/client" />export interface ImportMetaEnv {readonly VITE_APP_TITLE: stringreadonly VITE_APP_BASE_URL: stringreadonly VITE_APP_TOKEN_KEY?: stringreadonly VITE_UPLOAD_URL?: string
}interface ImportMeta {readonly env: ImportMetaEnv
}

然后,我们使用 来封装 axios

先引入 axios, 以及必要的类型

import axios, { AxiosInstance,InternalAxiosRequestConfig, AxiosRequestConfig, AxiosError, AxiosResponse,} from 'axios';

在这里,我们引入了 axios,以及一些本次封装中会使用到的类型,

使用ts进行二次封装时,最好 ctrl+左键 看一下源码中对应的类型声明,这对我们有很大的帮助和指导作用。

引入的类型

1、AxiosIntance: axios实例类型

image.png

2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型

image.png

注意: 以前的版本下,请求拦截器的 use方法 第一个参数类型是 AxiosRequestConfig,但在高版本下,更改为了 InternalAxiosRequestConfig,如果发现使用 AxiosRequestConfig时报错, 请看一下自己版本下的相关类型声明。这里提供我的:

image.png

image.png

3、AxiosRequestConfig: 请求体配置参数类型

image.png

4、AxiosError: 错误对象类型

image.png

5、AxiosResponse: 完整原始响应体类型

image.png

从源码提供的类型可以很清晰地看到各参数或者类、方法中对应的参数、方法类型定义,这可以非常直观地为我们指明路线

目标效果

通过这次基础封装,我们想要的实现的效果是:

  • API的参数只填写接口和其他配置项、可以规定后端返回数据中 data 的类型
  • API直接返回后端返回的数据
  • 错误码由响应拦截器统一处理
  • 预留 扩展其他进阶功能的空间
  • nice的代码提示

开始封装

骨架

axios 和其中的类型在前面已经引入, 这里就先写一个骨架

class HttpRequest {service: AxiosInstanceconstructor(){// 设置一些默认配置项this.service = axios.create({baseURL: import.meta.env.VITE_APP_BASE_URL,timeout: 5 * 1000});}
}const httpRequest = new HttpRequest()
export default httpRequest;

在拦截器封装之前

为了封装出更加合理的拦截器,为以及进阶封装时为 axios 配置更加强大的功能,你需要首先了解一下 axios 从发送一个请求到接收响应并处理,最后呈现给用户的流程。这样,对各部分的封装会有一个更加合理的设计。

image.png

axios请求流程 - chatGPT绘制

全局请求拦截器

class HttpRequest {// ...constructor() {// ...this.service.interceptors.request.use(// ...);}
}

axios v1.6.2 中,根据上面的接口请求拦截器的 use方法 接受三个参数, 均是可传项

image.png

  • onFulfilled: 在请求发送前执行, 接受一个 config 对象并返回处理后的新 config对象,一般在里面配置token等

    这里要注意一点, 高版本 axios 将它的参数类型修改为了 InternalAxiosRequestConfig

  • onRejected: onFulfilled 执行发生错误后执行,接收错误对象,一般我们请求没发送出去出现报错时,执行的就是这一步

  • options:其他配置参数,接收两个参数, 均是可传项,以后的进阶功能封装里可能会使用到

    • synchronous: 是否同步
    • runWhen: 接收一个类型为InternalAxiosRequestConfigconfig 参数,返回一个 boolean。触发时机为每次请求触发拦截器之前,runWhen返回 true, 则执行作用在本次请求上的拦截器方法, 否则不执行

了解了三个参数之后,思路就清晰了,然后我们可以根据需求进行全局请求拦截器的封装

class HttpRequest {// ...constructor() {// ...this.service.interceptors.request.use((config: InternalAxiosRequestConfig) => {/*** set your config*/if (import.meta.env.VITE_APP_TOKEN_KEY && getToken()) {// carry tokenconfig.headers[import.meta.env.VITE_APP_TOKEN_KEY] = getToken()}return config},(error: AxiosError) => {console.log('requestError: ', error)return Promise.reject(error);},{synchronous: false,runWhen: ((config: InternalAxiosRequestConfig) => {// do something// if return true, axios will execution interceptor methodreturn true})});}
}

全局响应拦截器

同样是三个参数,后两个和请求拦截器差不多,说第一个就行。

类型定义如下:

image.png

第一个参数同样是 onFulfilled,在返回响应结果之前执行,我们需要在这里面取出后端返回的数据,同时还要进行状态码处理。

从类型定义上可以看到,参数类型是一个泛型接口, 第一个泛型 T 用来定义后端返回数据的类型

先定义一下和后端约定好的返回数据格式:

我一般做项目时候约定的是这种,可以根据实际情况进行修改

./types/index.ts

export interface ResponseModel<T = any> {success: boolean;message: string | null;code: number | string;data: T;
}

因为里面定义了 code,所以还需要配置一份和后端约定好的 code 表,来对返回的 code 进行分类处理

./codeConfig.ts

// set code cofig
export enum CodeConfig {success = 200,notFound = 404,noPermission = 403
}

其实axios本身也提供了一份 HttpStatusCode

image.png
但最好根据项目组实际情况维护一份和后端约定好的 code

然后就可以开始封装响应拦截器了。要注意返回的类型

import { CodeConfig } from './codeConfig.ts'
import { ResponseModel } from './types/index.ts'
class HttpRequest {// ...constructor() {// ...this.service.interceptors.response.use((response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {const { data } = responseconst { code } = dataif (code) {if (code != HttpCodeConfig.success) {switch (code) {case HttpCodeConfig.notFound:// the method to handle this codebreak;case HttpCodeConfig.noPermission:// the method to handle this codebreak;default:break;}return Promise.reject(data.message)} else {return data}} else {return Promise.reject('Error! code missing!')}},(error: AxiosError) => {return Promise.reject(error);});}
}

在这个响应拦截器里,我们先通过解构赋值拿出了后端返回的响应数据 data, 然后提取出了里面约定好的 code,如果 code 是约定的表示一切成功的值,那么把响应数据返回, 否则根据 code 的不同值进行相应的处理。比如 把message里信息用 MessageBox 显示、登录过期清空token强制登出、无权限警告、重新请求等等

requst封装

重新封装 axios.request() 方法,传入一个config, 以后的进阶版本中,可能会修改传参,并在这个封装的 request() 中添加更多高级功能。但是在基础版本里,这一步看上去似乎有些冗余。

import { ResponseModel } from './types/index.ts'
class HttpRequest {// ...constructor(){/**/}request<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {/*** TODO: execute other methods according to config*/return new Promise((resolve, reject) => {try {this.service.request<ResponseModel<T>>(config).then((res: AxiosResponse['data']) => {resolve(res as ResponseModel<T>);}).catch((err) => {reject(err)})} catch (err) {return Promise.reject(err)}})}
}

CRUD

调用我们已经封装好的 request() 来封装 crud 请求,而不是直接调用 axios 自带的, 原因上面已经说了

import { ResponseModel } from './types/index.ts'
class HttpRequest {// ...get<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'GET', ...config })}post<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'POST', ...config })}put<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'PUT', ...config })}delete<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'DELETE', ...config })}
}

upload

文件上传封装,一般是表单形式上传,它有特定的 Content-Type 和数据格式,需要单独拿出来封装

先定义需要传入的数据类型 —— 和后端约定好的 name, 以及上传的文件数据 —— 本地临时路径或者Blob。在这里我是设置的上传文件的接口唯一,所以希望把接口url配置在环境变量里,在文件上传接口中不允许用户在接口的配置项参数里修改url,于是新定义了一个 UploadFileItemModel 类型, 不允许用户在 options 里再传入 urldata

若有多个文件上传接口url, 可以根据实际情况进行修改

./types/index.ts

export interface UploadFileItemModel {name: string,value: string | Blob
}export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'> 

一般来说,文件上传完成后,后端返回的响应数据中的data是被上传文件的访问url,所以这里泛型 T 设置的默认值是 string

import { UploadFileItemModel } from './types/index.ts'
class HttpRequest {// ...upload<T = string>(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise<ResponseModel<T>> | null {if (!import.meta.env.VITE_UPLOAD_URL) return nulllet fd = new FormData()fd.append(fileItem.name, fileItem.value)let configCopy: UploadRequestConfigif (!config) {configCopy = {headers: {'Content-Type': 'multipart/form-data'}}} else {config.headers!['Content-Type'] = 'multipart/form-data'configCopy = config}return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })}

完整代码:

类型文件

./types/index.ts

import { AxiosRequestConfig } from 'axios'
export interface ResponseModel<T = any> {success: boolean;message: string | null;code: number | string;data: T;
}export interface UploadFileItemModel {name: string,value: string | Blob
}/*** customize your uploadRequestConfig*/
export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'> 

code配置

./codeConfig.ts

// set code cofig
export enum CodeConfig {success = 200,notFound = 404,noPermission = 403
}

封装的axios

./axios.ts

import axios, { AxiosInstance,InternalAxiosRequestConfig, AxiosRequestConfig, AxiosError, AxiosResponse,} from 'axios';
import { CodeConfig } from './CodeConfig';
import { ResponseModel, UploadFileItemModel, UploadRequestConfig } from './types/index'
import { getToken } from '../token/index'class HttpRequest {service: AxiosInstanceconstructor() {this.service = axios.create({baseURL: import.meta.env.VITE_APP_BASE_URL,timeout: 5 * 1000});this.service.interceptors.request.use((config: InternalAxiosRequestConfig) => {/*** set your config*/if (import.meta.env.VITE_APP_TOKEN_KEY && getToken()) {config.headers[import.meta.env.VITE_APP_TOKEN_KEY] = getToken()}return config},(error: AxiosError) => {console.log('requestError: ', error)return Promise.reject(error);},{synchronous: falserunWhen: ((config: InternalAxiosRequestConfig) => {// do something// if return true, axios will execution interceptor methodreturn true})});this.service.interceptors.response.use((response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {const { data } = responseconst { code } = dataif (code) {if (code != HttpCodeConfig.success) {switch (code) {case HttpCodeConfig.notFound:// the method to handle this codebreak;case HttpCodeConfig.noPermission:// the method to handle this codebreak;default:break;}return Promise.reject(data.message)} else {return data}} else {return Promise.reject('Error! code missing!')}},(error: any) => {return Promise.reject(error);});}request<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {/*** TODO: execute other methods according to config*/return new Promise((resolve, reject) => {try {this.service.request<ResponseModel<T>>(config).then((res: AxiosResponse['data']) => {resolve(res as ResponseModel<T>);}).catch((err) => {reject(err)})} catch (err) {return Promise.reject(err)}})}get<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'GET', ...config })}post<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'POST', ...config })}put<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'PUT', ...config })}delete<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {return this.request({ method: 'DELETE', ...config })}upload<T = string>(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise<ResponseModel<T>> | null {if (!import.meta.env.VITE_UPLOAD_URL) return nulllet fd = new FormData()fd.append(fileItem.name, fileItem.value)let configCopy: UploadRequestConfigif (!config) {configCopy = {headers: {'Content-Type': 'multipart/form-data'}}} else {config.headers!['Content-Type'] = 'multipart/form-data'configCopy = config}return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })}
}const httpRequest = new HttpRequest()
export default httpRequest;

使用

历史上的今天开放API做个测试: https://api.vvhan.com/api/hotlist?type=history

拆分一下:

  • baseURL: ‘https://api.vvhan.com/api’
  • 接口url: ‘/hotlist?type=history’

把baseURL配置到环境变量里:

VITE_APP_BASE_URL = 'https://api.vvhan.com/api'

根据接口文档修改 ResponseModel, 因为这个接口的响应数据里没有code那些, 所以封装里的code相关逻辑就先注释了, 直接返回原始响应体中的 data

export interface ResponseModel<T> {data: Tsubtitle: stringsuccess: booleantitle: stringupdate_time: string
}

/src/api/types/hello.ts:定义后端返回给这个接口的数据中, data 的类型

export interface exampleModel {index: numbertitle: stringdesc: stringurl: stringmobilUrl: string
}

/src/api/example/index.ts:封装请求接口,使用 enum 枚举类型统一管理接口地址

import request from '@/utils/axios/axios'
import { exampleModel } from '../types/hello'enum API {example = '/hotlist?type=history'
}export const exampleAPI = () => {return request.get<exampleModel[]>({ url: API.example })
}

试一试:

<script setup lang="ts">
import HelloWorld from "../../components/HelloWorld.vue";
import { exampleAPI } from "@/api/hello";
exampleAPI().then((res) => {console.log('getData: ', res)const title = res.titleconst { data } = resconsole.log('list: ', data)
});
</script><template><div><HelloWorld msg="Vite + Vue + Tailwindcss + TypeScript" /></div>
</template>

提示很舒服

image.png

image.png

控制台打印的数据:

image.png

源码地址

v3-ts-tailwind-template中的axios封装文件

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

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

相关文章

鸿运主动安全监控云平台存在任意文件读取漏洞 附POC

@[toc] 鸿运主动安全监控云平台存在任意文件读取漏洞 附POC 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该文章仅供学习用途…

蓝桥杯物联网竞赛_STM32L071_5_串口接收发送数据

理论&#xff1a; 串口采取异步通信&#xff0c;即不依赖时钟节拍来接收或发送数据&#xff0c;而是采用互相约定的波特率传输数据。 波特率与单位时间传输的比特数有关&#xff0c;波特率越大传输的数据越多 传输一个比特花费的时间T 1 / 比特率 接受和发送数据的时候需要…

leetcode:455. 分发饼干

一、题目 原题链接&#xff1a;455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09; 函数原型&#xff1a;int findContentChildren(int* g, int gSize, int* s, int sSize) 二、思路 将胃口数组和饼干尺寸数组降序排序 遍历两个数组&#xff0c;从胃口数组中找到符合饼干尺…

网络入门---网络的大致了解

目录标题 网络发展的简单认识协议作用的理解协议的本质什么是协议分层网络通信所面对的问题OSI七层模型TCP/IP模型协议报头的理解局域网通信局域网通信基本原理报头的问题局域网的特点跨网的网络链接如何查看mac地址 网络发展的简单认识 通过之前的学习我们知道计算机是给人提…

使用docker-compose优雅部署nacos

查看代码中引入nacos版本 在应用的pom.xml中搜索nacos关键字&#xff0c;找到相关的nacos依赖 点击以来左边的图标&#xff0c;找到依赖管理器中的pom.xml&#xff0c;并全局搜索nacos&#xff0c;即可找到对应的nacos客户端版本 使用docker-compose部署nacos version: 3s…

Jmeter-分布式压测(远程启动服务器,windows)

1 前提条件 JDK已部署&#xff0c;版本一致Jmeter已部署&#xff0c;版本一致多台服务器连接的同一网络(例如&#xff1a;同一wifi)防火墙处于关闭状态&#xff08;或者对应默认端口处于开放状态&#xff09;虚拟网络适配器都处于关闭状态查找到每一台服务器的IP 2 主服务器配…

蓝桥杯第229题 迷宫与陷阱 BFS C++ 模拟 带你理解迷宫的深奥

题目 迷宫与陷阱 - 蓝桥云课 (lanqiao.cn)https://www.lanqiao.cn/problems/229/learning/?page1&first_category_id1&name%E8%BF%B7%E5%AE%AB%E4%B8%8E%E9%99%B7%E9%98%B1 思路和解题方法 首先&#xff0c;定义了一个结构体node来表示迷宫中的每个节点&#xff0c;包…

3种在ArcGIS Pro中制作山体阴影的方法

山体阴影可以更直观的展现地貌特点&#xff0c;表达真实的地形&#xff0c;这里为大家介绍一下在ArcGIS Pro中制作山体阴影的方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&#xff0c;常见的GIS数据…

揭秘Git高手的10个秘密武器:让你的工作效率飙升!

Git和GitHub是每个软件工程师都必须了解的最基本的工具。它们是开发人员日常工作不可或缺的一部分&#xff0c;每天都要与之互动。 精通Git不仅能简化你的日常操作&#xff0c;还能显著提高生产力。在这篇文章中&#xff0c;我们将探讨一组能够极大提升生产力的命令。 随着对…

适用于 Windows 的最佳电脑数据恢复软件是什么?

数据丢失是数字世界中令人不快的一部分&#xff0c;它会在某一时刻影响许多计算机用户。很容易意外删除一些重要文件&#xff0c;这可能会在您努力恢复它们时带来不必要的压力。幸运的是&#xff0c;数据恢复软件可以帮助恢复已删除的文件&#xff0c;即使您没有备份它们。这是…

智慧公厕为高速服务区公厕做出的贡献

在现代社会&#xff0c;科技的飞速发展改变了人们的生活方式&#xff0c;也深刻影响着城市的基础设施和公共服务。而在这个数字化时代的背景下&#xff0c;智慧公厕作为城市智能化管理的一部分&#xff0c;为高速服务区公厕带来了一系列的创新和贡献&#xff0c;为旅客的出行提…

动态规划经典例题leetcode思路代码详解

目录 动态规划基础篇例题 leetcode70题.爬楼梯 leetcode746题.使用最小花费爬楼梯 leetcode198题.打家劫舍 leetcode62题.不同路径 leetcode64题.最小路径和 leetcode63题.63不同路径II 动态规划基础篇例题 这一篇的例题解答是严格按照我上一篇写的动态规划三部曲做的&…

P9242 [蓝桥杯 2023 省 B] 接龙数列(dp+最长接龙序列+分类)

1. 计算0~9为结尾的最长子串长度 2. 对于每个数字&#xff0c;比较其开头可连接子串长度1 与 原来以其末位为末尾的子串长度 3. 更新以其末位为末尾的子串长度 #include<iostream> #include<string.h>using namespace std;// 相当于记录…

万户协同办公平台ezoffice SendFileCheckTemplateEdit.jsp接口存在SQL注入漏洞 附POC

@[toc] 万户协同办公平台ezoffice SendFileCheckTemplateEdit.jsp接口存在SQL注入漏洞 附POC 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文…

力扣141-环形链表

文章目录 力扣141-环形链表示例代码实现要点剖析 力扣141-环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测…

计算机基础知识60

MySQL分组 # 概念&#xff1a;分组是按照某个指定的条件将单个单个的个体分成一个个整体 # MySQL分组的关键字&#xff1a;group by # 分组一般配合聚合函数使用&#xff1a; sum max min avg count 基本的语法格式: group by 字段名 [having 条件表达式] # 单独使用 group by关…

再探Java集合系列—ArrayList

适用于什么场景&#xff1f; 检索比较多的场景&#xff0c;例如学生成绩管理系统&#xff0c;老师对学生的成绩进行排名或查询操作 ArrayList有哪些特点&#xff1f; 1、ArrayList集合底层采用了数组数据结构&#xff0c;是Object类型 2、动态数组。ArrayList的默认初始容量…

第1章 理解知识图谱(一)

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

SpringBoot : ch11 整合RabbitMQ

前言 在当今的互联网时代&#xff0c;消息队列成为了构建高可靠、高性能系统的重要组件之一。RabbitMQ作为一个可靠、灵活的消息中间件&#xff0c;被广泛应用于各种分布式系统中。 本篇博客将介绍如何使用Spring Boot整合RabbitMQ&#xff0c;实现消息的发送和接收。通过这种…

视频集中存储/磁盘阵列EasyCVR平台黑名单异常解决步骤是什么?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…