Plane @plane/decorators:基于 TypeScript 装饰器的 Express 声明式控制器库实现解析
Plane plane/decorators基于 TypeScript 装饰器的 Express 声明式控制器库实现解析【免费下载链接】plane Open-source Jira, Linear, Monday, and ClickUp alternative. Plane is a modern project management platform to manage tasks, sprints, docs, and triage.项目地址: https://gitcode.com/GitHub_Trending/pl/planePlane 的实时协作服务apps/live依赖一个内置的轻量 TypeScript 装饰器库plane/decorators用声明式语法定义 Express REST 控制器与 WebSocket 处理器。本文以该包的官方 README 为骨架逐条解析其装饰器 API、元数据reflect-metadata存储机制与路由注册流程并结合apps/live中真实控制器的接入方式说明如何在自己的 Express express-ws 服务中复用这套模式。1. 包定位与核心特性packages/decorators/README.md将其定位为A lightweight TypeScript decorator library for building Express.js controllers with a clean, declarative syntax. 文档列出的特性包括TypeScript-first 设计HTTP 方法装饰器GET、POST、PUT、PATCH、DELETEWebSocket 支持中间件支持无需构建步骤——可直接作用于 TypeScript 文件No build step required - works directly with TypeScript files。从源码结构看整个库由 4 个源文件组成职责划分非常清晰src/index.ts汇总导出controller、rest、websocket三个模块src/rest.tsController、Get/Post/Put/Patch/Delete、Middlewaresrc/websocket.tsWebSocketsrc/controller.tsregisterController注册入口负责把元数据翻译回 Express 路由。依赖方面各源文件首行均import reflect-metadata说明整个库的装饰器语义建立在 reflect-metadata 的Reflect.defineMetadata/Reflect.getMetadata之上而非 Class Transform 等运行时重写方案。2. 安装方式README 说明该包是 Plane pnpm workspace 的一员在 workspace 内的其他应用中这样引用{ dependencies: { plane/decorators: workspace:* } }仓库中 apps/live/package.json 正是当前唯一的实际消费方。3. 基本用法REST 控制器README 给出的标准示例如下原文继承import { Controller, Get, Post, BaseController } from plane/decorators; import { Router, Request, Response } from express; Controller(/api/users) class UserController extends BaseController { Get(/) async getUsers(req: Request, res: Response) { return res.json({ users: [] }); } Post(/) async createUser(req: Request, res: Response) { return res.json({ success: true }); } } // Register routes const router Router(); const userController new UserController(); userController.registerRoutes(router);要点说明Controller(/api/users)是类装饰器接收一个baseRoute作为该控制器下所有方法路由的前缀Get(/)、Post(/)是方法装饰器接收相对路由最终注册的完整路径为baseRoute route方法体就是普通的 Express handler签名(req: Request, res: Response)。3.1Controller的源码实现class 装饰器只做一件事——把baseRoute写入类的元数据export function Controller(baseRoute: string ): ClassDecorator { return function (target: Function) { Reflect.defineMetadata(baseRoute, baseRoute, target); }; }注意baseRoute有默认值即允许控制器不带前缀。3.2 HTTP 方法装饰器由工厂函数批量生成Get/Post/Put/Patch/Delete并非各自独立实现而是由 createHttpMethodDecorator 统一生成每个方法装饰器把两个键写入元数据function createHttpMethodDecorator(method: RestMethod): (route: string) MethodDecorator { return function (route: string): MethodDecorator { return function (target: object, propertyKey: string | symbol) { Reflect.defineMetadata(method, method, target, propertyKey); Reflect.defineMetadata(route, route, target, propertyKey); }; }; } export const Get createHttpMethodDecorator(get); export const Post createHttpMethodDecorator(post); export const Put createHttpMethodDecorator(put); export const Patch createHttpMethodDecorator(patch); export const Delete createHttpMethodDecorator(delete);这里的method值就是 Express Router 上对应的方法名router.get、router.post等这一点在注册阶段会用到。同时 controller.ts 中定义的HttpMethod类型比rest.ts的RestMethod更宽额外包含options | head | ws为 WebSocket 注册留出统一的方法标识空间。3.3Middleware支持叠加的多中间件Middleware 装饰器的实现细节值得注意——它不是覆盖而是追加export function Middleware(middleware: RequestHandler): MethodDecorator { return function (target: object, propertyKey: string | symbol) { const middlewares Reflect.getMetadata(middlewares, target, propertyKey) || []; middlewares.push(middleware); Reflect.defineMetadata(middlewares, middlewares, target, propertyKey); }; }因此一个方法上可以叠加多个Middleware(...)按声明顺序存入数组注册时再整体展开到 handler 之前见第 5 节。4. WebSocket 控制器README 的 WebSocket 示例原文继承import { Controller, WebSocket, BaseWebSocketController } from plane/decorators; import { Request } from express; import { WebSocket as WS } from ws; Controller(/ws/chat) class ChatController extends BaseWebSocketController { WebSocket(/) handleConnection(ws: WS, req: Request) { ws.on(message, (message) { ws.send(Received: ${message}); }); } } // Register WebSocket routes const router require(express-ws)(app).router; chatController.registerWebSocketRoutes(router);对照 websocket.ts 的实现WebSocket(route)与 HTTP 方法装饰器共用同一套元数据键区别仅在于method值固定为wsexport function WebSocket(route: string): MethodDecorator { return function (target: object, propertyKey: string | symbol) { Reflect.defineMetadata(method, ws, target, propertyKey); Reflect.defineMetadata(route, route, target, propertyKey); }; }需要注意示例中的依赖前提router.ws(...)不是原生 Express API而是由express-ws扩展挂载的。这正是 README 示例最后一行require(express-ws)(app)存在的原因Plane 的 live 服务同样在启动时执行expressWs(this.app)见 apps/live/src/server.ts。5. 注册流程剖析registerController 的三条链路README 的 API Reference 列出的公开符号为装饰器Controller(baseRoute)、Get/Post/Put/Patch/Delete(route)、WebSocket(route)、Middleware(middleware)类BaseControllerREST 控制器基类、BaseWebSocketControllerWebSocket 控制器基类。而从当前源码的导出面看controller.ts 额外暴露了统一注册入口registerController(router, Controller, dependencies)它是把装饰器写入的元数据转化为真实路由的核心。其执行分三步第一步带依赖地实例化控制器。export function registerController( router: Router, Controller: ControllerConstructor, dependencies: unknown[] [] ): void { // Create the controller instance with dependencies const instance new Controller(...dependencies);dependencies数组被展开为构造函数参数——这是该库内置的轻量依赖注入apps/live正是借此把 Hocuspocus 服务端实例注入到协作控制器见第 6 节。第二步自动判别控制器类型。通过遍历原型方法检查是否存在method ws的元数据从而把控制器分流到 REST 或 WebSocket 注册路径controller.ts。这意味着调用方无需区分两种控制器一个入口通吃。第三步REST 分支展开元数据并绑定路由。registerRestController 对每个方法读取三个元数据键const method Reflect.getMetadata(method, instance, methodName) as HttpMethod; const route Reflect.getMetadata(route, instance, methodName) as string; const middlewares (Reflect.getMetadata(middlewares, instance, methodName) as RequestHandler[]) || []; if (method route) { const handler instance[methodName] as unknown; if (typeof handler function method ! ws) { (router[method])(${baseRoute}${route}, ...middlewares, handler.bind(instance)); } }这里有三个工程细节路由拼接${baseRoute}${route}即第 3 节示例中/api/users/中间件展开...middlewares位于 handler 之前保证Middleware声明的顺序即执行顺序handler.bind(instance)把方法绑定到控制器实例因此方法体内可直接访问this上的实例属性——这是构造函数注入能够生效的前提。第三步WebSocket 分支带错误兜底的 ws 注册。registerWebSocketController 只处理method ws的方法且注册前会防御性地检查ws in router typeof router.ws function——若 router 未经 express-ws 增强则静默跳过避免运行时报错。handler 被包在 try/catch 中异常时记录WebSocket error in ${Controller.name}.${methodName}日志并以ws.close(1011, ...)关闭连接1011 为内部错误状态码与第 6 节协作控制器自己的错误处理形成双层兜底。关于 README 中BaseController/BaseWebSocketController两个基类与registerRoutes/registerWebSocketRoutes的说明在当前仓库源码中packages/decorators/src并未导出这两个类实际注册全部经由registerController完成可以推断 README 的继承式写法是该库早期形态或简化示意registerController才是当前仓库内的真实注册入口。6. 仓库实战apps/live 服务如何接入apps/live是 Plane 的实时协作文档服务Hocuspocus 编辑器同步 文档格式转换 PDF 导出它是装饰器库的完整落地样本。6.1 服务装配apps/live/src/server.ts 的构造函数中完成了 express-ws 增强与路由挂载this.app express(); expressWs(this.app); // 为 app 挂载 router.ws 能力 this.setupMiddleware(); // helmet / compression / cors / body parsing this.router express.Router(); this.app.use(env.LIVE_BASE_PATH, this.router);随后在setupRoutes中一次性注册所有控制器并通过第三参注入 Hocuspocus 实例server.tsprivate setupRoutes(hocuspocusServer: Hocuspocus) { CONTROLLERS.forEach((controller) registerController(this.router, controller, [hocuspocusServer])); }对照第 5 节的new Controller(...dependencies)这行代码同时演示了控制器数组统一注册 构造器依赖注入两条能力。6.2 最小 REST 控制器HealthControllerapps/live/src/controllers/health.controller.ts 是装饰器 → 路由最短路径的示范Controller(/health) export class HealthController { Get(/) async healthCheck(_req: Request, res: Response) { res.status(200).json({ status: OK, timestamp: new Date().toISOString(), version: env.APP_VERSION, }); } }它不继承任何基类、无构造依赖注册后得到GET {LIVE_BASE_PATH}/health/路由。6.3 构造器注入 WebSocketCollaborationControllerapps/live/src/controllers/collaboration.controller.ts 展示了第 6.1 节注入链的落地效果Controller(/collaboration) export class CollaborationController { private readonly hocusPocusServer: Hocuspocus; constructor(hocusPocusServer: Hocuspocus) { this.hocusPocusServer hocusPocusServer; // 由 registerController 注入 } WSDecorator(/) handleConnection(ws: WebSocket, req: Request) { this.hocusPocusServer.handleConnection(ws, req); ws.on(error, (error: Error) { logger.error(COLLABORATION_CONTROLLER: WebSocket connection error:, error); ws.close(1011, Internal server error); }); } }注意源码中把装饰器WebSocket别名为WSDecorator以避免与ws库的WebSocket类型冲突——这是同时使用两者时的推荐写法。由于该方法带method ws元数据registerController会自动把它分流到 WebSocket 注册分支第 5 节第二步最终落到router.ws({LIVE_BASE_PATH}/collaboration/, handler)。6.4 REST 请求校验DocumentControllerapps/live/src/controllers/document.controller.ts 展示了Post方法与 zod 校验的组合模式Controller(/convert-document) export class DocumentController { Post(/) async convertDocument(req: Request, res: Response) { const validatedData convertDocumentSchema.parse(req.body as TConvertRequestBody); // ... 调用 plane/editor 的 convertHTMLDocumentToAllFormats } }6.5Middleware的配套中间件apps/live/src/lib/auth-middleware.ts 提供了配合Middleware使用的密钥校验中间件requireSecretKey它读取请求头live-server-secret-key文档注释中还提及x-admin-secret-key作为管理端点的偏好头与env.LIVE_SERVER_SECRET_KEY比对不匹配时记录告警并返回 401。该文件的 JSDoc 示例第 24-31 行给出了标准用法import { Middleware } from plane/decorators; import { requireSecretKey } from /lib/auth-middleware; Get(/protected) Middleware(requireSecretKey) async protectedEndpoint(req: Request, res: Response) { // 只有 secret key 有效时才会执行 }这也解释了第 5 节中middlewares数组为什么必须排在 handler 之前展开。7. API 参考速查与使用前提综合 README 与源码完整 API 面如下符号类型作用源码位置Controller(baseRoute)类装饰器定义控制器基础路由前缀默认rest.ts#L18Get/Post/Put/Patch/Delete(route)方法装饰器声明 REST 端点rest.ts#L40-L44WebSocket(route)方法装饰器声明 WebSocket 端点method 记为wswebsocket.ts#L14Middleware(handler)方法装饰器为单个方法追加中间件可叠加rest.ts#L51registerController(router, Ctor, deps)函数统一注册入口实例化 类型判别 路由挂载controller.ts#L23使用前提与限制以当前仓库为准需要express-ws提供router.ws库会自行检测未增强时 WebSocket 路由静默不注册元数据依赖reflect-metadata各源文件已在入口处导入消费方无需额外配置但需要 TypeScript 以传统/标准装饰器语义编译一个控制器内若混有WebSocket方法整个控制器按 WebSocket 分支注册REST 方法会被跳过method ! ws的过滤在 REST 分支method ws的判断在 WS 分支——从源码结构看一个控制器建议只承载一种协议该包以workspace:*方式供 monorepo 内引用适用于 Plane 当前 pnpm workspace 环境。8. 小结plane/decorators用不到 200 行源码实现了一条装饰器声明 → reflect-metadata 存储 →registerController统一装配的闭环类级baseRoute、方法级method/route/middlewares三组元数据加上构造器依赖注入与 WebSocket 异常兜底覆盖了 Plane live 服务中健康检查、文档转换、Hocuspocus 协作等全部端点的声明需求。若在自有项目中复刻该模式最小路径是引入 express-ws 增强 router用Controller HTTP 方法装饰器声明端点再以registerController(router, YourController, [deps])一次性挂载。【免费下载链接】plane Open-source Jira, Linear, Monday, and ClickUp alternative. Plane is a modern project management platform to manage tasks, sprints, docs, and triage.项目地址: https://gitcode.com/GitHub_Trending/pl/plane创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →