【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇

马肤

温馨提示:这篇文章已超过427天没有更新,请注意相关的内容是否还可用!

摘要:本篇文章将带你实战TypeScript,结合源码,从零开始教你封装一个axios。文章首先介绍axios的基础知识,然后逐步深入,讲解如何从基础开始封装axios。通过本文,你将学会如何在TypeScript环境下进行axios的封装,掌握相关技巧和注意事项。这对于提高开发效率和代码质量具有重要意义。

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

目录

  • 前言
  • 版本
  • 环境变量配置
    • 引入的类型
      • 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的小伙伴们。同时,若文中存在一些错误或者设计不合理的地方,也欢迎大家指正。

            版本

            • axios : 1.6.2
            • TypeScript : 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:

              /// 
              export interface ImportMetaEnv {
                  readonly VITE_APP_TITLE: string
                  readonly VITE_APP_BASE_URL: string
                  readonly VITE_APP_TOKEN_KEY?: string
                  readonly 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实例类型

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第1张

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

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第2张

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

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第3张

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第4张

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

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第5张

              4、AxiosError: 错误对象类型

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第6张

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

              【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第7张

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

              目标效果

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

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

                开始封装

                骨架

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

                class HttpRequest {
                    service: AxiosInstance
                    constructor(){
                        // 设置一些默认配置项
                        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】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第8张

                axios请求流程 - chatGPT绘制

                全局请求拦截器

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

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

                【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第9张

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

                      全局响应拦截器

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

                      类型定义如下:

                      【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第10张

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

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

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

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

                      ./types/index.ts

                      export interface ResponseModel {
                          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

                      【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第11张

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

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

                      import { CodeConfig } from './codeConfig.ts'
                      import { ResponseModel } from './types/index.ts'
                      class HttpRequest {
                          // ...
                          constructor() {
                              // ...
                              this.service.interceptors.response.use(
                                  (response: AxiosResponse): 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: 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(config: AxiosRequestConfig): Promise {
                              /**
                               * TODO: execute other methods according to config
                               */
                              return new Promise((resolve, reject) => {
                                  try {
                                      this.service.request(config)
                                          .then((res: AxiosResponse['data']) => {
                                              resolve(res as ResponseModel);
                                          })
                                          .catch((err) => {
                                              reject(err)
                                          })
                                  } catch (err) {
                                      return Promise.reject(err)
                                  }
                              })
                          }
                      }
                      

                      CRUD

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

                      import { ResponseModel } from './types/index.ts'
                      class HttpRequest {
                          // ...
                         
                          get(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'GET', ...config })
                          }
                          post(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'POST', ...config })
                          }
                          put(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'PUT', ...config })
                          }
                          delete(config: AxiosRequestConfig): Promise {
                              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 = Omit 
                      

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

                      import { UploadFileItemModel } from './types/index.ts'
                      class HttpRequest {
                          // ...
                          upload(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise | 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 })
                          }
                      

                      完整代码:

                      类型文件

                      ./types/index.ts

                      import { AxiosRequestConfig } from 'axios'
                      export interface ResponseModel {
                          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 
                      

                      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: 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): 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(config: AxiosRequestConfig): Promise {
                              /**
                               * TODO: execute other methods according to config
                               */
                              return new Promise((resolve, reject) => {
                                  try {
                                      this.service.request(config)
                                          .then((res: AxiosResponse['data']) => {
                                              resolve(res as ResponseModel);
                                          })
                                          .catch((err) => {
                                              reject(err)
                                          })
                                  } catch (err) {
                                      return Promise.reject(err)
                                  }
                              })
                          }
                          get(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'GET', ...config })
                          }
                          post(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'POST', ...config })
                          }
                          put(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'PUT', ...config })
                          }
                          delete(config: AxiosRequestConfig): Promise {
                              return this.request({ method: 'DELETE', ...config })
                          }
                          upload(fileItem: UploadFileItemModel, config?: UploadRequestConfig): Promise | 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配置到环境变量里:

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

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

                        export interface ResponseModel {
                            data: T
                            subtitle: string
                            success: boolean
                            title: string
                            update_time: string
                        }
                        

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

                        export interface exampleModel {
                            index: number
                            title: string
                            desc: string
                            url: string
                            mobilUrl: 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({ url: API.example })
                        }
                        

                        试一试:

                        import HelloWorld from "../../components/HelloWorld.vue";
                        import { exampleAPI } from "@/api/hello";
                        exampleAPI().then((res) => {
                            console.log('getData: ', res)
                            const title = res.title
                            const { data } = res
                            console.log('list: ', data)
                        });
                        
                        
                          

                        提示很舒服

                        【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第12张

                        【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第13张

                        控制台打印的数据:

                        【axios】TypeScript实战,结合源码,从0到1教你封装一个axios - 基础封装篇,TypeScript实战,从源码角度封装Axios——基础封装篇 第14张

                        源码地址

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


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码