request插件使用教程
genn-request
是一个基于 Axios 封装的 HTTP 请求库。它像一个“超级快递员”,帮你发送和接收网络数据。通过插件(比如缓存)、拦截器(数据加工)和统一配置,让发送网络请求变得更简单、更强大,也更容易维护。
Source Repository: None
Chapters
- 请求配置 (RequestConfig)
- 请求核心 (RequestCore)
- 拦截器管理器 (AxiosInterceptorManager)
- 插件系统 (Middlewares)
- 缓存插件 (withCache)
- 响应转换器 (ResponseTransformer)
- 响应包装器 (ResponseWrapper)
- 版本管理与发布流程 (Changesets)
Chapter 1: 请求配置 (RequestConfig)
欢迎来到 genn-request
的世界!这是一个强大且易于上手的网络请求库。在这一章,我们将一起探索 genn-request
的基石之一 —— 请求配置 (RequestConfig)。
什么是请求配置 (RequestConfig)?为什么需要它?
想象一下,每次你要出门旅行,都需要准备一份详细的“出行计划单”。这份计划单会写清楚你的目的地、出行方式、需要携带的物品、预计的行程时间等等。
在网络请求的世界里,RequestConfig
就扮演着这份“出行计划单”的角色。它详细规定了每一次网络请求的方方面面:
- 目的地 (URL):你要请求的服务器地址和路径。
- 出行方式 (HTTP 方法):是
GET
请求(获取数据)、POST
请求(提交数据),还是其他? - 携带物品 (请求头和数据):你需要在请求中附加的额外信息(比如身份令牌)或要发送给服务器的数据。
- 预计耗时 (超时设置):如果请求太久没有响应,应该在什么时候放弃等待。
- 特殊服务选项:比如是否需要缓存这次请求的结果,或者如果失败了是否需要自动重试。
那么,为什么我们需要这样一个“计划单”呢?
假设你的应用中有很多地方都需要向同一个基础 API 地址(例如 https://api.example.com/v1
)发送请求,并且大部分请求的超时时间都希望设置为 5 秒。如果每次发送请求时都要手动输入这些信息,不仅繁琐,而且容易出错。如果将来基础 API 地址变了,或者默认超时时间需要调整,你就不得不修改应用中的每一处请求代码!
RequestConfig
就是为了解决这个问题而生的。它允许你:
- 全局设定:定义一套默认的“出行计划模板”,应用中所有的请求都会默认使用这套模板。
- 单次定制:对于特殊的“旅行”,你可以在默认模板的基础上进行个别调整,比如某个特定请求需要更长的超时时间,或者需要发送特殊的请求头。
通过 RequestConfig
,我们可以更高效、更统一地管理我们应用的网络请求行为。
如何使用请求配置?
genn-request
提供了非常灵活的方式来配置你的请求。主要有两种层面:全局配置和单次请求配置。
1. 全局配置:为所有请求设定“默认计划”
你可以使用 defineRequestConfig
函数来定义一套全局的默认配置。这就像是为你的所有“旅行”设定一个标准模板。
假设我们希望所有请求默认:
- 访问
https://api.example.com
这个基础 URL。 - 超时时间为 10000 毫秒 (10秒)。
- 请求头中包含
Content-Type: application/json
和一个自定义的客户端版本号X-Client-Version: 1.0.0
。
// 导入 defineRequestConfig 和 createRequest
import { defineRequestConfig, createRequest } from '@genn/genn-request';
// 定义全局配置
defineRequestConfig({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json', // 指定发送数据的格式
'X-Client-Version': '1.0.0' // 自定义一个请求头
}
});
// 创建请求实例时,它会自动应用上面定义的全局配置
const request = await createRequest();
// 现在,当我们使用 request 实例发起请求时...
// 比如:request.get({ url: '/users' });
// 它会自动使用 baseURL 'https://api.example.com'
// 完整的请求地址将是 'https://api.example.com/users'
// 并且会使用 10000ms 的超时设置和全局定义的 headers
在上面的例子中,我们调用了 defineRequestConfig
并传入了一个配置对象。之后,通过 await createRequest()
创建的 request
实例就会自动继承这些全局设置。这意味着,之后通过这个 request
实例发出的所有请求,都会默认使用这些配置,除非在发起具体请求时被特意覆盖。
2. 单次请求配置:为特定请求“定制计划”
有时候,全局配置并不适用于所有情况。某些特殊的请求可能需要不同的设置。genn-request
允许你在创建请求实例时传入配置,或者在发起单次请求时覆盖配置。
a. 创建请求实例时指定配置
你可以在调用 createRequest
时传入一个配置对象,这个配置会与全局配置合并,并且优先于全局配置。
import { createRequest } from '@genn/genn-request';
// 假设我们已经通过 defineRequestConfig 设置了全局 baseURL 和 timeout
// 创建一个新的请求实例,但为它指定不同的 baseURL 和 timeout
const specialRequest = await createRequest({
baseURL: 'https://api.special.com', // 覆盖全局 baseURL
timeout: 5000 // 覆盖全局 timeout
});
// 使用 specialRequest 发起的请求将使用 'https://api.special.com' 和 5000ms 超时
// specialRequest.get({ url: '/data' });
// 请求地址: 'https://api.special.com/data'
这里,specialRequest
实例会使用它在创建时指定的 baseURL
和 timeout
,而不是全局配置中的值。
b. 发起具体请求时指定配置
更细粒度的控制是在发起每一次具体请求(如 get
, post
等)时,直接传入配置。这会覆盖实例配置和全局配置。
import { createRequest } from '@genn/genn-request';
// 创建一个使用全局或默认配置的请求实例
const request = await createRequest({
baseURL: 'https://api.example.com',
timeout: 10000
});
// 发起一个 GET 请求,但为这次特定请求指定更短的超时时间
const response = await request.get({
url: '/products',
timeout: 3000 // 这个 timeout 只对本次 GET 请求有效
});
// 这个请求会访问 'https://api.example.com/products'
// 但超时时间是 3000ms,而不是实例配置的 10000ms
在这个例子中,虽然 request
实例的默认超时时间是 10000ms
,但这次对 /products
的 GET
请求,其超时时间被临时改为了 3000ms
。
这种分层配置的机制(全局 -> 实例 -> 单次请求)提供了极大的灵活性,让你能够轻松应对各种复杂的请求场景。
深入了解:RequestConfig
的内部机制
那么,genn-request
是如何管理和应用这些配置的呢?让我们简单了解一下其内部的工作流程。
当你使用 genn-request
时,配置的合并遵循一个清晰的优先级顺序:
单次请求配置 > 实例创建时配置 > 全局配置 (defineRequestConfig
) > 库内置默认配置
下面是一个简化的流程图,展示了配置是如何生效的:
核心代码解读
配置的管理主要发生在 src/utils/config.ts
文件中。
默认配置 (
defaultConfig
):genn-request
内部预设了一套最基础的配置,比如默认的Content-Type
。// 摘自 src/utils/config.ts (简化版) export const defaultConfig: RequestConfig<unknown, unknown> = Object.freeze({ baseURL: '/', timeout: 20000, // 默认超时时间 20 秒 headers: { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json', // 默认内容类型 }, // ... 其他默认设置 });
这份配置是所有配置的基础。
全局配置存储 (
globalConfig
和defineRequestConfig
): 当你调用defineRequestConfig(yourConfig)
时,你的配置yourConfig
会被存储在一个名为globalConfig
的内部变量中。// 摘自 src/utils/config.ts (简化版) let globalConfig: RequestConfig<unknown, unknown> = {}; export function defineRequestConfig( config: RequestConfig<unknown, unknown> | (() => RequestConfig<unknown, unknown>), ) { const rawConfig = typeof config === 'function' ? config() : config; // ... 省略了验证和克隆逻辑 globalConfig = cloneDeep(rawConfig); // 将用户定义的全局配置存储起来 }
配置合并 (
getRequestConfig
和mergeConfigs
): 当你调用createRequest(userInstanceConfig)
时,会触发一个配置合并的过程,由getRequestConfig
函数负责。// 摘自 src/utils/config.ts (简化版) export function getRequestConfig( config: RequestConfig<unknown, unknown> = {}, ): RequestConfig<unknown, unknown> { const defaultConf = cloneDeep(defaultConfig); // 1. 合并全局配置到默认配置 (全局配置优先级更高) const mergedConfig = mergeConfigs(defaultConf, globalConfig, false); // 2. 合并用户为本次实例传入的配置 (用户实例配置优先级最高) return mergeConfigs(mergedConfig, config, true); }
mergeConfigs
函数会智能地合并对象,确保高优先级的配置能够覆盖低优先级的。这个最终合并后的配置对象,会传递给 请求核心 (RequestCore) 来创建请求实例。当具体发起如
request.get(singleRequestConfig)
这样的请求时,还会发生一次局部的配置合并:实例持有的配置会作为基础,然后singleRequestConfig
会合并进来,形成本次请求最终使用的配置。
RequestConfig
接口有哪些常用属性?
RequestConfig
接口定义在 src/types/http/config.ts
中,它包含了许多可以配置的选项。这里列举一些非常常用的:
// 摘自 src/types/http/config.ts (部分常用属性)
export interface RequestConfig<TParams = any, TResponseData = any> {
baseURL?: string; // 基础URL,会拼接到url前面
timeout?: number; // 请求超时时间 (毫秒)
headers?: Record<string, any>; // 自定义请求头
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string; // HTTP请求方法
data?: TParams; // 请求体数据,通常用于 POST, PUT, PATCH 请求
params?: TParams; // URL查询参数,通常用于 GET 请求
path?: string; // 请求路径,不包含 baseURL
url?: string; // 完整的请求URL (如果提供,会覆盖 baseURL + path)
// --- 以下是一些 genn-request 特有的或增强的配置 ---
cacheKey?: string; // 自定义缓存键
retry?: number; // 失败重试次数
skipGlobalError?: boolean;// 是否跳过全局错误处理
// ... 还有更多高级配置,如拦截器、插件选项等
}
理解这些常用属性,可以帮助你更好地定制你的“出行计划单”,让网络请求完全按照你的意愿行事。
总结
在本章中,我们学习了:
- 请求配置 (
RequestConfig
) 的重要性:它像一份详细的“出行计划单”,用于定义网络请求的各种参数。 - 如何配置:可以通过
defineRequestConfig
设置全局默认配置,也可以在创建请求实例或发起单次请求时进行定制。 - 配置的优先级:单次请求配置 > 实例创建时配置 > 全局配置 > 库内置默认配置。
- 内部机制初探:了解了
defaultConfig
,globalConfig
,getRequestConfig
如何协同工作来合并和应用配置。 RequestConfig
接口中的一些常用属性。
RequestConfig
是 genn-request
灵活性的核心。掌握了它,你就掌握了控制网络请求行为的钥匙。
在下一章,我们将深入探讨 genn-request
的心脏——请求核心 (RequestCore),看看它是如何利用这些配置来实际执行网络请求的。
Chapter 2: 请求核心 (RequestCore)
在上一章 请求配置 (RequestConfig) 中,我们学习了如何像准备“出行计划单”一样,为我们的网络请求定义各种参数和设置。我们了解了全局配置、实例配置和单次请求配置,以及它们是如何合并生效的。
现在,计划单已经准备好了,谁来负责执行这份计划,真正地把我们的“包裹”(HTTP请求)送出去呢?这就是本章的主角——请求核心 (RequestCore) 的职责。
什么是请求核心 (RequestCore)?它解决了什么问题?
想象一下,RequestConfig
是我们精心填写的“快递订单”,上面写着收件人地址(URL)、物品类型(请求方法)、是否需要加急(超时设置)等等。那么,RequestCore
就是那位经验丰富的“快递调度中心主管”。
这位主管的核心任务是:
- 接收你的“快递订单”(API 调用和配置)。
- 仔细检查并打包(合并最终的请求参数)。
- 选择最佳的运输路线和方式(应用中间件和拦截器)。
- 将“包裹”(HTTP 请求)安全送达目的地(服务器)。
- 带回“签收回执”(服务器响应)。
简单来说,RequestCore
是 genn-request
中实际发送网络请求、管理请求生命周期、以及协调各种插件和拦截器工作的中心枢纽。 如果没有它,我们定义的各种 RequestConfig
就只是一纸空文,无法真正 تبدیل成实际的网络通信。
让我们通过一个简单的例子来看看 RequestCore
是如何工作的。假设我们想获取一个用户列表,API 地址是 /users
。
如何与请求核心 (RequestCore) 交互?
通常情况下,我们不会直接去实例化 RequestCore
类。genn-request
提供了一个更便捷的函数 createRequest
来帮助我们创建一个配置好的请求实例,这个实例内部就封装了 RequestCore
。
回顾一下上一章,我们使用 createRequest
来创建一个请求发送器:
// 导入 createRequest
import { createRequest } from '@genn/genn-request';
// 假设我们已经定义了全局配置
// defineRequestConfig({ baseURL: 'https://api.example.com' });
async function fetchUsers() {
// 1. 创建一个请求实例 (内部会创建一个 RequestCore)
const request = await createRequest({
// 我们可以为这个实例提供一些特定的配置
timeout: 5000, // 这个实例的所有请求默认超时5秒
});
// 2. 使用实例发送 GET 请求
try {
const response = await request.get({
url: '/users', // 请求路径
// 这里还可以传入单次请求的配置,比如特定的 headers
});
console.log('用户数据:', response.data);
} catch (error) {
console.error('请求失败:', error);
}
}
fetchUsers();
在上面的代码中:
await createRequest({...})
会返回一个request
对象。这个request
对象实际上就是RequestCore
的一个实例,它已经根据我们传入的配置(以及全局配置)初始化好了。- 当我们调用
request.get({ url: '/users' })
时,RequestCore
就开始工作了。它会接收到这个GET
请求的指令和相关的配置信息(如url
)。 RequestCore
会负责将这个请求发送到https://api.example.com/users
(假设baseURL
已配置),处理可能发生的错误,并最终返回服务器的响应。
所以,我们与 RequestCore
的主要交互方式就是通过 createRequest
创建实例,然后调用实例上的 get
, post
等方法。
请求核心 (RequestCore) 的内部运作探秘
RequestCore
就像一个勤奋的调度员,它在幕后做了很多工作。让我们来看看当一个请求发出时,它大致会经历哪些步骤。
1. 实例化:准备好“调度中心”
当我们调用 createRequest(userConfig)
时:
genn-request
内部首先会调用上一章提到的getRequestConfig(userConfig)
来合并所有的配置(默认配置、全局配置、用户传入的实例配置),得到一份最终的实例配置finalConfig
。- 然后,它会用这份
finalConfig
来创建RequestCore
的实例:new RequestCore(finalConfig)
。
RequestCore
的构造函数 (constructor
) 会做以下几件事情:
- 保存配置:将
finalConfig
保存起来,供后续请求使用。 - 创建 Axios 实例:
RequestCore
内部依赖一个强大的 HTTP客户端库axios
来发送实际的请求。它会根据finalConfig
(如baseURL
,timeout
) 创建一个axios
实例。 - 初始化拦截器管理器:为后续的 拦截器管理器 (AxiosInterceptorManager) 做准备。
- 初始化插件系统:为后续的 插件系统 (Middlewares) 做准备,并注册默认插件。
// src/index.ts (简化版)
import { RequestCore } from './core/request';
import { getRequestConfig } from './utils/config';
export async function createRequest(userConfig) {
const finalConfig = getRequestConfig(userConfig); // 1. 合并配置
return new RequestCore(finalConfig); // 2. 创建 RequestCore 实例
}
这段代码展示了 createRequest
的核心逻辑:获取最终配置,然后用它创建 RequestCore
。
// src/core/request.ts - RequestCore 构造函数 (简化版)
export class RequestCore {
private readonly instance: AxiosInstance; // Axios 实例
private readonly finalConfig: RequestConfig;
private pluginSystem = new PluginSystem();
public interceptors: AxiosInterceptorManager;
constructor(config) {
this.finalConfig = config; // 保存最终实例配置
// 基于配置创建 Axios 实例
this.instance = axios.create({
baseURL: this.finalConfig.baseURL,
timeout: this.finalConfig.timeout,
// ... 其他 Axios 配置
});
// 初始化拦截器管理器
this.interceptors = new AxiosInterceptorManager(this.instance);
// 注册实例配置中的拦截器 (如果配置了的话)
this.registerInstanceInterceptors(config?.interceptors);
// 注册默认中间件 (插件)
if (this.finalConfig.defaultMiddlewares?.length) {
this.finalConfig.defaultMiddlewares.forEach(this.usePlugin.bind(this));
}
}
// ... 其他方法
}
在 RequestCore
的构造函数中,它基于传入的配置创建了一个 axios
实例,并设置了拦截器和插件系统。
2. 请求处理:调度员开始工作
当我们调用 request.get({ url: '/users', config: singleRequestConfig })
这样的方法时:
- 准备单次请求配置:
RequestCore
的内部request
方法 (这是一个私有方法,get
,post
等方法都会调用它) 会首先调用prepareConfig
。这个函数会将我们为单次请求传入的singleRequestConfig
与实例自身的finalConfig
进行合并,得到本次请求最终要使用的配置。这里也包括处理 URL 路径参数、生成请求ID等。 - 插件系统执行:接下来,请求配置会交给 插件系统 (Middlewares)。插件可以对请求配置进行修改,或者在请求发送前后执行一些自定义逻辑(比如缓存、日志等)。
- 执行实际请求 (
executeRequest
):- 应用请求拦截器:在请求真正发出之前,拦截器管理器 (AxiosInterceptorManager) 中注册的请求拦截器会被执行。这些拦截器可以最后一次修改请求配置。
- 发送HTTP请求:使用内部的
axios
实例,根据最终的配置(URL, method, data, headers 等)发送 HTTP 请求到服务器。 - 应用响应拦截器:收到服务器的响应后,响应拦截器会被执行。它们可以修改响应数据,或者统一处理某些响应状态。
- 处理结果:
RequestCore
会将经过拦截器和插件处理的最终结果(或错误)返回给调用方。它还会负责处理像请求重试、取消重复请求等高级功能。
下面是一个简化的时序图,描述了从调用 request.get
到获取响应的流程:
关键代码片段解读
request
方法 (私有): 这是所有 HTTP 方法 (get
,post
等) 的统一入口。// src/core/request.ts (RequestCore 类内部简化版) private async request<TRequestParams, TResponseData>( method: string, options: { /* ... */ } ): Promise<ResponseWrapper<TRequestParams, TResponseData>> { // 1. 准备本次请求的最终配置 const config = this.prepareConfig<TRequestParams, TResponseData>({ ...(options.config || {}), method, path: options.url, }); // ... 设置 params 或 data ... // ... 处理路径参数,生成请求ID,处理取消请求 ... try { // 2. 通过插件系统执行请求 const response = await this.pluginSystem.execute(config, (finalConfig) => this.executeRequest(finalConfig), // 插件处理完后,执行这个回调 ); // ... 清理pending请求 ... return response; } catch (error) { // ... 清理pending请求,处理错误 ... return this.handleError(error, config); } }
这个方法的核心是调用
prepareConfig
准备配置,然后通过pluginSystem.execute
来执行请求,executeRequest
是实际发送 HTTP 请求的函数。executeRequest
方法 (私有): 负责应用请求拦截器、发送请求、应用响应拦截器。// src/core/request.ts (RequestCore 类内部简化版) private async executeRequest<TRequestParams, TResponseData>( config: RequestConfig<TRequestParams, TResponseData>, ): Promise<ResponseWrapper<TRequestParams, TResponseData>> { // 1. 应用请求拦截器 const processedConfig = await this.applyRequestInterceptors(config); // 2. 使用 axios 实例发送请求 const response = await this.instance.request({ // this.instance 是 axios 实例 url: processedConfig.path || '', method: processedConfig.method, data: processedConfig.data, params: processedConfig.params, // ... 其他 axios 参数 ... }); // 3. 应用响应拦截器并包装响应 return this.applyResponseInterceptors<TRequestParams, TResponseData>(response, processedConfig); }
这里,
this.instance.request()
就是真正发起网络通信的地方。applyRequestInterceptors
和applyResponseInterceptors
则分别调用了我们在 拦截器管理器 (AxiosInterceptorManager) 中定义的拦截逻辑。
RequestCore 的核心职责总结
通过上面的讲解,我们可以看到 RequestCore
承担了以下几个核心职责:
- 配置管理与合并:它接收并最终确定每一次请求所使用的配置,这是建立在我们在 请求配置 (RequestConfig) 中学到的基础上。
- 实例化和使用底层 HTTP 客户端 (Axios):它是
axios
的直接使用者,负责调用axios
来发送和接收数据。 - 执行请求生命周期:从准备配置、应用插件、应用请求拦截器、发送请求、接收响应、应用响应拦截器,到最终返回结果或处理错误,
RequestCore
管理着整个流程。 - 集成插件系统:它是 插件系统 (Middlewares) 的执行者,使得我们能够通过插件扩展请求行为。
- 集成拦截器:它与 拦截器管理器 (AxiosInterceptorManager) 紧密协作,应用开发者定义的拦截逻辑。
- 错误处理与重试:它提供了统一的错误处理机制,并能根据配置实现请求重试等高级功能。
- 请求取消:管理待处理的请求,并提供取消请求的功能。
RequestCore
就像是 genn-request
的引擎和大脑,默默地保证了每一次网络请求都能按照我们的预期顺利进行。
总结
在本章中,我们深入了解了 genn-request
的心脏——请求核心 (RequestCore):
- 我们知道了
RequestCore
是发送网络请求的中心枢纽,负责管理整个请求的生命周期。 - 我们学习了如何通过
createRequest
间接使用RequestCore
来发起请求。 - 我们探秘了
RequestCore
的内部工作流程,从实例创建到请求的发送、插件和拦截器的执行,再到响应的处理。 - 我们明确了
RequestCore
的核心职责,理解了它在genn-request
架构中的关键作用。
虽然我们通常不直接操作 RequestCore
类,但理解它的工作原理对于我们更好地使用 genn-request
、排查问题以及进行更高级的定制非常有帮助。
在了解了请求的“计划单” (请求配置 (RequestConfig)) 和“调度中心” (RequestCore
) 之后,下一章我们将聚焦于请求流程中的重要“安检口”和“VIP通道”——拦截器管理器 (AxiosInterceptorManager),看看它们是如何在请求发出前和响应返回后对数据进行处理的。
Chapter 3: 拦截器管理器 (AxiosInterceptorManager)
在上一章 请求核心 (RequestCore) 中,我们了解了 RequestCore
是如何作为“快递调度中心主管”,负责接收订单、打包、发送包裹(HTTP请求)并带回签收回执(服务器响应)的。它确保了我们的请求能够按照 请求配置 (RequestConfig) 中的“计划单”顺利执行。
现在,想象一下,在我们的“包裹”发出之前和收到“签收回执”之后,我们可能希望进行一些统一的检查或处理。比如,每个包裹发出前都要贴上“易碎品”标签,或者收到所有回执后,都要先检查是否有损坏再入库。这就是拦截器大显身手的地方。
什么是拦截器管理器 (AxiosInterceptorManager)?它解决了什么问题?
拦截器管理器,顾名思义,就是管理这些“检查站”和“加工厂”的工具。在 genn-request
中,它被称为 AxiosInterceptorManager
。
思考一个常见的场景:我们的应用几乎所有API请求都需要在请求头中携带一个认证令牌 (Token) 才能访问受保护的资源。如果我们每次发送请求时都手动添加这个 Token:
// 假设 request 是一个 RequestCore 实例
// 手动为每个请求添加 token
request.get({
url: '/user/profile',
config: { headers: { 'Authorization': 'Bearer your_token_here' } }
});
request.post({
url: '/orders',
data: { /* ... */ },
config: { headers: { 'Authorization': 'Bearer your_token_here' } }
});
这样做不仅非常繁琐,而且容易遗漏。如果获取 Token 的方式变了,或者 Token 的键名 (例如从 Authorization
变成 X-Auth-Token
) 变了,我们就得修改应用中所有发送请求的地方!
AxiosInterceptorManager
就是为了优雅地解决这类问题而设计的。它允许我们定义一些函数,这些函数会在请求发送前或响应接收后自动执行。
- 请求拦截器 (Request Interceptor):像是在请求发出前的“安检站”。它可以检查或修改即将发送的请求配置。例如,自动为每个请求添加
Authorization
请求头。 - 响应拦截器 (Response Interceptor):像是在收到响应后的“加工厂”。它可以检查或转换服务器返回的响应数据。例如,统一处理特定的错误码,或者在数据到达业务逻辑前进行格式转换。
AxiosInterceptorManager
负责注册这些拦截器,管理它们的执行顺序,并在适当的时候调用它们。
如何使用拦截器管理器?
genn-request
中的每个 RequestCore
实例都拥有一个自己的 interceptors
属性,它就是 AxiosInterceptorManager
的实例。我们可以通过它来添加、移除拦截器。
import { createRequest, RequestConfig, ResponseWrapper } from '@genn/genn-request';
async function main() {
const request = await createRequest({
baseURL: 'https://api.example.com'
});
// request.interceptors 就是 AxiosInterceptorManager 实例
}
1. 添加请求拦截器
请求拦截器是一个函数(或包含函数的对象),它接收请求配置 (RequestConfig
) 作为参数,并且必须返回一个请求配置(可以是修改后的,也可以是原始的)。
假设我们想在每个请求发送前,自动添加一个 X-Request-Source: web-app
的请求头。
// 导入 RequestConfig 类型
// import { RequestConfig } from '@genn/genn-request';
// 定义一个请求拦截器函数
const addSourceRequestInterceptor = (config: RequestConfig) => {
config.headers = {
...config.headers,
'X-Request-Source': 'web-app', // 添加自定义请求头
};
console.log('请求拦截器:添加了 X-Request-Source');
return config; // 必须返回 config
};
// 使用 request.interceptors.request.use() 注册它
const interceptorId = request.interceptors.request.use({
onFulfilled: addSourceRequestInterceptor,
name: 'source-adder', // 给拦截器起个名字,方便管理
priority: 10 // 设置优先级,数字越大优先级越高
});
// 现在,当我们发送请求时...
// await request.get({ url: '/data' });
// ...这个拦截器会自动运行,请求头中会包含 X-Request-Source
在上面的例子中:
onFulfilled
属性指向的函数会在请求成功发出前执行。name
是可选的,用于标识拦截器。priority
也是可选的,用于控制多个拦截器的执行顺序(稍后详述)。use
方法会返回一个 ID,我们可以用这个 ID 来移除拦截器。
2. 添加响应拦截器
响应拦截器同样是一个函数(或包含函数的对象)。它通常包含两个函数:
onFulfilled
: 当服务器成功响应(HTTP 状态码 2xx)时调用。它接收响应包装器 (ResponseWrapper
) 对象,并必须返回一个响应包装器对象(或一个解析为响应包装器对象的 Promise)。onRejected
: 当请求失败(例如网络错误、服务器返回非 2xx 状态码)时调用。它接收错误对象,并且应该返回一个被拒绝的 Promise (Promise.reject(error)
) 或者一个表示成功处理了错误的响应包装器。
假设我们想在每次收到响应后,打印响应的状态码。
// 导入 ResponseWrapper 类型
// import { ResponseWrapper } from '@genn/genn-request';
// 定义一个响应拦截器
const logResponseStatusInterceptor = (response: ResponseWrapper) => {
console.log(`响应拦截器:收到响应,状态码: ${response.status}`);
return response; // 必须返回 response
};
const logResponseErrorInterceptor = (error: any) => {
console.error('响应拦截器:请求出错', error.message);
return Promise.reject(error); // 必须返回一个被拒绝的 Promise
}
// 使用 request.interceptors.response.use() 注册它
request.interceptors.response.use({
onFulfilled: logResponseStatusInterceptor,
onRejected: logResponseErrorInterceptor,
name: 'status-logger',
priority: 10
});
// 当请求成功时,logResponseStatusInterceptor 会被调用
// await request.get({ url: '/data' });
// 当请求失败时,logResponseErrorInterceptor 会被调用
// await request.get({ url: '/non-existent-page' });
这里,如果请求成功,logResponseStatusInterceptor
会打印状态码。如果请求失败(比如404),logResponseErrorInterceptor
会打印错误信息。
3. 拦截器的属性:name
和 priority
name
(可选):给拦截器一个唯一的名称。这对于调试和管理多个拦截器非常有用。genn-request
会在内部为未命名的拦截器生成一个默认名称。priority
(可选, 默认为 0):当有多个拦截器时,priority
决定了它们的执行顺序。- 对于请求拦截器:
priority
值越大的拦截器越先执行。这就像安检,级别越高的检查越靠前。 - 对于响应拦截器:
priority
值越大的拦截器越先执行。这意味着,高优先级的响应拦截器会更早地接触到原始的服务器响应。
- 对于请求拦截器:
例如,我们有两个请求拦截器:一个添加认证头(高优先级),一个记录请求日志(低优先级)。
// 认证拦截器 (高优先级)
request.interceptors.request.use({
name: 'auth-header',
priority: 100, // 优先级高
onFulfilled: (config) => {
config.headers = { ...config.headers, 'Authorization': 'Bearer mytoken' };
console.log('认证拦截器执行');
return config;
}
});
// 日志拦截器 (低优先级)
request.interceptors.request.use({
name: 'request-logger',
priority: 10, // 优先级低
onFulfilled: (config) => {
console.log(`日志拦截器:发送 ${config.method} 请求到 ${config.url}`);
return config;
}
});
// 当发送请求时,控制台会先打印 "认证拦截器执行",然后打印 "日志拦截器:发送..."
// await request.get({ url: '/profile' });
因为“auth-header”的优先级 (100) 高于“request-logger”的优先级 (10),所以它会先执行。
4. 移除拦截器
use
方法会返回一个拦截器的 ID。我们可以使用这个 ID 和 eject
方法来移除特定的拦截器。
const noisyInterceptorId = request.interceptors.request.use({
onFulfilled: (config) => {
console.log('我是一个很吵的拦截器!');
return config;
}
});
// ... 一段时间后,我们不再需要这个拦截器了 ...
request.interceptors.request.eject(noisyInterceptorId);
// 之后发送的请求将不再触发这个 "noisyInterceptor"
还可以使用 clear
方法移除所有特定类型的拦截器:
typescript
request.interceptors.request.clear(); // 移除所有请求拦截器
request.interceptors.response.clear(); // 移除所有响应拦截器
拦截器管理器的内部运作
AxiosInterceptorManager
并不直接修改 axios
实例本身的拦截器链。相反,它维护着自己的拦截器列表,并在 请求核心 (RequestCore) 的特定阶段按顺序应用这些拦截器。
运作流程概览
- 注册:当你调用
request.interceptors.request.use(...)
或request.interceptors.response.use(...)
时,AxiosInterceptorManager
会将你提供的拦截器函数(及其元数据如name
,priority
)添加到一个内部的数组中(分别是customRequestInterceptors
和customResponseInterceptors
)。 - 排序:每次添加或移除拦截器后,管理器会根据拦截器的
priority
对这些数组进行排序,确保高优先级的拦截器排在前面。 - 应用(由 RequestCore 调用):
- 请求阶段:在 请求核心 (RequestCore) 准备好发送实际 HTTP 请求之前,它会调用一个类似
applyRequestInterceptors
的方法。这个方法会遍历AxiosInterceptorManager
中已排序的请求拦截器,并依次执行它们的onFulfilled
函数,将上一个拦截器返回的config
传递给下一个。 - 响应阶段:当
axios
返回响应(或错误)后,请求核心 (RequestCore) 会调用类似applyResponseInterceptors
的方法。这个方法会遍历已排序的响应拦截器,依次执行它们的onFulfilled
(处理成功响应) 或onRejected
(处理错误) 函数。
- 请求阶段:在 请求核心 (RequestCore) 准备好发送实际 HTTP 请求之前,它会调用一个类似
下面是一个简化的时序图,展示了拦截器是如何在请求流程中被应用的:
核心代码解读
我们来看一下 src/core/interceptors.ts
中 AxiosInterceptorManager
的关键部分:
构造函数和存储:
AxiosInterceptorManager
并不直接与axios
实例的拦截器交互,它只是维护自己的列表。// src/core/interceptors.ts (简化版) export class AxiosInterceptorManager<TRequestParams, TResponseData> { // 存储自定义的请求和响应拦截器 customRequestInterceptors: AxiosInterceptor<RequestInterceptor<TRequestParams, TResponseData>>[] = []; customResponseInterceptors: AxiosInterceptor<ResponseInterceptor<TRequestParams, TResponseData>>[] = []; constructor(instance: AxiosInstance) { // this.instance = instance; // 以前可能直接操作axios实例,现在主要管理自定义列表 } // ... }
这里的
customRequestInterceptors
和customResponseInterceptors
数组用于存储用户通过use
方法添加的拦截器。每个元素包含拦截器本身和其元数据(如ID)。添加拦截器 (
use
) 和排序 (sortInterceptors
): 当调用request.use
或response.use
时,拦截器被添加到相应的数组,并触发排序。// src/core/interceptors.ts (请求拦截器的 use 方法简化版) // request = { use: (...) => { ... } } private addRequestInterceptor( interceptor: RequestInterceptor<TRequestParams, TResponseData>, ): number { const id = Date.now() + Math.random(); // 生成唯一ID this.customRequestInterceptors.push({ id, interceptor: { ...interceptor, name: interceptor.name || `request-interceptor-${id}`, // 确保有名字 }, }); return id; } // 排序逻辑 private sortInterceptors(type: 'request' | 'response'): void { if (type === 'request') { this.customRequestInterceptors.sort( (a, b) => (b.interceptor.priority || 0) - (a.interceptor.priority || 0), ); } else { /* ... 类似响应拦截器的排序 ... */ } }
addRequestInterceptor
(以及类似的addResponseInterceptor
) 将拦截器对象包装一下(主要是为了生成ID和确保有name
)并存入数组。sortInterceptors
方法则根据priority
对数组进行降序排序。RequestCore
中应用拦截器:RequestCore
在其内部的executeRequest
方法(或者类似的辅助方法)中,会获取这些已排序的拦截器并按顺序执行它们。// src/core/request.ts (RequestCore 类中 applyRequestInterceptors 简化版) private async applyRequestInterceptors<TRequestParams, TResponseData>( config: RequestConfig<TRequestParams, TResponseData>, ): Promise<RequestConfig<TRequestParams, TResponseData>> { // 获取所有请求拦截器 (包括实例配置的和通过 interceptors API 添加的) const interceptors = [ ...(this.interceptors.customRequestInterceptors.map( // 从 AxiosInterceptorManager 获取 (i) => i.interceptor, ) as RequestInterceptor<TRequestParams, TResponseData>[]), ...(config.interceptors?.request || []), // 也合并 RequestConfig 中直接定义的 ].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); // 再次确保排序 let transformedConfig = config; for (const interceptor of interceptors) { if (interceptor.onFulfilled) { // 依次执行拦截器的 onFulfilled 方法 transformedConfig = await interceptor.onFulfilled(transformedConfig); } } return transformedConfig; // 返回最终被所有拦截器处理过的配置 }
在
applyRequestInterceptors
(以及类似的applyResponseInterceptors
) 中,RequestCore
会从AxiosInterceptorManager
实例(即this.interceptors
)获取已注册的customRequestInterceptors
,并结合可能在单次请求配置中定义的拦截器,然后统一排序并依次执行。
genn-request
提供的预定义拦截器辅助方法
AxiosInterceptorManager
类还提供了一些创建常用拦截器的辅助方法,方便开发者快速使用,例如:
createRequestLogger()
: 创建一个简单的请求日志拦截器。createResponseLogger()
: 创建一个简单的响应日志拦截器。createErrorHandler()
: 创建一个基础的错误处理拦截器,可以处理如 401, 403, 404, 500 等常见错误。createAuthInterceptor()
: 创建一个认证拦截器,用于自动添加Authorization
头。
你可以这样使用它们: ```typescript const request = await createRequest({ / ... / });
// 使用预定义的认证拦截器 request.interceptors.request.use( request.interceptors.createAuthInterceptor() );
// 使用预定义的错误处理拦截器 request.interceptors.response.use( request.interceptors.createErrorHandler() ); ``` 这使得添加常用功能变得更加简单。当然,你完全可以根据自己的需求编写更复杂的自定义拦截器。
总结
在本章中,我们深入了解了 genn-request
的拦截器系统:
- 拦截器的作用:它们如同网络请求路径上的“安检站”和“加工厂”,允许我们在请求发送前和响应接收后执行自定义逻辑。
AxiosInterceptorManager
:它是管理这些拦截器的核心,负责注册、移除和组织它们的执行顺序。- 如何使用:我们学习了如何通过
requestInstance.interceptors.request.use()
和requestInstance.interceptors.response.use()
来添加请求和响应拦截器,以及如何使用eject
来移除它们。 priority
的重要性:它决定了多个拦截器之间的执行顺序。- 内部机制:
AxiosInterceptorManager
维护自己的拦截器列表,并由 请求核心 (RequestCore) 在适当的时机调用这些拦截器。 - 预定义拦截器:
genn-request
提供了一些便捷的辅助方法来创建常用拦截器。
拦截器是 genn-request
中一个非常强大的特性,它使得我们可以将许多通用的请求处理逻辑(如认证、日志、错误处理、数据转换等)从业务代码中解耦出来,使代码更加清晰和易于维护。
在了解了请求配置、核心执行以及拦截器之后,我们的请求处理流程已经相当完善了。但是,genn-request
还提供了另一种强大的扩展机制——插件系统。下一章,我们将探索 插件系统 (Middlewares),看看它是如何让我们以更灵活的方式增强和定制请求行为的。
Chapter 4: 插件系统 (Middlewares)
欢迎来到 genn-request
教程的第四章!在上一章 拦截器管理器 (AxiosInterceptorManager) 中,我们学习了如何像设置“安检口”和“加工厂”一样,在请求发出前和响应返回后对数据进行统一处理。拦截器为我们提供了一种强大的方式来修改请求配置和响应数据。
现在,让我们来探索一个更强大、更灵活的扩展机制——插件系统 (Middlewares)。如果说拦截器是流水线上的特定工序,那么插件系统则允许我们为整个“生产流程”增加全新的、独立的“功能车间”。
什么是插件系统 (Middlewares)?它解决了什么问题?
想象一下,你正在搭建一个复杂的乐高模型。除了基本的积木块,你可能还需要一些特殊的功能模块,比如一个能发光的灯块,一个能旋转的马达块,或者一个能播放声音的音效块。
在 genn-request
中,插件系统 (Middlewares) 就扮演着这些“功能模块”的角色。它允许你像搭积木一样,为请求的整个生命周期增加各种附加功能。每一个插件(也叫中间件,Middleware)都是一个相对独立的“功能模块”。
例如:
- 缓存插件:可以记住之前请求的结果,如果再次请求同样的内容,就直接从“记忆”中读取,不用真的去麻烦服务器。
- 重试插件:当网络请求不小心失败时,它能像个耐心的助手一样,自动帮你再试几次。
- 日志插件:详细记录下每一次请求的“行程”,包括出发时间、目的地、带了什么“行李”、以及最终是成功还是失败,方便我们追踪和调试问题。
- 认证插件:自动为需要身份验证的请求添加必要的“通行证”(如 Token)。
这些插件可以串联起来,按顺序依次处理请求。当一个请求发出时,它会像接力棒一样,从一个插件传递到下一个插件,每个插件都可以对请求进行一些处理,或者在请求发送前后执行一些操作。这种机制极大地增强了 genn-request
的灵活性和可扩展性,让你可以根据项目的具体需求,自由组合和定制各种高级功能。
假设我们希望为应用中的所有网络请求都添加一个唯一的追踪ID(Trace ID),这样当出现问题时,我们就能方便地在日志系统中追踪到特定请求的完整链路。如果手动为每个请求都添加这个ID,会非常繁琐且容易出错。插件系统就能很好地解决这个问题。
如何使用插件?
使用插件主要涉及两个步骤:定义插件和注册插件。
1. 定义一个插件 (Defining a Middleware)
一个插件本质上是一个符合特定接口的对象。这个接口通常包含两个主要部分:
name
: 一个字符串,用来标识这个插件的名字,方便调试和管理。execute
: 一个函数,这是插件的核心逻辑所在。
execute
函数的结构比较特殊,它是一个“高阶函数”(返回另一个函数的函数),通常看起来像这样:
execute: (next) => async (config) => { /* 插件逻辑 */ }
让我们分解一下:
execute
函数接收一个参数next
。这个next
函数非常重要,它代表着“下一个处理环节”。当你调用await next(config)
时,控制权就会传递给插件链中的下一个插件,或者如果已经是最后一个插件,则会执行核心的请求发送逻辑。execute
函数本身需要返回一个异步函数async (config) => { ... }
。这个异步函数接收当前请求的配置对象config
作为参数。- 在这个异步函数内部,你可以在调用
next(config)
之前执行一些操作(比如修改config
,记录日志)。 - 调用
const response = await next(config)
来执行后续操作并获取响应。 - 在调用
next(config)
之后,你可以对返回的response
进行处理(比如记录响应信息,转换数据格式)。 - 最后,这个异步函数必须返回处理后的
response
(或者一个包装了响应的 Promise)。
- 在这个异步函数内部,你可以在调用
让我们来看一个非常简单的日志插件示例,它会在请求发送前和接收到响应后打印一些信息:
// 这是一个非常简单的日志插件示例
const simpleLoggerMiddleware = {
name: 'simple-logger', // 插件名称
execute: (next) => async (config) => {
// 在请求发送前执行
console.log(`[插件] ${config.method} 请求即将发送到: ${config.path}`);
// 调用 next,将控制权交给下一个插件或核心请求逻辑
// config 参数是经过当前插件处理后的配置
const response = await next(config);
// 在收到响应后执行
console.log(`[插件] 收到来自 ${config.path} 的响应,状态码: ${response.status}`);
// 返回响应
return response;
},
};
这个插件非常简单:
- 它有一个名字
simple-logger
。 - 它的
execute
函数会在请求发出前打印一条日志。 - 然后它调用
await next(config)
来继续请求流程,并等待响应。 - 收到响应后,它再打印一条日志。
- 最后返回响应。
2. 注册插件 (Registering a Middleware)
定义好插件后,你需要告诉 genn-request
在什么时候使用它。这通常通过 RequestCore
实例(即我们通过 createRequest
创建的 request
对象)的方法来完成。
genn-request
的 RequestCore
实例提供了一个 usePlugin
方法,可以用来注册插件。
import { createRequest } from '@genn/genn-request';
// 导入我们上面定义的 simpleLoggerMiddleware
async function setupRequest() {
const request = await createRequest({
baseURL: 'https://api.example.com',
// defaultMiddlewares 也可以在创建实例时直接提供一组默认插件
// defaultMiddlewares: [simpleLoggerMiddleware]
});
// 使用 usePlugin 方法注册插件
request.usePlugin(simpleLoggerMiddleware);
// 现在,通过这个 request 实例发出的所有请求都会经过 simpleLoggerMiddleware
try {
await request.get({ url: '/users' });
// 控制台会输出:
// [插件] GET 请求即将发送到: /users
// (实际请求发送...)
// [插件] 收到来自 /users 的响应,状态码: 200 (或其他)
} catch (error) {
// 错误处理
}
}
setupRequest();
在上面的例子中,我们首先创建了一个 request
实例,然后调用 request.usePlugin(simpleLoggerMiddleware)
将我们的日志插件注册了进去。之后,所有通过这个 request
实例发出的请求,都会自动执行 simpleLoggerMiddleware
中的逻辑。
你也可以在调用 createRequest
时,通过 defaultMiddlewares
选项直接传入一个插件数组,这些插件会被默认注册到新创建的实例上。
3. 插件的执行顺序 (Execution Order)
如果你注册了多个插件,它们会按照注册的顺序依次执行。第一个注册的插件最先执行它的“请求前”逻辑,最后一个注册的插件最先执行它的“请求后”逻辑(因为 next
调用形成了类似洋葱的层叠结构)。
想象一下穿衣服:
usePlugin(内衣插件)
usePlugin(衬衫插件)
usePlugin(外套插件)
请求发出前(穿衣服):内衣 -> 衬衫 -> 外套 收到响应后(脱衣服):外套 -> 衬衫 -> 内衣
理解这个顺序对于设计复杂的插件交互非常重要。
genn-request
内置插件示例:withTraceId
genn-request
提供了一些开箱即用的插件,以简化常见任务的处理。withTraceId
就是其中一个,它可以自动为每个请求的 URL 查询参数中添加一个唯一的追踪ID。
首先,你需要从 @genn/genn-request
中导入它(或者从其插件子模块,具体取决于库的导出结构):
import { createRequest, withTraceId } from '@genn/genn-request';
async function main() {
const request = await createRequest({
baseURL: 'https://api.example.com',
defaultMiddlewares: [
withTraceId({ // 使用追踪ID插件
paramName: 'customTraceId', // 自定义追踪ID的参数名,默认为 'gtraceid'
idLength: 16 // 自定义ID长度,默认为32
})
]
});
// 发起请求
const response = await request.get({ url: '/items' });
// 假设生成的 customTraceId 是 'ABC123XYZ789'
// 那么实际请求的 URL 可能是: https://api.example.com/items?customTraceId=ABC123XYZ789
console.log('请求已发送,追踪ID已添加');
}
main();
在这个例子中:
- 我们导入了
withTraceId
插件。 - 在创建
request
实例时,通过defaultMiddlewares
数组将withTraceId()
添加进去。 - 我们还给
withTraceId
传递了一个配置对象,指定了参数名为customTraceId
,ID长度为16
。 - 之后,通过这个
request
实例发出的GET
请求,其 URL 后面会自动附加如?customTraceId=随机生成的16位ID
这样的参数。
这只是众多内置插件中的一个例子。其他插件如 withCache
(用于缓存)、withAuth
(用于认证)、withRetry
(用于重试)等,都遵循类似的用法,通过简单的配置就能为你的请求流程添加强大的功能。
插件系统是如何工作的? (How do Middlewares work internally?)
理解插件系统内部的运作方式,能帮助我们更好地使用它,甚至编写自定义插件。
PluginSystem
核心类
在 genn-request
内部,有一个名为 PluginSystem
的类(通常位于 src/plugins/index.ts
)专门负责管理和执行这些插件。
插件注册 (
use
方法):当你调用request.usePlugin(myMiddleware)
时,实际上是调用了RequestCore
实例内部PluginSystem
实例的use
方法。这个方法会将传入的插件添加到一个内部的插件列表(数组)中。// src/plugins/index.ts (PluginSystem 类简化版) export class PluginSystem<TRequestParams, TResponseData> { private middlewares: Middleware<TRequestParams, TResponseData>[] = []; use(middleware: Middleware<TRequestParams, TResponseData>) { if (!middleware || typeof middleware !== 'object' || !middleware.execute) { throw new TypeError('中间件必须包含一个 execute 方法'); } this.middlewares.push(middleware); // 将插件添加到列表中 return this; // 支持链式调用 } // ... 其他方法 }
这个
use
方法很简单,就是把插件存起来。插件执行 (
execute
方法):当一个请求真正需要被处理时(例如,当你调用request.get(...)
),RequestCore
会调用其内部PluginSystem
实例的execute
方法。这个方法接收两个关键参数:config
: 当前请求的配置对象。baseExecutor
: 一个基础执行器函数。这个函数代表了插件链的“终点”,它通常是RequestCore
内部的一个方法(比如executeRequest
),负责实际调用 拦截器管理器 (AxiosInterceptorManager) 并通过 Axios 发送 HTTP 请求。
compose
函数:串联插件的魔法
PluginSystem
的 execute
方法的核心在于如何将插件列表中的所有插件以及 baseExecutor
巧妙地串联起来,形成一个处理链。这通常是通过一个名为 compose
的辅助函数(可能位于 src/utils/compose.ts
)来实现的。
compose
函数的作用是接收一个插件(中间件)数组和一个最终的执行函数(baseExecutor
),然后返回一个新的函数。当你调用这个新函数并传入 config
时,它会按照以下方式执行:
- 第一个插件的
execute
方法被调用,它接收到的next
参数是一个指向“第二个插件的执行逻辑”的函数。 - 当第一个插件调用
await next(config)
时,第二个插件的execute
方法被调用,它接收到的next
参数又指向“第三个插件的执行逻辑”,以此类推。 - 直到最后一个插件,它接收到的
next
参数会指向baseExecutor
。当它调用await next(config)
时,baseExecutor
被执行,实际的 HTTP 请求发生。 baseExecutor
完成后,结果会沿着调用链反向回传,每个插件都有机会在await next(config)
之后处理响应。
// src/plugins/index.ts (PluginSystem 类 execute 方法简化版)
async execute(
config: RequestConfig<TRequestParams, TResponseData>,
baseExecutor: (
conf: RequestConfig<TRequestParams, TResponseData>,
) => Promise<ResponseWrapper<TRequestParams, TResponseData>>,
): Promise<ResponseWrapper<TRequestParams, TResponseData>> {
// 使用 compose 函数将所有插件和基础执行器组合成一个调用链
const chain = compose(this.middlewares, baseExecutor);
// 执行这个组合后的调用链
return chain(config);
}
这里的 compose
函数是实现插件“洋葱模型”的关键。它确保了每个插件都能在请求处理流程中正确地插入自己的逻辑,并且控制权能够顺畅地在插件之间以及与核心请求逻辑之间传递。
请求流程图
下面是一个简化的时序图,展示了当一个请求通过插件系统时的流程:
这个图清晰地展示了请求如何“流入”插件链,经过每个插件的处理,到达核心执行器,然后响应又如何“流出”插件链。
插件 (Middlewares) vs 拦截器 (Interceptors)
你可能会问,插件和我们在上一章学习的 拦截器管理器 (AxiosInterceptorManager) 有什么区别呢?它们似乎都能在请求的某个阶段执行逻辑。
主要的区别在于它们的作用层面和灵活性:
拦截器 (Interceptors):
- 更接近底层的 HTTP 请求发送(它们是围绕
axios
调用的钩子)。 - 主要职责是修改请求配置(如添加headers)或转换响应数据/错误对象。
- 它们的执行位于插件链中
baseExecutor
(核心请求执行器)的内部。也就是说,当插件调用next()
最终触发到实际的HTTP请求发送时,请求拦截器会先执行,然后是HTTP请求,然后是响应拦截器。 - 相对来说,功能更聚焦于请求/响应的直接操作。
- 更接近底层的 HTTP 请求发送(它们是围绕
插件 (Middlewares):
- 作用于整个请求处理流程的更高层面,它们包裹了包括拦截器和实际HTTP调用在内的整个
baseExecutor
。 - 功能更强大和灵活。插件不仅可以修改请求配置和响应,还可以:
- 完全控制是否执行后续的插件或实际的HTTP请求(例如,缓存插件命中缓存后可以直接返回,不再继续)。
- 执行异步操作,比如在发送请求前先从某个地方获取动态数据。
- 实现复杂的逻辑,如请求重试、请求节流、请求去重、Mock数据等。
- 管理请求的整个生命周期,例如启动计时器、记录详细的链路追踪信息。
- 可以看作是
genn-request
在 Axios 拦截器之上提供的一套更高级、更通用的扩展机制。
- 作用于整个请求处理流程的更高层面,它们包裹了包括拦截器和实际HTTP调用在内的整个
简单来说,拦截器是请求发送前后的“小调整”,而插件则是可以改变整个请求流程的“大模块”。它们可以协同工作:插件负责宏观流程控制和高级功能,而拦截器则可以在插件流程的特定点(即实际HTTP调用前后)进行细致的数据处理。
总结
在本章中,我们深入探索了 genn-request
的插件系统:
- 插件的威力:它们像可插拔的“功能模块”,能够以搭积木的方式为请求流程增加各种强大的附加功能,如日志、缓存、认证、重试等。
- 如何使用插件:我们学习了如何定义一个插件(实现
name
和execute
方法),以及如何通过request.usePlugin()
或createRequest
的defaultMiddlewares
选项来注册它们。 next
函数的重要性:它是插件链中连接各个环节的“接力棒”,控制着请求流程的走向。- 内置插件:
genn-request
提供了一些实用的内置插件(如withTraceId
),可以帮助我们快速实现常用功能。 - 内部机制:我们了解了
PluginSystem
类和compose
函数是如何协同工作,将插件串联起来并按序执行的。 - 与拦截器的区别:插件提供了比拦截器更高层次、更灵活的扩展能力。
插件系统是 genn-request
实现高度可定制化和可扩展性的核心特性之一。掌握了它,你就能像搭乐高一样,随心所欲地构建出满足你特定需求的网络请求解决方案。
在下一章,我们将具体学习一个非常实用的内置插件:缓存插件 (withCache)。它完美地展示了插件系统如何通过模块化的方式,为我们的应用带来显著的性能提升和用户体验改善。
Chapter 5: 缓存插件 (withCache)
在上一章 插件系统 (Middlewares) 中,我们了解了如何通过插件为 genn-request
添加各种强大的“功能模块”。插件系统让我们可以像搭积木一样,灵活地扩展请求的生命周期。
现在,我们将深入学习一个非常实用的内置插件——缓存插件 (withCache)。想象一下,你的应用需要展示一个产品分类列表,这个列表可能好几个小时甚至一天都不会改变。如果每次用户访问这个页面,我们都重新向服务器请求一次这个列表,不仅会增加用户的等待时间,也会给服务器带来不必要的压力。这时候,缓存插件就能派上大用场了!
什么是缓存插件 (withCache)?它解决了什么问题?
withCache
插件就像一个应用中的“记忆助手”。对于那些不经常变化的 GET
请求(比如获取配置信息、产品分类、地区列表等),它会将第一次请求的结果“记住”(存储在内存中)。当同样的请求再次发生时,它会直接从“记忆”中提取结果,而不是重新向服务器发送请求。
这样做的好处显而易见:
- 更快的响应速度:从内存中读取数据远比通过网络向服务器请求数据快得多,用户几乎可以立即看到结果。
- 减轻服务器压力:减少了不必要的网络请求,服务器可以更专注于处理真正需要更新的数据或更复杂的业务逻辑。
- 提升用户体验:应用感觉更流畅、更灵敏。
这和浏览器缓存网页的原理非常相似。一旦资源被缓存,下次访问时就能更快加载。
如何使用 withCache
插件?
使用 withCache
非常简单。首先,你需要从 @genn/genn-request
中导入它,然后在创建的 request
实例上通过 usePlugin
方法注册它。
1. 启用缓存插件
让我们来看一个获取产品分类列表的例子:
import { createRequest, withCache } from '@genn/genn-request';
async function fetchProductCategories() {
const request = await createRequest({
baseURL: 'https://api.example.com',
});
// 使用 withCache 插件,并配置选项
request.usePlugin(withCache({
ttl: 5 * 60 * 1000, // 缓存有效期:5分钟 (单位:毫秒)
addCacheFlag: true, // 在响应中添加一个 __fromCache 标记
}));
// 第一次请求分类数据
console.log('第一次请求 /categories...');
const response1 = await request.get({ url: '/categories' });
console.log('响应1 数据:', response1.data);
console.log('响应1 是否来自缓存:', response1.__fromCache); // 通常是 false 或 undefined
// 5分钟内再次请求同样的数据
console.log('第二次请求 /categories...');
const response2 = await request.get({ url: '/categories' });
console.log('响应2 数据:', response2.data);
console.log('响应2 是否来自缓存:', response2.__fromCache); // 应该是 true
}
fetchProductCategories();
在上面的代码中:
- 我们创建了一个
request
实例。 - 调用
request.usePlugin(withCache({...}))
来启用缓存功能。ttl: 5 * 60 * 1000
设置了缓存的“存活时间”(Time To Live)为5分钟。这意味着5分钟内,相同的请求会从缓存中读取。超过5分钟后,缓存会失效,下次请求会重新从服务器获取。addCacheFlag: true
会在从缓存返回的响应对象上附加一个__fromCache: true
的属性,方便我们调试和确认缓存是否生效。
- 当我们第一次调用
request.get({ url: '/categories' })
时,请求会正常发送到服务器,获取数据,然后插件会将这个响应缓存起来。 - 当我们在5分钟内再次调用
request.get({ url: '/categories' })
时,插件会发现这个请求之前已经缓存过了,并且缓存还没过期,于是直接返回缓存中的数据,__fromCache
属性会是true
。
注意:默认情况下,withCache
插件只对 GET
请求有效。对于 POST
, PUT
, DELETE
等可能会修改数据的请求,缓存通常是不适用的。
2. 常用配置选项
withCache
插件接受一个配置对象,除了上面用到的 ttl
和 addCacheFlag
,还有一些其他常用的选项:
onlySuccessResponses
(布尔类型, 默认为true
): 如果为true
,则只有成功的响应(通常指 HTTP 状态码为 2xx,并且response.success
为true
,如果使用了响应转换器 (ResponseTransformer))才会被缓存。这可以防止缓存错误的或无效的响应。request.usePlugin(withCache({ ttl: 300000, // 5分钟 onlySuccessResponses: true, // 只缓存成功的请求 }));
statusCodes
(数字数组, 默认为[200]
): 一个包含 HTTP 状态码的数组。只有当响应的状态码在这个数组中时,响应才会被缓存。例如,如果你希望也缓存状态码为201
(Created) 的GET
请求响应(虽然不常见),你可以这样设置:request.usePlugin(withCache({ ttl: 300000, statusCodes: [200, 201], // 缓存状态码为 200 或 201 的响应 }));
generateCacheKey
(函数, 可选): 一个函数,用于自定义如何根据请求配置生成缓存的键 (key)。默认情况下,genn-request
会根据请求的method
,baseURL
,path
, 和params
(对于GET
请求) 或data
(对于其他方法,虽然缓存主要用于GET
) 来生成一个唯一的字符串作为缓存键。 如果你有特殊的缓存键生成需求,可以提供这个函数。对于初学者,通常不需要关心这个选项。// 这是一个非常简化的自定义缓存键生成逻辑示例 // request.usePlugin(withCache({ // generateCacheKey: (config) => { // // 假设我们只关心 URL 路径 // return `my-custom-key:${config.path}`; // } // }));
withCache
的内部工作原理
了解插件是如何在幕后工作的,可以帮助我们更好地使用它。
1. 简要流程
当一个 GET
请求通过启用了 withCache
插件的 request
实例发出时,大致会发生以下事情:
- 生成缓存键 (Cache Key):插件首先会根据当前请求的配置(URL、方法、参数等)生成一个唯一的字符串,作为这条请求在缓存中的“身份证”,我们称之为“缓存键”。
- 检查缓存:插件会拿着这个“缓存键”去内部的“记忆仓库”(通常是一个
Map
对象)查找,看看是否已经存有这条请求的“记忆”(即之前缓存的响应数据)。 - 缓存命中 (Cache Hit):
- 如果找到了对应的“记忆”,并且这份“记忆”还没有“过期”(
ttl
还没到),那么太棒了!插件会直接把这份“记忆”(缓存的响应)返回给调用者。请求不会真的发送到服务器。
- 如果找到了对应的“记忆”,并且这份“记忆”还没有“过期”(
- 缓存未命中 (Cache Miss) 或缓存过期:
- 如果没有找到“记忆”,或者“记忆”已经“过期”了,插件就会调用
next(config)
,将请求交给后续的插件处理,或者最终由 请求核心 (RequestCore) 发送到服务器。
- 如果没有找到“记忆”,或者“记忆”已经“过期”了,插件就会调用
- 存储新缓存:当从服务器获取到新的响应后,如果这个响应满足缓存条件(例如,请求是
GET
,响应是成功的,状态码在允许的列表内),插件就会把这个新的响应连同当前的过期时间一起存入“记忆仓库”,以备将来的相同请求使用。 - 返回响应:最后,无论是从缓存中获取的响应,还是从服务器获取的新响应,都会被返回给最初的调用者。
2. 时序图
下面是一个简化的时序图,描述了缓存插件处理请求的过程:
3. 核心代码解读 (简化版)
withCache
插件的核心逻辑位于 src/plugins/cache.ts
文件中。
CacheStore
类:genn-request
内部使用一个CacheStore
类(或者类似的机制)来实际存储缓存数据。你可以把它想象成一个带过期功能的Map
。// src/plugins/cache.ts (CacheStore 类的极简示意) class CacheStore { private store = new Map<string, CacheEntry>(); // 使用Map存储缓存 get<T>(key: string): T | undefined { const entry = this.store.get(key); if (entry && Date.now() < entry.expires) { // 检查是否过期 return entry.data; } if (entry) this.store.delete(key); // 过期则删除 return undefined; } set<T>(key: string, data: T, ttl: number): void { const expires = Date.now() + ttl; // 计算过期时间 this.store.set(key, { data, expires }); } // ... 其他方法如 delete, clear ... } interface CacheEntry<T = any> { data: T; // 缓存的数据 expires: number; // 过期时间戳 }
这个简化的
CacheStore
通过Map
存储数据,并在get
时检查过期,set
时记录过期时间。withCache
插件函数: 这是插件的主体,它返回一个符合 插件系统 (Middlewares) 接口的对象。// src/plugins/cache.ts (withCache 函数的极简示意) export function withCache(options: CacheOptions = {}): Middleware { const mergedOptions = { /* ... 合并默认选项和用户选项 ... */ }; const cacheStorage = mergedOptions.storage || new CacheStore(); // 获取缓存实例 return { name: 'withCache', execute: (next) => async (config) => { // 1. 只对 GET 请求进行缓存处理 if (config.method?.toUpperCase() !== 'GET') { return next(config); // 不是GET,直接执行下一个 } // 2. 生成缓存键 (config.cacheKey 是用户可指定的自定义键) const cacheKey = config.cacheKey || generateDefaultCacheKey(config, mergedOptions.ignoreQueryParams); // 3. 尝试从缓存获取 const cachedResponse = cacheStorage.get(cacheKey); if (cachedResponse) { // 4. 缓存命中,添加标记并返回 return { ...cachedResponse, __fromCache: true }; } // 5. 缓存未命中,执行实际请求 const response = await next(config); // 6. 检查是否应该缓存该响应 const shouldCache = /* 根据 options.onlySuccessResponses, options.statusCodes 等判断 */; if (shouldCache) { cacheStorage.set(cacheKey, response, mergedOptions.ttl); } // 7. 返回从服务器获取的响应 return response; }, }; }
这个插件的
execute
方法清晰地展示了前面描述的缓存逻辑:检查方法、生成键、查缓存、执行请求、存缓存。generateDefaultCacheKey
函数: 这个函数负责根据请求配置生成默认的缓存键。// src/plugins/cache.ts (generateDefaultCacheKey 函数的简化示意) function generateDefaultCacheKey(config, ignoreQueryParams = false) { const { method, baseURL, path, params } = config; const url = `${baseURL || ''}${path || ''}`; // 拼接基础URL和路径 // 对于GET请求,通常是 方法名:完整URL:参数JSON字符串 if (method?.toUpperCase() === 'GET' && !ignoreQueryParams) { return `${method}:${url}:${JSON.stringify(params || {})}`; } // 其他情况或忽略参数的简化处理 (实际会更复杂) return `${method}:${url}`; }
这个函数确保了对于相同的
GET
请求(URL和参数都相同),会生成相同的缓存键。
手动清除缓存
有时候,我们可能需要手动清除缓存。比如,当用户修改了某个资源后,我们希望相关的缓存立即失效,以便下次请求能获取到最新的数据。genn-request
提供了清除缓存的方法。
clearCache()
: 清除由withCache
插件管理的所有缓存。clearRequestCache(config, ignoreQueryParams?)
: 清除特定请求的缓存。你需要提供一个与当初缓存时相似的config
对象,以便插件能计算出正确的缓存键。
import { createRequest, withCache, clearCache, clearRequestCache }
from '@genn/genn-request';
async function manageCache() {
const request = await createRequest({ baseURL: 'https://api.example.com' });
request.usePlugin(withCache({ ttl: 60000 })); // 缓存1分钟
await request.get({ url: '/users/1' }); // 请求并缓存 /users/1
await request.get({ url: '/products' }); // 请求并缓存 /products
// 假设用户更新了ID为1的用户信息,我们想清除它的缓存
const userConfig = { method: 'GET', baseURL: 'https://api.example.com', path: '/users/1' };
clearRequestCache(userConfig);
console.log("用户 /users/1 的缓存已清除");
// 或者,如果发生了全局数据更新,可能需要清除所有缓存
clearCache();
console.log("所有缓存已清除");
}
manageCache();
在上面的例子中,clearRequestCache
允许我们精确地清除某个特定请求的缓存,而 clearCache
则会清空所有内存缓存。
总结
在本章中,我们学习了:
withCache
插件的作用:它像一个“记忆助手”,通过缓存不经常变化的GET
请求结果,来提升应用性能和减轻服务器压力。- 如何使用
withCache
:通过request.usePlugin(withCache(options))
启用,并可以配置ttl
(有效期)、addCacheFlag
(缓存标记)、onlySuccessResponses
(只缓存成功响应)等选项。 - 内部工作机制:插件通过生成缓存键、检查缓存、在未命中时执行请求并存储新缓存的流程来工作。
- 手动清除缓存:可以使用
clearCache()
清除所有缓存,或使用clearRequestCache()
清除特定请求的缓存。
withCache
插件是 genn-request
中一个非常实用的功能,它能简单有效地优化你的应用。善用缓存,能给用户带来更流畅的体验。
在下一章,我们将探讨另一个重要的概念:响应转换器 (ResponseTransformer)。它能帮助我们统一处理和规范化从服务器返回的响应数据结构,使业务逻辑代码更加简洁。
Chapter 6: 响应转换器 (ResponseTransformer)
在上一章 缓存插件 (withCache) 中,我们学习了如何通过 withCache
插件来“记住”请求结果,从而提升应用的响应速度和用户体验。这一章,我们将探讨另一个非常实用的特性,它能帮助我们整理和规范从后端服务器返回的数据——响应转换器 (ResponseTransformer)。
什么是响应转换器 (ResponseTransformer)?它解决了什么问题?
想象一下,你正在与多个不同的后端API打交道。这些API可能由不同的团队开发,或者遵循不同的设计规范,导致它们返回的数据格式千差万别:
- API A 可能返回:
json { "code": 200, "message": "获取成功", "data": { "userId": 1, "username": "小明" } }
- API B 可能返回:
json { "status": "OK", "result": { "items": [{ "id": 101, "name": "商品A" }] }, "errorCode": null }
- API C 可能返回:
json { "successful": true, "payload": { "configValue": "test" }, "error": "" }
如果你的前端代码需要直接处理这些五花八门的格式,那将是一场噩梦!你需要为每种格式编写不同的逻辑来提取实际的业务数据、判断请求是否成功、以及获取错误信息。这不仅使代码变得冗余复杂,而且非常容易出错。
响应转换器 (ResponseTransformer) 就像一位专业的“数据整理师”。它的核心任务就是将这些来自不同API、结构各异的原始响应数据,按照你预设的规则,统一梳理成一种干净、一致的格式。
例如,我们可以定义规则,总是从 response.data.data
(对于API A) 或 response.data.result
(对于API B) 中提取我们真正需要的业务数据,并通过 response.data.success === true
(或 response.data.status === "OK"
) 来判断请求是否真的成功了。
经过响应转换器处理后,你的前端代码就能期望得到一个结构统一的响应对象,比如:
// 统一后的响应结构示例
interface StandardizedResponse<T> {
success: boolean; // 请求是否成功
data: T; // 实际的业务数据
message?: string; // 提示信息 (可选)
code?: string | number; // 业务状态码 (可选)
_originalResponse?: any; // 原始响应,供调试 (可选)
}
这样,无论后端API返回什么“奇形怪状”的数据,你的业务逻辑代码总能以相同的方式来访问数据和判断状态,大大简化了开发工作。
如何使用响应转换器?
好消息是,genn-request
默认启用了响应转换器,并且提供了一套合理的默认转换规则。这套默认规则通常期望后端返回类似下面这样的结构:
{
"success": true, // 或 false,表示业务是否成功
"data": { /* 这里是实际的业务数据 */ },
"message": "操作成功", // 或 "具体的错误信息"
"code": "0" // 或其他业务相关的状态码
}
1. 使用默认的响应转换
如果你后端API的响应格式与上述默认期望的结构相似(或者 genn-request
的默认提取逻辑恰好适用),那么你几乎不需要做任何额外配置。
import { createRequest } from '@genn/genn-request';
async function fetchDataWithDefaultTransformer() {
// 创建请求实例,默认启用响应转换
const request = await createRequest({
baseURL: 'https://api.example.com'
});
// 假设后端返回:
// { success: true, data: { name: "示例商品", price: 99 }, message: "获取成功" }
try {
const response = await request.get({ url: '/product/1' });
// response.success 会根据后端返回的 success 字段被设置为 true
// response.data 会是 { name: "示例商品", price: 99 } (即原始响应中的 data.data 部分)
if (response.success) {
console.log('商品名称:', response.data.name); // 直接访问业务数据
console.log('商品价格:', response.data.price);
} else {
// 如果 response.success 为 false, response.errorInfo 会包含错误信息
console.error('获取失败:', response.errorInfo?.message);
}
} catch (error) {
// 网络错误等其他错误
console.error('请求异常:', error);
}
}
fetchDataWithDefaultTransformer();
在这个例子中,genn-request
的默认响应转换器会自动:
- 将
response.success
设置为后端返回的success
字段的值。 - 将
response.data
设置为后端返回的data
字段(即业务数据本身)。 - 如果
success
为false
,则会尝试从message
和code
字段提取错误信息到response.errorInfo
。
2. 自定义响应转换规则
如果你的后端API返回的数据结构与默认期望的不同,你可以通过在创建 request
实例时传入 transformOptions
对象来自定义转换规则。
假设你的一个后端API返回如下结构:
json
{
"status_code": 200, // 用 status_code 表示 HTTP 状态,也用于业务成功判断
"result": { "item_id": "xyz", "item_name": "特殊物品" }, // 业务数据在 result 字段
"error_msg": null // 错误信息在 error_msg 字段
}
我们可以这样配置 transformOptions
:
import { createRequest } from '@genn/genn-request';
async function fetchDataWithCustomTransformer() {
const customRequest = await createRequest({
baseURL: 'https://api.custom.com',
transformOptions: {
// 定义如何从原始响应中提取实际的业务数据
dataExtractor: (originalResponse) => originalResponse.data.result,
// 定义如何判断请求是否成功
isSuccess: (originalResponse) => originalResponse.data.status_code === 200,
// 定义如何提取错误信息(当 isSuccess 返回 false 时)
errorExtractor: (originalResponse) => ({
message: originalResponse.data.error_msg || '未知业务错误',
code: originalResponse.data.status_code
}),
// 可选:是否在转换后的响应中保留原始响应体
keepOriginalData: true // 如果为true, 原始响应会在 response._originalResponse.data
}
});
try {
const response = await customRequest.get({ url: '/item/xyz' });
if (response.success) { // response.success 会是 true (因为 status_code === 200)
// response.data 会是 { item_id: "xyz", item_name: "特殊物品" }
console.log('物品名称:', response.data.item_name);
if (response._originalResponse) {
// console.log('原始响应数据:', response._originalResponse.data);
}
} else {
console.error('获取物品失败:', response.errorInfo?.message, '代码:', response.errorInfo?.code);
}
} catch (error) {
console.error('请求异常:', error);
}
}
fetchDataWithCustomTransformer();
transformOptions
中的常用选项包括:
dataExtractor(originalResponse: AxiosResponse): any
: 一个函数,接收原始的 AxiosResponse 对象,返回你希望作为最终response.data
的业务数据。默认情况下,它尝试返回originalResponse.data.data
。isSuccess(originalResponse: AxiosResponse): boolean
: 一个函数,接收原始的AxiosResponse
对象,返回一个布尔值表示业务请求是否成功。默认情况下,它检查originalResponse.data.success === true
。errorExtractor(originalResponse: AxiosResponse): { message: string; code: string | number }
: 一个函数,当isSuccess
返回false
时调用,用于从原始响应中提取错误信息和错误码。默认会尝试从originalResponse.data.message
和originalResponse.data.code
提取。extractData: boolean
(默认为true
): 是否启用数据提取。如果设为false
,即使isSuccess
为true
,response.data
也会是原始响应的data
部分,而不是dataExtractor
的结果。keepOriginalData: boolean
(默认为false
): 如果设为true
,转换后的响应对象response
上会有一个_originalResponse
属性,其值是原始的AxiosResponse
对象,方便调试或访问未被提取的原始数据。
3. 禁用响应转换
在某些情况下,你可能希望完全禁用响应转换,直接处理后端返回的原始数据。可以将 transformResponse
配置项设为 false
。
import { createRequest } from '@genn/genn-request';
async function fetchDataWithoutTransformer() {
const rawRequest = await createRequest({
baseURL: 'https://api.raw.com',
transformResponse: false // 禁用响应转换
});
try {
const response = await rawRequest.get({ url: '/get-raw-data' });
// response.data 将直接是后端返回的未经处理的数据体
// response.success 属性可能不存在,或者其值取决于原始Axios响应
console.log('原始数据:', response.data);
// 你需要自己判断成功与否,例如:
if (response.status === 200 && response.data.someField === 'expectedValue') {
// ...
}
} catch (error) {
console.error('请求异常:', error);
}
}
fetchDataWithoutTransformer();
当 transformResponse
设置为 false
后,genn-request
返回的 响应包装器 (ResponseWrapper) 中的 data
字段将是 axios
返回的原始 response.data
,而 success
字段的行为将依赖于 axios
本身(通常是基于HTTP状态码,但不会有业务层面的成功判断)。
响应转换器的内部运作
响应转换器在 genn-request
中通常是作为一个高优先级的响应拦截器来实现的。这意味着它在其他大多数响应处理逻辑(比如自定义的响应拦截器或某些插件的响应后处理)之前执行。
运作流程概览
- 当 请求核心 (RequestCore) 通过底层的
axios
实例接收到来自服务器的原始HTTP响应 (一个AxiosResponse
对象) 后。 - 如果
transformResponse
配置没有被禁用,这个原始响应会传递给响应转换拦截器。 - 转换拦截器会根据你提供的
transformOptions
(或者是默认的选项) 来处理这个原始响应:- 它调用
isSuccess(originalResponse)
来判断业务是否成功,并将结果赋值给最终ResponseWrapper
对象的success
属性。 - 如果业务成功 (且
extractData
未被禁用),它调用dataExtractor(originalResponse)
来提取业务数据,并将结果赋值给ResponseWrapper
的data
属性。 - 如果业务失败,它调用
errorExtractor(originalResponse)
来提取错误详情,并填充到ResponseWrapper
的errorInfo
属性中。 - 如果
keepOriginalData
为true
,原始的AxiosResponse
会被保存在ResponseWrapper
的_originalResponse
属性上。
- 它调用
- 经过转换器处理后,这个被“整理”过的 响应包装器 (ResponseWrapper) 对象才会继续传递给后续的响应拦截器、插件,并最终返回给你的业务代码。
简化时序图
核心代码解读
响应转换器的主要逻辑位于 src/plugins/responseTransformer.ts
文件中的 createResponseTransformer
函数。
// src/plugins/responseTransformer.ts (createResponseTransformer 函数简化示意)
export function createResponseTransformer<TRequestParams, TResponseData>(
options: ResponseTransformerOptions = {},
): ResponseInterceptor<TRequestParams, TResponseData> {
// 合并用户选项和默认选项
const {
extractData = true,
dataExtractor = (res) => res.data?.data, // 默认从 res.data.data 提取
isSuccess = (res) => res.data?.success === true, // 默认检查 res.data.success
errorExtractor = (res) => ({ /* ... */ }),
keepOriginalData = false,
} = options;
return {
name: 'response-transformer', // 拦截器名称
priority: 90, // 优先级较高,确保它在其他通用响应拦截器之前执行
onFulfilled: (responseWrapper) => { // responseWrapper 是 genn-request 的响应包装对象
// 防止重复转换
if (responseWrapper.__transformedByResponseTransformer) {
return responseWrapper;
}
const originalAxiosResponse = responseWrapper._originalResponse; // 获取原始Axios响应
// 1. 判断业务是否成功
const success = isSuccess(originalAxiosResponse);
responseWrapper.success = success;
// 2. 如果成功,提取数据
if (success && extractData) {
const extracted = dataExtractor(originalAxiosResponse);
if (keepOriginalData) {
responseWrapper.data = extracted;
// (responseWrapper as any).__originalData = originalAxiosResponse.data; // 实际实现会更严谨
} else {
responseWrapper.data = extracted;
}
} else if (!success) {
// 3. 如果失败,提取错误信息
const errorDetails = errorExtractor(originalAxiosResponse);
responseWrapper.errorInfo = {
...responseWrapper.errorInfo, // 保留可能已有的网络错误信息
message: errorDetails.message,
code: errorDetails.code,
type: 'business', // 标记为业务错误
};
}
// (如果 !extractData 且 success,responseWrapper.data 会保留原始 response.data)
// 标记已转换
responseWrapper.__transformedByResponseTransformer = true;
return responseWrapper;
},
// onRejected 通常不由响应转换器处理,它主要处理成功的HTTP响应中包含的业务失败情况
};
}
这个拦截器对象会被注册到 拦截器管理器 (AxiosInterceptorManager) 中。
请求核心 (RequestCore) 在其构造函数中,会检查 transformResponse
配置项。如果不是 false
,就会使用 createResponseTransformer
创建一个转换器实例,并将其添加为响应拦截器:
// src/core/request.ts (RequestCore 构造函数中相关部分简化示意)
export class RequestCore {
constructor(config?: RequestConfig<unknown, unknown>) {
// ... 获取和合并配置到 this.finalConfig ...
this.interceptors = new AxiosInterceptorManager(this.instance);
// 如果启用了响应转换 (transformResponse 默认为 true)
if (this.finalConfig.transformResponse !== false) {
// 使用配置中的 transformOptions 创建并注册响应转换拦截器
this.interceptors.response.use(
createResponseTransformer(this.finalConfig.transformOptions)
);
}
// ... 注册其他拦截器和插件 ...
}
// ...
}
这样,每当一个成功的HTTP响应(状态码2xx)返回时,这个转换拦截器就会自动工作,整理数据结构,然后才把结果交给后续的处理环节。
总结
在本章中,我们学习了:
- 响应转换器 (ResponseTransformer) 的重要性:它能将后端API返回的各种不同格式的数据,统一整理成标准、一致的结构,极大简化前端业务逻辑的复杂度。
- 如何使用响应转换器:
genn-request
默认启用响应转换,并有一套预设规则。- 可以通过
transformOptions
(包含dataExtractor
,isSuccess
,errorExtractor
等) 自定义转换逻辑。 - 可以通过设置
transformResponse: false
来完全禁用转换。
- 内部运作机制:响应转换器本质上是一个高优先级的响应拦截器,它在请求成功返回后,对原始响应数据进行解析和重构,然后更新 响应包装器 (ResponseWrapper) 对象。
掌握了响应转换器,你就能更从容地面对来自后端的多样化数据格式,让你的前端代码更加健壮和易于维护。
在下一章,我们将更深入地了解 响应包装器 (ResponseWrapper) 本身。这个对象是 genn-request
中所有请求方法(如 get
, post
)最终返回给你的结果,响应转换器也是围绕着它进行操作的。理解它的完整结构和包含的信息,对充分利用 genn-request
的能力至关重要。
Chapter 7: 响应包装器 (ResponseWrapper)
欢迎来到 genn-request
教程的第七章!在上一章 响应转换器 (ResponseTransformer) 中,我们学习了如何将后端返回的各种“原始包裹”统一整理成我们期望的格式。那么,这些经过精心整理的“包裹”最终会以什么样的形式交到我们手中呢?这就是本章的主角——响应包装器 (ResponseWrapper) 要为我们解答的问题。
为什么需要响应包装器 (ResponseWrapper)?
想象一下,每次你收到一个快递包裹,除了里面的商品本身,你还会得到一张“包裹签收单”。这张签收单上通常会包含很多有用的信息:
- 核心货物:你购买的实际商品。
- 包裹状态:是完好无损,还是运输途中有所损坏?
- 运输详情:由哪家快递公司承运,追踪单号是多少?
- 成功/失败标记:你是否成功签收?如果失败,原因是什么?
在网络请求的世界里,服务器返回的原始响应(比如 axios
直接给我们的 AxiosResponse
对象)可能只包含了最核心的“货物”(即 data
数据体)和一些基础的“运输详情”(如HTTP状态码和响应头)。但对于一个更完善的请求库来说,我们往往需要更多信息来帮助我们更好地处理结果。
genn-request
引入了 响应包装器 (ResponseWrapper) 的概念,它就是我们之前提到的那份内容详尽的“包裹签收单”。它不仅仅包含了从服务器获取的实际数据,还封装了更多有用的上下文信息,比如:
- 请求是否真的业务成功 (而不仅仅是HTTP状态码200)?
- 如果出错了,具体的错误信息和类型是什么?
- 这份数据是直接从服务器来的,还是从缓存中读取的?
- 这次请求的具体配置参数是什么?
通过 ResponseWrapper
,genn-request
为我们提供了一个标准化的、信息更丰富的响应对象,使得我们可以更优雅、更一致地处理各种请求结果。
什么是响应包装器 (ResponseWrapper)?
ResponseWrapper
是 genn-request
中所有请求方法(如 request.get()
、request.post()
等)最终返回给你的对象的类型。它对服务器返回的原始结果进行了标准化封装和增强。
根据我们开篇的描述,一个 ResponseWrapper
对象通常包含以下关键信息:
data: TResponseData
:实际的业务数据。这通常是经过 响应转换器 (ResponseTransformer) 处理和提取后的核心内容。success: boolean
:一个布尔值,明确标记了这次请求在业务层面是否成功。这比单纯依赖 HTTP 状态码更可靠。例如,HTTP 状态码可能是200 OK
,但后端业务逻辑可能因为某些原因(如参数无效)认为操作失败,并通过success: false
来告知。status: number
:标准的 HTTP 状态码(如200
,404
,500
)。headers: object
:HTTP 响应头。errorInfo?: ErrorInfo
:如果success
为false
,或者发生了网络错误、请求错误等,这里会包含详细的错误信息对象,如错误消息 (message
)、错误码 (code
)、错误类型 (type
) 等。config?: RequestConfig
:本次请求所使用的 请求配置 (RequestConfig) 对象。_originalResponse?: AxiosResponse
:可选的,原始的axios
响应对象,供需要访问更底层信息的场景使用。- 一些标志位:
__fromCache?: boolean
:如果数据来自 缓存插件 (withCache),则为true
。__fromMock?: boolean
:如果数据来自模拟数据,则为true
。__transformedByResponseTransformer?: boolean
:如果此响应已被 响应转换器 (ResponseTransformer) 处理过,则为true
。
拥有了这样一个结构清晰、信息全面的 ResponseWrapper
,我们就能更轻松地编写健壮的业务逻辑了。
如何使用响应包装器 (ResponseWrapper)?
当你使用 genn-request
发起任何请求时,你得到的 Promise
解析后的值就是一个 ResponseWrapper
对象。
示例1:处理成功的请求
import { createRequest } from '@genn/genn-request';
// 假设 request 是一个已配置的 genn-request 实例
// const request = await createRequest({ baseURL: 'https://api.example.com' });
async function fetchUserProfile() {
try {
const response = await request.get({ url: '/api/user/123' });
if (response.success) { // 检查业务是否成功
console.log('用户数据:', response.data); // { id: 123, name: '张三' }
console.log('HTTP状态码:', response.status); // 200
console.log('来自缓存吗?', response.__fromCache); // 可能为 true 或 false/undefined
} else {
// 处理业务失败的情况
console.error('获取用户信息业务失败:', response.errorInfo?.message);
console.error('错误代码:', response.errorInfo?.code);
}
} catch (error) {
// 处理网络错误或其他请求级错误 (error 本身通常也是一个 ResponseWrapper,但 success 为 false)
console.error('请求异常:', error.errorInfo?.message || error.message);
}
}
fetchUserProfile();
在这个例子中:
- 我们调用
request.get()
发起请求。 await
之后得到的response
就是一个ResponseWrapper
对象。- 我们首先检查
response.success
来判断业务是否成功。 - 如果成功,就可以安全地使用
response.data
。 - 同时也可以访问
response.status
等其他信息。
示例2:处理业务失败的请求
假设后端API对于不存在的用户返回 HTTP 200
,但在响应体中标记业务失败:
json
// API /api/user/999 返回:
// HTTP Status: 200
// Body: { "success": false, "message": "用户不存在", "code": "USER_NOT_FOUND" }
我们的代码可以这样处理: ```typescript // ...接上例 async function fetchNonExistentUser() { const response = await request.get({ url: '/api/user/999' });
if (response.success) { console.log('这不应该发生,因为用户不存在'); } else { console.log('业务处理结果:', response.success); // false console.log('错误提示:', response.errorInfo?.message); // "用户不存在" console.log('业务错误码:', response.errorInfo?.code); // "USER_NOT_FOUND" console.log('HTTP状态码:', response.status); // 200 } }
fetchNonExistentUser();
``
即使HTTP状态码是
200,
response.success也会因为 [响应转换器 (ResponseTransformer)](06_响应转换器__responsetransformer__.md) 的处理而变为
false,并且
response.errorInfo` 会包含后端返回的业务错误信息。
示例3:检查数据是否来自缓存
如果我们使用了 缓存插件 (withCache): ```typescript // ...假设 request 实例已启用 withCache 插件 async function fetchDataPossiblyFromCache() { const response = await request.get({ url: '/api/categories' });
if (response.success) { console.log('分类数据:', response.data); if (response.fromCache) { console.log('太棒了!数据是从缓存中快速加载的!'); console.log('缓存时间戳:', response.cacheTimestamp); } else { console.log('数据是从服务器新鲜获取的。'); } } }
fetchDataPossiblyFromCache();
``
通过检查
response.__fromCache` 属性,我们可以知道数据是来自缓存还是网络。
ResponseWrapper
的关键属性详解
让我们更深入地了解一下 ResponseWrapper
中那些重要的属性:
data?: TResponseData
- 用途:包含从服务器获取并经过处理的实际业务数据。
- 来源:通常是 响应转换器 (ResponseTransformer) 从原始响应中提取出来的。如果转换器未启用或配置为不提取数据,则可能是原始响应体。
success?: boolean
- 用途:一个至关重要的布尔标志,表明请求在业务层面是否成功。
- 来源:主要由 响应转换器 (ResponseTransformer) 根据其配置的
isSuccess
规则来设定。它解决了HTTP状态码(如200)不能完全代表业务成功的场景。
status?: number
- 用途:标准的HTTP状态码,如
200
(成功),401
(未授权),404
(未找到),500
(服务器错误) 等。 - 来源:直接来自底层HTTP客户端(如Axios)的响应。
- 用途:标准的HTTP状态码,如
headers?: object
- 用途:服务器返回的HTTP响应头。
- 来源:来自底层HTTP客户端。
errorInfo?: ErrorInfo
- 用途:当
success
为false
时,或者发生网络/请求级错误时,这里会提供一个结构化的错误信息对象。 - 结构 (通常包含):
message: string
:人类可读的错误消息。code: string | number
:错误码(可以是业务错误码或HTTP状态码)。type: string
:错误类型,如'business'
(业务错误),'network'
(网络错误),'timeout'
(超时),'server'
(服务器端HTTP错误),'client'
(客户端HTTP错误)。isNetworkError: boolean
:是否是网络连接问题。isTimeoutError: boolean
:是否是请求超时。isServerError: boolean
:是否是HTTP 5xx系列错误。isClientError: boolean
:是否是HTTP 4xx系列错误。retried: number
:请求被重试的次数。timestamp: number
:错误发生的时间戳。
- 来源:由 响应转换器 (ResponseTransformer) (针对业务错误) 或
genn-request
的核心错误处理逻辑 (针对网络/HTTP错误) 填充。
- 用途:当
config?: RequestConfig<TParams, TResponseData>
- 用途:导致此响应的原始 请求配置 (RequestConfig)。
- 来源:发起请求时传入的配置与全局/实例配置合并后的结果。在需要重试或调试时非常有用。
_originalResponse?: AxiosResponse<TResponseData>
- 用途:原始的
axios
响应对象。提供此属性是为了在需要时可以访问未经genn-request
包装的底层响应数据。 - 来源:底层
axios
库。
- 用途:原始的
__fromCache?: boolean
,__fromMock?: boolean
,__cacheTimestamp?: number
- 用途:这些标志位指示响应数据的来源。
- 来源:通常由相应的插件设置,例如 缓存插件 (withCache) 会设置
__fromCache
和__cacheTimestamp
。
__transformedByResponseTransformer?: boolean
- 用途:一个内部标志,表明此
ResponseWrapper
对象是否已经过 响应转换器 (ResponseTransformer) 的处理。 - 来源:由响应转换器自身设置。
- 用途:一个内部标志,表明此
ResponseWrapper
是如何诞生的?(内部机制)
了解 ResponseWrapper
是如何被构建和填充的,能帮助我们更好地理解 genn-request
的工作流程。
- 接收原始响应:当 请求核心 (RequestCore) 使用底层的
axios
实例发送HTTP请求后,它会从axios
收到一个原始的AxiosResponse
对象。 - 初步包装:
RequestCore
会立即将这个AxiosResponse
对象作为基础,创建一个ResponseWrapper
的“初稿”。此时,data
,status
,headers
,_originalResponse
等字段会被填充。success
可能会根据HTTP状态码进行初步判断(例如,2xx 状态码暂时视为success: true
)。 - 通过响应拦截器链:这个“初稿”的
ResponseWrapper
对象会被传递给一系列已注册的响应拦截器(包括在 拦截器管理器 (AxiosInterceptorManager) 中定义的)。- 响应转换器大显身手:其中一个非常重要的拦截器就是我们上一章学习的 响应转换器 (ResponseTransformer)(如果启用的话)。它会根据配置的规则,修改
ResponseWrapper
的success
状态,提取并设置data
,如果业务失败则填充errorInfo
。它还会设置__transformedByResponseTransformer
标志。 - 其他自定义的响应拦截器也可能在这个阶段对
ResponseWrapper
进行进一步的修改。
- 响应转换器大显身手:其中一个非常重要的拦截器就是我们上一章学习的 响应转换器 (ResponseTransformer)(如果启用的话)。它会根据配置的规则,修改
- 插件的后处理:经过所有响应拦截器处理后,
ResponseWrapper
会被传递给 插件系统 (Middlewares) 中定义的插件的“请求后”处理逻辑。例如,缓存插件 (withCache) 可能会在这里将响应存入缓存,并给ResponseWrapper
添加__fromCache
标记(如果是从缓存读取的话,这个标记在更早阶段就添加了)。 - 最终交付:最后,这个经过层层加工、信息完备的
ResponseWrapper
对象才会被Promise
解析并返回给调用方(你的业务代码)。
如果请求过程中发生错误(如网络错误、超时、或非2xx的HTTP状态码导致 axios
抛出错误),RequestCore
的错误处理逻辑会直接构建一个 success: false
的 ResponseWrapper
,并填充 errorInfo
和相关的错误属性,然后同样可能经过响应拦截器的错误处理分支 (onRejected
) 和插件的错误处理逻辑。
简化的时序图
ResponseWrapper
接口定义
ResponseWrapper
的具体结构定义在 src/types/http/response.ts
文件中。让我们看一下它的核心部分:
// 摘自 src/types/http/response.ts
import { AxiosError, AxiosResponse, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios';
import { RequestConfig } from './config';
export interface ErrorInfo { // 详细错误信息结构
message: string;
code: string | number;
type: string; // 例如: 'business', 'network', 'timeout'
isNetworkError: boolean;
// ... 其他错误相关标志和信息
retried: number;
timestamp: number;
}
export interface ResponseWrapper<TParams, TResponseData> {
data?: TResponseData; // 业务数据
status?: number; // HTTP 状态码
statusText?: string; // HTTP 状态文本
headers?: RawAxiosResponseHeaders | AxiosResponseHeaders; // 响应头
config?: RequestConfig<TParams, TResponseData>; // 请求配置
request?: unknown; // 底层请求对象 (通常是 XMLHttpRequest 或 http.ClientRequest)
success?: boolean; // 业务成功标志
error?: AxiosError; // 原始 Axios 错误对象 (如果发生请求级错误)
errorInfo?: ErrorInfo; // 详细错误信息
__fromMock?: boolean; // 是否来自模拟数据
__fromCache?: boolean; // 是否来自缓存
__cacheTimestamp?: number; // 缓存时间戳
__transformedByResponseTransformer?: boolean; // 是否被响应转换器处理过
_originalResponse?: AxiosResponse<TResponseData>; // 原始 Axios 响应对象
}
这个接口清晰地展示了 ResponseWrapper
所能携带的各种信息。
ResponseWrapper
的构建过程片段
在 genn-request
的核心代码 src/core/request.ts
中,我们可以找到 ResponseWrapper
是如何被创建和填充的。
处理成功响应时 (简化版 applyResponseInterceptors
):
```typescript
// 摘自 src/core/request.ts (RequestCore 类内部)
private async applyResponseInterceptors<TParams, TResponseData>(
response: AxiosResponse<TResponseData, TParams>, // 这是 Axios 返回的原始响应
config: RequestConfig<TParams, TResponseData>,
): Promise<ResponseWrapper<TParams, TResponseData>> {
// 1. 创建 ResponseWrapper 的“初稿”
let wrappedResponse: ResponseWrapper<TParams, TResponseData> = {
config,
data: response?.data,
headers: response?.headers,
status: response?.status,
statusText: response?.statusText,
_originalResponse: response,
success: true, // 初始假设HTTP 2xx 就是成功
error: undefined,
};
// 2. 获取所有响应拦截器 (包括响应转换器) 并按优先级排序 const interceptors = [ / ... 获取并排序拦截器 ... / ];
// 3. 依次应用拦截器,每个拦截器都可以修改 wrappedResponse for (const interceptor of interceptors) { if (interceptor.onFulfilled) { wrappedResponse = await interceptor.onFulfilled(wrappedResponse); } }
return wrappedResponse; // 返回最终被“加工”好的 ResponseWrapper
}
``
这里,原始的
AxiosResponse被用来初始化
wrappedResponse,然后它会经过一系列拦截器的处理,其中 [响应转换器 (ResponseTransformer)](06_响应转换器__responsetransformer__.md) 会重点修改
success,
data, 和
errorInfo` 字段。
处理错误时 (简化版 handleError
):
```typescript
// 摘自 src/core/request.ts (RequestCore 类内部)
private handleError<TParams, TResponseData>(
error: AxiosError<TResponseData, TParams>, // 这是 Axios 抛出的错误
config: RequestConfig<TParams, TResponseData>,
): Promise<ResponseWrapper<TParams, TResponseData>> {
// ... 其他错误处理逻辑,如重试 ...
const enhancedError = this.enhanceError(error, config); // 丰富错误信息
// 1. 创建表示错误的 ResponseWrapper const errorResponse: ResponseWrapper<TParams, TResponseData> = { config, data: error.response?.data, // 可能有错误响应体 headers: error.response?.headers, status: error.response?.status, statusText: error.response?.statusText, _originalResponse: error.response, success: false, // 明确标记为失败 error: enhancedError, errorInfo: { // 填充详细的 ErrorInfo message: enhancedError.message, code: this.getErrorCode(error), // 获取错误码 (HTTP状态码或Axios错误码) type: this.getErrorType(error), // 获取错误类型 ('network', 'timeout', etc.) // ... 其他 ErrorInfo 字段 }, };
// 2. 错误也可能经过响应拦截器的 onRejected 分支处理
// return this.applyErrorInterceptors(errorResponse, config);
return Promise.resolve(errorResponse); // 简化:直接返回错误包装器
}
``
当发生请求级错误时,
handleError方法会创建一个
success为
false的
ResponseWrapper,并尽力填充
errorInfo` 以提供最多的诊断信息。
总结
在本章中,我们深入了解了 genn-request
的 响应包装器 (ResponseWrapper):
- 它是一份关于请求结果的详尽“包裹签收单”,提供了比原始HTTP响应更丰富、更标准化的信息。
- 它的核心属性包括实际数据
data
、业务成功标志success
、HTTP状态码status
、详细错误信息errorInfo
、以及请求配置config
等。 - 我们学习了如何使用
ResponseWrapper
来判断业务成功与否、获取数据、处理错误,以及检查数据来源(如缓存)。 - 我们还探究了
ResponseWrapper
的诞生过程:它由 请求核心 (RequestCore) 基于原始AxiosResponse
创建,并经过一系列响应拦截器(特别是 响应转换器 (ResponseTransformer))和插件的加工,最终形成一个内容全面的对象。
ResponseWrapper
是你与 genn-request
交互的最终产物,理解它的结构和含义,能让你更有效地利用 genn-request
提供的各种功能,编写出更清晰、更健壮的应用程序。
至此,我们已经探索了 genn-request
的大部分核心概念。在最后一章,我们将了解一下 genn-request
项目本身是如何进行 版本管理与发布流程 (Changesets) 的,这对于想要参与贡献或理解库的迭代方式的开发者会很有帮助。
Chapter 8: 版本管理与发布流程 (Changesets)
欢迎来到 genn-request
教程的最后一章!在上一章 响应包装器 (ResponseWrapper) 中,我们详细了解了 genn-request
如何将请求结果以一个内容丰富的对象形式交到我们手中。至此,我们已经学习了 genn-request
的所有核心代码模块和它们的使用方法。
在本章,我们将换一个视角,不再关注具体的代码功能模块,而是探讨 genn-request
这个项目本身是如何进行维护和迭代的。这就像是我们不仅学会了如何驾驶一辆汽车,还要了解一下它的“保养手册”和“新车型发布规范”。这对于希望为 genn-request
贡献代码,或者想更深入理解其版本演进的开发者来说,会非常有帮助。
为什么需要规范的版本管理与发布流程?
想象一下,你正在使用一个你非常依赖的软件库(比如 genn-request
)。如果这个库的更新毫无章法:
- 你不知道新版本修复了什么问题,或者增加了什么功能。
- 版本号的升级规则混乱,你无法判断一个更新是小修补还是重大的功能变化,导致你不敢轻易升级。
- 如果多个人一起维护这个库,没有统一的流程,很容易造成代码冲突和版本混乱。
一个清晰、规范的版本管理与发布流程,正是为了解决这些问题。它能确保:
- 变更可追溯:每一次代码的改动都有记录,并且能清晰地体现在更新日志 (Changelog) 中。
- 版本有意义:版本号的变动遵循一定的规则(如语义化版本 Semantic Versioning),让用户能预估更新的影响。
- 协作更顺畅:为贡献者提供明确的指引,如何提交代码,如何记录变更。
- 发布自动化:减少手动操作,降低出错风险,提高发布效率。
genn-request
项目使用了一个名为 Changesets 的工具来帮助我们实现这些目标。
什么是 Changesets?它在 genn-request
中扮演什么角色?
Changesets 是一个用于管理 JavaScript/TypeScript 项目(尤其是 monorepo 项目,即一个仓库包含多个包)版本和生成更新日志的 CLI 工具。它鼓励一种“意图驱动”的发布流程。
在 genn-request
项目中,Changesets 承担了以下核心职责:
- 记录变更意图:每当开发者完成一项代码改动(比如修复一个故障、增加一个新配置选项),都需要创建一个“changeset 文件”。这个文件简要描述了这个改动的性质(是修复bug、新增功能,还是重大更新)以及具体的变更内容。
- 自动生成更新日志 (CHANGELOG):在发布新版本时,Changesets 会自动收集所有未发布的 changeset 文件,并将它们的内容汇总整理到
CHANGELOG.md
文件中。这样,用户就能清晰地看到每个版本带来了哪些具体的改变。 - 规范版本号提升:根据收集到的 changeset 文件中标记的变更类型(patch, minor, major),Changesets 会自动计算出下一个合适的版本号,并更新项目的
package.json
文件。 - 简化发布流程:Changesets 提供了一系列命令,帮助维护者自动化版本提升、日志生成、打标签和发布到 npm 等步骤。
简单来说,Changesets 就像是 genn-request
的“首席记录官”和“发布助理”。它确保了每一次代码迭代都清晰、可追溯,并且发布过程规范高效。
如果你想为 genn-request
贡献代码
如果你发现 genn-request
有个bug想修复,或者有个新功能想添加,并希望贡献你的代码,那么你就需要了解如何配合 Changesets 流程。
1. 进行代码更改
像往常一样,克隆 genn-request
的仓库,创建你的分支,然后进行代码的修改或添加。别忘了编写必要的测试!
git clone http://gitlab.genn.cn/genn-fe-framework/genn-request # 克隆仓库 (地址仅为示例)
cd genn-request
git checkout -b my-awesome-feature # 创建你的特性分支
# ... 进行代码修改 ...
2. 创建一个 Changeset 文件
在你完成了代码更改并通过了测试之后,你需要使用 Changesets CLI 来记录你的变更。genn-request
项目已经配置好了相关的 pnpm
脚本。
打开你的终端,在项目根目录下运行: ```bash pnpm cs
或者
pnpm run cs
这个命令会启动一个交互式的问答过程:
* **选择包 (Select packages)**:它会问你这次更改影响了哪个包。对于 `genn-request` (如果它是一个单包项目),你通常会选择 `@genn/genn-request`。按空格键选中,然后按回车键确认。
* **选择版本类型 (major/minor/patch)**:
* `patch` (补丁):如果你只是修复了一个 bug,并且没有改变任何公开的 API。
* `minor` (次版本):如果你增加了一个新功能,但保持了向后兼容性 (即现有的代码不会因为升级而坏掉)。
* `major` (主版本):如果你的更改不向后兼容 (例如,删除了一个方法,或者改变了现有方法的行为,导致用户升级后可能需要修改他们的代码)。
根据你的更改性质,使用方向键选择合适的类型,然后按回车。
* **填写变更摘要 (Summary)**:用一两句简短的话描述你的更改。这部分内容会直接出现在 `CHANGELOG.md` 中,所以尽量写清楚。例如:“修复了全局配置在某些情况下未生效的问题”,或者“为 `withCache` 插件添加了 `maxEntries` 配置选项”。
完成这些步骤后,Changesets 会在项目根目录下的 `.changeset` 文件夹里创建一个新的 Markdown 文件,文件名通常是随机生成的,例如 `pretty-lions-laugh.md`。
这个文件的内容大概是这样的:
--- "@genn/genn-request": patch # 或者 minor, major ---
修复了当 xxx 时,yyy 无法正常工作的问题。 ``` 这个文件非常重要,它记录了你这次贡献的“意图”。
3. 提交代码和 Changeset 文件
最后,将你的代码更改和新创建的 .changeset/*.md
文件一起提交到你的分支,并发起合并请求 (Merge Request / Pull Request)。
git add .
git commit -m "feat: 添加 xxx 功能并创建 changeset" # 你的提交信息
git push origin my-awesome-feature
之后,项目维护者在合并你的代码时,这个 changeset 文件也会被合并进去。
genn-request
的发布流程(维护者视角概览)
当项目维护者决定发布一个新版本时(例如,在合并了若干个包含 changeset 的贡献之后),Changesets 工具会帮助他们完成大部分工作。
1. 版本化 (Versioning)
维护者会运行一个命令(通常是 pnpm changelog
或 changeset version
),Changesets 会:
- 扫描
.changeset
文件夹下所有未发布的 changeset 文件。 - 根据这些文件中声明的变更类型(
patch
,minor
,major
),决定下一个版本号。例如,如果有一个minor
类型的变更,并且当前版本是1.1.0
,那么下一个版本就会是1.2.0
。如果只有patch
类型的变更,就会是1.1.1
。 - 将这些 changeset 文件的内容汇总,生成或更新
CHANGELOG.md
文件。你可以从本教程最开始提供的CHANGELOG.md
文件片段看到它的格式。 - 更新
package.json
文件中的version
字段为新的版本号。 - 删除已经被处理过的
.changeset
文件。
之后,维护者会将 CHANGELOG.md
和 package.json
的变动提交到代码仓库。
# 维护者操作
pnpm changelog # 或者 changeset version
git add CHANGELOG.md package.json .changeset # .changeset 目录可能只包含剩余的空文件或被清空
git commit -m "chore: version packages"
2. 发布 (Publishing)
版本信息更新并提交后,维护者就可以将新版本发布到 npm 了。这通常通过 pnpm release
(或 changeset publish
) 命令完成。这个命令会:
- 构建项目(如果需要)。
- 在 Git 仓库中为新版本打上一个标签 (tag),例如
v1.2.0
。 - 将代码包发布到 npm 仓库。
genn-request
项目还在 scripts/publish.js
文件中提供了一个名为 publish:wizard
的脚本 (通过 pnpm publish:wizard
执行),它封装了创建 changeset、版本化和发布的整个流程,通过交互式提问引导维护者完成发布,进一步简化了操作。
下面是一个简化的发布流程图: ```mermaid sequenceDiagram participant 开发者 participant Changesets工具 participant Git仓库 participant NPM仓库
开发者->>Changesets工具: pnpm cs (创建 changeset 文件)
开发者->>Git仓库: 提交代码和 .changeset 文件
Note over 开发者, Git仓库: (维护者合并PR)
开发者->>Changesets工具: pnpm changelog (或 changeset version)
Changesets工具->>Git仓库: 更新 package.json 和 CHANGELOG.md
Changesets工具-->>开发者: 版本已更新
开发者->>Git仓库: 提交版本更新
开发者->>Git仓库: 打上版本标签
开发者->>Changesets工具: pnpm release (或 changeset publish)
Changesets工具->>NPM仓库: 发布新版本包
```
Changesets 对 genn-request
用户的益处
即使你只是 genn-request
的使用者,不参与贡献代码,Changesets 带来的规范流程对你也有好处:
- 清晰的更新日志:你可以随时查阅
CHANGELOG.md
文件(通常在项目的 GitHub/GitLab 页面或 npm 包页面都能找到),了解每个版本修复了哪些 bug、增加了哪些新功能,或者有哪些不兼容的变更。这能帮助你决定是否要升级,以及如何升级。 - 可靠的版本号:由于遵循语义化版本,你可以根据版本号的变化(例如从
1.2.5
到1.3.0
是一个minor
更新,可能包含新功能;从1.2.5
到2.0.0
是一个major
更新,可能需要你修改代码)来评估升级的风险和工作量。
内部机制简介
.changeset
目录:这个目录是 Changesets 的核心,所有由pnpm cs
命令生成的.md
文件都存放在这里。每个文件代表一个独立的变更集。- Changesets CLI: 这是执行
changeset version
,changeset publish
等命令的工具。它读取.changeset
目录中的文件,并与package.json
和CHANGELOG.md
交互。 - 配置文件 (
.changeset/config.json
): Changesets 允许一些配置,例如设置CHANGELOG.md
的生成方式,或者配置当发布失败时是否自动回滚 Git 标签等。对于初学者,通常不需要关心这个文件。 CONTRIBUTING.md
:genn-request
项目的CONTRIBUTING.md
文件(如本教程开头提供的示例)详细说明了贡献者应该如何遵循包括 Changesets 在内的开发流程。scripts/publish.js
: 这是genn-request
项目自定义的一个发布辅助脚本,它将 Changesets 的多个命令串联起来,提供更友好的交互式发布体验,减少了维护者手动执行多个命令的繁琐。
总结
在本章中,我们了解了 genn-request
项目是如何通过 Changesets 工具来管理其版本和发布流程的:
- 我们知道了 Changesets 的核心作用是记录变更、自动生成更新日志和规范化版本号。
- 如果你想为
genn-request
贡献代码,你需要使用pnpm cs
命令来创建一个 changeset 文件,描述你的改动。 - 我们概览了维护者使用 Changesets 进行版本化和发布的步骤。
- 对于
genn-request
的普通用户来说,这个流程带来了清晰的CHANGELOG.md
和可靠的语义化版本号。
虽然这部分内容不直接涉及 genn-request
的API使用,但它揭示了项目维护的幕后故事,有助于我们理解一个开源(或内部共享)库是如何健康迭代的。
至此,genn-request
的入门教程系列就全部结束了。我们从最基础的 请求配置 (RequestConfig) 开始,一步步探索了 请求核心 (RequestCore)、拦截器管理器 (AxiosInterceptorManager)、强大的 插件系统 (Middlewares)(包括具体的 缓存插件 (withCache)),以及如何通过 响应转换器 (ResponseTransformer) 来规范数据,最后还了解了统一的 响应包装器 (ResponseWrapper)。希望这个系列教程能帮助你快速上手并熟练使用 genn-request
,让你的网络请求处理更加高效和愉快!
感谢你的学习!如果你在使用过程中有任何问题或建议,欢迎通过项目仓库的 Issue 提出。
Generated by AI Codebase Knowledge Builder