实战指南:封装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文件:

# 设置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文件:

# 设置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文件:

# 设置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文件:

# 设置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部分,增加:

"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文件:

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文件:

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

index.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统一放在该文件中:

#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文件

<link rel="stylesheet" href="css/loading.css">

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

启动应用的loading

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

<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>

在浏览器中的效果:

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

网络请求的loading

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

<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一闪而过的问题:

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类型声明将会放在该文件中进行管理和维护:

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

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

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

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

// api/refreshToken.ts文件
import request from '@/utils/request'

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

接下来回到本文的主题,封装axios。在src/utils目录中新建request.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请求。

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

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

请求拦截器

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

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

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

响应拦截器

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

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

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

封装请求方法

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

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并重发请求。

// 刷新令牌
const tryRefreshToken = async (config: InternalAxiosRequestConfig) => {
...
}

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

测试

我们在src/api目录中创建test.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文件中测试发起请求:

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服务

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

再启动我们的应用:

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

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

原文链接:https://juejin.cn/post/7349087888049618979 作者:辰火流光

(0)
上一篇 2024年3月23日 下午4:59
下一篇 2024年3月23日 下午5:09

相关推荐

发表回复

登录后才能评论