网络请求与拦截
Vue-Bag-Admin 把网络请求放在 @bag/request 里处理。它同时支持 axios 和原生 fetch,主要管这些事:
- Token 注入
- HTTP 状态码拦截
- 业务错误转异常
- 宿主自定义错误处理
- 面向 PWA 场景的离线缓存与离线写入队列
- 页面层的
loading/data/error执行状态管理 - 基于 Lodash 的防抖 / 节流请求控制
基础使用
你可以在业务插件中直接使用 http 实例:
import { http } from '@bag/request'
const fetchUsers = async () => {
const data = await http.get('/api/users', {
params: { page: 1 }
})
return data
}
// 发起 POST 请求
const createUser = async (payload) => {
const data = await http.post('/api/users', payload)
return data
}默认情况下,响应拦截器直接返回 response.data,业务层不用再手动解构 .data。
如果你更想直接使用原生 fetch,也可以使用 fetchHttp:
import { fetchHttp } from '@bag/request'
const fetchUsers = async () => {
const data = await fetchHttp.get('/api/users', {
params: { page: 1 }
})
return data
}初始化配置
为了不把请求层和宿主绑死,Token 获取和错误处理都在启动阶段通过 setupHttp() 注入。
import { setupHttp } from '@bag/request'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
setupHttp({
baseURL: import.meta.env.VITE_API_URL,
getToken: () => userStore.token,
onUnauthorized: () => {
userStore.logout()
window.location.href = '/login'
},
onForbidden: () => {
console.warn('当前账号没有访问权限')
},
onServerError: () => {
console.error('服务异常,请稍后重试')
},
resolveError: (payload) => {
const data = payload as
| { success?: boolean; code?: string | number; message?: string }
| undefined
if (data?.success === false) {
return {
code: data.code,
message: data.message || 'Request failed'
}
}
return null
}
})如果项目更偏向原生 API,也可以直接用 setupFetch():
import { setupFetch } from '@bag/request'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
setupFetch({
baseURL: import.meta.env.VITE_API_URL,
getToken: () => userStore.token,
onUnauthorized: () => {
userStore.logout()
window.location.href = '/login'
},
onForbidden: () => {
console.warn('当前账号没有访问权限')
},
onServerError: () => {
console.error('服务异常,请稍后重试')
},
resolveError: (payload) => {
const data = payload as
| { success?: boolean; code?: string | number; message?: string }
| undefined
if (data?.success === false) {
return {
code: data.code,
message: data.message || 'Request failed'
}
}
return null
}
})SetupHttpConfig 支持什么
setupHttp() 主要接这几类配置:
axios原生配置:如baseURL、timeout、headers- 宿主注入项:如
getToken、错误钩子、业务错误识别函数 - PWA 缓存项:如缓存命名空间、缓存策略、缓存时长
setupFetch() 也有一组对应配置:
- 原生
fetch配置:如baseURL、headers、credentials - 宿主注入项:如
getToken、错误钩子、业务错误识别函数 - PWA 增强项:如离线缓存策略、断网入队、联网后自动重放
PWA 模式
如果你的管理端希望在弱网、断网,或者被安装为桌面应用后还能保持基本可用,可以直接在 setupHttp() 或 setupFetch() 里开启 pwa 配置。
axios 版本适合继续沿用 http 的项目:
import { http, setupHttp } from '@bag/request'
setupHttp({
baseURL: import.meta.env.VITE_API_URL,
getToken: () => localStorage.getItem('token'),
onUnauthorized: () => {
window.location.href = '/login'
},
pwa: {
enabled: true,
cache: {
enabled: true,
strategy: 'cache-first',
ttl: 5 * 60 * 1000
}
}
})
const dashboardData = await http.get('/dashboard', {
pwa: {
strategy: 'cache-first'
}
})fetch 版本适合同时要离线队列的项目:
import {
clearOfflineQueue,
fetchHttp,
getNetworkStatus,
replayOfflineQueue,
setupFetch,
subscribeNetworkStatus
} from '@bag/request'
setupFetch({
baseURL: import.meta.env.VITE_API_URL,
getToken: () => localStorage.getItem('token'),
onUnauthorized: () => {
window.location.href = '/login'
},
pwa: {
enabled: true,
cache: {
enabled: true,
strategy: 'cache-first',
ttl: 5 * 60 * 1000
},
queue: {
enabled: true,
autoReplay: true
}
}
})
const stopListening = subscribeNetworkStatus((status) => {
console.log('network status:', status)
})
const dashboardData = await fetchHttp.get('/dashboard', {
pwa: {
strategy: 'stale-while-revalidate'
}
})
try {
await fetchHttp.post('/orders', {
title: '离线创建订单'
})
} catch (error) {
if (getNetworkStatus() === 'offline') {
console.log('request has been queued')
}
}
await replayOfflineQueue()
clearOfflineQueue()
stopListening()当前这层 PWA 模式主要覆盖三件事:
http和fetchHttp的GET/HEAD请求默认支持缓存优先读取fetchHttp的写请求在断网时可进入离线队列fetchHttp的离线队列可在网络恢复后自动或手动重放
高频请求控制
如果页面里有搜索联想、自动保存、按钮防连点这类高频交互,可以直接使用 @bag/request 提供的 debounceRequest() 和 throttleRequest()。
这层底层还是 Lodash 的 debounce / throttle,只是针对请求补了三点:
- 返回值保持为
Promise,可以继续await - 提供
cancel()、flush()、pending()便于页面控制 - 直接包裹
http、fetchHttp或你自己的 API 函数都可以
搜索联想
import { debounceRequest, http } from '@bag/request'
const fetchUsers = debounceRequest(
(keyword: string) =>
http.get('/api/users', {
params: {
keyword
}
}),
300
)
const users = await fetchUsers('bag admin')自动保存
import { fetchHttp, throttleRequest } from '@bag/request'
const saveDraft = throttleRequest(
(payload: { title: string; content: string }) => fetchHttp.post('/api/drafts', payload),
1000,
{
leading: false,
trailing: true
}
)
await saveDraft({
title: '草稿标题',
content: '草稿内容'
})重复提交保护
import { throttleRequest, http } from '@bag/request'
const submitOrder = throttleRequest(
(orderId: string) =>
http.post('/api/orders/submit', {
orderId
}),
1500,
{
leading: true,
trailing: false
}
)
await submitOrder('order_1001')控制方法
import { debounceRequest, http } from '@bag/request'
const fetchUsers = debounceRequest(
(keyword: string) =>
http.get('/api/users', {
params: {
keyword
}
}),
300,
{
maxWait: 1000
}
)
fetchUsers('a')
fetchUsers('ab')
fetchUsers('abc')
if (fetchUsers.pending()) {
await fetchUsers.flush()
}
fetchUsers.cancel('component unmount')默认行为:
debounceRequest()默认尾触发throttleRequest()默认首尾都触发- 两者都支持
leading、trailing、maxWait - 它们只限制调用频率,不会取消已经发出的真实请求
页面请求状态
如果页面里总是在手写 loading、data、error、run,可以用 useRequest() 把这层状态收起来。
import { http, useRequest } from '@bag/request'
interface UserProfile {
id: string
nickname: string
}
const userRequest = useRequest((userId: string) => http.get<UserProfile>(`/api/users/${userId}`), {
strategy: 'latest'
})
await userRequest.run('user_1')
console.log(userRequest.loading.value)
console.log(userRequest.data.value)
console.log(userRequest.error.value)常用方法
import { fetchHttp, useRequest } from '@bag/request'
const articleRequest = useRequest(
(articleId: string) => fetchHttp.get(`/api/articles/${articleId}`),
{
defaultData: null
}
)
await articleRequest.run('article_1')
await articleRequest.refresh()
articleRequest.cancel('dialog closed')
articleRequest.reset()loading:当前这个请求控制器是否还有活跃任务data:最近一次有效成功结果error:最近一次有效失败结果run(...args):执行请求并返回Promiserefresh():用上一次参数重新执行cancel():让当前活跃请求失效,并立即退出当前控制器的 loading 统计reset():清空当前控制器状态,并把data恢复到defaultData
并发策略
useRequest() 现在支持三种执行策略:
latest:默认值。新请求开始时,旧请求立即失效,适合搜索、筛选、切页parallel:允许多个请求并发执行,谁最后回来谁覆盖datablock:执行中再次调用run()会直接抛错,适合按钮防重复提交
支持的缓存策略有:
network-firstcache-firststale-while-revalidatecache-onlynetwork-only
错误处理模型
1. HTTP 状态码错误
当接口返回 401、403、5xx 时,请求层会分别触发:
onUnauthorizedonForbiddenonServerError
2. 业务错误
很多后端会返回 HTTP 200,但实际业务失败,例如:
{
"success": false,
"code": "PRODUCT_NOT_FOUND",
"message": "商品不存在"
}这时可以通过 resolveError() 把它识别为业务错误,再转成异常抛出。
3. HttpError
请求层会抛出 HttpError,里面包含:
messagecodestatuspayloadresponse
业务层可以在 catch 中集中处理:
import { HttpError, http } from '@bag/request'
try {
await http.get('/api/products/1')
} catch (error) {
if (error instanceof HttpError) {
console.error(error.status, error.code, error.message)
}
}拦截器流程
- 请求阶段:发起请求时,执行
getToken()获取最新凭证,并自动附加上Authorization: Bearer <Token>请求头。 - 响应拦截:
- 默认提取响应数据。
- 若
resolveError()识别出业务错误,则转成HttpError抛出。 - 若捕获到
401/403/5xx,则触发对应钩子。
使用建议
- 插件页面里只写业务请求,不要重复处理 Token
- 宿主应用在入口注入
setupHttp()或setupFetch() - 用
resolveError()适配你的后端业务错误格式 - 页面层优先用
useRequest()收口执行状态,避免重复维护loading、data、error - PWA 模式更适合列表、详情、配置查询这类可缓存读请求
- 如果项目主线仍是
axios,可以先只启用setupHttp({ pwa: { cache } }) - 离线队列更适合 JSON / 字符串请求体,不建议直接拿来排队文件上传
- 页面层
catch时优先判断HttpError
