Skip to content

资源请求监听 ​

本文档涵盖 @jiess/http 的请求状态监听机制,包括 Loading 类、拦截器监听、上传进度监控和状态管理集成。

核心 API 请参阅 资源请求工具,快速上手请参阅 介绍&上手。


架构概览 ​

                          ┌─────────────────────────────┐
                          │    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) 会在响应完成后调用。

构造函数 ​

typescript
new Loading(tag, keys, fn, wait?)
参数类型默认值说明
tagstring—观察器唯一标识,相同 tag 会覆盖旧实例
keysstring[]—要监听的接口 httpKey 数组
fnFunction()=>{}状态变化回调,fn(isLoading: boolean)
waitnumber300最少加载等待时间(毫秒),避免闪烁

基本用法 ​

typescript
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)。

工作原理 ​

  1. 接口请求开始时,Loading.request(httpKey) 检查是否匹配监听列表
  2. 若缓存为空(尚无正在进行的请求),标记开始时间,调用 fn(true)
  3. 接口响应完成时,Loading.response(httpKey) 移除缓存记录
  4. 所有缓存清空后,计算耗时:若不足 wait,延迟到 wait 后再调用 fn(false)
typescript
// 手动触发(通常不需要,拦截器已自动处理)
Loading.request('module_user_getUser')  // 标记开始
Loading.response('module_user_getUser')  // 标记完成

多实例协同 ​

可以为不同粒度的 UI 区域创建多个 Loading 实例:

typescript
// 页面级 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 分层设计:

typescript
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-page4个接口粗顶部进度条
模块dashboard-user2个用户接口中用户区骨架屏
模块dashboard-order2个订单接口中订单区骨架屏

优势:

  • 用户数据先返回 → 用户区立即渲染,订单区继续骨架屏,不需要等全部接口
  • 页面级进度条在所有请求完成后消失,保证全局感知
  • 每个 Loading 实例独立的 wait 时间,可分别调优避免闪烁

自定义最小等待时间 ​

为避免 Loading 闪烁,建议设置 wait 参数:

typescript
new Loading('smooth', keys, setLoading, 300) // 最少显示 300ms

若实际请求在 300ms 内完成,会延迟到 300ms 后才调用 fn(false),确保用户能看到加载指示。

清理实例 ​

typescript
Loading.cleanup('page-loading') // 清理指定实例
Loading.cleanup()               // 清理全部实例

拦截器监听 ​

通过在全局和接口级拦截器中注入 Loading 逻辑,可以统一管理请求状态。

全局加载状态 ​

利用请求计数器实现全局 loading:

typescript
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 ​

在接口级拦截器中管理单个接口的加载状态:

typescript
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 的分层效果:

typescript
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>
  )
}

联动要点:

  1. Loading.request(key) 在全局 request 拦截器中触发 → 所有组件级 Loading 观察者自动感知
  2. Loading.response(key) 在全局 responseSuccess/responseError 中触发 → Loading 状态正确关闭
  3. 组件只需在 useEffect 中创建 Loading 实例并指定监听范围,无需手动管理 loading 状态
  4. 错误拦截器中也必须调用 Loading.response(),否则一个请求失败会导致全局 loading 永远不关闭

上传进度监控 ​

uploadProgress 静态方法 ​

通过读取 File.stream() 实现进度监听:

typescript
Http.uploadProgress(file, (progress, uploadedSize) => {
  console.log(`进度: ${Math.round(progress)}%, 已上传: ${uploadedSize} 字节`)
})
参数类型说明
fileFile要上传的文件对象
callbackFunction(progress: number, uploadedSize: number) => void

完整上传示例 ​

typescript
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)
})

上传速度计算 ​

typescript
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
})

请求日志与性能监控 ​

请求耗时日志 ​

通过拦截器记录每次请求的耗时:

typescript
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
    }
  }
})

慢请求告警 ​

typescript
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 调试开关 ​

通过环境变量控制缓存哈希的调试日志:

bash
# 开启调试
process.env.JIESS_HTTP_DEBUG = 'true'

开启后控制台输出:

[Cache.hashBody] hash=7n9x2k len=256 elapsed=1ms body={"name":"张三",...

生产环境不设置该变量即为关闭。

手动清理缓存 ​

typescript
import { clearAllCaches } from '@jiess/http/src/Request/Cache'

// 清理全部缓存(如登出时)
clearAllCaches()

状态管理库集成 ​

React 集成示例 ​

typescript
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 集成示例 ​

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>

请求队列 ​

控制并发请求数量,避免瞬时大量请求压垮服务器:

typescript
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)
}

最佳实践 ​

  1. 分层 Loading:页面级用 Loading 类,组件级用拦截器 + 本地状态
  2. 适度防抖:设置合理的 wait 参数(建议 200-500ms),避免 Loading 闪烁
  3. 及时清理:组件卸载时调用 Loading.cleanup(tag) 释放资源
  4. 错误兜底:在 responseError 拦截器中确保 loading 状态正确关闭
  5. 生产环境关闭 debug:JIESS_HTTP_DEBUG 仅用于开发/故障排查
  6. 请求队列:高并发场景使用队列限制并发数,保护后端服务

文档导航 ​

文档内容
介绍&上手快速开始、配置规则、基本调用
资源请求工具Http / Factory / Request / Cache 类详解、安全机制、错误类型