Skip to content

资源请求工具 — API 参考 ​

本文档是 @jiess/http 核心类的详细参考,涵盖 Http 入口、Factory 工厂、Request 请求执行器、Cache 缓存引擎,以及完整的配置与类型说明。

快速上手请参阅 介绍&上手。


架构概览 ​

Http                          ← 对外入口,管理全局配置与 API 分组
  └─ create()  → Factory      ← 工厂类,链式注入参数 → 拦截器 → 发起请求
                   └─ toRequest()  → Request   ← 执行请求(fetch / XHR),支持重试
                                       └─ send()  → Cache  ← 缓存引擎,并发去重分发

Http 类 ​

构造函数 ​

typescript
new Http(config?: TypeConfig)

TypeConfig 完整定义如下(继承自 UnObject,允许任意额外属性——除下列显式字段外,还可自由扩展自定义配置项):

typescript
type TypeConfig = UnObject & {
  name?: string                        // 模块名称
  timeout?: number                     // 超时时间(毫秒)
  useFetch?: boolean                   // 是否使用 fetch,默认 true
  useCache?: boolean                   // 是否启用缓存,默认 true
  headers?: StrObject                  // 全局请求头(键值均为 string)
  baseURL?: string                     // 全局基础 URL(通过 UnObject 扩展支持)
  maxBodySize?: number                 // 请求体大小限制(字节),默认 10MB
  debug?: boolean                      // 调试模式,默认 false
  httpCacheParam?: {
    waitTime: number                   // 缓存有效期(毫秒)
  } & UnObject                         // 允许扩展额外缓存配置
  retry?: {
    count?: number                     // 最大重试次数,默认 0
    delay?: number                     // 重试延迟(毫秒),默认 1000
    backoff?: 'linear' | 'exponential' // 退避策略,默认 'linear'
    onRetry?: (error: any, count: number) => void  // 重试回调
  }
  interceptors?: {
    request?: Function                 // 请求拦截器
    responseSuccess?: Function         // 响应成功拦截器
    responseError?: Function           // 响应错误拦截器
  }
  customFetch?: Function               // 自定义 fetch 实现
  httpBefore?: Function                // 请求前钩子
  onInitXHR?: Function                 // XHR 初始化钩子
}

create(groupName, param) ​

创建一组 API 接口,返回的对象中包含所有定义的方法及元信息。

参数:

参数类型说明
groupNamestring分组名称,用于生成唯一标识
paramTypeCreate键为接口名,值为数组 [method, url, baseData?, interceptors?]

返回值属性:

属性类型说明
$moduleNamestring模块唯一标识(UUID)
$groupNamestring分组名称
$moduleGroupstring模块+分组组合标识
$groupKeysstring[]该组所有接口的唯一标识数组
[接口名]Function异步函数,(arg1?, arg2?, info?) => Promise<Response>

示例:

typescript
const api = http.create('user', {
  getUser: [
    'GET',
    '/users/:id',
    { responseType: 'json' },           // 基础数据
    { responseError: (e) => {...} }     // 接口级拦截器
  ],
  createUser: ['POST', '/users']
})

// 调用
const user = await api.getUser(1)
// api.$moduleGroup  →  "uuid_user"
// api.$groupKeys    →  ["uuid_user_getUser", "uuid_user_createUser"]

getRequestHeaders(baseData, info) ​

合并基础数据中的 headers 与调用时传入的 headers。

typescript
const headers = http.getRequestHeaders(
  { headers: { Authorization: 'Bearer xxx' } },
  { headers: { 'X-Request-ID': 'req-1' } }
)
// → { Authorization: 'Bearer xxx', 'X-Request-ID': 'req-1' }

getRequestData(method, arg1, arg2) ​

根据 HTTP 方法分配参数语义:

方法arg1 语义arg2 语义
GET / DELETE / HEADquery 查询参数body 请求体
POST / PUT / PATCHbody 请求体query 查询参数

uploadProgress(file, callback) 静态方法 ​

监听文件读取流进度,适用于上传场景。

typescript
Http.uploadProgress(file, (progress, uploadedSize) => {
  console.log(`进度: ${Math.round(progress)}%, 已上传: ${uploadedSize} 字节`)
})

Factory 类 ​

Factory 是 Http 与 Request 之间的桥梁,负责将全局配置、接口定义、调用参数链式组合后发起请求。

构造函数 ​

typescript
new Factory(config: TypeConfig, url: string, method: string, httpKey: string)

链式方法 ​

addRequestData(data) ​

注入请求参数,如 responseType、query、body 等。可多次调用,同名属性后覆盖前。

typescript
.addRequestData({ responseType: 'blob' })
.addRequestData({ body: { name: '张三' } })

addHeaderData(headers) ​

注入额外的请求头,合并到已有 headers 中。

typescript
.addHeaderData({ Authorization: 'Bearer xxx' })

addInterceptors(interceptors) ​

注入接口级拦截器对象。

typescript
.addInterceptors({
  request: (config) => { ... },
  responseError: (error) => { ... }
})

toRequest() ​

终结方法,执行完整的请求流程(含重试),返回 Promise<Response>。

重试机制 ​

由 config.retry 配置控制的重试流程:

请求失败 → 检查 retry.count → 计算延迟 → onRetry 回调 → 重发请求
                                                ↓
                                   达到最大次数 → responseError 拦截器 → 抛出
typescript
// 指数退避:第1次等1000ms → 第2次等2000ms → 第3次等4000ms
const http = new Http({
  retry: {
    count: 3,
    delay: 1000,
    backoff: 'exponential',
    onRetry: (error, attempt) => {
      console.warn(`重试第 ${attempt} 次:`, error.message)
    }
  }
})

拦截器执行顺序 ​

1. 全局 request 拦截器
2. 接口级 request 拦截器
3. [实际网络请求]
4. 全局 responseSuccess / responseError 拦截器
5. 接口级 responseSuccess / responseError 拦截器
typescript
const http = new Http({
  interceptors: {
    request: (config) => {
      // ① 全局:添加认证 token
      config.headers['Authorization'] = `Bearer ${getToken()}`
      return config
    },
    responseError: (error) => {
      // ④ 全局:统一错误上报
      reportError(error)
      return error
    }
  }
})

const api = http.create('user', {
  save: [
    'POST', '/users', {},
    {
      request: (config) => {
        // ② 接口级:添加时间戳
        config.headers['X-Timestamp'] = Date.now().toString()
        return config
      },
      responseError: (error) => {
        // ⑤ 接口级:自定义降级
        return { fallback: true }
      }
    }
  ]
})

请求方法详解 ​

GET 请求 ​

用于获取资源,参数放在 URL 查询字符串中,支持缓存。

typescript
const api = http.create('user', {
  getUsers: ['GET', '/users'],              // 获取列表
  getUser:  ['GET', '/users/:id'],          // 获取详情(路径参数)
  search:   ['GET', '/users/search'],       // 条件搜索
})

// 无参数
await api.getUsers()

// 对象参数 → 自动转为 query string
await api.getUsers({ page: 1, limit: 10, status: 'active' })
// → GET /users?page=1&limit=10&status=active

// 数字/字符串参数 → 替换路径 :id
await api.getUser(1)
// → GET /users/1

// 混合参数:arg1 = query,arg2 = body(GET 很少传 body)
await api.search({ keyword: '张三' }, { ext: 'value' })
// → GET /users/search?keyword=张三,body = { ext: 'value' }

POST 请求 ​

用于创建资源,参数作为请求体发送,不支持缓存。

typescript
const api = http.create('user', {
  createUser: ['POST', '/users'],
  batchCreate: ['POST', '/users/batch'],
})

// 对象 → JSON 请求体
await api.createUser({ name: '张三', age: 25, email: 'zhangsan@test.com' })
// → POST /users
//    Content-Type: application/json
//    Body: {"name":"张三","age":25,"email":"zhangsan@test.com"}

// arg1 = body,arg2 = query
await api.createUser({ name: '张三' }, { timestamp: Date.now() })
// → POST /users?timestamp=1700000000
//    Body: {"name":"张三"}

// FormData 上传
const formData = new FormData()
formData.append('name', '张三')
formData.append('avatar', file)
await api.createUser(formData)
// → type 未指定,不设 content-type,由浏览器自动加 boundary

PUT 请求 ​

用于完整替换资源,参数作为请求体,不支持缓存。

typescript
const api = http.create('user', {
  updateUser: ['PUT', '/users/:id'],
  replaceConfig: ['PUT', '/config/full'],
})

// 路径参数 + 请求体
await api.updateUser(1, {
  name: '李四',
  age: 26,
  email: 'lisi@test.com'
})
// → PUT /users/1
//    Body: {"name":"李四","age":26,"email":"lisi@test.com"}

// 全量替换
await api.replaceConfig({ theme: 'dark', lang: 'zh-CN' })

PATCH 请求 ​

用于部分修改资源,只传需要修改的字段,不支持缓存。

typescript
const api = http.create('user', {
  patchUser: ['PATCH', '/users/:id'],
})

// 只传要更新的字段
await api.patchUser(1, { age: 27 })
// → PATCH /users/1
//    Body: {"age":27}

// 可以带查询参数
await api.patchUser(1, { status: 'active' }, { version: 2 })
// → PATCH /users/1?version=2
//    Body: {"status":"active"}

DELETE 请求 ​

用于删除资源,参数优先作为查询参数,不支持缓存。

typescript
const api = http.create('user', {
  deleteUser: ['DELETE', '/users/:id'],
  batchDelete: ['DELETE', '/users'],
})

// 单个删除(路径参数)
await api.deleteUser(1)
// → DELETE /users/1

// 批量删除(query string)
await api.batchDelete({ ids: '1,2,3' })
// → DELETE /users?ids=1,2,3

// 带请求体删除(少见)
await api.batchDelete({ ids: '1,2,3' }, { force: true })
// → DELETE /users?ids=1,2,3
//    Body: {"force":true}

传参对照总表 ​

方法用途arg1 语义arg2 语义是否缓存accept
GET获取资源querybody✅['GET','/api',{...}]
POST创建资源bodyquery❌['POST','/api',{...}]
PUT完整替换bodyquery❌['PUT','/api/:id',{...}]
PATCH部分修改bodyquery❌['PATCH','/api/:id',{...}]
DELETE删除资源querybody❌['DELETE','/api/:id',{...}]

Request 类 ​

实际执行 HTTP 请求的底层类,负责构建 URL、headers、body,并调用 fetch 或 XHR。

构造函数 ​

typescript
new Request(param: TypeRequestParam, config?: UnObject)

TypeRequestParam 继承自 TypeConfig(意味着它包含 TypeConfig 的全部字段,如 timeout/baseURL/headers/retry 等,并在其基础上新增以下请求专用字段):

typescript
type TypeRequestParam = {
  url: string           // 完整 URL(已拼装 baseURL)
  httpKey: string       // 接口唯一标识
  mainURL: string       // 原始 URL(不含 baseURL)
  method: string        // HTTP 方法
  body?: unknown        // 请求体
  query?: UnObject      // 查询参数(追加到 URL)
  responseType?: string // 响应解析类型(json/text/blob/arrayBuffer...)
  type?: string         // Content-Type 来源类型(json/form/text/html/自定义 MIME)
  useCache?: boolean    // 是否使用缓存(来自 TypeConfig)
  headers?: StrObject   // 请求头(来自 TypeConfig)
  // ... 以及 TypeConfig 的全部字段(timeout/baseURL/retry/interceptors 等)
} & TypeConfig

内部流程 ​

constructor()
  ├── initFullUrl()      → 拼接 query 参数到 URL
  ├── initHeaders()      → headers 标准化(转小写)→ sanitizeContentType()
  ├── initBody()         → 根据 type 序列化 body → checkBodySize()
  └── send()             → Cache 缓存判断 → request()
        ├── initResult() → httpBefore 钩子
        └── fetch / XHR  → 解析响应(json/blob/text/arrayBuffer)

initBody 的 content-type 推断 ​

type 参数行为Content-Type
'json'JSON.stringify(body)application/json
'form'序列化为 URL 编码application/x-www-form-urlencoded
'text'原样保留text/plain
'html'原样保留text/html
自定义字符串校验合法性后使用合法则原值,非法则 application/octet-stream
未指定不设置 content-type(如文件上传)—

checkBodySize ​

Constructor 中自动执行,检查请求体大小:

typescript
// 默认限制 10MB
const { maxBodySize = 10 * 1024 * 1024 } = this.config

超限抛出 Error('请求体过大 (size > maxBodySize bytes)')。

responseType 响应类型详解 ​

responseType 决定了 send() 如何解析服务端返回的数据,影响 responseText 的最终形态:

responseType解析方式responseText 类型适用场景
'json'response.json()object / arrayRESTful API 返回 JSON
'text'(默认)response.text()string纯文本、HTML、XML
'blob'response.blob()Blob下载文件、图片预览
'arraybuffer'response.arrayBuffer()ArrayBuffer二进制数据、音频处理
'form'→ Content-Type 设为表单stringURL 编码表单提交
typescript
const api = http.create('file', {
  // JSON 数据
  getData: ['GET', '/api/data', { responseType: 'json' }],

  // 纯文本
  getText: ['GET', '/api/text', { responseType: 'text' }],

  // 下载图片
  getImage: ['GET', '/api/image', { responseType: 'blob' }],

  // 二进制文件
  getBinary: ['GET', '/api/binary', { responseType: 'arraybuffer' }],
})

// JSON → responseText 是解析后的对象
const { responseText: data } = await api.getData()
console.log(data.users)

// Blob → 可用 createObjectURL 预览
const { responseText: blob } = await api.getImage()
imgEl.src = URL.createObjectURL(blob)

// ArrayBuffer → 可传给 AudioContext 等 API
const { responseText: buffer } = await api.getBinary()
audioContext.decodeAudioData(buffer)

注意:responseType 仅影响响应解析方式,不影响请求 Content-Type。如需自定义请求 Content-Type,请在基础数据中设置 headers 或使用 type 参数。


Cache 类 ​

缓存引擎,核心功能为并发请求去重分发:相同请求只发一次网络调用,结果分发给所有等待者。

缓存状态机 ​

状态码含义行为
1000未开始等待第一个请求完成,回调入队
2000已完成(成功)直接 resolve 缓存值
3000已完成(失败)直接 reject 并移除缓存项

缓存键生成 ​

typescript
// 缓存键格式
`${httpKey}::${url}::${sortedQuery}::${sortedHeaders}::${bodyHash}`
  • httpKey:接口唯一标识(模块_分组_接口名)
  • url:完整请求 URL
  • sortedQuery:排序后的查询参数 key=value 串
  • sortedHeaders:排序 + 小写处理后的请求头
  • bodyHash:请求体的 DJB2 32位哈希(36进制)

哈希算法 ​

使用 DJB2 算法:

hash(0) = 5381
hash(i) = ((hash(i-1) << 5) + hash(i-1)) + charCode(i)
hash_final = hash >>> 0 (转无符号 32 位)

调试模式(process.env.JIESS_HTTP_DEBUG === 'true')下会打印:

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

关键行为 ​

  • 同一 httpKey + URL + query + headers + body 完全相同的并发请求,只发一次
  • 所有等待回调共享同一结果(成功或失败)
  • 缓存项在 waitTime 后自动清除;waitTime 为 0 则立即清除
  • 以下场景不缓存:POSt/PUT/PATCH/DELETE 方法、自定义 type 参数、显式设置 useCache: false

安全机制 ​

Content-Type 校验 ​

参考独立工具模块 [validators.ts](file:///d:/WORKSPACE/JIESS/jiess-2024-6-16/JIESS/TS-LIB/jiess-http/src/utils/validators.ts),提供以下导出:

导出类型说明
CONTENT_TYPE_REGEXRegExp/^[a-zA-Z][a-zA-Z0-9\-+.]*\/[a-zA-Z][a-zA-Z0-9\-+.]*$/
DEFAULT_CONTENT_TYPEstring'application/octet-stream'
isValidContentType(ct)(string) => boolean校验 MIME 类型合法性
sanitizeContentType(ct)(string?) => string清洗非法值,回退默认
typescript
import { isValidContentType, sanitizeContentType } from '@jiess/http/src/utils/validators'

isValidContentType('application/json')                     // true
isValidContentType('<script>alert(1)</script>')           // false
isValidContentType('text/plain\r\nX-Injected:malicious')  // false

sanitizeContentType('application/json')                    // 'application/json'
sanitizeContentType('<script>alert(1)</script>')           // 'application/octet-stream'
sanitizeContentType(undefined)                             // 'application/octet-stream'

Request 类的 initHeaders() 和 initBody() 中已自动调用 sanitizeContentType 和 isValidContentType,无需手动处理。

URL 编码 ​

路径参数中的字符串和非纯数字参数会通过 encodeURIComponent() 编码,防止 XSS 注入:

typescript
// 源码中 buildUrl 对 g1/g2 调用 encodeURIComponent
api.getUser('<img src=x>')  // URL → /users/%3Cimg%20src%3Dx%3E

请求体大小限制 ​

默认 10MB,可通过 maxBodySize 配置调整(单位:字节)。超出时在构造函数阶段即抛出 Error('请求体过大 (size > maxBodySize bytes)')。

typescript
const http = new Http({ maxBodySize: 2 * 1024 * 1024 })  // 限制 2MB

maxBodySize 极端值行为表 ​

配置值行为说明
未设置 / undefined / null默认 10MB!= null 判断为假,回退默认值
1024限制 1024 字节正常生效
0零限制,任何非空 body 都拦截0 != null 为真,严格生效
负数 (如 -1)所有 body 都会超限-1 != null 为真,任何 bodySize > -1 均成立
Infinity永不拦截合法值,任何有限 body 均小于它

⚠️ 注意:maxBodySize: 0 与 maxBodySize: null 是不同的语义:

  • 0 → 零容忍,所有请求体都被拒绝(包括 { a: 1 } 这种最小 JSON)
  • null / 不传 → 回退为默认 10MB

如果你需要禁用请求体大小检查,请设置 maxBodySize: Infinity 而不是 0。

与 falsy body 的联动 ​

修复后 body = 0、body = false 等 falsy 值会被正常序列化(如 JSON.stringify(0) → "0"),因此也会计入请求体大小检查。例如:

typescript
// maxBodySize: 1 → "0" 序列化后 2 字节 → 拦截抛错
new Http({ maxBodySize: 1 }).create('test', { post: ['POST', '/api'] }).post(0)

maxBodySize: 0 实战场景 ​

场景一:只读 API 网关 — 后端仅提供 GET 查询,不应接受任何请求体。

typescript
const readOnlyApi = new Http({
  baseURL: 'https://query-api.example.com',
  maxBodySize: 0,  // 零容忍:任何 POST/PUT/PATCH 带 body 立即拦截
})

const api = readOnlyApi.create('readonly', {
  search: ['GET', '/search'],
  detail: ['GET', '/detail/:id'],
})

// GET 正常(无 body,不触发检查)
await api.search({ q: 'hello' })  // ✅ 通过

// 误用的 POST 在构造函数阶段即抛错
await api.search.post({ q: 'hello' })
// ❌ Error: 请求体过大 (21 > 0 bytes)

场景二:安全沙箱环境 — 审计模式,记录所有尝试提交的请求但禁止实际执行。

typescript
const auditSandbox = new Http({
  baseURL: 'https://internal.example.com',
  maxBodySize: 0,
  interceptors: {
    request: (config) => {
      // 请求体大小检查在拦截器之前执行,所以此处不会被触发
      console.warn('[审计] 拦截到一次非法写请求', config)
      return config
    },
  },
})

const api = auditSandbox.create('sandbox', {
  create: ['POST', '/resource'],
  update: ['PUT', '/resource/:id'],
})

try {
  await api.create({ name: 'test' })  // ❌ 被 maxBodySize:0 拦截
} catch (err) {
  // 安全审计日志已在构造函数中产生
  reportSecurityEvent(err)
}

场景三:逐接口精细控制 — 全局宽松,特定敏感接口零容忍。

typescript
const mixedApi = new Http({ baseURL: 'https://api.example.com' })
// 全局不设 maxBodySize → 默认 10MB

// 普通接口允许 body
const normalApi = mixedApi.create('normal', {
  submit: ['POST', '/submit'],
})

// 敏感接口:通过内联配置覆盖,禁止任何请求体
const sensitiveApi = mixedApi.create('sensitive', {
  delete: ['DELETE', '/admin/delete', {
    maxBodySize: 0,  // 仅此接口生效
  }],
})

await normalApi.submit({ data: 'ok' })  // ✅ 10MB 内放行
await sensitiveApi.delete({ id: 1 })     // ❌ 0 byte 限制立即拦截

maxBodySize 配置最佳实践与常见陷阱 ​

最佳实践 ​

1. 始终使用显式数值,避免依赖默认值语义 ​

typescript
// ❌ 不推荐:依赖 10MB 默认值,当库升级后默认值可能变化
const http = new Http({ baseURL: 'https://api.example.com' })

// ✅ 推荐:显式声明限制值,可读且不依赖默认值
const http = new Http({
  baseURL: 'https://api.example.com',
  maxBodySize: 10 * 1024 * 1024,  // 10MB
})

2. 根据接口粒度分层设置 ​

typescript
// ✅ 全局宽松 + 敏感接口收紧
const http = new Http({
  baseURL: 'https://api.example.com',
  maxBodySize: 10 * 1024 * 1024,  // 全局 10MB
})

const generalApi = http.create('general', {
  upload:  ['POST', '/upload'],
})

const adminApi = http.create('admin', {
  bulkDelete: ['DELETE', '/admin/delete', {
    maxBodySize: 1 * 1024 * 1024,  // 单接口限制 1MB
  }],
})

3. 用 Infinity 禁用检查,而不是很大的数字 ​

typescript
// ❌ 不推荐:魔法数字,语义模糊
const http = new Http({ maxBodySize: 999999999 })

// ✅ 推荐:语义明确,永不拦截
const http = new Http({ maxBodySize: Infinity })

4. 在沙箱/审计场景使用 maxBodySize: 0 ​

typescript
// ✅ 审计模式:捕获所有写操作但不执行
const auditHttp = new Http({
  maxBodySize: 0,
  interceptors: {
    request: (config) => {
      auditLogger.log('ATTEMPTED_WRITE', config)
      return config  // 不会执行到这里,maxBodySize:0 已经抛出
    }
  }
})

常见陷阱 ​

陷阱 1:maxBodySize: 0 ≠ "不限制" ​

这是最常见的误解。语义对照表:

你想要的正确的配置错误配置(陷阱)
不限制大小maxBodySize: InfinitymaxBodySize: 0 ❌
回退默认 10MB不设置 / undefined / nullmaxBodySize: 0 ❌
禁止所有请求体maxBodySize: 0 ✅—
typescript
// ❌ 开发者误以为 0 表示"不限制"
const http = new Http({ maxBodySize: 0 })
await http.create('api', { submit: ['POST', '/submit'] }).submit({ data: 'hello' })
// 💥 Error: 请求体过大 (17 > 0 bytes) — 所有 body 都被拒绝!

// ✅ 正确做法
const http = new Http({ maxBodySize: Infinity })

陷阱 2:falsy body 值也会被计入检查 ​

修复后 body = 0、body = false 会被正确序列化为 JSON 并参与大小检查,而非被静默丢弃。

typescript
const http = new Http({ maxBodySize: 5 })

// body = 0 → JSON.stringify(0) → "0"(2 字节)→ 2 ≤ 5 → ✅ 放行
// body = false → JSON.stringify(false) → "false"(6 字节)→ 6 > 5 → ❌ 拦截!

// ❌ 之前的行为:body = false 被静默丢弃为 null,检查被跳过
// ✅ 现在的行为:body = false 被序列化为 "false",参与大小检查

陷阱 3:拦截器捕获不到 maxBodySize 错误 ​

maxBodySize 检查发生在 Request 构造函数中(见 [Request/index.ts](file:///d:/WORKSPACE/JIESS/jiess-2024-6-16/JIESS/TS-LIB/jiess-http/src/Request/index.ts#L87-L95)),早于拦截器 request 钩子和网络请求。错误会被 toRequest() 的 catch 捕获并通过 responseError 拦截器处理。

typescript
const http = new Http({
  maxBodySize: 0,
  interceptors: {
    request: (config) => {
      // ⚠️ 当 maxBodySize:0 时,这段代码永远不会执行
      // 因为 Request 构造函数阶段就已经抛出了
      console.log('请求拦截器', config)
      return config
    },
    responseError: (error) => {
      // ✅ 但这里可以捕获到 maxBodySize 导致的错误
      console.error('捕获到大小限制错误', error.message)
      return Promise.reject(error)
    }
  }
})

陷阱 4:GET 请求不受影响,但同名路由的 POST 会被拦截 ​

typescript
const http = new Http({ maxBodySize: 0 })

const api = http.create('readonly', {
  search: ['GET', '/search'],      // ✅ GET 无 body,不受影响
  report: ['POST', '/report'],     // ❌ POST 携带 body 会被立即拦截
})

await api.search({ q: 'hello' })   // ✅ 正常执行
await api.report({ data: 'x' })    // ❌ 抛出:请求体过大

动态配置的架构展望 ​

当前 maxBodySize 是静态配置:在 Http 实例化时写入 TypeConfig,流经 Factory,最终在 Request.checkBodySize() 中以单一取值点读取:

Http.config  →  Factory.config  →  Request.checkBodySize()
                                     └── const maxBodySize = this.config.maxBodySize != null
                                           ? this.config.maxBodySize
                                           : 10 * 1024 * 1024

优点:简单、可预测、不会发生配置丢失。 局限:无法在运行时根据上下文(用户角色、环境变量、请求路径)动态调整。

若要支持动态配置,需改动三处 ​

层级文件改动内容
类型层[index.ts](file:///d:/WORKSPACE/JIESS/jiess-2024-6-16/JIESS/TS-LIB/jiess-http/src/index.ts#L28)maxBodySize 类型扩展为 number | (() => number) | (() => Promise<number>)
请求层[Request/index.ts](file:///d:/WORKSPACE/JIESS/jiess-2024-6-16/JIESS/TS-LIB/jiess-http/src/Request/index.ts#L87-L95)checkBodySize() 改为异步,先 resolve 函数再取值
调度层[Factory/index.ts](file:///d:/WORKSPACE/JIESS/jiess-2024-6-16/JIESS/TS-LIB/jiess-http/src/Factory/index.ts#L52-L106)toRequest() 中 await req.checkBodySize() 而非构造时同步调用

推荐方案:函数式配置 + 环境变量优先 ​

改动量最小的方案是让 maxBodySize 同时支持函数和环境变量覆盖:

typescript
// 类型定义变更(minimal impact)
export type TypeConfig = UnObject & {
  maxBodySize?: number | (() => number) | (() => Promise<number>)
  // ... 其他字段不变
}

// checkBodySize() 重构
async checkBodySize(bodyStr: unknown) {
  const ENV_OVERRIDE = process.env.JIESS_HTTP_MAX_BODY_SIZE
  let maxBodySize: number

  if (ENV_OVERRIDE !== undefined) {
    maxBodySize = parseInt(ENV_OVERRIDE, 10)
  } else if (typeof this.config.maxBodySize === 'function') {
    maxBodySize = await (this.config.maxBodySize as Function)()
  } else {
    maxBodySize = this.config.maxBodySize != null
      ? (this.config.maxBodySize as number)
      : 10 * 1024 * 1024
  }

  if (bodyStr) {
    const bodySize = JSON.stringify(bodyStr).length
    if (bodySize > maxBodySize) {
      throw new Error(`请求体过大 (${bodySize} > ${maxBodySize} bytes)`)
    }
  }
}

使用示例:

typescript
// 场景 A:环境变量覆盖(CI/CD 统一管控)
// export JIESS_HTTP_MAX_BODY_SIZE=1048576
const http = new Http({ baseURL: 'https://api.example.com' })
// 实际生效 1MB,忽略代码中的默认 10MB

// 场景 B:函数动态读取(按用户角色)
const http = new Http({
  maxBodySize: () => {
    return currentUser.isAdmin()
      ? 50 * 1024 * 1024   // 管理员 50MB
      : 1 * 1024 * 1024    // 普通用户 1MB
  }
})

// 场景 C:异步函数(从远程配置中心拉取)
const http = new Http({
  maxBodySize: async () => {
    const remoteConfig = await fetchConfigFromCenter()
    return remoteConfig.uploadLimit
  }
})

⚠️ 注意事项:如果采用异步方案,checkBodySize() 和 initBody() 都需要变为异步方法,连锁影响 Request 构造函数及下游调用链。建议先在 Factory.toRequest() 中显式 await req.checkBodySize(),逐步迁移而非一次性全改。


错误类型 ​

@jiess/http 提供了丰富的错误类体系:

typescript
import {
  HttpError,                    // 基础错误类(code, message, config, response, timestamp)
  TimeoutError,                 // 408 请求超时
  RequestBodyTooLargeError,     // 413 请求体过大
  NetworkError,                 // 网络错误
  AbortError,                   // 499 请求取消
  InvalidParameterError,        // 400 无效参数
  AuthenticationError,          // 401 认证失败
  AuthorizationError,           // 403 权限不足
  NotFoundError,                // 404 资源未找到
  ServerError                   // 500 服务器错误
} from '@jiess/http'

每个错误类都继承自 HttpError,具有 code、message、config、response、timestamp 属性及 toJSON() 方法。

使用示例:

typescript
import { AuthenticationError, NotFoundError } from '@jiess/http'

const http = new Http({
  interceptors: {
    responseError: (error) => {
      if (error instanceof AuthenticationError) {
        redirectToLogin()
      } else if (error instanceof NotFoundError) {
        show404Page()
      }
      return Promise.reject(error)
    }
  }
})

接口调用方式对比 ​

方式代码量适用场景
直接使用 http.create()最少常规 CRUD 接口
链式 Factory中等需要精细控制参数的场景
直接 new Request最多完全自定义的底层请求
typescript
// 方式一:http.create(推荐)
const api = http.create('user', { getUser: ['GET', '/users/:id'] })
await api.getUser(1)

// 方式二:Factory 链式(高级)
const factory = new Factory(config, '/users/1', 'GET', 'myKey')
await factory.addHeaderData({ Authorization: 'Bearer xxx' }).toRequest()

// 方式三:Request 底层(完全控制)
const request = new Request({
  url: 'https://api.example.com/users/1',
  method: 'GET',
  httpKey: 'myKey',
  mainURL: '/users/1'
})
await request.send()