Nest.js从0到1搭建博客系统---Ts+Axios 二次封装(12)

axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

封装的目的

  • 简化请求的配置
  • 统一处理请求和响应
  • 处理特殊业务逻辑场景
  • 提高维护性,减少代码量

封装实现功能

  • 全局拦截
  • 实例拦截
  • 单个请求拦截
  • 控制全局loaing
  • 控制全局消息提示
  • 取消接口重复请求

目录结构

types.ts 类型文件

ts 复制代码
/*
 * @Author: vhen
 * @Date: 2024-01-20 23:25:05
 * @LastEditTime: 2024-01-23 17:26:09
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\http\errorHandler.ts
 *
 */

export const HttpStatus = (status: number) => {
  let message: string = "";
  switch (status) {
    case 400:
      message = "请求错误(400)";
      break;
    case 401:
      message = "token失效,请重新登录(401)";
      break;
    case 403:
      message = "拒绝访问(403)";
      break;
    case 404:
      message = "请求出错(404)";
      break;
    case 406:
      message = "请求的格式不可得(406)";
      break;
    case 408:
      message = "请求超时(408)";
      break;
    case 410:
      message = "请求的资源被永久删除,且不会再得到的(410)";
      break;
    case 500:
      message = "服务器故障(500)";
      break;
    case 501:
      message = "服务未实现(501)";
      break;
    case 502:
      message = "网络错误(502)";
      break;
    case 503:
      message = "服务不可用,服务器暂时过载或维护(503)";
      break;
    case 504:
      message = "网络超时(504)";
      break;
    case 505:
      message = "HTTP版本不受支持(505)";
      break;
    default:
      message = `服务器异常,请联系管理员!`;
  }
  return message;
}

httpRequest.tsx 网络请求封装文件

tsx 复制代码
/*
 * @Author: vhen
 * @Date: 2024-01-20 18:34:37
 * @LastEditTime: 2024-01-23 02:37:07
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\http\httpRequest.tsx
 *
 */
import { Spin, notification } from 'antd';
import axios, { AxiosError, AxiosInstance, AxiosResponse, Canceler } from 'axios';
import ReactDOM from 'react-dom/client';

import { HttpStatus } from './errorHandler';
import type { IResult, RequestConfig, RequestInterceptors } from './types';

class HttpRequest {
  // axios 实例
  private instance: AxiosInstance

  // 拦截器对象
  private interceptors?: RequestInterceptors<AxiosResponse>

  // 存储请求配置
  private requestConfig: RequestConfig = {
    isShowMessage: true,
    isShowSuccessMessage: false,
    isShowErrorMessage: true,
    cancelRequest: true,
    isShowLoading: true,
  }

  // 处理loading
  private requestCount: number = 0

  // 存放取消请求控制器Map
  private abortControllerMap: Map<string, AbortController | Canceler> = new Map()

  constructor(config: RequestConfig) {
    this.requestConfig = Object.assign(this.requestConfig, config)
    // axios.create创建axios实例,接收传递进来的配置
    this.instance = axios.create(config)
    this.interceptors = config?.interceptors
    this.setupInterceptors()
  }

  /**
   * 初始化拦截器
   */
  private setupInterceptors() {
    // 给每一个instance实例都添加拦截器
    this.instance.interceptors.request.use(
      (config: any) => {
        this.requestConfig = Object.assign(this.requestConfig, config)
        this.showLoading()
        // 如果有重复的,在这里就去除掉
        this.removePending(config as RequestConfig)
        // 添加下次的判定
        this.addPending(config as RequestConfig)
        // 一般会请求拦截里面加token,用于后端的验证
        const token = localStorage.getItem("token") as string
        if (token) {
          config.headers!.Authorization = `Bearer ${token}`;
        }
        console.log('全局请求成功的拦截')
        return config
      },
      (error: AxiosError) => {
        this.hideLoading()
        return Promise.reject(error)
      },
    )
    this.instance.interceptors.response.use(
      (response) => {
        this.hideLoading()
        const { data, status } = response
        console.log('全局响应成功的拦截')
        this.removePending(response.config as RequestConfig)
        if (status === 200) {
          return data
        }
        if (this.requestConfig.isShowMessage && this.requestConfig.isShowErrorMessage) {
          notification.error({
            message: '温馨提示',
            description: HttpStatus(status),
          })
        }
        return HttpStatus(status)
      },
      (error) => {
        this.hideLoading()
        return Promise.reject(error);
      },
    )
    // 针对特定的HttpRequest实例,比如HttpRequest2,添加拦截器
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptors,
      this.interceptors?.requestInterceptorsCatch,
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptors,
      this.interceptors?.responseInterceptorsCatch,
    )

  }

  /**
   * 生成每个请求唯一的键
   * @param config
   * @returns
   */
  private getPendingKey(config: RequestConfig): string {
    let { data } = config
    const { url, method, params } = config
    if (typeof data === 'string') data = JSON.parse(data)
    return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
  }

  /**
   * 储存每个请求唯一值,用于取消请求
   * @param config
   */
  private addPending(config: RequestConfig) {
    if (this.requestConfig?.cancelRequest) {
      const pendingKey = this.getPendingKey(config)
      config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
        if (!this.abortControllerMap.has(pendingKey))
          this.abortControllerMap.set(pendingKey, cancel)
      })
    }
  }

  /**
   * 删除重复的请求
   * @param config
   */
  private removePending(config: RequestConfig) {
    if (this.requestConfig?.cancelRequest) {
      const pendingKey = this.getPendingKey(config)
      if (this.abortControllerMap.has(pendingKey)) {
        const abortController = this.abortControllerMap.get(pendingKey)
        if (abortController instanceof AbortController) {
          abortController.abort()
        } else {
          abortController!(pendingKey)
        }
        this.abortControllerMap.delete(pendingKey)
      }
    }
  }

  /**
   * 显示loading
   */
  private showLoading() {
    if (this.requestConfig.isShowLoading && this.requestCount < 1) {
      const dom = document.createElement('div')
      dom.setAttribute('id', 'r-loading')
      dom.style.display = "flex"
      dom.style.alignItems = 'center'
      dom.style.justifyContent = 'center'
      dom.style.width = "100%"
      dom.style.height = "100%"
      if (this.requestConfig?.loadDom) {
        (document.getElementById(this.requestConfig.loadDom) as HTMLElement).appendChild(dom)
      } else {
        document.body.appendChild(dom)
      }

      ReactDOM.createRoot(dom).render(<Spin size="large" />)
    }
    this.requestCount++
  }

  /**
   * 隐藏loading
   */
  private hideLoading() {
    if (this.requestConfig.isShowLoading) {
      this.requestCount--
      if (this.requestCount === 0) {
        if (this.requestConfig.loadDom) {
          (document.getElementById(this.requestConfig.loadDom) as HTMLElement).removeChild(document.getElementById('r-loading') as HTMLElement)
        } else {
          document.body.removeChild(document.getElementById('r-loading') as HTMLElement)
        }
      }
    }
  }

  public request<T = any>(config: RequestConfig<T>): Promise<IResult<T>> {
    if (config?.url) {
      // 外层带https或http开头的url,不需要拼接baseURL,否则就读取实例中 baseUR
      const isHttp = config.url.indexOf('http') !== -1
      config.url = isHttp ? config.url : this.requestConfig.baseURL + config.url
    }
    return new Promise<IResult<T>>((resolve, reject) => {
      // 单个请求设置拦截器
      if (config.interceptors?.requestInterceptors) {
        this.instance.interceptors.request.use(config.interceptors.requestInterceptors)
      }
      this.instance
        .request<any, T>(config)
        .then(res => {
          // 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
          if (config.interceptors?.responseInterceptors) {
            res = config.interceptors.responseInterceptors(res)
          }
          resolve(res as IResult<T>)
        })
        .catch((error: any) => {
          reject(error)
        })
    })
  }

  // GET类型的网络请求
  public get<T = any>(url: string, config?: RequestConfig): Promise<AxiosResponse<IResult<T>>> {
    return this.instance.get(url, config)
  }

  // POST类型的网络请求
  public post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<AxiosResponse<IResult<T>>> {
    return this.instance.post(url, data, config);
  }

  // PUT类型的网络请求
  public put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<AxiosResponse<IResult<T>>> {
    return this.instance.put(url, data, config);
  }

  // DELETE类型的网络请求
  public delete<T = any>(url: string, config?: RequestConfig): Promise<AxiosResponse<IResult<T>>> {
    return this.instance.delete(url, config);
  }
}

export default HttpRequest;

index.ts 请求配置文件

ts 复制代码
/*
 * @Author: vhen
 * @Date: 2024-01-20 18:34:37
 * @LastEditTime: 2024-01-23 17:33:34
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\http\index.ts
 *
 */
import HttpRequest from "./httpRequest";

//  配置1
export const http1 = new HttpRequest({
  baseURL: 'https://api.vvhan.com/api',
  cancelRequest: true,
  interceptors: {
    requestInterceptors: config => {
      console.log('实例请求拦截器')

      return config
    },
    // 响应拦截器
    responseInterceptors: result => {
      console.log('实例响应拦截器')
      return result
    },
  }
})
//  配置2
export const http2 = new HttpRequest({
  baseURL: "https://jsonplaceholder.typicode.com",
  interceptors: {
    requestInterceptors: config => {
      console.log('实例请求拦截器')

      return config
    },
    // 响应拦截器
    responseInterceptors: result => {
      console.log('实例响应拦截器')
      return result
    },
  }
})
  • 最终实现

测试

  • 测试一
ts 复制代码
 import { http1, http2 } from '@/http';
 const getHistoryList = () => {
    http1.request({
      url: '/hotlist?type=history',
      method: 'post',
      loadDom: 'box',
      interceptors: {
        requestInterceptors(res) {
          console.log('单接口请求成功的拦截')

          return res
        },
        requestInterceptorsCatch(error) {
          console.log('单接口请求失败的拦截');
          return error
        },
        responseInterceptors(result) {
          console.log('单接口响应成功的拦截')
          return result
        },
        responseInterceptorsCatch(error) {
          console.log('单接口响应失败的拦截');
          return error
        }
      },
    }).then(res => {
      console.log('请求结果', res);
    })
  }

  useEffect(() => {
    getHistoryList()
  }, [])
  • 测试二
ts 复制代码
  useEffect(() => {
    getHistoryList()
    getHistoryList()
  }, [])
  • 测试三(局部挂载请求loading)
  • 测试四
ts 复制代码
 http1.get('/hotlist?type=history', {
      loadDom: 'box',
 })
// http1.post('/hotlist?type=history', {
//      loadDom: 'box',
// })

github

项目地址:nest_vhen_blog

结束语

本人菜鸟一枚,笔勤能使手快,多练能使手巧;如有不足之处,望海涵,欢迎关注一起学习,一起成长

相关推荐
洋流5 小时前
0基础进大厂,React框架基础篇:创建你的第一个React框架项目——梦开始的地方
react.js
多啦C梦a6 小时前
React 表单界的宫斗大戏:受控组件 VS 非受控组件,谁才是正宫娘娘?
前端·javascript·react.js
红衣信8 小时前
useContext 与 useReducer 的组合使用
前端·react.js·面试
归于尽8 小时前
key、JSX、Babel编译、受控组件与非受控组件、虚拟DOM考点解析
前端·react.js·面试
讨厌吃蛋黄酥9 小时前
`useState`是同步还是异步?深入解析闭包陷阱与解决方案
前端·react.js
東南9 小时前
知其然,知其所以然,前端系列之React
前端·react.js
TE-茶叶蛋9 小时前
React 服务器组件 (RSC)
服务器·前端·react.js
_一两风10 小时前
如何从零开始创建一个 React 项目
前端·react.js
然我13 小时前
JSX:看似 HTML 的 “卧底”?
前端·react.js·面试
AliciaIr13 小时前
深入React key的奥秘:从Diff算法到性能优化的核心考点
前端·react.js