【axios】TypeScript实战,联合源码,从0到1教你封装一个axios - 基础封装 ...

种地  金牌会员 | 2024-7-20 13:44:31 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 846|帖子 846|积分 2538

本文已入选 [2023-11-29]|CSDN天天值得看|移动开发
  
  
媒介

axios 是一个盛行的网络哀求库,简单易用。但现实上,我们开发时间经常会出于不同的需求对它进行各种程度的封装。
近来在制作自己的脚手架时,写了一个Vue3+ts+Vite项目模板,其中利用TypeScript对axios的基础哀求功能进行了简单的封装,在这里梳理一下思路,也留作一个记录,为后续其他功能封装做准备。
希望这篇文章能够帮助到刚学习axios和ts的小伙伴们。同时,若文中存在一些错误大概设计不公道的地方,也欢迎大家指正。
版本



  • axios : 1.6.2
  • TypeScript : 5.3.2
环境变量设置

一般我们会利用环境变量来统一管理一些数据,比如网络哀求的 baseURL 。这个项目模板中,我将文件上传的接口地址、token的key也设置在了环境变量里。
.env.development
  1. # .env.production 和这个一样
  2. # the APP baseURL
  3. VITE_APP_BASE_URL = 'your_base_url'
  4. # the token key
  5. VITE_APP_TOKEN_KEY = 'your_token_key'
  6. # the upload url
  7. VITE_UPLOAD_URL = 'your_upload_url'
  8. # app title
  9. VITE_APP_TITLE = 'liushi_template'
复制代码
环境变量类型声明文件 env.d.ts:
  1. /// <reference types="vite/client" />
  2. export interface ImportMetaEnv {
  3.     readonly VITE_APP_TITLE: string
  4.     readonly VITE_APP_BASE_URL: string
  5.     readonly VITE_APP_TOKEN_KEY?: string
  6.     readonly VITE_UPLOAD_URL?: string
  7. }
  8. interface ImportMeta {
  9.     readonly env: ImportMetaEnv
  10. }
复制代码
然后,我们利用 类 来封装 axios
先引入 axios, 以及须要的类型
  1. import axios,
  2.     { AxiosInstance,
  3.       InternalAxiosRequestConfig,
  4.       AxiosRequestConfig,
  5.       AxiosError,
  6.       AxiosResponse,
  7.     } 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 和其中的类型在前面已经引入, 这里就先写一个骨架
  1. class HttpRequest {
  2.     service: AxiosInstance
  3.     constructor(){
  4.         // 设置一些默认配置项
  5.         this.service = axios.create({
  6.             baseURL: import.meta.env.VITE_APP_BASE_URL,
  7.             timeout: 5 * 1000
  8.         });
  9.     }
  10. }
  11. const httpRequest = new HttpRequest()
  12. export default httpRequest;
复制代码
在拦截器封装之前

为了封装出更加公道的拦截器,为以及进阶封装时为 axios 设置更加强大的功能,你需要起首相识一下 axios 从发送一个哀求到接收响应并处置惩罚,末了出现给用户的流程。这样,对各部分的封装会有一个更加公道的设计。

axios哀求流程 - chatGPT绘制

全局哀求拦截器

  1. class HttpRequest {
  2.     // ...
  3.     constructor() {
  4.         // ...
  5.         this.service.interceptors.request.use(
  6.             // ...
  7.         );
  8.     }
  9. }
复制代码
在 axios v1.6.2 中,根据上面的接口哀求拦截器的 use方法 接受三个参数, 均是可传项



  • onFulfilled: 在哀求发送前执行, 接受一个 config 对象并返回处置惩罚后的新 config对象,一般在里面设置token等
    这里要留意一点, 高版本 axios 将它的参数类型修改为了 InternalAxiosRequestConfig
  • onRejected: onFulfilled 执行发生错误后执行,接收错误对象,一般我们哀求没发送出去出现报错时,执行的就是这一步
  • options:其他设置参数,接收两个参数, 均是可传项,以后的进阶功能封装里大概会利用到


    • synchronous: 是否同步



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

相识了三个参数之后,思路就清晰了,然后我们可以根据需求进行全局哀求拦截器的封装
  1. class HttpRequest {
  2.     // ...
  3.     constructor() {
  4.         // ...
  5.         this.service.interceptors.request.use(
  6.             (config: InternalAxiosRequestConfig) => {
  7.                 /**
  8.                  * set your config
  9.                  */
  10.                 if (import.meta.env.VITE_APP_TOKEN_KEY && getToken()) {
  11.                     // carry token
  12.                     config.headers[import.meta.env.VITE_APP_TOKEN_KEY] = getToken()
  13.                 }
  14.                 return config
  15.             },
  16.             (error: AxiosError) => {
  17.                 console.log('requestError: ', error)
  18.                 return Promise.reject(error);
  19.             },
  20.             {
  21.                 synchronous: false,
  22.                 runWhen: ((config: InternalAxiosRequestConfig) => {
  23.                     // do something
  24.                     // if return true, axios will execution interceptor method
  25.                     return true
  26.                 })
  27.             }
  28.         );
  29.     }
  30. }
复制代码
全局响应拦截器

同样是三个参数,后两个和哀求拦截器差不多,说第一个就行。
类型定义如下:

第一个参数同样是 onFulfilled,在返反响应结果之前执行,我们需要在这里面取出后端返回的数据,同时还要进行状态码处置惩罚。
从类型定义上可以看到,参数类型是一个泛型接口, 第一个泛型 T 用来定义后端返回数据的类型
先定义一下和后端约定好的返回数据格式:
我一般做项目时间约定的是这种,可以根据现实环境进行修改
./types/index.ts
  1. export interface ResponseModel<T = any> {
  2.     success: boolean;
  3.     message: string | null;
  4.     code: number | string;
  5.     data: T;
  6. }
复制代码
因为里面定义了 code,以是还需要设置一份和后端约定好的 code 表,来对返回的 code 进行分类处置惩罚
./codeConfig.ts
  1. // set code cofig
  2. export enum CodeConfig {
  3.     success = 200,
  4.     notFound = 404,
  5.     noPermission = 403
  6. }
复制代码
其实axios本身也提供了一份 HttpStatusCode

但最好根据项目组现实环境维护一份和后端约定好的 code
然后就可以开始封装响应拦截器了。要留意返回的类型
  1. import { CodeConfig } from './codeConfig.ts'
  2. import { ResponseModel } from './types/index.ts'
  3. class HttpRequest {
  4.     // ...
  5.     constructor() {
  6.         // ...
  7.         this.service.interceptors.response.use(
  8.             (response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {
  9.                 const { data } = response
  10.                 const { code } = data
  11.                 if (code) {
  12.                     if (code != HttpCodeConfig.success) {
  13.                         switch (code) {
  14.                             case HttpCodeConfig.notFound:
  15.                                 // the method to handle this code
  16.                                 break;
  17.                             case HttpCodeConfig.noPermission:
  18.                                 // the method to handle this code
  19.                                 break;
  20.                             default:
  21.                                 break;
  22.                         }
  23.                         return Promise.reject(data.message)
  24.                     } else {
  25.                         return data
  26.                     }
  27.                 } else {
  28.                     return Promise.reject('Error! code missing!')
  29.                 }
  30.             },
  31.             (error: AxiosError) => {
  32.                 return Promise.reject(error);
  33.             }
  34.         );
  35.     }
  36. }
复制代码
在这个响应拦截器里,我们先通过解构赋值拿出了后端返回的响应数据 data, 然后提取出了里面约定好的 code,如果 code 是约定的表现一切成功的值,那么把响应数据返回, 否则根据 code 的不同值进行相应的处置惩罚。比如 把message里信息用 MessageBox 体现、登录过期清空token强制登出、无权限告诫、重新哀求等等
requst封装

重新封装 axios.request() 方法,传入一个config, 以后的进阶版本中,大概会修改传参,并在这个封装的 request() 中添加更多高级功能。但是在基础版本里,这一步看上去似乎有些冗余。
  1. import { ResponseModel } from './types/index.ts'
  2. class HttpRequest {
  3.     // ...
  4.     constructor(){/**/}
  5.     request<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
  6.         /**
  7.          * TODO: execute other methods according to config
  8.          */
  9.         return new Promise((resolve, reject) => {
  10.             try {
  11.                 this.service.request<ResponseModel<T>>(config)
  12.                     .then((res: AxiosResponse['data']) => {
  13.                         resolve(res as ResponseModel<T>);
  14.                     })
  15.                     .catch((err) => {
  16.                         reject(err)
  17.                     })
  18.             } catch (err) {
  19.                 return Promise.reject(err)
  20.             }
  21.         })
  22.     }
  23. }
复制代码
CRUD

调用我们已经封装好的 request() 来封装 crud 哀求,而不是直接调用 axios 自带的, 缘故原由上面已经说了
  1. import { ResponseModel } from './types/index.ts'
  2. class HttpRequest {
  3.     // ...
  4.    
  5.     get<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
  6.         return this.request({ method: 'GET', ...config })
  7.     }
  8.     post<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
  9.         return this.request({ method: 'POST', ...config })
  10.     }
  11.     put<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
  12.         return this.request({ method: 'PUT', ...config })
  13.     }
  14.     delete<T = any>(config: AxiosRequestConfig): Promise<ResponseModel<T>> {
  15.         return this.request({ method: 'DELETE', ...config })
  16.     }
  17. }
复制代码
upload

文件上传封装,一般是表单情势上传,它有特定的 Content-Type 和数据格式,需要单独拿出来封装
先定义需要传入的数据类型 —— 和后端约定好的 name, 以及上传的文件数据 —— 本地临时路径大概Blob。在这里我是设置的上传文件的接口唯一,以是希望把接口url设置在环境变量里,在文件上传接口中不允许用户在接口的设置项参数里修改url,于是新定义了一个 UploadFileItemModel 类型, 不允许用户在 options 里再传入 url 和 data
若有多个文件上传接口url, 可以根据现实环境进行修改
./types/index.ts
  1. export interface UploadFileItemModel {
  2.     name: string,
  3.     value: string | Blob
  4. }
  5. export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'>
复制代码
一般来说,文件上传完成后,后端返回的响应数据中的data是被上传文件的访问url,以是这里泛型 T 设置的默认值是 string
  1. import { UploadFileItemModel } from './types/index.ts'
  2. class HttpRequest {
  3.     // ...
  4.     upload<T = string>(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise<ResponseModel<T>> | null {
  5.         if (!import.meta.env.VITE_UPLOAD_URL) return null
  6.         let fd = new FormData()
  7.         fd.append(fileItem.name, fileItem.value)
  8.         let configCopy: UploadRequestConfig
  9.         if (!config) {
  10.             configCopy = {
  11.                 headers: {
  12.                     'Content-Type': 'multipart/form-data'
  13.                 }
  14.             }
  15.         } else {
  16.             config.headers!['Content-Type'] = 'multipart/form-data'
  17.             configCopy = config
  18.         }
  19.         return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy })
  20.     }
复制代码
完整代码:

类型文件

./types/index.ts
  1. import { AxiosRequestConfig } from 'axios'export interface ResponseModel<T = any> {
  2.     success: boolean;
  3.     message: string | null;
  4.     code: number | string;
  5.     data: T;
  6. }
  7. export interface UploadFileItemModel {    name: string,    value: string | Blob}/** * customize your uploadRequestConfig */export type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'>
复制代码
code设置

./codeConfig.ts
  1. // set code cofig
  2. export enum CodeConfig {
  3.     success = 200,
  4.     notFound = 404,
  5.     noPermission = 403
  6. }
复制代码
封装的axios

./axios.ts
  1. import axios,
  2.     { AxiosInstance,
  3.       InternalAxiosRequestConfig,
  4.       AxiosRequestConfig,
  5.       AxiosError,
  6.       AxiosResponse,
  7.     } from 'axios';
  8. import { CodeConfig } from './CodeConfig';import { ResponseModel, UploadFileItemModel, UploadRequestConfig } from './types/index'import { getToken } from '../token/index'class HttpRequest {    service: AxiosInstance    constructor() {        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: false                runWhen: ((config: InternalAxiosRequestConfig) => {                    // do something                    // if return true, axios will execution interceptor method                    return true                })            }        );        this.service.interceptors.response.use(            (response: AxiosResponse<ResponseModel>): AxiosResponse['data'] => {                const { data } = response                const { code } = data                if (code) {                    if (code != HttpCodeConfig.success) {                        switch (code) {                            case HttpCodeConfig.notFound:                                // the method to handle this code                                break;                            case HttpCodeConfig.noPermission:                                // the method to handle this code                                break;                            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 null        let fd = new FormData()        fd.append(fileItem.name, fileItem.value)        let configCopy: UploadRequestConfig        if (!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设置到环境变量里:
  1. VITE_APP_BASE_URL = 'https://api.vvhan.com/api'
复制代码
根据接口文档修改 ResponseModel, 因为这个接口的响应数据里没有code那些, 以是封装里的code相关逻辑就先解释了, 直接返回原始响应体中的 data
  1. export interface ResponseModel<T> {
  2.     data: T
  3.     subtitle: string
  4.     success: boolean
  5.     title: string
  6.     update_time: string
  7. }
复制代码
/src/api/types/hello.ts:定义后端返回给这个接口的数据中, data 的类型
  1. export interface exampleModel {
  2.     index: number
  3.     title: string
  4.     desc: string
  5.     url: string
  6.     mobilUrl: string
  7. }
复制代码
/src/api/example/index.ts:封装哀求接口,利用 enum 枚举类型统一管理接口地址
  1. import request from '@/utils/axios/axios'
  2. import { exampleModel } from '../types/hello'
  3. enum API {
  4.     example = '/hotlist?type=history'
  5. }
  6. export const exampleAPI = () => {
  7.     return request.get<exampleModel[]>({ url: API.example })
  8. }
复制代码
试一试:
  1. <script setup lang="ts">
  2. import HelloWorld from "../../components/HelloWorld.vue";
  3. import { exampleAPI } from "@/api/hello";
  4. exampleAPI().then((res) => {
  5.     console.log('getData: ', res)
  6.     const title = res.title
  7.     const { data } = res
  8.     console.log('list: ', data)
  9. });
  10. </script>
  11. <template>
  12.   <div>
  13.     <HelloWorld msg="Vite + Vue + Tailwindcss + TypeScript" />
  14.   </div>
  15. </template>
复制代码
提示很惬意


控制台打印的数据:

源码地址

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

种地

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表