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

结束语

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

相关推荐
牛仔只喝牛奶2 小时前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师5 小时前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie7 小时前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj508 小时前
前端基础之《React(11)—state声明式》
前端·react.js
张元清9 小时前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js
析数塔9 小时前
2026前端框架深度解析:React Compiler、Angular Zoneless、Vue 3.5重写游戏规则
前端·vue.js·react.js
杖雍皓9 小时前
没有服务器如何构建现代 Web 应用:平台选型与组合策略
运维·服务器·前端·react.js·github·netlify·vercel
sun_weitao9 小时前
微前端框架之qiankun 怎么用?vue3+vite+qiankun 和 react + qiankun
前端·react.js·前端框架
名字还没想好☜1 天前
React useEffect 依赖数组踩坑:闭包陷阱与清理函数
前端·javascript·react.js·react·useeffect
我是大卫1 天前
【图】React源码解析-第四部分:事件与跨层传递
react.js·源码