本系列从零搭建一个后台系统,技术选型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