【axios】TypeScript实战,结合源码,从0到1教你封装一个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/181364.shtml

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

相关文章

【古月居《ros入门21讲》学习笔记】15_ROS中的坐标系管理系统

目录 说明&#xff1a; 1. 机器人中的坐标变换 tf功能包能干什么&#xff1f; tf坐标变换如何实现 2. 小海龟跟随实验 安装 ros-melodic-turtle-tf 实验命令 运行效果 说明&#xff1a; 1. 本系列学习笔记基于B站&#xff1a;古月居《ROS入门21讲》课程&#xff0c;且使…

KT1404C语音芯片做的板子连接usb到电脑出来空的盘符 怎么处理?

一、问题简介 KT1404C画的板子&#xff0c;连接usb到电脑&#xff0c;出来空的盘符&#xff0c;可以确定KT404C没问题放别的板子OK&#xff0c;就是这个板子不正常&#xff0c;并且芯片5脚的电压输出是3.5v &#xff0c;正常的板子是3.3v&#xff0c;什么问题呢&#xff1f; 问…

数据结构之二叉树与堆以及力扣刷题函数扩展

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 数据结构初阶 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力 目录 1.前言 2.树 2.1概念 2.2树的相关概念 3.…

Python自动化办公:PDF文件的加密与解密

在本篇文章中&#xff0c;我们将介绍如何使用PyPDF2库对PDF文件进行加密和解密操作。 包括如何给PDF文件添加密码&#xff0c;以及如何从受密码保护的PDF文件中删除密码。 注&#xff1a;删除密码的操作&#xff0c;前提是需要知道密码哦 1. 安装PyPDF2库 首先&#xff0c;…

2023.11.26使用opencv调节图片亮度

2023.11.26使用opencv调节图片亮度 测试一些opencv对图片的处理效果&#xff0c;方法比较简单&#xff0c;找出所有像素点&#xff0c;然后将RGB三色的亮度分别进行调节即可&#xff0c;同类可以进行像素级的处理。测试结果和项目代码如下&#xff1a; 使用OpenCV调节图拍亮…

物理层之码分复用(内含相关例题)

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

数字人透明屏幕的技术原理是什么?

数字人透明屏幕的技术原理主要包括人脸识别和全息影像技术。其中&#xff0c;人脸识别技术是通过摄像头捕捉游客的面部表情和动作&#xff0c;并将其转化为数据指令&#xff0c;以便与数字人物进行互动。而全息影像技术则是利用透明屏幕&#xff0c;通过全息投影的方式将数字人…

基于Java+Vue+uniapp微信小程序商品展示系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

【古月居《ros入门21讲》学习笔记】12_服务端Server的编程实现

目录 说明&#xff1a; 1. 服务模型 说明 2. 实现过程&#xff08;C&#xff09; 创建服务器代码&#xff08;C&#xff09; 配置服务器代码编译规则 编译 运行 3. 实现过程&#xff08;Python&#xff09; 创建服务器代码&#xff08;Python&#xff09; 运行效果 说…

解决electron-build打包后运行app报错:cannot find module xxx

现象&#xff1a; 关于这个问题查了很多资料&#xff0c;也问了chatgpt都没有找到答案。 最后只能靠自己了。 于是冷静下来回想一下细节。突然发现了一个特别点。 eletron-builder打包时&#xff0c;强制要求eletron-builder和eletron必须都放在devDependencies 否则&#…

echarts图表滚动条带动页面窗口滚动条的问题

网上搜了很多方法不管用&#xff0c;后来发现每次滚动echarts或者左右滑动echarts下方都会报错&#xff0c;报错提示如下&#xff0c;看看你们的图表是否这样报错&#xff1a; 报错信息如下&#xff1a;Unable to preventDefault inside passive event listener invocation 原…

基于隐马尔可夫模型的一种流水印

文章信息 论文题目&#xff1a;An Invisible Flow Watermarking for Traffic Tracking: A Hidden Markov Model Approach 期刊&#xff08;会议&#xff09;&#xff1a;ICC 2019 - 2019 IEEE International Conference on Communications (ICC) 时间&#xff1a;2019 级别&am…

Elsevier的latex模板

1.关于Elsevier的letex模板&#xff08; version 2.3 2021-05-11)一开始直接编辑出现The key ‘stm/author/&#xff1c;options&#xff1e;‘ is unknown and 的问 LaTeX Error: The key stm/author/<options> is unknown and is being ignored. 关于Elsevier的letex…

居家适老化设计第三十四条---卫生间之照明

居家适老化卫生间照明设计需要考虑以下几个方面&#xff1a;1. 光源选择&#xff1a;选择适合老年人眼睛的柔和光源&#xff0c;避免刺眼和眩光的发生。可以选择LED灯具&#xff0c;因为它们具有节能、寿命长和可调光的特点。2. 光线布置&#xff1a;在不同区域设置不同的光线&…

使用docker命令_进入容器_登录mysql服务_并执行sql语句---Docker工作笔记005

今天就用到了,不得不说用docker用到的还是少,记录一下,常用的也就这些吧. 首先执行: docker ps [root@localhost dataease-1.18.9]# docker ps CONTAINER ID IMAGE COMMAND CREATED …

数据分享 I 全国各市城镇化率,shapeflie格式,附数据可视化

基本信息. ​数据名称: 城镇化率 数据格式: shapeflie 数据时间: 2006-2020年 数据几何类型: 面 数据坐标系: WGS84坐标系 数据来源&#xff1a;网络公开数据 数据可视化. 安徽省各市2015年城镇化率 广东省各市2015年城镇化率 江西省各市2015年城镇化率

【Redis6快速深入学习04】Redis字符串(String)的使用和原理

Redis字符串(String) 4.1 Redis 中 String 的简介 String 是Redis五种最基本的类型之一&#xff0c;在使用时可以理解成与Memcached一模一样的类型&#xff0c;一个key对应一个value。 String 类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如 jpg 图片或…

【开源】基于Vue+SpringBoot的学校热点新闻推送系统

项目编号&#xff1a; S 047 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S047&#xff0c;文末获取源码。} 项目编号&#xff1a;S047&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 新闻类型模块2.2 新闻档案模块2.3 新…

贪心算法策略实现

贪心算法 贪心算法&#xff1a;基于某种情况进行一个排序。 贪心算法得到的是优良解&#xff0c;而非全局最优解。需要证明局部最优解 全局最优解 经典贪心算法 —— 会议问题 对于这个问题 &#xff0c;我们提出贪心策略&#xff1a; 策略1&#xff1a;按照会议的持续时间长…

Spring的创建

文章目录 前言 一、创建一个Maven项目 二、添加spring框架支持 2.1在项目的pom.xml添加spring框架支持&#xff0c;xml配置如下  2.2添加包 总结 前言 我们通常所说的 Spring 指的是 Spring Framework&#xff08;Spring 框架&#xff09;&#xff0c;它是⼀个开源框架&a…