基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。 前端(Gin+React+shadcn/ui): - 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、 反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放 - 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info) - 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环) - 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史 - 按用户+标签策略的查询授权(服务端强制合并策略) 后端(Go+Gin+JSON DB): - 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流 - LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录 - 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建 - Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、 凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入 - DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签 - 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页 - 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录 修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、 健康检查 CreatedAt 零值等 10+ 存量 bug。 数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移; HKDF 加密前缀保留(已加密凭证不受影响)。
13 KiB
03 — 请求层与数据流
参考:vue-vben-admin(RequestClient + refresh 队列)、soybean-admin(五钩子契约 + flat 返回)、vue-pure-admin(PureHttp 请求类)、ant-design-pro(ErrorShowType 契约);补充:gin-vue-admin(loading 防抖、401 事件总线)
一、统一响应契约
1.1 契约即协议
请求层的第一件事不是封装 axios,而是和后端签一份响应契约。两种主流格式:
// 格式 A:Go Gin / 多数国内后端惯例(gva 等)
{ code: 0, data: {...}, msg: 'success' } // code === 0 成功
// 细分扩展:0 成功 / 200 成功 / 2 MFA 待验证 / 3 强制改密
// 格式 B:adp(含错误展示声明)
{ success: true, data: {...},
errorCode?: number, errorMessage?: string, showType?: ErrorShowType }
1.2 adp 的杀手锏:错误展示分级
// ant-design-pro: src/requestErrorConfig.ts
enum ErrorShowType {
SILENT = 0, // 静默:调用方自行处理
WARN_MESSAGE = 1, // 轻提示 message.warning
ERROR_MESSAGE = 2, // 错误提示 message.error
NOTIFICATION = 3, // 通知栏 notification
REDIRECT = 9, // 重定向到错误页
}
- 后端在响应里声明这个错误该怎么展示,前端按枚举分发——错误处理策略从「前端猜」变成「契约说」
- 前端保留逃生口:
request(url, { skipErrorHandler: true })单请求关闭全局处理
推荐:在
{code, data, msg}上加可选字段show_type,前端默认ERROR_MESSAGE,登录页/轮询接口传SILENT。改动只在请求层一处。
二、请求层结构:从单例到客户端类
2.1 三档演进
档1 单例 + 双拦截器(gva,230 行单文件)
适合:单应用、单一鉴权方式
档2 工厂五钩子(soybean:RequestOption 契约 + createFlatRequest)
适合:错误契约复杂、想要扁平返回与错误码 env 化
档3 客户端类 + 预设拦截器(vben RequestClient / pure PureHttp)
适合:多实例(业务 client / 裸 client)、上传下载 SSE 一等公民
2.2 vben 的 RequestClient 精髓
// vben: packages/effects/request/src/request-client/request-client.ts
class RequestClient {
private instance: AxiosInstance
addRequestInterceptor / addResponseInterceptor // 拦截器不写死,外部组装
upload / download / postSSE // 传输形态一等公民
}
// 预设拦截器(preset-interceptors.ts)按需装配:
defaultResponseInterceptor({ codeField, dataField, successCode }) // 解包策略可配置
authenticateResponseInterceptor // 401 + refresh
errorMessageResponseInterceptor // status→文案
应用组装处(apps/web-antd/src/api/request.ts)导出两个实例:
requestClient:responseReturn: 'data',业务用(自动剥壳)baseRequestClient:responseReturn: 'raw',刷新 token 等裸接口用(刷新接口自己不能被自己的拦截器拦)
2.3 soybean 的五钩子契约与 flat 返回(★★★★☆ 工厂化范式)
soybean 把请求协议下沉为 workspace 包 packages/axios,业务侧只实现五个钩子:
interface RequestOption<ResponseData, ApiData> {
transform: (res) => ApiData // 解包
onRequest: (config) => config // 注入 token
isBackendSuccess: (res) => boolean // 业务 code 判定
onBackendFail: (res, instance) => Promise<AxiosResponse | null> // 失败处理(可重发)
onError: (error) => void // 错误提示
}
// createRequest:失败抛异常(传统风格)
// createFlatRequest:返回 { data, error, response },业务层 if (!error) —— Rust 风格扁平返回
另两条可迁移细节:错误码策略 env 化(VITE_SERVICE_LOGOUT_CODES 直接登出 / MODAL_LOGOUT_CODES 模态登出 / EXPIRED_TOKEN_CODES 刷新重发,三级以配置区分);错误消息栈去重(errMsgStack 防并发轰炸)。pure 的 PureHttp 类(196 行单文件)则是轻量级范本:qs 序列化数组参数、请求前本地判断 expires 提前刷新(而非 401 后补救)、per-request 回调可覆盖全局拦截器。
三、Token 与 401:单飞刷新是铁律
3.1 问题:并发 401 的竞态
请求A ──401──▶ 刷新token(旧1) ──▶ 重放A ✔
请求B ──401──▶ 刷新token(旧1) ──▶ 后端拒(旧1已消费)──▶ B 被踢出登录 ✘
3.2 正确姿势:单飞标志 + 等待队列(vben)
// vben: authenticateResponseInterceptor
if (client.isRefreshing) {
return new Promise((resolve) => client.refreshTokenQueue.push((token) => {
config.headers.Authorization = `Bearer ${token}`
resolve(client.instance.request(config)) // 拿新 token 后重放
}))
}
client.isRefreshing = true
try { const newToken = await doRefresh(...) } finally { client.isRefreshing = false }
config.__isRetryRequest = true // 防无限循环
pure 用同构的静态成员实现(isRefreshing + requests[] 队列,刷新成功 requests.forEach(cb => cb(token)) 重放);soybean 用共享 Promise 单飞(state.refreshTokenPromise,1 秒后置 null)。三家殊途同归:并发 N 个过期请求只触发一次刷新。
pure 的补充思路:提前刷新——token 三元组(accessToken/expires/refreshToken)存 Cookie,请求前本地判断 expires - now <= 0 主动刷新,把过期处理从「响应后补救」变成「请求前预防」(白名单短路 /refresh-token、/login 防死循环)。
3.3 刷新失败后的三种落法
| 落法 | 出处 | 适用 |
|---|---|---|
| 全局错误对话框确认后清 token 跳登录(事件总线单例防重复弹) | gva emitter.emit('show-error') |
多请求并发过期时体验最好(只弹一个框) |
| 登录过期 Modal(原页面保留,重登后继续) | vben loginExpiredMode: 'modal' |
长表单场景,避免填一半全丢 |
| 防抖单次跳转(最简可靠) | 常见轻实现 | 跳转加防抖标志;错误救援页(500/403)只清不跳,防按钮失效 |
共同底线:并发 N 个 401 只产生一次副作用(一个弹窗/一次跳转);登录页与轮询接口静默(
SILENT)。
四、全局 loading 防抖(补充模式:gva)
// gva: utils/request.js(要点提炼)
let activeAxios = 0 // 并发计数器
let timer = null
const showLoading = (persist) => {
if (timer) clearTimeout(timer)
activeAxios++
if (persist) { ... return } // 长任务:立即显示且不自动关
timer = setTimeout(() => showFullScreenLoading(), 400) // 400ms 防闪烁
}
const closeLoading = () => {
activeAxios--
if (activeAxios <= 0) { clearTimeout(timer); hide(); } // 全部结束才隐藏
}
// 兜底:30s 强制关闭;beforeunload 重置;单请求 donNotShowLoading 关闭
三个数字都是经验值:400ms(快请求不闪 loading)、30s(loading 不可能永远转)、计数器(并发请求只显示一个 loading)。若引入全局 loading,按此方案。
五、取消与防错序
5.1 为什么列表页必须取消旧请求
用户输入 "a" → 请求1发出
用户补输入 "ab" → 请求2发出
请求2先回 → 渲染 "ab" 结果 ✔
请求1后回 → 覆盖渲染 "a" 结果 ✘ ← 错序覆盖(stale response)
5.2 双重防护(列表页标准实现)
// composables/usePagedQuery.js —— 取消 + 版本号双保险
let abortController = null
let requestVersion = 0
const fetchList = async () => {
abortController?.abort() // ① AbortController 取消旧请求
const currentVersion = ++requestVersion // ② 版本号令牌
const res = await fetcher(params)
if (currentVersion !== requestVersion) return // 过期响应直接丢弃
tableData.value = res.results
}
这是列表页的强制标准:所有带筛选/分页的列表页必须走「AbortController + 版本号」双重防护,禁止页面手写裸请求循环。
注:vben v5 并未内置请求去重(取消能力委托 axios 原生 config,axios.isCancel 静默透传)——不要假设大而全的框架替你解决了这个问题。soybean 另有 cancelAllRequest(nanoid 请求 ID + AbortControllerMap)作页面级批量取消。
5.3 重试与缓存策略
- 重试:仅幂等 GET 且网络类错误(timeout / network error)自动重试,1-2 次指数退避(soybean 内置 axios-retry;需要时引入 axios-retry,禁止手写循环重试)。禁止对 POST / PUT / DELETE 自动重试
- 缓存:字典 / 枚举类走缓存型 store(gva
dictionary.js范式:缓存 key 生成 + 参数校验,避免重复请求);列表页交给组件态 + 取消,不做全局缓存;React 端用 React Query 的queryKey失效模型(adp 范式:mutation 成功 →invalidateQueries) - 判定:同一数据被两个页面重复请求同一接口 = 该进缓存层,或该提升到全局 store 了
六、状态分层:什么数据进 store
6.1 判定表
| 数据 | 归属 | 标杆范式 |
|---|---|---|
| token / 用户信息 / 权限码 | 全局 store,持久化 pick 白名单(只挑必要字段) | vben persist.pick;pure Cookie 三元组 + localStorage 用户信息 |
| 服务端菜单 / 动态路由表 | store(内存),不落盘(重登重拉,防止旧菜单闪现) | vben access store;pure 可选缓存需显式配置 |
| 列表数据 / 详情 | 不进全局 store:组件内 usePagedQuery / React Query | adp:queryKey:[资源名],mutation 成功 invalidateQueries |
| 字典 / 短枚举 | 缓存型 store,带缓存 key 去重请求 | gva dictionary.js、soybean dict |
| 主题 / UI 偏好 | 独立于业务 store 的偏好系统 | vben PreferenceManager(见 06 章) |
| 筛选 / 分页 / Tab 状态 | URL query,永远不复制进 store | 通用 |
6.2 登录编排(vben 范式)
// store/auth.ts —— 登录是一个编排函数,不是散落的事件
async function authLogin(params) {
const { accessToken } = await loginApi(params)
setAccessToken(accessToken)
const [userInfo, accessCodes] = await Promise.all([fetchUserInfo(), getAccessCodesApi()])
router.push(homePath)
}
配套要求:禁止在生产日志打印 token 片段;isLoggedIn 以内存态 + token 存在性为准,不依赖 localStorage 残留字段。pure 的 useXxxStoreHook() 模式保证拦截器/守卫等 setup 外代码也能安全取 store。
七、双端一致性要求(迁移期)
同产品双栈并存时,请求层必须满足:
| 项 | 要求 |
|---|---|
| token key | 两端同名(推荐 <app>.auth.token;迁移期「读旧写新」一个版本) |
| refresh 机制 | 两端同构(单飞 + 队列重放),副端不允许退化成「401 直接跳登录」 |
| 响应契约 | 同一份 code/data/msg 约定与错误分级 |
| mock | 显式模式(dev 开关/页面级声明),禁止运行时因网络错误静默回退假数据(adp:mock 是构建模式不是运行时兜底) |
八、最佳实践清单
✅ 要做
- 组件永远 import
api/<域>函数,不直接碰 axios 实例(禁止 store 绕过 API 层裸调) - 每个请求函数带注释(用途/参数/返回)+ 类型(
request<Api.Xxx>或 JSDoc) - 刷新 token 的请求用裸 client,绕过业务拦截器
- 401 副作用全局单例(一个弹窗/一次跳转),登录页与轮询接口静默
- 列表请求一律可取消(AbortController)+ 版本号防错序
❌ 不要
- 不要在拦截器里
return error而非Promise.reject(error)(gva 现存 bug,调用方 catch 不到) - 不要在组件里 try/catch 后吞错不提示(错误要么展示要么显式静默)
- 不要把 token 存两套 key
- 不要在请求层 import 具体页面组件(保持依赖单向)
- 不要对同一资源做无缓存的重复并发请求
九、AI 检查清单(对比目标工程逐项核对)
P0
- 是否存在组件/store 绕过 API 层直接调 axios 实例的代码?
- 401 刷新是否单飞(isRefreshing/队列/共享 Promise 任一实现)?并发过期是否只产生一次副作用?
- 是否有「网络错误静默回退 mock」类逻辑在生产路径上?
- 拦截器错误分支是否
Promise.reject?是否有 token 打印类调试日志残留?
P1
- 列表页是否全部走「取消 + 版本号」方案?手写裸请求循环的页面列出迁移清单
- 错误提示是否分级(showType / silent 逃生口)?登录页与轮询是否静默?
- token/用户信息持久化是否 pick 白名单?菜单/路由是否落盘(不应落盘)?
- 字典/枚举是否有缓存层?重复并发请求同一资源的情况列出
P2
- 是否值得升级到档 2/3(五钩子工厂 / RequestClient 类)?判断标准:多实例需求或错误契约复杂度
- 是否引入 axios-retry(幂等 GET)与错误码 env 化?
下一篇:04-路由与权限控制 —— meta 是路由的灵魂。