资源请求监听
本文档涵盖 @jiess/http 的请求状态监听机制,包括 Loading 类、拦截器监听、上传进度监控和状态管理集成。
架构概览
┌─────────────────────────────┐
│ Loading 观察者注册中心 │
│ Loading.loadCaches │
│ │
Http.create() │ 监听 httpKey 集合 → │
└→ Factory.toRequest()│ 自动触发 request/response │
├→ request 拦截 ───→ Loading.request(key) │
└→ response 拦截 ───→ Loading.response(key) │
↓
回调 fn(isLoading)
→ 组件的 loading 状态Loading 类
Loading 是监听接口执行状态的核心类,通过接口唯一标识(httpKey)精确跟踪一个或多个接口的进行状态,并在所有被监听接口完成时触发回调。
接口标识规则
每个 API 接口在创建时自动生成唯一标识:
httpKey = ${moduleName}_${groupName}_${endpointName}
// 示例: "abc123_user_getUser"该标识在分组的 $groupKeys 数组中可查(参考 资源请求工具 文档)。
注意:在 Factory 的拦截器执行过程中,
request和response拦截器的成功/失败回调已自动集成 Loading 状态管理,即Loading.request(key)会在请求拦截器执行后调用,Loading.response(key)会在响应完成后调用。
构造函数
new Loading(tag, keys, fn, wait?)| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tag | string | — | 观察器唯一标识,相同 tag 会覆盖旧实例 |
keys | string[] | — | 要监听的接口 httpKey 数组 |
fn | Function | ()=>{} | 状态变化回调,fn(isLoading: boolean) |
wait | number | 300 | 最少加载等待时间(毫秒),避免闪烁 |
基本用法
import Loading from '@jiess/http/src/Loading'
const loading = new Loading(
'page-loading', // tag 唯一标识
['module_user_getUser', 'module_user_getOrders'], // 监听的接口
(isLoading) => { // 状态回调
if (isLoading) {
showLoadingSpinner()
} else {
hideLoadingSpinner()
}
},
500 // 最少显示 500ms
)当所有监听的接口都完成后,fn(false) 才会被调用。期间任意一个接口开始请求都会触发 fn(true)。
工作原理
- 接口请求开始时,
Loading.request(httpKey)检查是否匹配监听列表 - 若缓存为空(尚无正在进行的请求),标记开始时间,调用
fn(true) - 接口响应完成时,
Loading.response(httpKey)移除缓存记录 - 所有缓存清空后,计算耗时:若不足
wait,延迟到wait后再调用fn(false)
// 手动触发(通常不需要,拦截器已自动处理)
Loading.request('module_user_getUser') // 标记开始
Loading.response('module_user_getUser') // 标记完成多实例协同
可以为不同粒度的 UI 区域创建多个 Loading 实例:
// 页面级 Loading
new Loading('page-level', [
'module_user_getUser',
'module_order_getOrders'
], setPageLoading)
// 用户卡片 Loading
new Loading('user-card', [
'module_user_getUser'
], setUserCardLoading)
// 订单列表 Loading
new Loading('order-list', [
'module_order_getOrders'
], setOrderListLoading)
// 三者互相独立:
// 加载用户 → pageLevel=true, userCard=true, orderList=false
// 加载订单 → pageLevel=true, userCard=false, orderList=true
// 全部完成 → 三者均为 false多实例完整案例
以下示例展示一个数据看板页面的完整 Loading 分层设计:
import Loading from '@jiess/http/src/Loading'
// -------- 接口定义(来自 http.create) --------
const userApi = http.create('user', {
getProfile: ['GET', '/users/:id/profile'],
getStats: ['GET', '/users/:id/stats'],
})
const orderApi = http.create('order', {
getRecentOrders: ['GET', '/orders/recent'],
getOrderStats: ['GET', '/orders/stats'],
})
function DashboardPage({ userId }: { userId: number }) {
// 三个独立的 loading 状态
const [pageLoading, setPageLoading] = useState(false)
const [userLoading, setUserLoading] = useState(false)
const [orderLoading, setOrderLoading] = useState(false)
useEffect(() => {
// 页面级:监听该页所有接口
new Loading('dashboard-page', [
'module_user_getProfile', 'module_user_getStats',
'module_order_getRecentOrders', 'module_order_getOrderStats'
], setPageLoading, 400)
// 用户区域:只监听用户接口
new Loading('dashboard-user', [
'module_user_getProfile', 'module_user_getStats'
], setUserLoading, 300)
// 订单区域:只监听订单接口
new Loading('dashboard-order', [
'module_order_getRecentOrders', 'module_order_getOrderStats'
], setOrderLoading, 300)
return () => {
Loading.cleanup('dashboard-page')
Loading.cleanup('dashboard-user')
Loading.cleanup('dashboard-order')
}
}, [userId])
// 业务逻辑:根据不同的 loading 状态控制 UI
return (
<div className="dashboard">
{/* 页面顶部的全局进度条 */}
{pageLoading && <TopProgressBar />}
{/* 用户区域:独立的骨架屏 */}
<section className="user-section">
<h3>用户信息</h3>
{userLoading
? <Skeleton />
: <UserCard userId={userId} />
}
</section>
{/* 订单区域:独立的骨架屏 */}
<section className="order-section">
<h3>最近订单</h3>
{orderLoading
? <Skeleton />
: <OrderTable userId={userId} />
}
</section>
{/* 仅当全部完成时显示导出按钮 */}
{!pageLoading && <ExportButton />}
</div>
)
}设计说明:
| 级别 | tag | 监听接口 | 粒度 | UI 效果 |
|---|---|---|---|---|
| 页面 | dashboard-page | 4个接口 | 粗 | 顶部进度条 |
| 模块 | dashboard-user | 2个用户接口 | 中 | 用户区骨架屏 |
| 模块 | dashboard-order | 2个订单接口 | 中 | 订单区骨架屏 |
优势:
- 用户数据先返回 → 用户区立即渲染,订单区继续骨架屏,不需要等全部接口
- 页面级进度条在所有请求完成后消失,保证全局感知
- 每个 Loading 实例独立的
wait时间,可分别调优避免闪烁
自定义最小等待时间
为避免 Loading 闪烁,建议设置 wait 参数:
new Loading('smooth', keys, setLoading, 300) // 最少显示 300ms若实际请求在 300ms 内完成,会延迟到 300ms 后才调用 fn(false),确保用户能看到加载指示。
清理实例
Loading.cleanup('page-loading') // 清理指定实例
Loading.cleanup() // 清理全部实例拦截器监听
通过在全局和接口级拦截器中注入 Loading 逻辑,可以统一管理请求状态。
全局加载状态
利用请求计数器实现全局 loading:
let requestCount = 0
const http = new Http({
interceptors: {
request: (config) => {
requestCount++
document.body.classList.add('global-loading')
return config
},
responseSuccess: (response) => {
requestCount--
if (requestCount === 0) {
document.body.classList.remove('global-loading')
}
return response
},
responseError: (error) => {
requestCount--
if (requestCount === 0) {
document.body.classList.remove('global-loading')
}
return error
}
}
})接口级 Loading
在接口级拦截器中管理单个接口的加载状态:
const api = http.create('user', {
getUser: [
'GET', '/users/:id', {},
{
responseSuccess: (response) => {
// 请求成功后关闭该接口的 loading
setUserLoading(false)
return response
},
responseError: (error) => {
setUserLoading(false)
return error
}
}
]
})Loading 与拦截器联动实战
Loading 类与拦截器可以组合使用,实现全局遮罩 + 局部 Loading 的分层效果:
import Http from '@jiess/http'
import Loading from '@jiess/http/src/Loading'
// -------- 全局遮罩 --------
const http = new Http({
baseURL: 'https://api.example.com',
interceptors: {
request: (config) => {
// 请求开始时自动通知 Loading
Loading.request(config.httpKey)
document.body.classList.add('global-loading')
return config
},
responseSuccess: (response) => {
Loading.response(response.config.httpKey)
if (!document.querySelectorAll('.global-loading').length) {
document.body.classList.remove('global-loading')
}
return response
},
responseError: (error) => {
Loading.response(error.config?.httpKey || '')
document.body.classList.remove('global-loading')
return error
}
}
})
// -------- API 分组 --------
const userApi = http.create('user', {
getUser: ['GET', '/users/:id'],
getUsers: ['GET', '/users'],
})
const orderApi = http.create('order', {
getOrders: ['GET', '/orders'],
})
// -------- 组件级 Loading --------
function DashboardPage() {
const [pageLoading, setPageLoading] = useState(false)
useEffect(() => {
// 监听页面相关的全部接口
new Loading('dashboard', [
'module_user_getUsers',
'module_order_getOrders'
], setPageLoading, 400)
return () => Loading.cleanup('dashboard')
}, [])
// 手动触发数据加载
const loadAll = async () => {
// setPageLoading 会被 Loading 自动控制,无需手动 set
const [users, orders] = await Promise.all([
userApi.getUsers(),
orderApi.getOrders()
])
renderUI(users, orders)
}
return (
<div>
{pageLoading && <Spinner />}
<button onClick={loadAll} disabled={pageLoading}>
{pageLoading ? '加载数据中...' : '加载数据'}
</button>
</div>
)
}联动要点:
Loading.request(key)在全局request拦截器中触发 → 所有组件级 Loading 观察者自动感知Loading.response(key)在全局responseSuccess/responseError中触发 → Loading 状态正确关闭- 组件只需在
useEffect中创建 Loading 实例并指定监听范围,无需手动管理 loading 状态 - 错误拦截器中也必须调用
Loading.response(),否则一个请求失败会导致全局 loading 永远不关闭
上传进度监控
uploadProgress 静态方法
通过读取 File.stream() 实现进度监听:
Http.uploadProgress(file, (progress, uploadedSize) => {
console.log(`进度: ${Math.round(progress)}%, 已上传: ${uploadedSize} 字节`)
})| 参数 | 类型 | 说明 |
|---|---|---|
file | File | 要上传的文件对象 |
callback | Function | (progress: number, uploadedSize: number) => void |
完整上传示例
import Http from '@jiess/http'
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
// 监听进度
Http.uploadProgress(file, (progress, uploadedSize) => {
const percent = Math.round(progress)
progressBar.style.width = `${percent}%`
progressBar.textContent = `${percent}%`
})
// 执行上传
const formData = new FormData()
formData.append('file', file)
await api.uploadFile({}, formData)
})上传速度计算
let startTime = Date.now()
let lastSize = 0
Http.uploadProgress(file, (progress, size) => {
const elapsed = (Date.now() - startTime) / 1000
if (elapsed > 0) {
const speed = (size - lastSize) / elapsed // 字节/秒
console.log(`速度: ${(speed / 1024).toFixed(2)} KB/s`)
}
startTime = Date.now()
lastSize = size
})请求日志与性能监控
请求耗时日志
通过拦截器记录每次请求的耗时:
const http = new Http({
interceptors: {
request: (config) => {
config._startTime = Date.now()
return config
},
responseSuccess: (response) => {
const duration = Date.now() - (response.config._startTime || 0)
console.log(`[${response.config.method}] ${response.config.url} — ${duration}ms`)
return response
},
responseError: (error) => {
const duration = Date.now() - (error.config._startTime || 0)
console.error(`[${error.config.method}] ${error.config.url} — ${duration}ms (失败)`)
return error
}
}
})慢请求告警
responseSuccess: (response) => {
const duration = Date.now() - (response.config._startTime || 0)
if (duration > 2000) {
console.warn(`慢请求: ${response.config.url} (${duration}ms)`)
// 上报到监控系统
reportSlowRequest(response.config.url, duration)
}
return response
}缓存状态管理
Debug 调试开关
通过环境变量控制缓存哈希的调试日志:
# 开启调试
process.env.JIESS_HTTP_DEBUG = 'true'开启后控制台输出:
[Cache.hashBody] hash=7n9x2k len=256 elapsed=1ms body={"name":"张三",...生产环境不设置该变量即为关闭。
手动清理缓存
import { clearAllCaches } from '@jiess/http/src/Request/Cache'
// 清理全部缓存(如登出时)
clearAllCaches()状态管理库集成
React 集成示例
import { useState, useEffect } from 'react'
import Loading from '@jiess/http/src/Loading'
import { userApi } from './api/user'
function UserPage() {
const [loading, setLoading] = useState(false)
const [user, setUser] = useState(null)
useEffect(() => {
const loadingWatcher = new Loading(
'user-page',
['module_user_getUser'],
setLoading,
300
)
return () => Loading.cleanup('user-page')
}, [])
const fetchUser = async (id: number) => {
const result = await userApi.getUser(id)
setUser(result.responseText)
}
return (
<div>
{loading && <Spinner />}
{user && <UserCard data={user} />}
<Button onClick={() => fetchUser(1)} disabled={loading}>
{loading ? '加载中...' : '获取用户'}
</Button>
</div>
)
}Vue 集成示例
<template>
<div>
<Spinner v-if="loading" />
<UserCard v-if="user" :data="user" />
<button @click="fetchUser(1)" :disabled="loading">
{{ loading ? '加载中...' : '获取用户' }}
</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Loading from '@jiess/http/src/Loading'
import { userApi } from './api/user'
const loading = ref(false)
const user = ref(null)
let watcher
onMounted(() => {
watcher = new Loading('user-page', ['module_user_getUser'], (v) => loading.value = v, 300)
})
onUnmounted(() => Loading.cleanup('user-page'))
const fetchUser = async (id) => {
const result = await userApi.getUser(id)
user.value = result.responseText
}
</script>请求队列
控制并发请求数量,避免瞬时大量请求压垮服务器:
class RequestQueue {
private queue: any[] = []
private running = 0
constructor(private maxConcurrent = 3) {}
add<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject })
this.process()
})
}
private async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) return
this.running++
const { request, resolve, reject } = this.queue.shift()
try {
resolve(await request())
} catch (e) {
reject(e)
} finally {
this.running--
this.process()
}
}
}
// 使用
const queue = new RequestQueue(5)
for (let i = 0; i < 20; i++) {
queue.add(() => api.getUser(i)).then(console.log)
}最佳实践
- 分层 Loading:页面级用 Loading 类,组件级用拦截器 + 本地状态
- 适度防抖:设置合理的
wait参数(建议 200-500ms),避免 Loading 闪烁 - 及时清理:组件卸载时调用
Loading.cleanup(tag)释放资源 - 错误兜底:在
responseError拦截器中确保 loading 状态正确关闭 - 生产环境关闭 debug:
JIESS_HTTP_DEBUG仅用于开发/故障排查 - 请求队列:高并发场景使用队列限制并发数,保护后端服务
文档导航
| 文档 | 内容 |
|---|---|
| 介绍&上手 | 快速开始、配置规则、基本调用 |
| 资源请求工具 | Http / Factory / Request / Cache 类详解、安全机制、错误类型 |