Skip to content

@ahoo-wang/fetcher-decorator

@ahoo-wang/fetcher-decorator 包使用 TypeScript 装饰器实现声明式 API 服务定义。无需编写命令式的 HTTP 调用,您只需用装饰器注解定义 API 类,框架就会在运行时自动生成实现。

源码: packages/decorator/src/

安装

bash
pnpm add @ahoo-wang/fetcher-decorator reflect-metadata

WARNING

reflect-metadata 是必需的对等依赖。在使用装饰器之前,必须在应用程序入口点导入一次:

typescript
import 'reflect-metadata';

架构

mermaid
graph TB
    subgraph sg_1 ["Decorator Layer"]
        API["@api() class decorator"]
        EP["@get/@post/@put/@delete/@patch<br>endpoint decorators"]
        PARAM["@path/@query/@header/@body/@request/@attribute<br>parameter decorators"]
    end

    subgraph sg_2 ["Metadata Layer"]
        AM["ApiMetadata<br>Class-level config"]
        EM["EndpointMetadata<br>Method-level config"]
        PM["ParameterMetadata<br>Argument bindings"]
        FM["FunctionMetadata<br>Merged config"]
    end

    subgraph sg_3 ["Execution Layer"]
        RE["RequestExecutor<br>Builds and runs the request"]
        LIFE["ExecuteLifeCycle<br>beforeExecute / afterExecute"]
        FETCHER["@ahoo-wang/fetcher<br>Interceptor pipeline"]
    end

    API --> AM
    EP --> EM
    PARAM --> PM
    AM --> FM
    EM --> FM
    PM --> FM
    FM --> RE
    RE --> LIFE
    RE --> FETCHER

    style API fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style EP fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PARAM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style AM fill:#161b22,stroke:#30363d,color:#e6edf3
    style EM fill:#161b22,stroke:#30363d,color:#e6edf3
    style PM fill:#161b22,stroke:#30363d,color:#e6edf3
    style FM fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RE fill:#161b22,stroke:#30363d,color:#e6edf3
    style LIFE fill:#161b22,stroke:#30363d,color:#e6edf3
    style FETCHER fill:#161b22,stroke:#30363d,color:#e6edf3

装饰器执行流程

当调用一个被装饰的方法时,框架会执行一个明确定义的管道:

mermaid
sequenceDiagram
autonumber

    participant App as Application
    participant Method as Decorated Method
    participant RE as RequestExecutor
    participant FM as FunctionMetadata
    participant Fetcher as Fetcher
    participant LC as ExecuteLifeCycle
    participant IM as Interceptor Chain

    App->>Method: userService.getUser(123)
    Method->>RE: execute([123])
    RE->>FM: resolveExchangeInit(args)
    FM->>FM: Map @path params to path: {id: 123}
    FM->>FM: Map @query params to query: {...}
    FM->>FM: Map @header params to headers: {...}
    FM->>FM: Map @body to body: {...}
    FM->>FM: Resolve attributes, timeout, fetcher
    FM-->>RE: {request, attributes}
    RE->>Fetcher: resolveExchange(request, options)
    Fetcher-->>RE: FetchExchange

    alt Target implements ExecuteLifeCycle
        RE->>LC: beforeExecute(exchange)
        LC-->>RE: Modified exchange
    end

    RE->>IM: interceptors.exchange(exchange)
    IM-->>RE: Completed exchange

    alt Target implements ExecuteLifeCycle
        RE->>LC: afterExecute(exchange)
    end

    RE->>RE: extractResult()
    RE-->>App: Result (Response, JSON, etc.)

定义 API 类

@api 装饰器

@api 类装饰器为类中所有方法设置基础路径、请求头、超时时间和 Fetcher 实例。(apiDecorator.ts:232)

typescript
import { api, get, post, put, del, path, query, body, header, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import 'reflect-metadata';

interface User {
  id: number;
  name: string;
  email: string;
}

@api('/api/v1/users', {
  headers: { 'X-Api-Version': '1.0' },
  timeout: 10000,
  fetcher: 'api', // 使用 'api' 命名的 fetcher
})
class UserService {
  @get('/')
  getUsers(
    @query('page') page: number,
    @query('limit') limit: number,
  ): Promise<User[]> {
    throw autoGeneratedError();
  }

  @get('/:id')
  getUser(@path('id') id: number): Promise<User> {
    throw autoGeneratedError();
  }

  @post('/')
  createUser(@body() user: Omit<User, 'id'>): Promise<User> {
    throw autoGeneratedError();
  }

  @put('/:id')
  updateUser(
    @path('id') id: number,
    @body() user: Partial<User>,
  ): Promise<User> {
    throw autoGeneratedError();
  }

  @del('/:id')
  deleteUser(@path('id') id: number): Promise<void> {
    throw autoGeneratedError();
  }
}

端点装饰器

装饰器HTTP 方法源码
@get(path?, metadata?)GETendpointDecorator.ts:101
@post(path?, metadata?)POSTendpointDecorator.ts:126
@put(path?, metadata?)PUTendpointDecorator.ts:151
@del(path?, metadata?)DELETEendpointDecorator.ts:176
@patch(path?, metadata?)PATCHendpointDecorator.ts:201
@head(path?, metadata?)HEADendpointDecorator.ts:229
@options(path?, metadata?)OPTIONSendpointDecorator.ts:254

每个端点装饰器都接受可选的 MethodEndpointMetadata 来覆盖类级别的设置:

typescript
@get('/special', {
  headers: { 'X-Special': 'true' },
  timeout: 30000,
  resultExtractor: ResultExtractors.Json,
})
specialEndpoint(): Promise<any> {
  throw autoGeneratedError();
}

参数装饰器

参数装饰器将函数参数映射到 HTTP 请求组件。当未提供显式名称时,参数名会自动从函数签名中提取。(parameterDecorator.ts:199)

装饰器类型映射到对象展开
@path(name?)ParameterType.PATHURL 路径段是 - 对象键成为路径参数
@query(name?)ParameterType.QUERYURL 查询字符串是 - 对象键成为查询参数
@header(name?)ParameterType.HEADER请求头是 - 对象键成为请求头
@body()ParameterType.BODY请求体
@request()ParameterType.REQUEST基础请求对象
@attribute(name?)ParameterType.ATTRIBUTEExchange 属性是 - 对象和 Map
mermaid
graph LR
    subgraph sg_1 ["Method Arguments"]
        A1["id: number"]
        A2["filters: object"]
        A3["auth: string"]
        A4["userData: User"]
        A5["request: ParameterRequest"]
        A6["attr: Map"]
    end

    subgraph sg_2 ["Parameter Decorators"]
        D1["@path('id')"]
        D2["@query()"]
        D3["@header('Authorization')"]
        D4["@body()"]
        D5["@request()"]
        D6["@attribute()"]
    end

    subgraph sg_3 ["Request Components"]
        R1["urlParams.path = {id: 123}"]
        R2["urlParams.query = {...filters}"]
        R3["headers['Authorization'] = auth"]
        R4["body = userData"]
        R5["Merged into request"]
        R6["exchange.attributes"]
    end

    A1 --> D1 --> R1
    A2 --> D2 --> R2
    A3 --> D3 --> R3
    A4 --> D4 --> R4
    A5 --> D5 --> R5
    A6 --> D6 --> R6

    style A1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A5 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A6 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style D1 fill:#161b22,stroke:#30363d,color:#e6edf3
    style D2 fill:#161b22,stroke:#30363d,color:#e6edf3
    style D3 fill:#161b22,stroke:#30363d,color:#e6edf3
    style D4 fill:#161b22,stroke:#30363d,color:#e6edf3
    style D5 fill:#161b22,stroke:#30363d,color:#e6edf3
    style D6 fill:#161b22,stroke:#30363d,color:#e6edf3
    style R1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R5 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R6 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

对象参数展开

路径、查询和请求头装饰器支持对象参数。当传入对象时,其键值对会被展开为独立的参数:

typescript
@api('/api')
class SearchService {
  @get('/search')
  search(
    @query() filters: { limit: number; offset: number; sort: string },
  ): Promise<SearchResult[]> {
    throw autoGeneratedError();
  }
}

// 调用时:
service.search({ limit: 10, offset: 20, sort: 'name' });
// => GET /api/search?limit=10&offset=20&sort=name

@request 装饰器

@request() 装饰器允许传入 ParameterRequest 对象以完全控制请求。它会与端点级别的配置合并,参数请求具有更高优先级。(parameterDecorator.ts:372)

typescript
@api('/api/users')
class UserService {
  @post('/')
  createUser(@request() req: ParameterRequest): Promise<User> {
    throw autoGeneratedError();
  }
}

// 使用:
service.createUser({
  path: '/api/users',
  headers: { 'X-Idempotency-Key': 'abc123' },
  body: { name: 'John' },
  timeout: 30000,
});

@attribute 装饰器

@attribute() 装饰器将数据传递到 exchange 属性中,可被管道中的任何拦截器读取。(parameterDecorator.ts:408)

typescript
@api('/api/orders')
class OrderService {
  @post('/')
  createOrder(
    @body() order: Order,
    @attribute('tenantId') tenantId: string,
  ): Promise<Order> {
    throw autoGeneratedError();
  }
}

生命周期钩子(ExecuteLifeCycle)

类可以实现 ExecuteLifeCycle 接口来挂钩请求执行管道。OpenAI 包 使用此机制根据是否启用流式传输来动态切换结果提取器。(executeLifeCycle.ts:23)

mermaid
graph TD
    START["Method Called"] --> BUILD["Build FetchExchange"]
    BUILD --> BEFORE["beforeExecute(exchange)"]
    BEFORE --> INTERCEPT["Interceptor Chain"]
    INTERCEPT --> AFTER["afterExecute(exchange)"]
    AFTER --> RESULT["extractResult()"]
    RESULT --> RETURN["Return to caller"]

    style START fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BUILD fill:#161b22,stroke:#30363d,color:#e6edf3
    style BEFORE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style INTERCEPT fill:#161b22,stroke:#30363d,color:#e6edf3
    style AFTER fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RESULT fill:#161b22,stroke:#30363d,color:#e6edf3
    style RETURN fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
typescript
import { api, get, ExecuteLifeCycle, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import type { FetchExchange } from '@ahoo-wang/fetcher';

@api('/api/data')
class DataService implements ExecuteLifeCycle {
  async beforeExecute(exchange: FetchExchange): Promise<void> {
    // 从会话中添加租户 ID
    exchange.ensureRequestHeaders()['X-Tenant-Id'] = getTenantId();
    // 添加请求跟踪
    exchange.attributes.set('requestId', crypto.randomUUID());
  }

  async afterExecute(exchange: FetchExchange): Promise<void> {
    // 记录完成信息
    const requestId = exchange.attributes.get('requestId');
    console.log(`Request ${requestId} completed: ${exchange.response?.status}`);
  }

  @get('/items')
  getItems(): Promise<Item[]> {
    throw autoGeneratedError();
  }
}

EndpointReturnType

默认情况下,被装饰的方法返回提取的结果(如解析后的 JSON)。您可以将此行为更改为返回整个 FetchExchange 对象。(endpointReturnTypeCapable.ts:14)

描述
EndpointReturnType.RESULT返回提取的结果(默认)
EndpointReturnType.EXCHANGE返回完整的 FetchExchange 对象
typescript
import { EndpointReturnType } from '@ahoo-wang/fetcher-decorator';

@api('/api/users', { returnType: EndpointReturnType.EXCHANGE })
class UserService {
  @get('/')
  getUsers(): Promise<FetchExchange> {
    throw autoGeneratedError();
  }
}

元数据解析

FunctionMetadata 类将 API 级别、端点级别和参数元数据合并为单一的解析配置。端点级别的值覆盖 API 级别的值,参数装饰器的值覆盖两者。(functionMetadata.ts:43)

mermaid
graph TD
    subgraph sg_1 ["Priority (Highest to Lowest)"]
        P1["Parameter Decorators<br>@path, @query, @header, @body"]
        P2["Endpoint Metadata<br>@get path, metadata arg"]
        P3["API Metadata<br>@api basePath, options"]
    end

    P1 --> MERGE["FunctionMetadata<br>resolveExchangeInit()"]
    P2 --> MERGE
    P3 --> MERGE
    MERGE --> FINAL["Final FetchRequest"]

    style P1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style P2 fill:#161b22,stroke:#30363d,color:#e6edf3
    style P3 fill:#161b22,stroke:#30363d,color:#e6edf3
    style MERGE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FINAL fill:#161b22,stroke:#30363d,color:#e6edf3

autoGeneratedError

autoGeneratedError() 函数创建一个占位错误,既能满足 ESLint 的 no-unused-vars 规则,又能表明方法体在运行时会被替换。(generated.ts:41)

typescript
import { autoGeneratedError } from '@ahoo-wang/fetcher-decorator';

@get('/users/:id')
getUser(@path('id') id: number): Promise<User> {
  throw autoGeneratedError(id); // 参数会被接受但会被忽略
}

完整示例

typescript
import 'reflect-metadata';
import { api, get, post, del, path, query, body, header, autoGeneratedError } from '@ahoo-wang/fetcher-decorator';
import { NamedFetcher, ResultExtractors } from '@ahoo-wang/fetcher';

// 设置:创建命名 fetcher
new NamedFetcher('myApi', {
  baseURL: 'https://api.example.com',
  headers: { 'Accept': 'application/json' },
  timeout: 5000,
});

interface Product {
  id: string;
  name: string;
  price: number;
}

interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}

@api('/api/v2/products', { fetcher: 'myApi' })
class ProductService {
  @get('/')
  listProducts(
    @query() filters: ProductFilters,
    @query('page') page: number = 1,
    @query('limit') limit: number = 20,
  ): Promise<Product[]> {
    throw autoGeneratedError();
  }

  @get('/:id')
  getProduct(
    @path('id') id: string,
    @header('Accept-Language') locale: string = 'en',
  ): Promise<Product> {
    throw autoGeneratedError();
  }

  @post('/')
  createProduct(@body() product: Omit<Product, 'id'>): Promise<Product> {
    throw autoGeneratedError();
  }

  @del('/:id')
  deleteProduct(@path('id') id: string): Promise<void> {
    throw autoGeneratedError();
  }
}

// 使用
const products = new ProductService();
const items = await products.listProducts({ category: 'electronics' }, 1, 10);

导出 API 总结

导出类型源码
api装饰器apiDecorator.ts
getpostputdelpatchheadoptions装饰器endpointDecorator.ts
pathqueryheaderbodyrequestattribute装饰器parameterDecorator.ts
ApiMetadata接口apiDecorator.ts
EndpointMetadata接口endpointDecorator.ts
ParameterType枚举parameterDecorator.ts
ParameterMetadata接口parameterDecorator.ts
FunctionMetadatafunctionMetadata.ts
RequestExecutorrequestExecutor.ts
ExecuteLifeCycle接口executeLifeCycle.ts
EndpointReturnType枚举endpointReturnTypeCapable.ts
autoGeneratedError函数generated.ts
buildRequestExecutor函数apiDecorator.ts
getParameterNames函数reflection.ts

相关页面

  • Fetcher(核心) - 装饰器委托的 HTTP 客户端
  • OpenAI - 使用装饰器配合 ExecuteLifeCycle 的实际案例
  • Generator - 从 OpenAPI 规范自动生成基于装饰器的 API 类
  • Wow - DDD/CQRS 基于装饰器的客户端
  • 包概览 - 生态系统中的所有包

基于 Apache License 2.0 发布。