后台系统从零搭建(二)—— 系统架构设计2之Axios请求封装

322 阅读4分钟

本系列从零搭建一个后台系统,技术选型React18 + ReactRouter7 + Vite4 + Antd5 + zustand + TS。 这个系列文章将会从零开始,一步一步搭建一个后台系统,这个系统将会包括登录、权限、菜单、用户、角色等功能。

系统架构设计包含:路由封装、Axios请求封装、环境变量封装、storage模块封装(sessionStorage、localStorage)、公共函数封装(日期、金额、权限...)、通用交互定义(删除二次确认、列表、面包屑...)、接口全貌概览等。

已实现的项目地址,如果需要接口,还需要运行接口项目

后台系统从零搭建(一)—— 项目基础
后台系统从零搭建(二)—— 系统架构设计1之路由封装
后台系统从零搭建(二)—— 系统架构设计2之Axios请求封装
后台系统从零搭建(二)—— 系统架构设计3之环境变量封装
后台系统从零搭建(二)—— 系统架构设计4之CSSModule、主题、登录页
后台系统从零搭建(二)—— 系统架构设计5之公共布局Layout
后台系统从零搭建(二)—— 系统架构设计6之zustand状态管理
后台系统从零搭建(二)—— 系统架构设计7之菜单和路由的关联
后台系统从零搭建(三)—— 具体页面之用户管理(通用的增删改查逻辑和form-render)
后台系统从零搭建(三)—— 具体页面之菜单管理和角色管理
后台系统从零搭建(三)—— 具体页面之部门管理(抽离通用的增删改查逻辑)
后台系统从零搭建(四)—— 终结篇之权限系统怎么设计-RBAC模式

本文主要介绍系统架构设计之Axios请求封装。

1 安装Axios

安装Axios,并封装请求。

pnpm add axios

2 请求封装 - src/utils/request.ts

// src/utils/request.ts
import axios from 'axios'

const request = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL as string,
  timeout: 10000,
})

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    // 请求头添加token
    // config.headers['Authorization'] = 'Bearer ' + token
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    return Promise.reject(error)
  },
)

export default request

3 使用请求 - src/views/Login/index.tsx

// src/views/Login/index.tsx

import { Button } from 'antd'
import request from '@/utils/request'

const Login = () => {
  const handleLogin = () => {
    request.post('/login', { username: 'admin', password: '123456' }).then((res) => {
      console.log(res)
    })
  }

  return (
    <div>
      <Button onClick={handleLogin}>登录</Button>
    </div>
  )
}

export default Login

4 额外备注

请求拦截器和响应拦截器可以在request.ts中配置,也可以在App.tsx中配置。

// src/App.tsx
import request from '@/utils/request'
import { QueryClient, QueryClientProvider } from 'react-query'

const queryClient = new QueryClient()

request.interceptors.request.use(
  (config) => {
    // 请求头添加token
    // config.headers['Authorization'] = 'Bearer ' + token
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)

request.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    return Promise.reject(error)
  },
)

function App() {
  return (
    <div className='App'>
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router}>
          <Suspense fallback={<Spin fullscreen size='default' tip='页面正在加载...' />}>
            <Router />
          </Suspense>
        </RouterProvider>
      </QueryClientProvider>
    </div>
  )
}
export default App

5.扩展AxiosRequestConfig属性

AxiosRequestConfig是Axios请求的配置项,我们可以扩展AxiosRequestConfig属性,这样就可以在请求中使用自定义属性。

// src/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'

declare module 'axios' {
  interface AxiosRequestConfig {
    // 自定义属性 默认值为 false
    isHideLoading?: boolean
    isHideError?: boolean
  }
}

6.定义全局类型

在项目根目录下新建建global.d.ts中定义全局类型

// global.d.ts
declare type G_IResponse<T = any> = {
  data: T
  code: number
  message: string
  success: boolean
}

使用的时候在任意文件中都可以使用G_IResponse类型。

如果是在Window对象上加一个自定义属性,可以这样定义,这样就可以在任意文件中使用window.hua属性不会有类型报错信息。

// global.d.ts
interface Window {
  // 自定义属性
  hua: string
}

如果按照jQuery,希望在全局使用$,可以这样定义,这样就可以在任意文件中使用$不会有类型报错信息。

// global.d.ts
declare function jQuery(selector: string): any
declare function $(selector: string): any

如果想申明一个全局变量,可以这样定义,这样就可以在任意文件中使用hua不会有类型报错信息。

// global.d.ts
declare const hua: string
declare function huaFn(): void

以前经常遇到vue文件不识别类型,也是同理给给vue文件添加类型,以下是示例

// global.d.ts
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

给第三方库添加或者类型,一般需要安装对应的类型文件,如@types/xxx,如果没有对应的类型文件,可以在global.d.ts中定义。比如上面的axios.AxiosRequestConfig。

// global.d.ts
declare module 'axios' {
  // 会在axios模块中找到AxiosRequestConfig接口,然后添加自定义属性,这样就可以在请求中使用自定义属性
  interface AxiosRequestConfig {
    // 自定义属性 默认值为 false
    isHideLoading?: boolean
    isHideError?: boolean
  }
}

!注意别忘记在tsconfig.json中引入global.d.ts文件

{
  "include": ["src", "global.d.ts"]
}

使用扩展属性,默认显示加载和错误提示

上面已经扩展了axios的相关属性,之前默认是展示加载和错误提示,具体请求的时候,有时候不希望显示,那么可以进行额外的配置

import axios from 'axios'
import { message } from 'antd'
import { showLoading, hideLoading } from '@/utils/loading'

// 扩展 AxiosRequestConfig 接口 用于自定义配置
declare module 'axios' {
  interface AxiosRequestConfig {
    isHideLoading?: boolean
    isHideError?: boolean
  }
}


const BASE_URL = import.meta.env.VITE_BASE_URL
// const BASE_URL = config.baseURL

// 创建 Axios 实例
export const request = axios.create({
  baseURL: BASE_URL,
  timeout: 10000,
  timeoutErrorMessage: '请求超时,请稍后重试',
  withCredentials: true, // 跨域请求时是否需要使用凭证
})

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    (!config.isHideLoading) && showLoading()
   
  
    // 添加通用请求头,例如 token
    const token = localStorage.getItem('token')
    if (token) {
      // Bearer是JWT的认证头部信息,后面加一个空格,然后加上token,这样后端就可以通过请求头部信息获取到token
      config.headers.Authorization = `Bearer ${token}`
    }

    return config
  },
  (error) => {
    // 请求错误时关闭加载状态
    hideLoading()
    return Promise.reject(error)
  },
)

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    // 关闭加载状态
    hideLoading()
    console.log('response', response)
    const { data } = response
    const { success, code, message: msg } = data
    if (!success) {
      if (response.config.isHideError) {
        return Promise.reject({ code, msg, data })
      }
      switch (code) {
        case 401:
          message.error('请重新登录')
          // 登陆相关逻辑
          if (location.pathname !== '/login') {
            location.href = '/login?callback=' + encodeURIComponent(location.href)
          }
          break

        case 500:
          message.error('服务器错误')
          break
        default:
          message.error(msg || '请求失败,请稍后重试')
      }
      return Promise.reject(data)
    }
    return data.data // 直接返回响应数据,简化后续处理
  },
  (error) => {
    // 关闭加载状态
    hideLoading()

    // 统一错误处理
    if (error.response) {
      switch (error.response.status) {

        case 404:
          message.error('请求资源未找到')
          break
        case 500:
          message.error('服务器错误')
          break
        default:
          message.error('请求失败,请稍后重试')
      }
    } else if (error.message.includes('timeout')) {
      message.error('请求超时,请稍后重试')
    } else {
      message.error('网络错误,请检查网络连接')
    }

    return Promise.reject(error)
  },
)

export default request