实战指南:封装axios并增加loading效果

本文是《React管理平台》第七节

通过本文我们将学会环境变量的配置,以及axios封装

.env 环境变量

首先我们在项目根目录中创建三个文件,分别对应开发环境(.env.development)、生产环境(.env.production)和预发布环境(.env.stag)。

  • NODE_ENV:该变量用于设置当前的环境模式,例如开发模式(development)或生产模式(production)。
  • VITE_BASE_API:服务器接口地址,根据不同的环境设置不同的url。
  • VITE_UPLOAD_API:文件上传API接口地址,与上面类似,也是根据不同的环境设置不同的url。
  • VITE_MOCK:Mock的开关,用来决定是否启用模拟数据。如果设置为true,那么你的应用将使用在本地设定的Mock数据,而不会调用真实的API,便于开发和测试。
  • VITE_MOCK_API:如果mock开关打开,那么该值将被用作mock服务器的地址。

.env.development文件:

bash 复制代码
# 设置NODE_ENV环境模式
NODE_ENV=development

# 接口API地址
VITE_BASE_API = http://127.0.0.1:3000/api

# 上传API
VITE_UPLOAD_API =

# mock 开关
VITE_MOCK = true

# MOCK API
VITE_MOCK_API = http://localhost:3000

.env.production文件:

bash 复制代码
# 设置NODE_ENV环境模式
NODE_ENV=production

# 接口API地址
VITE_BASE_API = /api

# 上传API
VITE_UPLOAD_API =

# mock 开关
VITE_MOCK = true

# MOCK API
VITE_MOCK_API = http://localhost:3000

.env.production文件:

bash 复制代码
# 设置NODE_ENV环境模式
NODE_ENV=production

# 接口API地址
VITE_BASE_API = /api

# 上传API
VITE_UPLOAD_API =

# mock 开关
VITE_MOCK = true

# MOCK API
VITE_MOCK_API = http://localhost:3000

.env.stag文件:

bash 复制代码
# 设置NODE_ENV环境模式
NODE_ENV=production

# 接口API地址
VITE_BASE_API = /api

# 上传API
VITE_UPLOAD_API =

# mock 开关
VITE_MOCK = true

# MOCK API
VITE_MOCK_API = http://localhost:3000

开发环境变量、生产环境变量不过多介绍,也不用做额外的配置,这里强调下预发布环境变量,它是我们开发完代码打包为预发布版本放在服务器上的,需要额外配置,在package.json 文件的script部分,增加:

json 复制代码
"scripts": {
  // ...
  "build:stg": "tsc && vite build --mode stag",
  // ...
},

现在我们运行打包命令pnpm build:stg即可打包项目并读取的是.env.stag文件中的配置

配置

我们去src目录中创建config文件夹,这个文件夹我们打算放置所有从代码中抽离出来的公共配置,一来防止我们去源码中寻找对应的代码消耗多余时间;二来是多处用到的内容抽离出来统一管理和维护。

在config目录中我们创建3个文件:index.tsnet.config.tsapp.config.ts,这三个文件使用匿名方式(export default)导出。

  • net.config.ts文件是与axios相关的配置,包括API接口地址,请求超时时间,请求失败的错误信息,预期的成功状态代码,以及服务器返回的数据字段名。在该文件中我们使用import.meta.env.VITE_MOCK读取环境变量的字段用于判断是否开启mock并赋值相应的接口地址。
  • app.config.ts文件主要存放应用级别的配置。这包括应用的标题,公开访问(无需登录即可访问)的路由白名单等,在今后的章节中会用到。
  • index.ts文件主要是集合了以上两个配置文件的内容,它将这两个配置文件的导出内容解构合并,并将合并后的结果作为默认导出。这样的话,在其他模块需要使用这些配置时,只需要从此文件中引入即可,方便管理和使用。

net.config.ts文件:

ts 复制代码
export default {
  // API接口地址
  baseURL: import.meta.env.VITE_MOCK === 'true' ? import.meta.env.VITE_MOCK_API : import.meta.env.VITE_BASE_API,

  // 网络请求的超时时间
  requestTimeout: 10000,

  // 请求超时时的错误信息
  timeoutErrorMessage: '请求超时,请稍后重试',

  // 请求成功时服务端返回的状态码
  successCode: [200, 0],

  // 服务端返回的状态字段名
  statusName: 'code',
  // 服务端返回的消息的字段名
  messageName: 'message'
}

app.config.ts文件:

ts 复制代码
export default {
  // 应用的标题
  title: 'React Admin',
  // 应用的路由白名单,白名单内的路由可以在未登录的情况下访问
  routeWhiteList: ['/login', '/register', '/callback', '/404', '/403']
}

index.ts文件:

ts 复制代码
import newConfig from './net.config.ts'
import appConfig from './app.config.ts'

export default {
  ...appConfig,
  ...newConfig
}

至此我们的配置准备工作已完成,接下来我们增加loading动画。

loading动画

loading动画有两种,分别是进入应用的loading动画与请求发起时的loading动画,我们在项目根目录的public目录中创建css/loading.css文件,这两种动画的css统一放在该文件中:

css 复制代码
#root .loader-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 90vh;
  min-height: 90vh;
}
#root .loader-container > h1 {
  font-size: 28px;
  font-weight: bolder;
  margin-left: 10px;
}

#root .loader {
  display: inline-block;
  position: relative;
  width: 80px;
  height: 80px;
}

#root .loader div {
  position: absolute;
  border: 4px solid #1890ff;
  opacity: 1;
  border-radius: 50%;
  animation: loader 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}

#root .loader div:nth-child(2) {
  animation-delay: -0.5s;
}

#loading {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 20px;
}

#loading .loading {
  animation: rotate linear 1.5s infinite;
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@keyframes loader {
  0% {
    top: 36px;
    left: 36px;
    width: 0;
    height: 0;
    opacity: 1;
  }
  100% {
    top: 0px;
    left: 0px;
    width: 72px;
    height: 72px;
    opacity: 0;
  }
}

并在根目录的index.html文件中导入loading.css文件

ini 复制代码
<link rel="stylesheet" href="css/loading.css">

之后,我们先来看进入应用的loading动画

启动应用的loading

index.html文件的id="root"子元素中增加:

xml 复制代码
<div id="root">
  <div class="loader-container">
    <div class="loader">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
    <h1>React Admin</h1>
  </div>
</div>

在浏览器中的效果:

网络请求的loading

index.html文件的id="root"兄弟节点中增加:

ini 复制代码
<div id="loading" style="display: none">
  <svg
    t="1682858040467"
    class="icon loading"
    viewBox="0 0 1024 1024"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    p-id="7810"
    width="64"
    height="64"
  >
    <path
      d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 1 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z"
      fill="#1677ff"
      p-id="7811"
    ></path>
  </svg>
  <p>Loading....</p>
</div>

网络请求的loading元素默认不显示,即display="none",之后我们会在网络请求发起时通过id="loading"获取节点并去掉display="none"

控制网络请求的loading显示/隐藏

我们在src/utils目录中新建loading.ts文件,该文件的作用是调用showLoading函数显示网络请求loading动画,调用hideLoading函数隐藏网络请求动画,其中count用来修复在一个页面中多次发起网络请求时的闪烁问题,而定时器用来修复网络请求速度快时loading一闪而过的问题:

ts 复制代码
let count = 0
let loadingTimeoutId: NodeJS.Timeout | number

export const showLoading = () => {
  clearTimeout(loadingTimeoutId)
  if (count === 0) {
    const loading = document.getElementById('loading')
    loading?.style.setProperty('display', 'flex')
  }
  count++
}

export const hideLoading = () => {
  count--
  if (count === 0) {
    // Set a delay before actually hiding the loading screen (500ms in this case)
    loadingTimeoutId = setTimeout(() => {
      const loading = document.getElementById('loading')
      loading?.style.setProperty('display', 'none')
    }, 500)
  }
}

封装axios

在封装axios之前,我们需要在src/types目录中创建api.ts文件,今后我们API接口的ts类型声明将会放在该文件中进行管理和维护:

ts 复制代码
// api.ts
export interface Result<T = any> {
  code: number
  data: T
  msg: string
}

我们在constants目录中创建httpStatusCodes.ts文件,用来描述状态码对应的中文描述,以便在后端未返回错误的消息信息时匹配状态码对应的中文描述,该文件的内容是:

ts 复制代码
export const CODE_MESSAGE: { [key: number]: string } = {
  200: '服务器成功返回请求的数据',
  201: '新建或修改数据成功',
  202: '一个请求已经进入后台排队(异步任务)',
  204: '删除数据成功',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作',
  401: '用户没有权限(令牌、用户名、密码错误)',
  403: '用户得到授权,但是访问是被禁止的',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
  406: '请求的格式不可得',
  410: '请求的资源被永久删除,且不会再得到的',
  422: '当创建一个对象时,发生一个验证错误',
  500: '服务器发生错误,请检查服务器',
  502: '网关错误',
  503: '服务不可用,服务器暂时过载或维护',
  504: '网关超时'
}

至于刷新令牌的接口,我们临时写一个不存在的接口,在实际开发中再与后端对接:

ts 复制代码
// api/refreshToken.ts文件
import request from '@/utils/request'

export function refreshToken() {
  return request.get<{ token: string }>('/refreshToken')
}

接下来回到本文的主题,封装axios。在src/utils目录中新建request.ts文件

ts 复制代码
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { showLoading, hideLoading } from '@/utils/loading.ts'
import { Result } from '@/types/api'
import { message } from './AntdGlobal'
import storage from './localStorage'
import config from '@/config/net.config.ts'
import router from '@/router'
import { CODE_MESSAGE } from '@/constants/httpStatusCodes.ts'
import { refreshToken } from '@/api/refreshToken.ts'

declare module 'axios' {
  interface AxiosRequestConfig {
    isShowLoading?: boolean
    isShowError?: boolean
  }
}

interface IConfig {
  isShowLoading?: boolean
  isShowError?: boolean
}

const { baseURL, messageName, requestTimeout, statusName, successCode, timeoutErrorMessage } = config

let refreshToking = false
const requests: (() => void)[] = []

/**
 * 请求拦截器配置
 * @param config
 */
const requestConfig = (config: InternalAxiosRequestConfig) => {
  if (config.isShowLoading) showLoading()
  const token = storage.get('token')
  // 规范写法
  if (token) config.headers!.Authorization = 'Bearer ' + token
  // 非规范写法
  // if (token) config.headers['token'] = token
  return config
}
/**
 * 刷新令牌
 * @param config
 */
const tryRefreshToken = async (config: InternalAxiosRequestConfig) => {
  if (!refreshToking) {
    refreshToking = true
    try {
      const {
        data: { token }
      } = await refreshToken()

      if (token) {
        storage.set('token', token)
        requests.forEach(req => req())
        requests.length = 0
        return instance(requestConfig(config))
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error('刷新令牌错误:', error)
      router.navigate('/login', { replace: true }).then(() => {})
    } finally {
      refreshToking = false
    }
  } else {
    return new Promise(resolve => {
      requests.push(() => {
        resolve(instance(requestConfig(config)))
      })
    })
  }
}

const responseData = async ({ config, data, status, statusText }: AxiosResponse) => {
  hideLoading()
  if (config.responseType === 'blob') return data
  let code: number = data && data[statusName] ? data[statusName] : status
  if (successCode.includes(data[statusName])) code = 200
  switch (code) {
    case 200:
      return data
    case 401:
      router.navigate('/login?redirect=' + encodeURIComponent(location.href), { replace: true }).then(() => {
        storage.remove('token')
        message.error(data.msg)
      })
      break
    case 402:
      // 刷新令牌
      return await tryRefreshToken(config)
  }
  if (config.isShowError === true) {
    const errorMessage =
      data && data[messageName] ? data[messageName] : CODE_MESSAGE[code] ? CODE_MESSAGE[code] : statusText
    message.error(errorMessage)
  }
  return Promise.reject(data)
}

// 创建 axios 实例
const instance = axios.create({
  baseURL: baseURL,
  timeout: requestTimeout,
  timeoutErrorMessage: timeoutErrorMessage,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

// 请求拦截器
instance.interceptors.request.use(requestConfig, (error: AxiosError) => Promise.reject(error))

// 响应拦截器
instance.interceptors.response.use(responseData, (error: AxiosError) => {
  hideLoading()
  message.error(error.message)
  return Promise.reject(error.message)
})

export default {
  get<T>(
    url: string,
    params?: object,
    options: IConfig = { isShowLoading: true, isShowError: true }
  ): Promise<Result<T>> {
    return instance.get(url, { params, ...options })
  },
  post<T>(url: string, params?: object, options: IConfig = { isShowLoading: true, isShowError: true }): Promise<T> {
    return instance.post(url, params, options)
  }
}

配置Axios实例

创建一个Axios实例且只用这个实例发送所有的HTTP请求。

ts 复制代码
// 创建 axios 实例
const instance = axios.create({
  baseURL: baseURL,
  timeout: requestTimeout,
  timeoutErrorMessage: timeoutErrorMessage,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

这段代码创建了一个Axios实例并配置了默认的基础URL、请求超时时间和错误信息,以及默认的请求头。

请求拦截器

请求拦截器允许在发送请求之前对请求数据做处理,比如统一添加Token。

ts 复制代码
// 请求拦截器
instance.interceptors.request.use(requestConfig, (error: AxiosError) => Promise.reject(error))

这里添加了一个请求拦截器,如果开启了显示Loading的选项,则显示Loading动画。如果本地存储中有token,会将其添加到请求头中。

响应拦截器

响应拦截器能在后端返回数据后对数据进行处理。

ts 复制代码
// 响应拦截器
instance.interceptors.response.use(responseData, (error: AxiosError) => {
  hideLoading()
  message.error(error.message)
  return Promise.reject(error.message)
})

我们配置的响应拦截器会在请求完成后关闭Loading动画,并进行错误处理。如果是正常响应,会根据返回的状态码确认是否是成功请求,如果遇到401错误码,可能是Token失效,就会引导用户重新登录,如果是其他错误,显示错误信息。

封装请求方法

为了简化代码并提高可读性,我们可以封装常用的请求方法。

ts 复制代码
export default {
  get<T>(
    url: string,
    params?: object,
    options: IConfig = { isShowLoading: true, isShowError: true }
  ): Promise<Result<T>> {
    return instance.get(url, { params, ...options })
  },
  ...
}

以上封装后的get请求,在发起请求时将显示Loading动画并在遇到错误时显示错误信息。

刷新Token逻辑

在执行需要验证的请求时,如果检测到Token过期,可尝试自动刷新Token并重发请求。

ts 复制代码
// 刷新令牌
const tryRefreshToken = async (config: InternalAxiosRequestConfig) => {
  ...
}

这段代码处理了Token刷新的逻辑。如果Token过期,应用程序将尝试刷新Token,并重新发起之前的请求。

测试

我们在src/api目录中创建test.ts文件:

ts 复制代码
import request from '@/utils/request'

interface Posts {
  author: string
  id: number
  title: string
}

export function getPosts(params?: { id: number; title: string; author: string }) {
  return request.get<Posts[]>('/posts', params)
}

pages/Welcome/Welcome.tsx文件中测试发起请求:

tsx 复制代码
import { useEffect } from 'react'
import { getPosts } from '@/api/test.ts'

export const Welcome = () => {
  useEffect(() => {
    const fetchPosts = async () => {
      const response = await getPosts()
      console.log(response)
    }

    // Immediately invoke the async function
    fetchPosts()
  }, [])

  return <div></div>
}

我们首先启动之前章节中准备的mock服务

再启动我们的应用:

因为我们在main.tsx文件中开启了严格模式,所以会看到有两次请求,这是正常的。至此axios封装完毕!

相关推荐
我是大头鸟19 分钟前
SpringMVC 通过ajax 前后端数据交互
前端·javascript·ajax
北观止27 分钟前
批量删除OpenStack实例
linux·前端·chrome·openstack
爱笑的眼睛111 小时前
React Native 入门 jsx tsx 基础语法
javascript·react native·react.js
BillKu1 小时前
Vue3中AbortController取消请求的用法详解
前端·javascript·vue.js
heroboyluck2 小时前
rust 全栈应用框架dioxus
前端·rust·dioxus
不思念一个荒废的名字3 小时前
【黑马JavaWeb+AI知识梳理】后端Web基础01 - Maven
java·前端·maven
hunteritself3 小时前
OpenAI 上新:轻量版 Deep Research、GPT-4o 升级、o3 限额翻倍、生图 API 发布!| AI Weekly 4.21-4.27
前端·人工智能·科技·深度学习·chatgpt
刺客-Andy3 小时前
React 第三十六节 Router 中 useParams 的具体使用及详细介绍
前端·react.js·前端框架
黄同学real4 小时前
Vue 项目中运行 `npm run dev` 时发生的过程
前端·vue.js·npm
黄同学real4 小时前
vue 优化策略,大白话版本
前端·javascript·vue.js