Package source code analysis of Vue-vben-admin Vue3+TS Axios

Vue-vben-admin Vue3+TS Axios package source code analysis
Preface
1. Recently, Vue3+TS was used to refactor the previous Vue2 project, so I wanted to learn from the industry's better code. I searched on Git for a long time and was recommended by my colleagues. , I found that Vue-vben-admin produced by anncwd is very good. Up to now, there are 5.9k stars on git. After reading this template, I think the best written in it is for Axios Encapsulated, here I simply analyze the source code, which also involves some knowledge points of TS+Vue3, the most important thing is that we can find the particularly excellent places written in it, and we can also use it in our own projects.

 

1. Let's take a look at the directory structure in the source code first.

insert image description here
2. Directory analysis, this folder is divided into 6 files, among which index.ts is the entry file, Axios is the main class for secondary packaging of axios, axiosCancel is the package for cancellation request class canceler, and axiosTransform.ts is the definition Created a class that covers all hook functions for data processing regardless of errors or failures. checkStatus.ts is a processing function for when the backend returns code other than 200. helper is a function for processing time. These 6 files complement each other and build a The powerful secondary packaging of axios, let's take a look at the source code of each file

2. index.ts

1. Before looking at the source code of index.ts, I must take you to add a few knowledge points, so as to avoid a few places in it.

(1) The Partial<T> class in ts actually provides us with such a class in ts. Partial
can accept a T generic type, and can make all attributes in T optional. In other words, any type of organization, after Partial processing, all internal attributes become optional.

 interface A {
   a:string
   b:number
 }
 const a:A = {   //  error 因为A类型必须具备两个属性
   a:'张三'
 }
 
 const b:Partial<A> = {  // successful  经过Partial处理后,所有属性值变为可选
   a:'张三'
 }
 //看一下他的源码
 type Partial<T> = {
    [P in keyof T]?: T[P];
 };

 //其实就是给每一个属性加了一个?操作符而已,将所有属性变为非必须的了而已

(2)encodeURIComponent

//这个是JavaScript的原生Api ,可以把字符串作为 URI 组件进行编码。字母数字保持原来的样子,当时汉字或者其他所有符号都会将其编码为数字,字母或者%,

var uri="http://w3cschool.cc/my test.php?name=ståle&car=saab";
encodeURIComponent(uri)

// 输出:http%3A%2F%2Fw3cschool.cc%2Fmy%20test.php%3Fname%3Dst%C3%A5le%26car%3Dsaab

With the above supplements, let's take a look at the code directly. I have made comments, but the following code will depend on some types or functions. Let's go through it first. Let's mainly look at the structure, let's start

import { AxiosTransform } from './axiosTransform'
import { AxiosResponse, AxiosRequestConfig } from 'axios'
import { Result, RequestOptions, CreateAxiosOptions } from './types'
import { errorResult } from './const'
import { ResultEnum, RequestEnum, ContentTypeEnum,} from '../../../enums/httpEnum'
import { useMessage } from '@/hooks/web/useMessage'
import { isString, isObject, isBlob } from '@/utils/is'
import { formatRequestDate } from './helper'
import { setObjToUrlParams, deepMerge } from '@/utils'
import { getToken } from '@/utils/auth'
import { checkStatus } from './checkStatus'
import VAxios from './Axios'
const { createMessage, createErrorModal } = useMessage()
/**
 * 请求处理
 */
const transform: AxiosTransform = {   // 所谓transform 本质上就是  transform  这个对象中拥有 多个处理数据的钩子
  /**
   * 请求成功处理
   * 但依然要根据后端返回code码进行判断
   */
  transformRequestData: (  //  对后端返回的数据做处理, 这是在http状态码为200的时候
    res: AxiosResponse<Result>,
    options: RequestOptions
  ) => {
    const { isTransformRequestResult } = options
    if (!isTransformRequestResult) return res.data  // 不处理 直接返回res.data
    const { data } = res
    if (!data) return errorResult // 错误处理
    const { code, msg: message } = data
    if (code === ResultEnum.UNLOGIN) {
      const msg = '请重新登陆!'
      createMessage.error(msg)
      Promise.reject(new Error(msg))
      location.replace('/login')
      return errorResult
    }
    if (code === ResultEnum.BACKERROR) {
      const msg = '操作失败,系统异常!'
      createMessage.error(msg)
      Promise.reject(new Error(msg))
      return errorResult
    }

    const hasSuccess =
      data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS  // 200
    if (!hasSuccess) {  // 非200 非 401 非500 的情况 直接返回后端提示的错误
      if (message) {
        if (options.errorMessageMode === 'modal') {
          createErrorModal({ title: '错误提示', content: message })
        } else {
          createMessage.error(message)
        }
      }
      Promise.reject(new Error(message))
      return errorResult
    }

    if (code === ResultEnum.SUCCESS) return data  // 200 返回data

    if (code === ResultEnum.TIMEOUT) {
      const timeoutMsg = '登录超时,请重新登录!'
      createErrorModal({
        title: '操作失败',
        content: timeoutMsg,
      })
      Promise.reject(new Error(timeoutMsg))
      return errorResult
    }
    return errorResult
  },

  /*
  *  请求发送之前的钩子 说白了,本质上这个函数就是在处理发送之前的参数  用户永远只需要传params就可以传参数了
  */
  beforeRequestHook: (config: AxiosRequestConfig, options: RequestOptions) => {
    const { apiUrl, joinParamsToUrl, formatDate } = options
    if (apiUrl && isString(apiUrl)) {
      config.url = `${apiUrl}${config.url}`
    }

    if (config.method?.toUpperCase() === RequestEnum.GET) {  // 对get方法做处理,避免浏览器缓存数据,导致数刷新不及时
      const now = new Date().getTime()
      if (!isString(config.params)) {
        config.params = Object.assign(config.params || {}, { _t: now })
      } else {
        config.url = config.url + '/' + encodeURIComponent(config.params)
        config.params = undefined
      }
    } else {
      // 这个是post 或者 其他的非get方式的固定写法 必须将参数放在data当中
      if (!isString(config.params)) {
        formatDate && formatRequestDate(config.params)
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(config.url as string, config.params)
        } else {
          config.data = config.params
        }
        config.params = undefined
      } else {
        config.url = config.url + '/' + encodeURIComponent(config.params)
        config.params = undefined
      }
    }
    return config
  },

  // 请求拦截, 添加token  没什么说的
  requestInterceptors: (config) => {
    const token = getToken()
    if (token) {
      config.headers.Authorization = token
    }
    return config
  },

  // 当http状态码非200时的错误处理 
  responseInterceptorsCatch: (error: any) => {
    //todo
    const { response, code, message } = error || {}
    const err: string = error.toString()
    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
        createMessage.error('接口请求超时,请刷新页面重试!')
      }
      if (err && err.includes('Network Error')) {
        createErrorModal({
          title: '网络异常',
          content: '网络异常,请检查您的网络连接是否正常!',
        })
      }
    } catch (error) {
      throw new Error(error)
    }
    let msg = `服务器发生异常!`
    // 这是处理http状态码发生异常时的可能,使用服务器返回的msg提示,但状态取决于http状态码
    if (
      response &&
      response.data &&
      isObject(response.data) &&   // 假如data是个一般对象,说明可以判断
      response.data.code === ResultEnum.ERROR
    ) {
      msg = response.data.msg
      checkStatus(error.response && error.response.status, msg)
    } else if (response && response.data && isBlob(response.data)) {
      const text = new FileReader()
      text.readAsText(response.data)
      text.onload = function () {   //  本质还是解析再进行处理
        const obj = JSON.parse(text.result as string)
        msg = obj.code === ResultEnum.ERROR ? obj.msg : '服务器发生异常!'
        checkStatus(error.response && error.response.status, msg)
      }
    } else {
      // 使用默认的message提示
      checkStatus(error.response && error.response.status, msg)
    }
    return Promise.reject(error)
  },
}
function createAxios(opt?: Partial<CreateAxiosOptions>) {  // 把CreateAxiosOptions 中的每个属性变为可选的
  return new VAxios(   // 这个http会返回一个Vaxios的实例对象  在不传任何参数的情况下 默认传递以下的参数
    deepMerge(
      {
        timeout: 6 * 10 * 1000,
        headers: { 'Content-type': ContentTypeEnum.JSON },
        transform,
        requestOptions: {
          isTransformRequestResult: true, //是否转换结果
          joinParamsToUrl: false, //是否将参数添加到url
          formatDate: true, //是否格式化时间参数
          errorMessageMode: 'none', //错误消息的提示模式
          apiUrl: process.env.VUE_APP_API_URL, //api前缀
        },
      },
      opt || {}
    )
  )
}
export const http = createAxios()

Let me summarize what this index does.

1. Exporting a function call is actually exporting a VAxios instance object, and passing a lot of parameters by default, the parameters are these things

{
  timeout: 6 * 10 * 1000,
   headers: { 'Content-type': ContentTypeEnum.JSON },
   transform,
   requestOptions: {
     isTransformRequestResult: true, //是否转换结果
     joinParamsToUrl: false, //是否将参数添加到url
     formatDate: true, //是否格式化时间参数
     errorMessageMode: 'none', //错误消息的提示模式
     apiUrl: process.env.VUE_APP_API_URL, //api前缀
   },
 }

 

Later, it will decide how to process the data based on the values ​​of these parameters. The most important thing we need to look at is the content of transform. This is the key point.

2. tansform This is an object, which actually contains multiple hook functions, beforeRequestHook to process data before requesting, request interceptor requestInterceptors to add token, transformRequestData after successful request, that is, how to return data when the response returns successfully, Because sometimes we need to return the entire response, such as when downloading a file, sometimes we only need to return response.data, responseInterceptorsCatch, error handling when the http status code is not 200. In layman's terms, it can be understood that the transform object is a bunch of hook functions for processing data. Or do different logical operations according to the data situation.

3. In the end, at this time, we know that we are going to see what VAxios is, which is the core file.
Let's take a look! ! ! !

二、Axios.ts

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import {
  CreateAxiosOptions,
  UploadFileParams,
  RequestOptions,
  Result,
} from './types'
import { isFunction } from '@/utils/is'
import { ContentTypeEnum } from '../../../enums/httpEnum'
import { cloneDeep } from 'lodash-es'
import { errorResult } from './const'
import { AxiosCanceler } from './axiosCancel'
export default class VAxios {
  private axiosInstance: AxiosInstance   // axios实例本身
  private readonly options: CreateAxiosOptions  // 传递进来的 options

  constructor(options: CreateAxiosOptions) {
    this.options = options
    this.axiosInstance = axios.create(options)
    this.setupInterceptors()  //  将当前实例添加响应拦截
  }

  private createAxios(config: CreateAxiosOptions): void {  //  更新实例
    this.axiosInstance = axios.create(config)
  }

  getAxios(): AxiosInstance {
    return this.axiosInstance
  }

  //重新配置axios
  configAxios(config: CreateAxiosOptions) {
    if (!this.axiosInstance) {
      return
    }
    this.createAxios(config)
  }

  setHeader(headers: any): void {
    if (!this.axiosInstance) return
    Object.assign(this.axiosInstance.defaults.headers, headers)
  }

  private getTransform() {
    const { transform } = this.options
    return transform
  }
  /**
   * 拦截器配置
   */
  private setupInterceptors() {
    const transform = this.getTransform()
    if (!transform) return
    const {
      requestInterceptors,
      requestInterceptorsCatch,
      responseInterceptors,
      responseInterceptorsCatch,
    } = transform

    const axiosCanceler = new AxiosCanceler()

    //请求拦截器
    this.axiosInstance.interceptors.request.use(
      // 本项目中这里实际上是在添加token
      (config: AxiosRequestConfig) => {
        const {
          headers: { ignoreCancelToken } = { ignoreCancelToken: false },
        } = config
        !ignoreCancelToken && axiosCanceler.addPending(config)
        if (requestInterceptors && isFunction(requestInterceptors)) {
          config = requestInterceptors(config)
        }
        return config
      },
      undefined
    )
    //请求拦截器错误捕获
    requestInterceptorsCatch &&
      isFunction(requestInterceptorsCatch) &&
      // 本项目中这里其实没有任何处理
      this.axiosInstance.interceptors.request.use(
        undefined,
        requestInterceptorsCatch
      )
    //响应拦截器
    // 本项目中没啥用
    this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
      res && axiosCanceler.removePending(res.config)
      if (responseInterceptors && isFunction(responseInterceptors)) {
        res = responseInterceptors(res)
      }
      return res
    }, undefined)
    //响应拦截器错误捕获
    // http状态码为非200时的错误捕获
    responseInterceptorsCatch &&
      isFunction(responseInterceptorsCatch) &&
      this.axiosInstance.interceptors.response.use(
        undefined,
        responseInterceptorsCatch
      )
  }
  //文件上传
  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
    const formData = new window.FormData()
    const { useBean = true } = params
    if (useBean && params.data) {
      formData.append('bean', JSON.stringify(params.data))
    } else if (!useBean && params.data) {
      Object.keys(params.data).forEach((key) => {
        if (!params.data) return
        const value = params.data[key]
        if (Array.isArray(value)) {
          value.forEach((item) => {
            formData.append(`${key}[]`, item)
          })
          return
        }
        formData.append(key, params.data[key])
      })
    }
    params.file =
      Object.prototype.toString.call(params.file) === '[object File]'
        ? params.file
        : ''
    formData.append(params.name || 'file', params.file)
    const { requestOptions } = this.options
    if (requestOptions?.apiUrl) {
      config.url = requestOptions.apiUrl + config.url
    }
    const opt: RequestOptions = Object.assign({}, requestOptions)
    const transform = this.getTransform()
    const { requestCatch, transformRequestData } = transform || {}
    return new Promise((resolve, reject) => {
      this.axiosInstance
        .request<any, AxiosResponse<Result>>({
          ...config,
          data: formData,
          headers: {
            'Content-type': ContentTypeEnum.FORM_DATA,
          },
        })
        .then((res: AxiosResponse<Result>) => {
          if (transformRequestData && isFunction(transformRequestData)) {
            const ret = transformRequestData(res, opt)
            ret !== errorResult ? resolve(ret) : reject(new Error(ret))
            return
          }
          resolve((res as unknown) as Promise<T>)
        })
        .catch((e: Error) => {
          if (requestCatch && isFunction(requestCatch)) {
            reject(requestCatch(e))
            return
          }
          reject(e)
        })
    })
  }

  request<T = any>(
    config: AxiosRequestConfig,
    options?: RequestOptions
  ): Promise<T> {
    let conf: AxiosRequestConfig = cloneDeep(config)
    const transform = this.getTransform()
    const { requestOptions } = this.options
    const opt: RequestOptions = Object.assign({}, requestOptions, options)
    const { beforeRequestHook, requestCatch, transformRequestData } =
      transform || {}
    if (beforeRequestHook && isFunction(beforeRequestHook)) {
      conf = beforeRequestHook(conf, opt)  // conf 就是真正发送请求的url对象 || opt 就是原始对象和传递进来的对象的合集
    }
    return new Promise((resolve, reject) => {
      this.axiosInstance
        .request<any, AxiosResponse<Result>>(conf)
        .then((res: AxiosResponse<Result>) => {
          if (transformRequestData && isFunction(transformRequestData)) {
            const ret = transformRequestData(res, opt)
            ret !== errorResult ? resolve(ret) : reject(new Error(ret))
            return
          }
          resolve((res as unknown) as Promise<T>)
        })
        .catch((e: Error) => {
          if (requestCatch && isFunction(requestCatch)) {
            reject(requestCatch(e))
            return
          }
          reject(e)
        })
    })
  }
}

I'm also going to summarize what it did.

1. One sentence summary, first defines a class VAxios, this class has some private properties, some methods, and ends.

2. These attributes are the basic configuration items, that is, the Options I mentioned above. The second attribute is the axios instance object created based on these configuration items. All requests are actually sent using this instance object. sent.

3. There are some methods that are basically unnecessary, such as obtaining the current instance and changing the options configuration, which are basically unused and very simple. In the end, it is the setInterceptor method, the request method and the upload method that need to be looked at. The core of which is that the methods previously configured in transform will be used one by one in the process of sending requests by axios. So we can understand it as extending some hook functions.


3. Summarize
the idea of ​​this axios package. I conclude that what it exposes is relatively simple, so it is very easy to use. The upload is packaged and multiple hooks are expanded. This directory structure is more demanding. , it will be more clear and convenient to maintain, of course, the premise is to understand, in short, let's work together.

Guess you like

Origin blog.csdn.net/weixin_48927323/article/details/126050980