西部数码网站管理助手2.0,会展设计专业发展前景,公司网站建设注意点,企业密信app下载安装目录 前言版本环境变量配置引入的类型1、AxiosIntance: axios实例类型2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型3、AxiosRequestConfig: 请求体配置参数类型4、AxiosError: 错误对象类型5、AxiosResponse: 完整原始响应体类型 目标效果开始封装骨架… 目录 前言版本环境变量配置引入的类型1、AxiosIntance: axios实例类型2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型3、AxiosRequestConfig: 请求体配置参数类型4、AxiosError: 错误对象类型5、AxiosResponse: 完整原始响应体类型 目标效果开始封装骨架在拦截器封装之前全局请求拦截器全局响应拦截器requst封装CRUDupload 完整代码:类型文件code配置封装的axios 使用源码地址 前言
axios 是一个流行的网络请求库简单易用。但实际上我们开发时候经常会出于不同的需求对它进行各种程度的封装。
最近在制作自己的脚手架时写了一个Vue3tsVite项目模板其中使用TypeScript对axios的基础请求功能进行了简单的封装在这里梳理一下思路也留作一个记录为后续其他功能封装做准备。
希望这篇文章能够帮助到刚学习axios和ts的小伙伴们。同时若文中存在一些错误或者设计不合理的地方也欢迎大家指正。
版本
axios 1.6.2TypeScript 5.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 typesvite/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实例类型 2、InternalAxiosRequestConfig: 高版本下AxiosRequestConfig的拓展类型 注意: 以前的版本下请求拦截器的 use方法 第一个参数类型是 AxiosRequestConfig但在高版本下更改为了 InternalAxiosRequestConfig如果发现使用 AxiosRequestConfig时报错, 请看一下自己版本下的相关类型声明。这里提供我的: 3、AxiosRequestConfig: 请求体配置参数类型 4、AxiosError: 错误对象类型 5、AxiosResponse: 完整原始响应体类型 从源码提供的类型可以很清晰地看到各参数或者类、方法中对应的参数、方法类型定义这可以非常直观地为我们指明路线
目标效果
通过这次基础封装我们想要的实现的效果是
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 从发送一个请求到接收响应并处理最后呈现给用户的流程。这样对各部分的封装会有一个更加合理的设计。 axios请求流程 - chatGPT绘制
全局请求拦截器
class HttpRequest {// ...constructor() {// ...this.service.interceptors.request.use(// ...);}
}在 axios v1.6.2 中根据上面的接口请求拦截器的 use方法 接受三个参数, 均是可传项 onFulfilled: 在请求发送前执行, 接受一个 config 对象并返回处理后的新 config对象一般在里面配置token等 这里要注意一点, 高版本 axios 将它的参数类型修改为了 InternalAxiosRequestConfig onRejected: onFulfilled 执行发生错误后执行接收错误对象一般我们请求没发送出去出现报错时执行的就是这一步 options其他配置参数接收两个参数, 均是可传项以后的进阶功能封装里可能会使用到 synchronous: 是否同步 runWhen: 接收一个类型为InternalAxiosRequestConfig的 config 参数返回一个 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})});}
}全局响应拦截器
同样是三个参数后两个和请求拦截器差不多说第一个就行。
类型定义如下: 第一个参数同样是 onFulfilled在返回响应结果之前执行我们需要在这里面取出后端返回的数据同时还要进行状态码处理。
从类型定义上可以看到参数类型是一个泛型接口 第一个泛型 T 用来定义后端返回数据的类型
先定义一下和后端约定好的返回数据格式:
我一般做项目时候约定的是这种可以根据实际情况进行修改
./types/index.ts
export interface ResponseModelT 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 但最好根据项目组实际情况维护一份和后端约定好的 code
然后就可以开始封装响应拦截器了。要注意返回的类型
import { CodeConfig } from ./codeConfig.ts
import { ResponseModel } from ./types/index.ts
class HttpRequest {// ...constructor() {// ...this.service.interceptors.response.use((response: AxiosResponseResponseModel): 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(){/**/}requestT any(config: AxiosRequestConfig): PromiseResponseModelT {/*** TODO: execute other methods according to config*/return new Promise((resolve, reject) {try {this.service.requestResponseModelT(config).then((res: AxiosResponse[data]) {resolve(res as ResponseModelT);}).catch((err) {reject(err)})} catch (err) {return Promise.reject(err)}})}
}CRUD
调用我们已经封装好的 request() 来封装 crud 请求而不是直接调用 axios 自带的, 原因上面已经说了
import { ResponseModel } from ./types/index.ts
class HttpRequest {// ...getT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: GET, ...config })}postT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: POST, ...config })}putT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: PUT, ...config })}deleteT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: DELETE, ...config })}
}upload
文件上传封装一般是表单形式上传它有特定的 Content-Type 和数据格式需要单独拿出来封装
先定义需要传入的数据类型 —— 和后端约定好的 name, 以及上传的文件数据 —— 本地临时路径或者Blob。在这里我是设置的上传文件的接口唯一所以希望把接口url配置在环境变量里在文件上传接口中不允许用户在接口的配置项参数里修改url于是新定义了一个 UploadFileItemModel 类型, 不允许用户在 options 里再传入 url 和 data
若有多个文件上传接口url, 可以根据实际情况进行修改
./types/index.ts
export interface UploadFileItemModel {name: string,value: string | Blob
}export type UploadRequestConfig OmitAxiosRequestConfig, url | data 一般来说文件上传完成后后端返回的响应数据中的data是被上传文件的访问url所以这里泛型 T 设置的默认值是 string
import { UploadFileItemModel } from ./types/index.ts
class HttpRequest {// ...uploadT string(fileItem: UploadFileItemModel, config?: UploadRequestConfig): PromiseResponseModelT | 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-dataconfigCopy config}return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })}完整代码:
类型文件
./types/index.ts
import { AxiosRequestConfig } from axios
export interface ResponseModelT 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 OmitAxiosRequestConfig, 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/indexclass 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: AxiosResponseResponseModel): 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);});}requestT any(config: AxiosRequestConfig): PromiseResponseModelT {/*** TODO: execute other methods according to config*/return new Promise((resolve, reject) {try {this.service.requestResponseModelT(config).then((res: AxiosResponse[data]) {resolve(res as ResponseModelT);}).catch((err) {reject(err)})} catch (err) {return Promise.reject(err)}})}getT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: GET, ...config })}postT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: POST, ...config })}putT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: PUT, ...config })}deleteT any(config: AxiosRequestConfig): PromiseResponseModelT {return this.request({ method: DELETE, ...config })}uploadT string(fileItem: UploadFileItemModel, config?: UploadRequestConfig): PromiseResponseModelT | 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-dataconfigCopy 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?typehistory
拆分一下:
baseURL: ‘https://api.vvhan.com/api’接口url: ‘/hotlist?typehistory’
把baseURL配置到环境变量里:
VITE_APP_BASE_URL https://api.vvhan.com/api根据接口文档修改 ResponseModel, 因为这个接口的响应数据里没有code那些, 所以封装里的code相关逻辑就先注释了, 直接返回原始响应体中的 data
export interface ResponseModelT {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/helloenum API {example /hotlist?typehistory
}export const exampleAPI () {return request.getexampleModel[]({ url: API.example })
}试一试:
script setup langts
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)
});
/scripttemplatedivHelloWorld msgVite Vue Tailwindcss TypeScript //div
/template提示很舒服 控制台打印的数据: 源码地址
v3-ts-tailwind-template中的axios封装文件