Vue3 EMS 项目实战:Axios 请求封装与全局 Loading 联动设计-04
Vue3 EMS 项目实战:Axios 请求封装与全局 Loading 联动设计
系列:组件与工具专题
本篇主题:request.ts+useLoadingStore+ 智能 message + 文件导出
源码:src/utils/request.ts、src/stores/loading.ts、src/utils/common.ts
一、为什么需要统一的请求层?
EMS 项目接口上百个,如果每个页面自己处理:
- Token 注入
- 401 登出
- Loading 开关
- 业务 code 判断
- 文件流下载
代码会重复且容易漏。本项目在 request.ts 中集中处理,业务层 then 里直接拿 data,无需再判断 code === 0。
二、Axios 实例与拦截器架构
export const rM = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
})
2.1 请求拦截:Token + 语言 + Loading 计数
instance.interceptors.request.use((config) => {
const isToken = config.headers['noToken'] !== true
if (isToken) {
config.headers[getTokenName()] = getToken()
config.headers['Accept-Language'] = i18n.global.locale.value
}
// GET 请求自动剔除空字符串和 undefined 参数
if (config.method === 'get' && config.params) {
config.params = Object.keys(config.params).reduce((pre, cur) => {
if (config.params[cur] !== '' && config.params[cur] !== undefined) {
pre[cur] = config.params[cur]
}
return pre
}, {})
}
const loadingStore = useLoadingStore()
config.id = loadingStore.addRequest()
if (!config.noLoading) {
loadingStore.startLoading()
}
return config
})
设计要点:
| 能力 | 实现方式 |
|---|---|
| 跳过 Token | 请求头 noToken: true |
| 跳过 Loading | 配置 noLoading: true |
| 国际化 | 自动带 Accept-Language |
| FormData | 自动改 Content-Type |
| x-www-form-urlencoded | qs.stringify 序列化 |
2.2 响应拦截:业务码统一消化
if (code === '0') {
if (method === 'get') return resData
if (['put', 'post', 'delete'].includes(method!)) {
if (noMsg) return resData ?? 'success'
message(msg) // 智能识别 success/warn/error
return resData ?? 'success'
}
} else if (code === '401') {
// 弹窗确认 + 10 秒超时自动登出
$confirm(msg).then(() => useUserStore().logOut())
return Promise.reject(resData)
} else if (code === '431') {
// 表单字段级校验失败
ElMessage.error(`${field}: ${message}`)
return Promise.reject(resData)
}
业务层写法对比:
// ❌ 传统写法
const res = await axios.get('/api/list')
if (res.data.code === '0') {
list.value = res.data.data
} else {
ElMessage.error(res.data.msg)
}
// ✅ 本项目写法
getList(params).then(resData => {
list.value = resData
})
2.3 文件流导出
responseType 为 blob / arraybuffer 时走 exportFileFn:
export const exportFileFn = (data: any) => {
// 失败时后端返回 JSON,需 FileReader 解析错误信息
if (data.data.type === 'application/json') {
const reader = new FileReader()
reader.readAsText(data.data, 'utf-8')
reader.onload = () => {
const { msg } = JSON.parse(reader.result as string)
ElMessage.error(msg)
}
return
}
// 成功:从 content-disposition 解析文件名并触发下载
const fileName = decodeURIComponent(data.headers['content-disposition']).split("''")[1]
const blob = new Blob([data.data])
// ... 创建 a 标签下载
}
三、Loading Store:多请求并发计数
全局 Loading 不能「发请求就 true、回来就 false」,并发请求会闪烁。本项目用 Symbol 队列 计数:
export const useLoadingStore = defineStore('loading', () => {
const loading = ref(false)
const activeRequest = ref<any[]>([])
function addRequest() {
let symbol = Symbol('request')
activeRequest.value.push(symbol)
return symbol
}
function removeRequest(symbol?: symbol) {
const index = activeRequest.value.indexOf(symbol)
if (index > -1) activeRequest.value.splice(index, 1)
}
function startLoading() { loading.value = true }
function stopLoading() { loading.value = false }
return { loading, addRequest, removeRequest, startLoading, stopLoading }
})
响应拦截器在 success / error 分支都会 removeRequest,再 stopLoading。
与 HForm / HDialog 联动:查询按钮、确认按钮绑定 :disabled="loadingStore.loading",防止重复提交。
四、智能 message:按后缀识别消息类型
后端返回的 msg 可能是「操作成功」「参数异常 error」等混合格式,message() 根据后缀关键词自动选择 ElMessage 类型:
export const message = (message: string) => {
let warnStrings = ['warn', 'exception', 'timeout', ...]
let errorStrings = ['error', ...]
let successStrings = ['success', ...]
// 匹配后缀 → info / warning / error / success
ElMessage.closeAll()
funcArr[funcIndex]!(message)
}
这样业务层无需关心该用 ElMessage.success 还是 .error。
五、组件级 Loading:useLoad
全局 HTTP Loading 不适合局部区块(如组态图加载)。useLoad 提供轻量封装:
export const useLoad = () => {
const loading = ref(false)
const withLoading = <T>(promise: Promise<T>): Promise<T> => {
loading.value = true
promise.finally(() => { loading.value = false })
return promise
}
return { loading, withLoading }
}
// 用法
const { loading, withLoading } = useLoad()
withLoading(getWebtopoProjectSvgByStationId(id)).then(res => { ... })
// 模板
<div v-loadingh="loading">...</div>
六、设备参数下发:useProcessing 轮询
EMS 读写设备参数是异步任务,需要轮询下发状态:
export const useProcessing = (IssueType: IssueType) => {
return (id: number, cmdType: CmdType, readLoad: Ref<boolean>) => {
return new Promise((resolve, reject) => {
const timer = setInterval(() => {
getIssueDetail(IssueType, id, cmdType).then(res => {
if (res.issueStatus !== '1') { // 非进行中
clearInterval(timer)
res.issueStatus === '2' ? resolve(res) : reject(res)
}
})
}, 1000)
setTimeout(() => { clearInterval(timer); reject('') }, 31000) // 31s 超时
})
}
}
典型场景:参数配置页下发读/写指令,按钮 loading 直到设备响应或超时。
七、本篇小结
| 模块 | 职责 |
|---|---|
fetchData / rM |
Token、语言、业务码、文件导出 |
useLoadingStore |
并发请求 Loading 计数 |
message() |
按后缀智能提示 |
useLoad |
组件局部 Loading |
useProcessing |
设备指令轮询 |
踩坑:
- 并发请求时不要用单个 boolean 控制全局 Loading
- blob 导出失败时后端可能返回 JSON,要先判断
type - 401 要防重复弹窗,用
isOperated标记
源码索引:src/utils/request.ts、src/stores/loading.ts、src/utils/common.ts
更多推荐

所有评论(0)