request插件使用教程

2025 年 5 月 8 日 星期四
26

request插件使用教程

genn-request 是一个基于 Axios 封装的 HTTP 请求库。它像一个“超级快递员”,帮你发送和接收网络数据。通过插件(比如缓存)、拦截器(数据加工)和统一配置,让发送网络请求变得更简单、更强大,也更容易维护。

Source Repository: None

Chapters

  1. 请求配置 (RequestConfig)
  2. 请求核心 (RequestCore)
  3. 拦截器管理器 (AxiosInterceptorManager)
  4. 插件系统 (Middlewares)
  5. 缓存插件 (withCache)
  6. 响应转换器 (ResponseTransformer)
  7. 响应包装器 (ResponseWrapper)
  8. 版本管理与发布流程 (Changesets)

Chapter 1: 请求配置 (RequestConfig)

欢迎来到 genn-request 的世界!这是一个强大且易于上手的网络请求库。在这一章,我们将一起探索 genn-request 的基石之一 —— 请求配置 (RequestConfig)

什么是请求配置 (RequestConfig)?为什么需要它?

想象一下,每次你要出门旅行,都需要准备一份详细的“出行计划单”。这份计划单会写清楚你的目的地、出行方式、需要携带的物品、预计的行程时间等等。

在网络请求的世界里,RequestConfig 就扮演着这份“出行计划单”的角色。它详细规定了每一次网络请求的方方面面:

  • 目的地 (URL):你要请求的服务器地址和路径。
  • 出行方式 (HTTP 方法):是 GET 请求(获取数据)、POST 请求(提交数据),还是其他?
  • 携带物品 (请求头和数据):你需要在请求中附加的额外信息(比如身份令牌)或要发送给服务器的数据。
  • 预计耗时 (超时设置):如果请求太久没有响应,应该在什么时候放弃等待。
  • 特殊服务选项:比如是否需要缓存这次请求的结果,或者如果失败了是否需要自动重试。

那么,为什么我们需要这样一个“计划单”呢?

假设你的应用中有很多地方都需要向同一个基础 API 地址(例如 https://api.example.com/v1)发送请求,并且大部分请求的超时时间都希望设置为 5 秒。如果每次发送请求时都要手动输入这些信息,不仅繁琐,而且容易出错。如果将来基础 API 地址变了,或者默认超时时间需要调整,你就不得不修改应用中的每一处请求代码!

RequestConfig 就是为了解决这个问题而生的。它允许你:

  1. 全局设定:定义一套默认的“出行计划模板”,应用中所有的请求都会默认使用这套模板。
  2. 单次定制:对于特殊的“旅行”,你可以在默认模板的基础上进行个别调整,比如某个特定请求需要更长的超时时间,或者需要发送特殊的请求头。

通过 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 实例会使用它在创建时指定的 baseURLtimeout,而不是全局配置中的值。

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,但这次对 /productsGET 请求,其超时时间被临时改为了 3000ms

这种分层配置的机制(全局 -> 实例 -> 单次请求)提供了极大的灵活性,让你能够轻松应对各种复杂的请求场景。

深入了解:RequestConfig 的内部机制

那么,genn-request 是如何管理和应用这些配置的呢?让我们简单了解一下其内部的工作流程。

当你使用 genn-request 时,配置的合并遵循一个清晰的优先级顺序: 单次请求配置 > 实例创建时配置 > 全局配置 (defineRequestConfig) > 库内置默认配置

下面是一个简化的流程图,展示了配置是如何生效的:

核心代码解读

配置的管理主要发生在 src/utils/config.ts 文件中。

  1. 默认配置 (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', // 默认内容类型
      },
      // ... 其他默认设置
    });

    这份配置是所有配置的基础。

  2. 全局配置存储 (globalConfigdefineRequestConfig): 当你调用 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); // 将用户定义的全局配置存储起来
    }
  3. 配置合并 (getRequestConfigmergeConfigs): 当你调用 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 接口中的一些常用属性

RequestConfiggenn-request 灵活性的核心。掌握了它,你就掌握了控制网络请求行为的钥匙。

在下一章,我们将深入探讨 genn-request 的心脏——请求核心 (RequestCore),看看它是如何利用这些配置来实际执行网络请求的。


Chapter 2: 请求核心 (RequestCore)

在上一章 请求配置 (RequestConfig) 中,我们学习了如何像准备“出行计划单”一样,为我们的网络请求定义各种参数和设置。我们了解了全局配置、实例配置和单次请求配置,以及它们是如何合并生效的。

现在,计划单已经准备好了,谁来负责执行这份计划,真正地把我们的“包裹”(HTTP请求)送出去呢?这就是本章的主角——请求核心 (RequestCore) 的职责。

什么是请求核心 (RequestCore)?它解决了什么问题?

想象一下,RequestConfig 是我们精心填写的“快递订单”,上面写着收件人地址(URL)、物品类型(请求方法)、是否需要加急(超时设置)等等。那么,RequestCore 就是那位经验丰富的“快递调度中心主管”。

这位主管的核心任务是:

  1. 接收你的“快递订单”(API 调用和配置)。
  2. 仔细检查并打包(合并最终的请求参数)。
  3. 选择最佳的运输路线和方式(应用中间件和拦截器)。
  4. 将“包裹”(HTTP 请求)安全送达目的地(服务器)。
  5. 带回“签收回执”(服务器响应)。

简单来说,RequestCoregenn-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();

在上面的代码中:

  1. await createRequest({...}) 会返回一个 request 对象。这个 request 对象实际上就是 RequestCore 的一个实例,它已经根据我们传入的配置(以及全局配置)初始化好了。
  2. 当我们调用 request.get({ url: '/users' }) 时,RequestCore 就开始工作了。它会接收到这个 GET 请求的指令和相关的配置信息(如 url)。
  3. 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 }) 这样的方法时:

  1. 准备单次请求配置RequestCore 的内部 request 方法 (这是一个私有方法,get, post 等方法都会调用它) 会首先调用 prepareConfig。这个函数会将我们为单次请求传入的 singleRequestConfig 与实例自身的 finalConfig 进行合并,得到本次请求最终要使用的配置。这里也包括处理 URL 路径参数、生成请求ID等。
  2. 插件系统执行:接下来,请求配置会交给 插件系统 (Middlewares)。插件可以对请求配置进行修改,或者在请求发送前后执行一些自定义逻辑(比如缓存、日志等)。
  3. 执行实际请求 (executeRequest)
    • 应用请求拦截器:在请求真正发出之前,拦截器管理器 (AxiosInterceptorManager) 中注册的请求拦截器会被执行。这些拦截器可以最后一次修改请求配置。
    • 发送HTTP请求:使用内部的 axios 实例,根据最终的配置(URL, method, data, headers 等)发送 HTTP 请求到服务器。
    • 应用响应拦截器:收到服务器的响应后,响应拦截器会被执行。它们可以修改响应数据,或者统一处理某些响应状态。
  4. 处理结果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() 就是真正发起网络通信的地方。applyRequestInterceptorsapplyResponseInterceptors 则分别调用了我们在 拦截器管理器 (AxiosInterceptorManager) 中定义的拦截逻辑。

RequestCore 的核心职责总结

通过上面的讲解,我们可以看到 RequestCore 承担了以下几个核心职责:

  1. 配置管理与合并:它接收并最终确定每一次请求所使用的配置,这是建立在我们在 请求配置 (RequestConfig) 中学到的基础上。
  2. 实例化和使用底层 HTTP 客户端 (Axios):它是 axios 的直接使用者,负责调用 axios 来发送和接收数据。
  3. 执行请求生命周期:从准备配置、应用插件、应用请求拦截器、发送请求、接收响应、应用响应拦截器,到最终返回结果或处理错误,RequestCore 管理着整个流程。
  4. 集成插件系统:它是 插件系统 (Middlewares) 的执行者,使得我们能够通过插件扩展请求行为。
  5. 集成拦截器:它与 拦截器管理器 (AxiosInterceptorManager) 紧密协作,应用开发者定义的拦截逻辑。
  6. 错误处理与重试:它提供了统一的错误处理机制,并能根据配置实现请求重试等高级功能。
  7. 请求取消:管理待处理的请求,并提供取消请求的功能。

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. 拦截器的属性:namepriority

  • 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) 的特定阶段按顺序应用这些拦截器。

运作流程概览

  1. 注册:当你调用 request.interceptors.request.use(...)request.interceptors.response.use(...) 时,AxiosInterceptorManager 会将你提供的拦截器函数(及其元数据如 name, priority)添加到一个内部的数组中(分别是 customRequestInterceptorscustomResponseInterceptors)。
  2. 排序:每次添加或移除拦截器后,管理器会根据拦截器的 priority 对这些数组进行排序,确保高优先级的拦截器排在前面。
  3. 应用(由 RequestCore 调用)
    • 请求阶段:在 请求核心 (RequestCore) 准备好发送实际 HTTP 请求之前,它会调用一个类似 applyRequestInterceptors 的方法。这个方法会遍历 AxiosInterceptorManager 中已排序的请求拦截器,并依次执行它们的 onFulfilled 函数,将上一个拦截器返回的 config 传递给下一个。
    • 响应阶段:当 axios 返回响应(或错误)后,请求核心 (RequestCore) 会调用类似 applyResponseInterceptors 的方法。这个方法会遍历已排序的响应拦截器,依次执行它们的 onFulfilled (处理成功响应) 或 onRejected (处理错误) 函数。

下面是一个简化的时序图,展示了拦截器是如何在请求流程中被应用的:

核心代码解读

我们来看一下 src/core/interceptors.tsAxiosInterceptorManager 的关键部分:

  1. 构造函数和存储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实例,现在主要管理自定义列表
      }
      // ...
    }

    这里的 customRequestInterceptorscustomResponseInterceptors 数组用于存储用户通过 use 方法添加的拦截器。每个元素包含拦截器本身和其元数据(如ID)。

  2. 添加拦截器 (use) 和排序 (sortInterceptors): 当调用 request.useresponse.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 对数组进行降序排序。

  3. 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;
  },
};

这个插件非常简单:

  1. 它有一个名字 simple-logger
  2. 它的 execute 函数会在请求发出前打印一条日志。
  3. 然后它调用 await next(config) 来继续请求流程,并等待响应。
  4. 收到响应后,它再打印一条日志。
  5. 最后返回响应。

2. 注册插件 (Registering a Middleware)

定义好插件后,你需要告诉 genn-request 在什么时候使用它。这通常通过 RequestCore 实例(即我们通过 createRequest 创建的 request 对象)的方法来完成。

genn-requestRequestCore 实例提供了一个 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 调用形成了类似洋葱的层叠结构)。

想象一下穿衣服:

  1. usePlugin(内衣插件)
  2. usePlugin(衬衫插件)
  3. 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 方法。这个方法接收两个关键参数:

    1. config: 当前请求的配置对象。
    2. baseExecutor: 一个基础执行器函数。这个函数代表了插件链的“终点”,它通常是 RequestCore 内部的一个方法(比如 executeRequest),负责实际调用 拦截器管理器 (AxiosInterceptorManager) 并通过 Axios 发送 HTTP 请求。

compose 函数:串联插件的魔法

PluginSystemexecute 方法的核心在于如何将插件列表中的所有插件以及 baseExecutor 巧妙地串联起来,形成一个处理链。这通常是通过一个名为 compose 的辅助函数(可能位于 src/utils/compose.ts)来实现的。

compose 函数的作用是接收一个插件(中间件)数组和一个最终的执行函数(baseExecutor),然后返回一个新的函数。当你调用这个新函数并传入 config 时,它会按照以下方式执行:

  1. 第一个插件的 execute 方法被调用,它接收到的 next 参数是一个指向“第二个插件的执行逻辑”的函数。
  2. 当第一个插件调用 await next(config) 时,第二个插件的 execute 方法被调用,它接收到的 next 参数又指向“第三个插件的执行逻辑”,以此类推。
  3. 直到最后一个插件,它接收到的 next 参数会指向 baseExecutor。当它调用 await next(config) 时,baseExecutor 被执行,实际的 HTTP 请求发生。
  4. 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请求,然后是响应拦截器。
    • 相对来说,功能更聚焦于请求/响应的直接操作。
  • 插件 (Middlewares)

    • 作用于整个请求处理流程的更高层面,它们包裹了包括拦截器和实际HTTP调用在内的整个 baseExecutor
    • 功能更强大和灵活。插件不仅可以修改请求配置和响应,还可以:
      • 完全控制是否执行后续的插件或实际的HTTP请求(例如,缓存插件命中缓存后可以直接返回,不再继续)。
      • 执行异步操作,比如在发送请求前先从某个地方获取动态数据。
      • 实现复杂的逻辑,如请求重试、请求节流、请求去重、Mock数据等。
      • 管理请求的整个生命周期,例如启动计时器、记录详细的链路追踪信息。
    • 可以看作是 genn-request 在 Axios 拦截器之上提供的一套更高级、更通用的扩展机制。

简单来说,拦截器是请求发送前后的“小调整”,而插件则是可以改变整个请求流程的“大模块”。它们可以协同工作:插件负责宏观流程控制和高级功能,而拦截器则可以在插件流程的特定点(即实际HTTP调用前后)进行细致的数据处理。

总结

在本章中,我们深入探索了 genn-request 的插件系统:

  • 插件的威力:它们像可插拔的“功能模块”,能够以搭积木的方式为请求流程增加各种强大的附加功能,如日志、缓存、认证、重试等。
  • 如何使用插件:我们学习了如何定义一个插件(实现 nameexecute 方法),以及如何通过 request.usePlugin()createRequestdefaultMiddlewares 选项来注册它们。
  • next 函数的重要性:它是插件链中连接各个环节的“接力棒”,控制着请求流程的走向。
  • 内置插件genn-request 提供了一些实用的内置插件(如 withTraceId),可以帮助我们快速实现常用功能。
  • 内部机制:我们了解了 PluginSystem 类和 compose 函数是如何协同工作,将插件串联起来并按序执行的。
  • 与拦截器的区别:插件提供了比拦截器更高层次、更灵活的扩展能力。

插件系统是 genn-request 实现高度可定制化和可扩展性的核心特性之一。掌握了它,你就能像搭乐高一样,随心所欲地构建出满足你特定需求的网络请求解决方案。

在下一章,我们将具体学习一个非常实用的内置插件:缓存插件 (withCache)。它完美地展示了插件系统如何通过模块化的方式,为我们的应用带来显著的性能提升和用户体验改善。


Chapter 5: 缓存插件 (withCache)

在上一章 插件系统 (Middlewares) 中,我们了解了如何通过插件为 genn-request 添加各种强大的“功能模块”。插件系统让我们可以像搭积木一样,灵活地扩展请求的生命周期。

现在,我们将深入学习一个非常实用的内置插件——缓存插件 (withCache)。想象一下,你的应用需要展示一个产品分类列表,这个列表可能好几个小时甚至一天都不会改变。如果每次用户访问这个页面,我们都重新向服务器请求一次这个列表,不仅会增加用户的等待时间,也会给服务器带来不必要的压力。这时候,缓存插件就能派上大用场了!

什么是缓存插件 (withCache)?它解决了什么问题?

withCache 插件就像一个应用中的“记忆助手”。对于那些不经常变化的 GET 请求(比如获取配置信息、产品分类、地区列表等),它会将第一次请求的结果“记住”(存储在内存中)。当同样的请求再次发生时,它会直接从“记忆”中提取结果,而不是重新向服务器发送请求。

这样做的好处显而易见:

  1. 更快的响应速度:从内存中读取数据远比通过网络向服务器请求数据快得多,用户几乎可以立即看到结果。
  2. 减轻服务器压力:减少了不必要的网络请求,服务器可以更专注于处理真正需要更新的数据或更复杂的业务逻辑。
  3. 提升用户体验:应用感觉更流畅、更灵敏。

这和浏览器缓存网页的原理非常相似。一旦资源被缓存,下次访问时就能更快加载。

如何使用 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();

在上面的代码中:

  1. 我们创建了一个 request 实例。
  2. 调用 request.usePlugin(withCache({...})) 来启用缓存功能。
    • ttl: 5 * 60 * 1000 设置了缓存的“存活时间”(Time To Live)为5分钟。这意味着5分钟内,相同的请求会从缓存中读取。超过5分钟后,缓存会失效,下次请求会重新从服务器获取。
    • addCacheFlag: true 会在从缓存返回的响应对象上附加一个 __fromCache: true 的属性,方便我们调试和确认缓存是否生效。
  3. 当我们第一次调用 request.get({ url: '/categories' }) 时,请求会正常发送到服务器,获取数据,然后插件会将这个响应缓存起来。
  4. 当我们在5分钟内再次调用 request.get({ url: '/categories' }) 时,插件会发现这个请求之前已经缓存过了,并且缓存还没过期,于是直接返回缓存中的数据,__fromCache 属性会是 true

注意:默认情况下,withCache 插件只对 GET 请求有效。对于 POST, PUT, DELETE 等可能会修改数据的请求,缓存通常是不适用的。

2. 常用配置选项

withCache 插件接受一个配置对象,除了上面用到的 ttladdCacheFlag,还有一些其他常用的选项:

  • onlySuccessResponses (布尔类型, 默认为 true): 如果为 true,则只有成功的响应(通常指 HTTP 状态码为 2xx,并且 response.successtrue,如果使用了响应转换器 (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 实例发出时,大致会发生以下事情:

  1. 生成缓存键 (Cache Key):插件首先会根据当前请求的配置(URL、方法、参数等)生成一个唯一的字符串,作为这条请求在缓存中的“身份证”,我们称之为“缓存键”。
  2. 检查缓存:插件会拿着这个“缓存键”去内部的“记忆仓库”(通常是一个 Map 对象)查找,看看是否已经存有这条请求的“记忆”(即之前缓存的响应数据)。
  3. 缓存命中 (Cache Hit)
    • 如果找到了对应的“记忆”,并且这份“记忆”还没有“过期”(ttl 还没到),那么太棒了!插件会直接把这份“记忆”(缓存的响应)返回给调用者。请求不会真的发送到服务器。
  4. 缓存未命中 (Cache Miss) 或缓存过期
    • 如果没有找到“记忆”,或者“记忆”已经“过期”了,插件就会调用 next(config),将请求交给后续的插件处理,或者最终由 请求核心 (RequestCore) 发送到服务器。
  5. 存储新缓存:当从服务器获取到新的响应后,如果这个响应满足缓存条件(例如,请求是 GET,响应是成功的,状态码在允许的列表内),插件就会把这个新的响应连同当前的过期时间一起存入“记忆仓库”,以备将来的相同请求使用。
  6. 返回响应:最后,无论是从缓存中获取的响应,还是从服务器获取的新响应,都会被返回给最初的调用者。

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 字段(即业务数据本身)。
  • 如果 successfalse,则会尝试从 messagecode 字段提取错误信息到 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.messageoriginalResponse.data.code 提取。
  • extractData: boolean (默认为 true): 是否启用数据提取。如果设为 false,即使 isSuccesstrueresponse.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 中通常是作为一个高优先级的响应拦截器来实现的。这意味着它在其他大多数响应处理逻辑(比如自定义的响应拦截器或某些插件的响应后处理)之前执行。

运作流程概览

  1. 请求核心 (RequestCore) 通过底层的 axios 实例接收到来自服务器的原始HTTP响应 (一个 AxiosResponse 对象) 后。
  2. 如果 transformResponse 配置没有被禁用,这个原始响应会传递给响应转换拦截器。
  3. 转换拦截器会根据你提供的 transformOptions (或者是默认的选项) 来处理这个原始响应:
    • 它调用 isSuccess(originalResponse) 来判断业务是否成功,并将结果赋值给最终 ResponseWrapper 对象的 success 属性。
    • 如果业务成功 (且 extractData 未被禁用),它调用 dataExtractor(originalResponse) 来提取业务数据,并将结果赋值给 ResponseWrapperdata 属性。
    • 如果业务失败,它调用 errorExtractor(originalResponse) 来提取错误详情,并填充到 ResponseWrappererrorInfo 属性中。
    • 如果 keepOriginalDatatrue,原始的 AxiosResponse 会被保存在 ResponseWrapper_originalResponse 属性上。
  4. 经过转换器处理后,这个被“整理”过的 响应包装器 (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)?
  • 如果出错了,具体的错误信息和类型是什么?
  • 这份数据是直接从服务器来的,还是从缓存中读取的?
  • 这次请求的具体配置参数是什么?

通过 ResponseWrappergenn-request 为我们提供了一个标准化的、信息更丰富的响应对象,使得我们可以更优雅、更一致地处理各种请求结果。

什么是响应包装器 (ResponseWrapper)?

ResponseWrappergenn-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:如果 successfalse,或者发生了网络错误、请求错误等,这里会包含详细的错误信息对象,如错误消息 (message)、错误码 (code)、错误类型 (type) 等。
  • config?: RequestConfig:本次请求所使用的 请求配置 (RequestConfig) 对象。
  • _originalResponse?: AxiosResponse:可选的,原始的 axios 响应对象,供需要访问更底层信息的场景使用。
  • 一些标志位:

拥有了这样一个结构清晰、信息全面的 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状态码是200response.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)的响应。
  • headers?: object

    • 用途:服务器返回的HTTP响应头。
    • 来源:来自底层HTTP客户端。
  • errorInfo?: ErrorInfo

    • 用途:当 successfalse 时,或者发生网络/请求级错误时,这里会提供一个结构化的错误信息对象。
    • 结构 (通常包含):
      • 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 是如何诞生的?(内部机制)

了解 ResponseWrapper 是如何被构建和填充的,能帮助我们更好地理解 genn-request 的工作流程。

  1. 接收原始响应:当 请求核心 (RequestCore) 使用底层的 axios 实例发送HTTP请求后,它会从 axios 收到一个原始的 AxiosResponse 对象。
  2. 初步包装RequestCore 会立即将这个 AxiosResponse 对象作为基础,创建一个 ResponseWrapper 的“初稿”。此时,data, status, headers, _originalResponse 等字段会被填充。success 可能会根据HTTP状态码进行初步判断(例如,2xx 状态码暂时视为 success: true)。
  3. 通过响应拦截器链:这个“初稿”的 ResponseWrapper 对象会被传递给一系列已注册的响应拦截器(包括在 拦截器管理器 (AxiosInterceptorManager) 中定义的)。
    • 响应转换器大显身手:其中一个非常重要的拦截器就是我们上一章学习的 响应转换器 (ResponseTransformer)(如果启用的话)。它会根据配置的规则,修改 ResponseWrappersuccess 状态,提取并设置 data,如果业务失败则填充 errorInfo。它还会设置 __transformedByResponseTransformer 标志。
    • 其他自定义的响应拦截器也可能在这个阶段对 ResponseWrapper 进行进一步的修改。
  4. 插件的后处理:经过所有响应拦截器处理后,ResponseWrapper 会被传递给 插件系统 (Middlewares) 中定义的插件的“请求后”处理逻辑。例如,缓存插件 (withCache) 可能会在这里将响应存入缓存,并给 ResponseWrapper 添加 __fromCache 标记(如果是从缓存读取的话,这个标记在更早阶段就添加了)。
  5. 最终交付:最后,这个经过层层加工、信息完备的 ResponseWrapper 对象才会被 Promise 解析并返回给调用方(你的业务代码)。

如果请求过程中发生错误(如网络错误、超时、或非2xx的HTTP状态码导致 axios 抛出错误),RequestCore 的错误处理逻辑会直接构建一个 success: falseResponseWrapper,并填充 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方法会创建一个successfalseResponseWrapper,并尽力填充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 承担了以下核心职责:

  1. 记录变更意图:每当开发者完成一项代码改动(比如修复一个故障、增加一个新配置选项),都需要创建一个“changeset 文件”。这个文件简要描述了这个改动的性质(是修复bug、新增功能,还是重大更新)以及具体的变更内容。
  2. 自动生成更新日志 (CHANGELOG):在发布新版本时,Changesets 会自动收集所有未发布的 changeset 文件,并将它们的内容汇总整理到 CHANGELOG.md 文件中。这样,用户就能清晰地看到每个版本带来了哪些具体的改变。
  3. 规范版本号提升:根据收集到的 changeset 文件中标记的变更类型(patch, minor, major),Changesets 会自动计算出下一个合适的版本号,并更新项目的 package.json 文件。
  4. 简化发布流程: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`。

这个文件的内容大概是这样的:
markdown

--- "@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 changelogchangeset 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.mdpackage.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.51.3.0 是一个 minor 更新,可能包含新功能;从 1.2.52.0.0 是一个 major 更新,可能需要你修改代码)来评估升级的风险和工作量。

内部机制简介

  • .changeset 目录:这个目录是 Changesets 的核心,所有由 pnpm cs 命令生成的 .md 文件都存放在这里。每个文件代表一个独立的变更集。
  • Changesets CLI: 这是执行 changeset version, changeset publish 等命令的工具。它读取 .changeset 目录中的文件,并与 package.jsonCHANGELOG.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

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...