diff --git a/apps/server/README-decorators.md b/apps/server/README-TC39-decorators.md similarity index 53% rename from apps/server/README-decorators.md rename to apps/server/README-TC39-decorators.md index b0372fe..f3d2e73 100644 --- a/apps/server/README-decorators.md +++ b/apps/server/README-TC39-decorators.md @@ -1,6 +1,13 @@ -# 路由装饰器使用指南 +# 路由装饰器使用指南(TC39 标准) -本项目已支持使用装饰器来自动注册路由,让控制器代码更加简洁和声明式。 +本项目使用符合 **TC39 Stage 3 提案**的标准装饰器语法,提供现代化的路由定义方式。 + +## TC39 装饰器优势 + +- ✅ **标准化**:符合 ECMAScript 官方标准提案 +- ✅ **类型安全**:完整的 TypeScript 类型支持 +- ✅ **性能优化**:无需 reflect-metadata 依赖 +- ✅ **未来兼容**:随着标准发展自动获得浏览器支持 ## 快速开始 @@ -22,7 +29,7 @@ export class MyController { @Post('/users') async createUser(ctx: Context) { - const userData = ctx.request.body; + const userData = (ctx.request as any).body; // 业务逻辑处理... return { id: 1, ...userData }; } @@ -30,7 +37,7 @@ export class MyController { @Put('/users/:id') async updateUser(ctx: Context) { const { id } = ctx.params; - const userData = ctx.request.body; + const userData = (ctx.request as any).body; // 业务逻辑处理... return { id, ...userData }; } @@ -58,7 +65,7 @@ this.routeScanner.registerControllers([ ## 可用装饰器 -### HTTP方法装饰器 +### HTTP方法装饰器(TC39 标准) - `@Get(path)` - GET 请求 - `@Post(path)` - POST 请求 @@ -66,10 +73,64 @@ this.routeScanner.registerControllers([ - `@Delete(path)` - DELETE 请求 - `@Patch(path)` - PATCH 请求 -### 控制器装饰器 +### 控制器装饰器(TC39 标准) - `@Controller(prefix)` - 控制器路由前缀 +## TC39 装饰器特性 + +### 1. 标准语法 +```typescript +// TC39 标准装饰器使用 addInitializer +@Get('/users') +async getUsers(ctx: Context) { + return userData; +} +``` + +### 2. 类型安全 +```typescript +// 完整的 TypeScript 类型检查 +@Controller('/api') +export class ApiController { + @Get('/health') + healthCheck(): { status: string } { + return { status: 'ok' }; + } +} +``` + +### 3. 无外部依赖 +```typescript +// 不再需要 reflect-metadata +// 使用内置的 WeakMap 存储元数据 +``` + +## 配置要求 + +### TypeScript 配置 + +```json +{ + "compilerOptions": { + "experimentalDecorators": false, // 关闭实验性装饰器 + "emitDecoratorMetadata": false, // 关闭元数据发射 + "target": "ES2022", // 目标 ES2022+ + "useDefineForClassFields": false // 兼容装饰器行为 + } +} +``` + +### 依赖 + +```json +{ + "dependencies": { + // 无需 reflect-metadata + } +} +``` + ## 路径拼接规则 最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径 @@ -113,16 +174,6 @@ async getUser(ctx: Context) { } ``` -## 路由参数 - -支持标准的Koa路由参数: - -```typescript -@Get('/users/:id') // 路径参数 -@Get('/users/:id/posts/:pid') // 多个参数 -@Get('/search') // 查询参数通过 ctx.query 获取 -``` - ## 现有路由 项目中已注册的路由: @@ -139,9 +190,42 @@ async getUser(ctx: Context) { - `DELETE /api/user/:id` - 删除用户 - `GET /api/user/search` - 搜索用户 +## 与旧版本装饰器的区别 + +| 特性 | 实验性装饰器 | TC39 标准装饰器 | +|------|-------------|----------------| +| 标准化 | ❌ TypeScript 特有 | ✅ ECMAScript 标准 | +| 依赖 | ❌ 需要 reflect-metadata | ✅ 零依赖 | +| 性能 | ❌ 运行时反射 | ✅ 编译时优化 | +| 类型安全 | ⚠️ 部分支持 | ✅ 完整支持 | +| 未来兼容 | ❌ 可能被废弃 | ✅ 持续演进 | + +## 迁移指南 + +从实验性装饰器迁移到 TC39 标准装饰器: + +1. **更新 tsconfig.json** + ```json + { + "experimentalDecorators": false, + "emitDecoratorMetadata": false + } + ``` + +2. **移除依赖** + ```bash + pnpm remove reflect-metadata + ``` + +3. **代码无需修改** + - 装饰器语法保持不变 + - 控制器代码无需修改 + - 自动兼容新标准 + ## 注意事项 -1. 需要安装依赖:`pnpm add reflect-metadata` -2. TypeScript配置需要开启装饰器支持 +1. 需要 TypeScript 5.0+ 支持 +2. 需要 Node.js 16+ 运行环境 3. 控制器类需要导出并在路由中间件中注册 4. 控制器方法应该返回数据而不是直接操作 `ctx.body` +5. TC39 装饰器使用 `addInitializer` 进行初始化,性能更优 diff --git a/apps/server/app.ts b/apps/server/app.ts index 50bc0c8..80be139 100644 --- a/apps/server/app.ts +++ b/apps/server/app.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import Koa from 'koa'; import { initMiddlewares } from './middlewares/index.ts'; diff --git a/apps/server/decorators/route.ts b/apps/server/decorators/route.ts index 8c2b0ab..99330a5 100644 --- a/apps/server/decorators/route.ts +++ b/apps/server/decorators/route.ts @@ -1,5 +1,3 @@ -import 'reflect-metadata'; - /** * 路由元数据键 */ @@ -20,70 +18,107 @@ export interface RouteMetadata { } /** - * 创建HTTP方法装饰器的工厂函数 + * 元数据存储(降级方案) + */ +const metadataStore = new WeakMap>(); + +/** + * 设置元数据(降级方案) + */ +function setMetadata(key: string | symbol, value: T, target: any): void { + if (!metadataStore.has(target)) { + metadataStore.set(target, new Map()); + } + metadataStore.get(target)!.set(key, value); +} + +/** + * 获取元数据(降级方案) + */ +function getMetadata(key: string | symbol, target: any): T | undefined { + return metadataStore.get(target)?.get(key); +} + +/** + * 创建HTTP方法装饰器的工厂函数(TC39标准) */ function createMethodDecorator(method: HttpMethod) { return function (path: string = '') { - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - // 获取现有的路由元数据 - const existingRoutes: RouteMetadata[] = (Reflect as any).getMetadata(ROUTE_METADATA_KEY, target.constructor) || []; + return function ( + target: (this: This, ...args: Args) => Return, + context: ClassMethodDecoratorContext Return> + ) { + // 在类初始化时执行 + context.addInitializer(function () { + // 使用 this.constructor 时需要类型断言 + const constructor = (this as any).constructor; - // 添加新的路由元数据 - const newRoute: RouteMetadata = { - method, - path, - propertyKey - }; + // 获取现有的路由元数据 + const existingRoutes: RouteMetadata[] = getMetadata(ROUTE_METADATA_KEY, constructor) || []; - existingRoutes.push(newRoute); + // 添加新的路由元数据 + const newRoute: RouteMetadata = { + method, + path, + propertyKey: String(context.name) + }; - // 保存路由元数据到类的构造函数上 - (Reflect as any).defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target.constructor); + existingRoutes.push(newRoute); - return descriptor; + // 保存路由元数据到类的构造函数上 + setMetadata(ROUTE_METADATA_KEY, existingRoutes, constructor); + }); + + return target; }; }; } /** - * GET 请求装饰器 + * GET 请求装饰器(TC39标准) * @param path 路由路径 */ export const Get = createMethodDecorator('GET'); /** - * POST 请求装饰器 + * POST 请求装饰器(TC39标准) * @param path 路由路径 */ export const Post = createMethodDecorator('POST'); /** - * PUT 请求装饰器 + * PUT 请求装饰器(TC39标准) * @param path 路由路径 */ export const Put = createMethodDecorator('PUT'); /** - * DELETE 请求装饰器 + * DELETE 请求装饰器(TC39标准) * @param path 路由路径 */ export const Delete = createMethodDecorator('DELETE'); /** - * PATCH 请求装饰器 + * PATCH 请求装饰器(TC39标准) * @param path 路由路径 */ export const Patch = createMethodDecorator('PATCH'); /** - * 控制器装饰器 + * 控制器装饰器(TC39标准) * @param prefix 路由前缀 */ export function Controller(prefix: string = '') { - return function (constructor: T) { - // 保存控制器前缀到元数据 - (Reflect as any).defineMetadata('prefix', prefix, constructor); - return constructor; + return function any>( + target: T, + context: ClassDecoratorContext + ) { + // 在类初始化时保存控制器前缀 + context.addInitializer(function () { + setMetadata('prefix', prefix, this); + }); + + return target; }; } @@ -91,12 +126,12 @@ export function Controller(prefix: string = '') { * 获取控制器的路由元数据 */ export function getRouteMetadata(constructor: any): RouteMetadata[] { - return (Reflect as any).getMetadata(ROUTE_METADATA_KEY, constructor) || []; + return getMetadata(ROUTE_METADATA_KEY, constructor) || []; } /** * 获取控制器的路由前缀 */ export function getControllerPrefix(constructor: any): string { - return (Reflect as any).getMetadata('prefix', constructor) || ''; + return getMetadata('prefix', constructor) || ''; } diff --git a/apps/server/package.json b/apps/server/package.json index ff87b8e..3b93eaf 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -12,8 +12,7 @@ "dependencies": { "@koa/router": "^14.0.0", "@prisma/client": "^6.15.0", - "koa": "^3.0.1", - "reflect-metadata": "^0.1.13" + "koa": "^3.0.1" }, "devDependencies": { "@tsconfig/node-ts": "^23.6.1", diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 69ee9b9..1b18d80 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -4,8 +4,7 @@ "@tsconfig/node-ts/tsconfig.json" ], "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "types": ["reflect-metadata"] + "target": "ES2022", + "useDefineForClassFields": false } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6582f5..e6f944e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: koa: specifier: ^3.0.1 version: 3.0.1 - reflect-metadata: - specifier: ^0.1.13 - version: 0.1.14 devDependencies: '@tsconfig/node-ts': specifier: ^23.6.1 @@ -1463,9 +1460,6 @@ packages: reduce-configs@1.1.1: resolution: {integrity: sha512-EYtsVGAQarE8daT54cnaY1PIknF2VB78ug6Zre2rs36EsJfC40EG6hmTU2A2P1ZuXnKAt2KI0fzOGHcX7wzdPw==} - reflect-metadata@0.1.14: - resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -2936,8 +2930,6 @@ snapshots: reduce-configs@1.1.1: {} - reflect-metadata@0.1.14: {} - resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {}