插件系统
HTTP 客户端的插件系统提供了强大的扩展能力,可以在请求的不同阶段插入自定义逻辑。
插件接口
typescript
interface ClientPlugin {
/**
* 请求前钩子
* @param url 请求 URL
* @param config 请求配置
* @returns 修改后的 URL 和配置
*/
beforeRequest?(
url: string,
config: RequestConfig
): Promise<PluginHookResult | void> | PluginHookResult | void
/**
* 响应后钩子
* @param response 响应对象
* @param url 请求 URL
* @param config 请求配置
* @returns 修改后的响应对象
*/
afterRespond?(
response: HTTPResponse,
url: string,
config: RequestConfig
): Promise<HTTPResponse | void> | HTTPResponse | void
}
interface PluginHookResult {
/** 修改后的 URL */
url?: string
/** 修改后的请求配置 */
config?: RequestConfig
}插件执行顺序
插件按照在 plugins 数组中的顺序执行:
- 按顺序执行所有插件的
beforeRequest钩子 - 发送实际的 HTTP 请求
- 按顺序执行所有插件的
afterRespond钩子
typescript
const http = new HTTPClient('', {
plugins: [
pluginA, // 先执行
pluginB, // 后执行
pluginC // 最后执行
]
})内置插件
TokenPlugin
令牌插件用于自动在请求头中添加认证令牌。
用法:
typescript
import { HTTPClient, TokenPlugin } from '@cat-kit/http'
const http = new HTTPClient('', {
plugins: [
TokenPlugin({
getter: () => localStorage.getItem('token'),
authType: 'Bearer',
headerName: 'Authorization'
})
]
})配置选项:
typescript
interface TokenPluginOptions {
/**
* 获取令牌的方法
* - 可以返回 string、null、undefined
* - 可以是同步或异步函数
*/
getter: () => string | null | undefined | Promise<string | null | undefined>
/**
* 请求头名称
* @default 'Authorization'
*/
headerName?: string
/**
* 授权类型
* - 'Bearer': 格式化为 'Bearer <token>'
* - 'Basic': 格式化为 'Basic <token>'
* - 'Custom': 使用自定义格式化函数
* @default 'Bearer'
*/
authType?: 'Bearer' | 'Basic' | 'Custom'
/**
* 自定义格式化方法
* - 仅在 authType 为 'Custom' 时使用
*/
formatter?: (token: string) => string
}示例:
typescript
// Bearer 令牌(最常见)
TokenPlugin({
getter: () => localStorage.getItem('access_token')
// 默认使用 Bearer,会添加请求头:Authorization: Bearer <token>
})
// 从异步存储获取令牌
TokenPlugin({
getter: async () => {
const token = await AsyncStorage.getItem('token')
return token
}
})
// Basic 认证
TokenPlugin({
getter: () => btoa('username:password'),
authType: 'Basic'
// 添加请求头:Authorization: Basic <encoded>
})
// 自定义格式
TokenPlugin({
getter: () => 'my-secret-token',
authType: 'Custom',
formatter: token => `Token ${token}`
// 添加请求头:Authorization: Token my-secret-token
})
// 自定义请求头名称
TokenPlugin({
getter: () => localStorage.getItem('api_key'),
headerName: 'X-API-Key',
authType: 'Custom',
formatter: token => token
// 添加请求头:X-API-Key: <token>
})交互示例
vue
Token 插件示例
演示如何自动在请求中添加 Token
请求详情
暂无请求记录
MethodOverridePlugin
方法重写插件用于绕过某些环境对特定 HTTP 方法的限制。
用法:
typescript
import { HTTPClient, MethodOverridePlugin } from '@cat-kit/http'
const http = new HTTPClient('', {
plugins: [
MethodOverridePlugin({
methods: ['DELETE', 'PUT', 'PATCH'],
overrideMethod: 'POST'
})
]
})配置选项:
typescript
interface MethodOverridePluginOptions {
/**
* 需要被重写的请求方法列表
* @default ['DELETE', 'PUT', 'PATCH']
*/
methods?: RequestMethod[]
/**
* 重写后使用的请求方法
* @default 'POST'
*/
overrideMethod?: RequestMethod
}工作原理:
- 检查请求方法是否在
methods列表中 - 如果是,将实际请求方法改为
overrideMethod - 在请求头中添加
X-HTTP-Method-Override字段,值为原始方法
示例:
typescript
// 默认配置:重写 DELETE、PUT、PATCH 为 POST
MethodOverridePlugin()
// 自定义重写规则
MethodOverridePlugin({
methods: ['PUT', 'PATCH'],
overrideMethod: 'POST'
})
// 使用示例
const http = new HTTPClient('', {
plugins: [MethodOverridePlugin()]
})
// 发送 DELETE 请求
await http.delete('/users/123')
// 实际发送的是 POST 请求,请求头包含:
// X-HTTP-Method-Override: DELETE使用场景:
- 某些网络环境只允许 GET 和 POST 方法
- 代理服务器限制了某些 HTTP 方法
- 需要与不支持 REST 方法的后端兼容
自定义插件
基础插件
创建一个简单的日志插件:
typescript
import type { ClientPlugin } from '@cat-kit/http'
function LoggerPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
console.log(`[请求] ${config.method} ${url}`)
console.log('请求配置:', config)
},
afterRespond(response, url, config) {
console.log(`[响应] ${config.method} ${url}`)
console.log('状态码:', response.code)
console.log('响应数据:', response.data)
return response
}
}
}
// 使用插件
const http = new HTTPClient('', {
plugins: [LoggerPlugin()]
})修改请求
创建一个添加时间戳的插件:
typescript
function TimestampPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
// 在查询参数中添加时间戳
return {
config: {
...config,
query: {
...config.query,
_t: Date.now()
}
}
}
}
}
}UI 交互插件
创建加载状态插件:
typescript
import { Snackbar } from '@varlet/ui'
function LoadingPlugin(): ClientPlugin {
let requestCount = 0
return {
beforeRequest() {
if (requestCount === 0) {
Snackbar.loading('请求中...')
}
requestCount++
},
afterRespond(response) {
requestCount--
if (requestCount === 0) {
Snackbar.clear()
}
return response
}
}
}交互示例
vue
加载状态示例
点击下方按钮发起请求,观察加载条效果
请求日志
交互式拦截
创建删除确认插件:
typescript
import { Dialog } from '@varlet/ui'
function ConfirmDeletePlugin(): ClientPlugin {
return {
async beforeRequest(url, config) {
if (config.method === 'DELETE') {
try {
await Dialog({
title: '确认删除',
message: '确定要删除该项吗?此操作不可恢复。'
})
} catch {
throw new Error('用户取消操作')
}
}
}
}
}交互示例
vue
交互式确认对话框
演示在执行删除操作前弹出确认对话框
项目 A
ID: 1
项目 B
ID: 2
项目 C
ID: 3
操作日志
暂无操作记录
修改响应
创建一个数据转换插件:
typescript
interface ApiResponse<T> {
code: number
data: T
message: string
}
function DataTransformPlugin(): ClientPlugin {
return {
afterRespond(response) {
// 假设后端返回的格式是 { code, data, message }
const apiResponse = response.data as ApiResponse<any>
// 转换为标准格式
return {
...response,
code: apiResponse.code,
data: apiResponse.data
}
}
}
}错误处理
创建一个统一的错误处理插件:
typescript
function ErrorHandlerPlugin(): ClientPlugin {
return {
afterRespond(response) {
if (response.code >= 400) {
// 处理 HTTP 错误
if (response.code === 401) {
console.error('未授权,请重新登录')
// 可以触发登录流程
} else if (response.code === 403) {
console.error('无权访问')
} else if (response.code === 404) {
console.error('资源不存在')
} else if (response.code >= 500) {
console.error('服务器错误')
}
}
return response
}
}
}请求重试
创建一个自动重试的插件:
typescript
function RetryPlugin(
maxRetries: number = 3,
delay: number = 1000
): ClientPlugin {
const retryCount = new WeakMap<any, number>()
return {
async afterRespond(response, url, config) {
// 如果请求成功,返回响应
if (response.code < 500) {
return response
}
// 获取当前重试次数
const count = retryCount.get(config) || 0
// 如果已达到最大重试次数,返回错误响应
if (count >= maxRetries) {
console.error(`请求失败,已重试 ${maxRetries} 次`)
return response
}
// 记录重试次数
retryCount.set(config, count + 1)
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, delay * (count + 1)))
console.log(`第 ${count + 1} 次重试: ${url}`)
// 注意:这里需要直接调用引擎,实际使用中可能需要其他方式
// 这只是示例,实际实现可能需要访问客户端实例
return response
}
}
}请求缓存
创建一个简单的请求缓存插件:
typescript
function CachePlugin(ttl: number = 60000): ClientPlugin {
const cache = new Map<string, { data: any; timestamp: number }>()
return {
beforeRequest(url, config) {
// 只缓存 GET 请求
if (config.method !== 'GET') {
return
}
const cacheKey = url + JSON.stringify(config.query)
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('使用缓存:', cacheKey)
// 注意:这里无法直接返回缓存的响应
// 实际实现可能需要在 afterRespond 中处理
}
},
afterRespond(response, url, config) {
// 只缓存成功的 GET 请求
if (config.method === 'GET' && response.code === 200) {
const cacheKey = url + JSON.stringify(config.query)
cache.set(cacheKey, {
data: response.data,
timestamp: Date.now()
})
}
return response
}
}
}性能监控
创建一个性能监控插件:
typescript
function PerformancePlugin(): ClientPlugin {
const timings = new WeakMap<any, number>()
return {
beforeRequest(url, config) {
timings.set(config, Date.now())
console.log(`[性能] 开始请求: ${url}`)
},
afterRespond(response, url, config) {
const startTime = timings.get(config)
if (startTime) {
const duration = Date.now() - startTime
console.log(`[性能] ${url} 耗时: ${duration}ms`)
// 可以将性能数据上报到监控系统
if (duration > 3000) {
console.warn(`[性能警告] 请求耗时过长: ${url} (${duration}ms)`)
}
}
return response
}
}
}插件组合
可以组合多个插件来构建强大的功能:
typescript
import { HTTPClient, TokenPlugin, MethodOverridePlugin } from '@cat-kit/http'
const http = new HTTPClient('/api', {
plugins: [
// 1. 添加令牌
TokenPlugin({
getter: () => localStorage.getItem('token')
}),
// 2. 方法重写
MethodOverridePlugin(),
// 3. 性能监控
PerformancePlugin(),
// 4. 错误处理
ErrorHandlerPlugin(),
// 5. 日志记录(最后执行)
LoggerPlugin()
]
})最佳实践
1. 插件职责单一
每个插件应该只负责一个功能:
typescript
// ✅ 好的做法:职责单一
function TokenPlugin() {
/* 只处理令牌 */
}
function LoggerPlugin() {
/* 只处理日志 */
}
// ❌ 不好的做法:功能混杂
function MixedPlugin() {
return {
beforeRequest(url, config) {
// 添加令牌
// 记录日志
// 添加时间戳
// ...太多功能
}
}
}2. 避免副作用
插件不应该修改原始的配置对象:
typescript
// ✅ 好的做法:返回新对象
function GoodPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
return {
config: {
...config,
headers: {
...config.headers,
'X-Custom': 'value'
}
}
}
}
}
}
// ❌ 不好的做法:直接修改
function BadPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
config.headers!['X-Custom'] = 'value' // 直接修改
return { config }
}
}
}3. 处理异步操作
插件可以返回 Promise:
typescript
function AsyncPlugin(): ClientPlugin {
return {
async beforeRequest(url, config) {
// 异步获取某些数据
const data = await fetchSomeData()
return {
config: {
...config,
headers: {
...config.headers,
'X-Data': data
}
}
}
}
}
}4. 错误处理
插件应该妥善处理错误:
typescript
function SafePlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
try {
// 可能失败的操作
const token = getToken()
return {
config: {
...config,
headers: {
...config.headers,
Authorization: `Bearer ${token}`
}
}
}
} catch (error) {
console.error('获取令牌失败:', error)
// 返回原配置
return
}
}
}
}常见问题
插件的执行顺序是怎样的?
插件按照数组顺序依次执行:
typescript
const http = new HTTPClient('', {
plugins: [pluginA, pluginB, pluginC]
})
// beforeRequest 执行顺序: A → B → C
// afterRespond 执行顺序: A → B → C如何在插件中取消请求?
插件本身不能直接取消请求,但可以抛出错误:
typescript
function ValidationPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
if (!isValid(config)) {
throw new Error('请求配置无效')
}
}
}
}插件可以访问其他插件的数据吗?
插件之间不能直接通信,但可以通过配置对象传递数据:
typescript
function PluginA(): ClientPlugin {
return {
beforeRequest(url, config) {
return {
config: {
...config,
_pluginAData: 'some data'
}
}
}
}
}
function PluginB(): ClientPlugin {
return {
beforeRequest(url, config) {
const dataFromA = (config as any)._pluginAData
// 使用 PluginA 设置的数据
}
}
}如何调试插件?
使用日志和断点:
typescript
function DebugPlugin(): ClientPlugin {
return {
beforeRequest(url, config) {
console.log('=== 请求前 ===')
console.log('URL:', url)
console.log('Config:', JSON.stringify(config, null, 2))
debugger // 断点
},
afterRespond(response) {
console.log('=== 响应后 ===')
console.log('Response:', JSON.stringify(response, null, 2))
debugger // 断点
return response
}
}
}