Skip to content

@jiess/http 介绍与上手 ​

@jiess/http 是一个配置式前端 HTTP 请求库,以分组为单位定义接口,让开发者用尽可能少的代码完成 API 的声明、调用与状态管理。


核心亮点 ​

特性说明
🔥 并发去重分发同一接口并发请求仅发一次网络调用,结果分发给所有等待者
📝 一行定义接口数组式配置,['GET', '/users/:id'] 即完成一个接口声明
🚀 请求重试机制支持线性/指数退避重试,可配置次数与延迟
🛡 安全防护Content-Type 校验、URL 编码、请求体大小限制
📦 请求合并缓存基于 DJB2 哈希的请求体指纹,精准识别重复请求
🔌 双层拦截器全局 + 接口级拦截器,按固定顺序执行

安装 ​

bash
npm install @jiess/http
# 或
yarn add @jiess/http

注意:该库默认依赖浏览器环境的 window.fetch 或 XMLHttpRequest;通过配置 customFetch 也可在 Node.js 等非浏览器环境使用。


快速开始 ​

第一步:创建 Http 实例 ​

typescript
import Http from '@jiess/http'

const http = new Http({
  baseURL: 'https://api.example.com',
  timeout: 10000,           // 超时时间(毫秒)
  headers: {                // 全局请求头
    'Content-Type': 'application/json'
  },
  useCache: true,           // 启用请求缓存
  httpCacheParam: {
    waitTime: 5000          // 缓存有效期(毫秒)
  }
})

第二步:定义 API 接口 ​

typescript
const userApi = http.create('user', {
  getUser:    ['GET',    '/users/:id'],
  getUsers:   ['GET',    '/users'],
  createUser: ['POST',   '/users'],
  updateUser: ['PUT',    '/users/:id'],
  deleteUser: ['DELETE', '/users/:id']
})

第三步:调用接口 ​

typescript
// GET 请求:第一个参数为路径参数
const user = await userApi.getUser(1)
// 请求 URL → /users/1

// GET 带查询参数:传入对象
const users = await userApi.getUsers({ page: 1, limit: 10 })
// 请求 URL → /users?page=1&limit=10

// POST 请求:第一个参数为请求体
const newUser = await userApi.createUser({
  name: '张三',
  age: 25
})

// PUT 请求:路径参数 + 请求体
await userApi.updateUser(1, { name: '李四' })

配置规则详解 ​

每个接口由数组定义,格式如下:

typescript
http.create('分组名', {
  接口名: [
    请求方法,        // 必填:GET / POST / PUT / PATCH / DELETE
    请求地址,        // 必填:字符串或函数
    基础数据?,       // 可选:headers / query / body 等默认参数
    拦截器?          // 可选:request / responseSuccess / responseError
  ]
})

基础配置 ​

typescript
const api = http.create('user', {
  getUser:    ['GET',    '/users/:id'],
  createUser: ['POST',   '/users'],
  updateUser: ['PUT',    '/users/:id'],
  deleteUser: ['DELETE', '/users/:id']
})

带基础数据的配置 ​

typescript
const api = http.create('user', {
  searchUsers: ['GET', '/users/search', {
    headers: { 'X-Custom-Header': 'search' },
    responseType: 'json'
  }]
})

带拦截器的配置 ​

typescript
const api = http.create('user', {
  getUser: [
    'GET',
    '/users/:id',
    {},  // 基础数据
    {    // 接口级拦截器
      responseError: (error) => {
        if (error.response?.status === 404) {
          return { defaultUser: true }
        }
        throw error
      }
    }
  ]
})

参数传递规则 ​

不同 HTTP 方法的参数语义不同:

方法第一个参数第二个参数
GET / DELETE / HEADquery 查询参数body 请求体
POST / PUT / PATCHbody 请求体query 查询参数
typescript
// POST:body → 请求体,query → 查询参数
await api.createUser({ name: '张三' }, { timestamp: Date.now() })
// URL: /users?timestamp=1700000000
// Body: {"name":"张三"}

// GET:query → 查询参数
await api.getUsers({ page: 1, status: 'active' })
// URL: /users?page=1&status=active

动态 URL ​

支持路径参数替换、函数式构建和异步函数:

typescript
const api = http.create('user', {
  // 路径参数替换 → /users/1
  byId: ['GET', '/users/:id'],

  // 同步函数式 → /users/1?name=张三&extra=value
  byFunc: ['GET', (id, name) => `/users/${id}?name=${encodeURIComponent(name)}&extra=value`],

  // 异步函数式 → 可异步获取参数后再构建
  byAsync: ['GET', async (token) => {
    const id = await resolveId(token)
    return `/users/${id}`
  }]
})

错误处理 ​

typescript
// 1. try-catch 基本处理
try {
  const user = await userApi.getUser(999)
} catch (error) {
  console.error('请求失败:', error)
}

// 2. 全局拦截器统一处理
const http = new Http({
  interceptors: {
    responseError: (error) => {
      switch (error.code) {
        case 401: /* 跳转登录 */ break
        case 403: /* 无权限提示 */ break
        case 404: /* 资源不存在 */ break
        default:  /* 通用错误 */  break
      }
      return error
    }
  }
})

完整端到端示例 ​

以下是一个从初始化到渲染的完整 React 组件示例,展示 @jiess/http 在实际项目中的典型用法:

typescript
// -------- api/http.ts --------
import Http from '@jiess/http'

export const http = new Http({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
  useCache: true,
  httpCacheParam: { waitTime: 5000 },
  retry: { count: 2, delay: 1000, backoff: 'exponential' },
  interceptors: {
    request: (config) => {
      const token = localStorage.getItem('token')
      if (token) config.headers['Authorization'] = `Bearer ${token}`
      return config
    },
    responseError: (error) => {
      if (error.code === 401) {
        localStorage.removeItem('token')
        window.location.href = '/login'
      }
      return error
    }
  }
})

// -------- api/user.ts --------
import { http } from './http'

export const userApi = http.create('user', {
  getUser:    ['GET',    '/users/:id'],
  getUsers:   ['GET',    '/users'],
  createUser: ['POST',   '/users'],
  updateUser: ['PUT',    '/users/:id'],
  deleteUser: ['DELETE', '/users/:id']
})

// -------- components/UserList.tsx --------
import { useState, useEffect } from 'react'
import { userApi } from '../api/user'
import type { TypeConfig } from '@jiess/http'

function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  const fetchUsers = async (page = 1) => {
    setLoading(true)
    setError(null)
    try {
      const result = await userApi.getUsers({ page, limit: 20 })
      // 响应结构(fetch 模式):{ responseText, response, config }
      // 响应结构(XHR 模式):{ responseText, xhr, config }
      setUsers(result.responseText.data || [])
    } catch (err) {
      setError('获取用户列表失败,请稍后重试')
      console.error('请求错误:', err)
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => { fetchUsers() }, [])

  if (error) return <div className="error-msg">{error}</div>

  return (
    <div>
      <h2>用户列表 {loading && '(加载中...)'}</h2>
      <table>
        <thead>
          <tr><th>姓名</th><th>年龄</th><th>操作</th></tr>
        </thead>
        <tbody>
          {users.map((u: any) => (
            <tr key={u.id}>
              <td>{u.name}</td>
              <td>{u.age}</td>
              <td>
                <button onClick={async () => {
                  try {
                    await userApi.deleteUser(u.id)
                    fetchUsers()  // 删除成功后刷新列表
                  } catch (err) {
                    alert('删除失败')
                  }
                }}>删除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      {/* 创建用户弹窗 */}
      <CreateUserDialog onSuccess={() => fetchUsers()} />
    </div>
  )
}

// -------- components/CreateUserDialog.tsx --------
function CreateUserDialog({ onSuccess }: { onSuccess: () => void }) {
  const [submitting, setSubmitting] = useState(false)
  const [formData, setFormData] = useState({ name: '', age: 0 })

  const handleSubmit = async () => {
    setSubmitting(true)
    try {
      await userApi.createUser(formData)
      onSuccess()        // 通知父组件刷新
      setFormData({ name: '', age: 0 })  // 重置表单
    } catch (err) {
      alert('创建失败: ' + (err as Error).message)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <div className="dialog">
      <input
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="姓名"
      />
      <input
        type="number"
        value={formData.age}
        onChange={(e) => setFormData({ ...formData, age: Number(e.target.value) })}
        placeholder="年龄"
      />
      <button onClick={handleSubmit} disabled={submitting}>
        {submitting ? '提交中...' : '创建'}
      </button>
    </div>
  )
}

代码要点:

  1. http 实例统一管理 baseURL、timeout、retry 和拦截器
  2. userApi 按业务域独立文件,create() 一行定义增删改查
  3. 组件通过 try/catch/finally 控制 loading/error/success 三态
  4. 删除/创建成功后调用 fetchUsers() 刷新,形成完整的 CRUD 闭环

全局配置速查 ​

配置项类型默认值说明
baseURLstring''全局基础 URL
timeoutnumberundefined请求超时(毫秒)
useFetchbooleantrue使用 fetch 或 XHR
useCachebooleantrue启用请求缓存
httpCacheParamobject{}缓存参数,含 waitTime
headersobject{}全局请求头
interceptorsobject{}全局拦截器
customFetchfunction—自定义 fetch 实现
httpBeforefunction—请求前钩子
maxBodySizenumber10MB请求体大小限制(字节)
debugbooleanfalse调试模式
retryobject—重试配置(见下文)

重试配置 ​

typescript
const http = new Http({
  retry: {
    count: 3,                       // 最大重试次数
    delay: 1000,                    // 延迟(毫秒)
    backoff: 'exponential',         // 'linear' | 'exponential'
    onRetry: (error, attempt) => {
      console.log(`第 ${attempt} 次重试,错误:`, error)
    }
  }
})
  • linear:每次延迟相同
  • exponential:延迟 = delay × 2^(attempt-1)

模块化组织 ​

推荐按业务域拆分 API 定义:

typescript
// api/http.ts
import Http from '@jiess/http'

export const http = new Http({
  baseURL: 'https://api.example.com',
  headers: { 'Content-Type': 'application/json' }
})

// api/user.ts
import { http } from './http'

export const userApi = http.create('user', {
  getUser:    ['GET',    '/users/:id'],
  createUser: ['POST',   '/users'],
})

// api/order.ts
import { http } from './http'

export const orderApi = http.create('order', {
  getOrders:  ['GET',  '/orders'],
  getOrder:   ['GET',  '/orders/:id'],
})

// api/index.ts
export { userApi } from './user'
export { orderApi } from './order'

常见问题 ​

如何处理跨域请求? ​

@jiess/http 不内置跨域处理,跨域问题需要由服务端配置 CORS 响应头。如果在开发阶段需要绕过跨域,可使用 webpack-dev-server proxy 或 Vite proxy:

typescript
// vite.config.ts
export default {
  server: {
    proxy: { '/api': 'http://localhost:8080' }
  }
}

请求超时了会怎样? ​

超时后,Factory.toRequest() 的 Promise.race 会触发超时 Promise,返回一个 { code: 408, msg: '请求超时' } 结构的错误对象。如果你配置了 retry,超时错误同样会触发重试:

typescript
const http = new Http({
  timeout: 5000,
  retry: { count: 2, delay: 1000 }
})
// 请求超过 5 秒 → 51000ms 后重试一次 → 最多重试 2 次 → 仍失败则抛出

如何取消一个请求? ​

使用 AbortController:

typescript
const controller = new AbortController()

// 方式一:接口配置中传入 signal
const api = http.create('user', {
  getUsers: ['GET', '/users', { signal: controller.signal }]
})

// 方式二:调用时传入 info 参数
api.getUsers({}, {}, { signal: controller.signal })

// 2秒后取消
setTimeout(() => controller.abort(), 2000)

GET 和 POST 的参数到底怎么传? ​

规则很简单——看方法名:

调用方式URL 效果说明
api.getUser(1)/users/1数字/字符串参数自动替换 :id
api.getUsers({ page: 1 })/users?page=1对象参数拼接为 query string
api.createUser({ name: '张三' })POST /users + body {"name":"张三"}POST/PUT/PATCH 时对象作为请求体
api.updateUser(1, { name: '李四' })PUT /users/1 + body {"name":"李四"}路径参数 + 请求体

响应数据怎么取? ​

接口返回结构因底层实现(fetch / XHR / customFetch)而略有差异,公共字段为 config:

typescript
{
  responseText?: any,    // 解析后的响应体(fetch 模式下根据 responseType 解析;XHR 模式始终存在)
  response?: Response,   // fetch 模式:原生 fetch Response 对象
  xhr?: XMLHttpRequest,  // XHR 模式:原生 XMLHttpRequest 对象
  config: TypeConfig     // 本次请求的完整配置
}

字段说明:

  • fetch 模式(useFetch: true 且未配置 customFetch):返回 { config, response, responseText? },responseText 在 responseType 对应方法可用时存在
  • XHR 模式(useFetch: false):返回 { config, xhr, responseText },注意字段名为 xhr 而非 response
  • customFetch 模式:返回 { config, ...customFetch返回值 },由用户自定义结构

取数据时通常使用 .responseText。如果后端返回 { code: 0, data: [...] },则在拦截器中做解包:

typescript
interceptors: {
  responseSuccess: (res) => {
    const data = res.responseText
    if (data.code !== 0) throw new Error(data.msg || '请求失败')
    return data.data  // 直接返回 data 字段
  }
}

缓存什么时候生效? ​

  • 仅 GET 请求才会缓存(POST/PUT/PATCH/DELETE 自动跳过)
  • 以下情况不会缓存:自定义 type 参数、显式 useCache: false、请求体包含文件
  • 缓存键 = httpKey + URL + query + headers + body 哈希,参数完全相同时命中
  • 缓存有效期由 httpCacheParam.waitTime 控制,过期自动清除

最佳实践 ​

  1. 按业务域拆分:不同业务模块独立定义 API 文件
  2. 统一错误拦截:在全局拦截器中处理 401/403/500 等通用错误
  3. 合理使用缓存:数据变化不频繁的接口开启缓存,实时数据关闭缓存
  4. 设置超时时间:根据接口预期响应时间合理配置,避免无限等待
  5. 路由级 Loading:结合 Loading 类监听接口状态,提供精确的加载反馈

文档导航 ​

文档内容
资源请求工具Http / Factory / Request / Cache 类详解、配置参考、安全机制、错误类型
资源请求监听Loading 类、上传进度、拦截器监听、状态管理集成