UNPKG

imean-service-engine

Version:

microservice engine

583 lines (458 loc) 14.1 kB
# Microservice Framework 一个轻量级的 TypeScript 微服务框架。提供了类型安全、自动客户端生成、请求重试等特性。 ## 特性 - 📝 完全的 TypeScript 支持 - 🔄 自动生成类型安全的客户端代码 - 🛡️ 使用 Zod 进行运行时类型验证 - 🔁 内置智能重试机制 - 🎯 支持幂等操作 - 🌟 优雅的装饰器 API - 🚦 优雅停机支持 - 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用 - 🌟 支持 Stream 流传输,客户端使用 AsyncIterator 迭代 - 🌟 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势: - 保持长连接,减少连接建立的开销 - 支持双向通信 - 使用 Brotli 压缩,减少数据传输量 - 自动重连和心跳检测 ## TODOs - [ ] 示例项目 - [ ] 微服务高级功能,熔断器、负载均衡等 ## 安装 ```typescript import { Action, Microservice, Module } from "imean-service-engine"; ``` ## 快速开始 ### 1. 定义数据模型 使用 Zod 定义你的数据模型: ```typescript import { z } from "zod"; const UserSchema = z.object({ id: z.string(), name: z.string(), age: z.number().min(0).max(150), }); type User = z.infer<typeof UserSchema>; ``` ### 2. 创建服务模块 使用装饰器定义你的服务模块和方法: ```typescript @Module("users", { description: "用户服务模块", version: "1.0.0", }) class UserService { private users = new Map<string, User>(); @Action({ description: "获取用户信息", params: [z.string()], returns: UserSchema, }) async getUser(id: string): Promise<User> { const user = this.users.get(id); if (!user) { throw new Error("用户不存在"); } return user; } @Action({ description: "创建新用户", params: [z.string(), z.number()], returns: UserSchema, }) async createUser(name: string, age: number): Promise<User> { const id = crypto.randomUUID(); const user = { id, name, age }; this.users.set(id, user); return user; } @Action({ description: "更新用户信息", params: [z.string(), z.string(), z.number()], returns: UserSchema, // 标记为幂等操作,支持自动重试 idempotence: true, }) async updateUser(id: string, name: string, age: number): Promise<User> { const user = this.users.get(id); if (!user) { throw new Error("用户不存在"); } const updatedUser = { ...user, name, age }; this.users.set(id, updatedUser); return updatedUser; } } ``` ### 3. 启动服务 ```typescript const service = new Microservice({ modules: [UserService], prefix: "/api", }); await service.init(); // 启动在 3000 端口 service.start(3000); ``` ### 4. 使用生成的客户端 访问服务根路径(如 `http://localhost:3000/client.ts`)会自动下载生成的 TypeScript 客户端代码。 使用生成的客户端: ```typescript const client = new MicroserviceClient({ baseUrl: "http://localhost:3000", }); // 创建用户 const user = await client.users.createUser("张三", 25); // 更新用户(支持自动重试) const updated = await client.users.updateUser(user.id, "张三丰", 30); // 获取用户 const found = await client.users.getUser(user.id); ``` ## 高级特性 ### 幂等性和重试机制 框架提供了智能的重试机制,但仅对标记为幂等的操作生效: 重试策略: - 仅对标记为 `idempotence: true` 的方法进行重试 - 重试间隔:500ms、1000ms、3000ms、5000ms - 最多重试 4### 优雅停机 在需要停止服务时,可以等待所有重试请求完成: ## API 参考 ### 装饰器 #### @Module(name: string, options: ModuleOptions) 定义一个服务模块。 ```typescript interface ModuleOptions { description?: string; version?: string; } ``` #### @Action(options: ActionOptions) 定义一个模块方法。 ```typescript interface ActionOptions { description?: string; params: z.ZodType<any>[]; // 参数类型定义 returns: z.ZodType<any>; // 返回值类型定义 idempotence?: boolean; // 是否是幂等操作 stream?: boolean; // 是否是流式操作 cache?: boolean; // 是否开启缓存 cacheTTL?: number; // 缓存过期时间(秒) } ``` ### Microservice #### constructor(options: MicroserviceOptions) 创建微服务实例。 ```typescript interface MicroserviceOptions { modules: (new () => any)[]; // 模块类数组 prefix?: string; // API 前缀,默认为 "/api" } ``` #### start(port?: number): void 启动服务器,默认端口为 3000### MicroserviceClient #### constructor(options: ClientOptions) 创建客户端实例。 ```typescript interface ClientOptions { baseUrl: string; // 服务器地址 prefix?: string; // API 前缀,默认为 "/api" headers?: Record<string, string>; // 自定义请求头 } ``` ## 类型安全 框架使用 Zod 进行运行时类型验证,确保: - 请求参数类型正确 - 返回值类型符合预期 - 自动生成的客户端代码类型完整 ## 最佳实践 ### 服务启动前检查 框架提供了 `startCheck` 方法用于在服务正式启动前进行必要的检查和初始化。这对于确保依赖服务(如数据库)可用非常有用。 ```typescript // main.ts import { startCheck } from "imean-service-engine"; // 数据库连接检查 async function checkDatabase() { try { const db = await connectDB({ host: "localhost", port: 5432, // ...其他配置 }); await db.ping(); console.log("✅ 数据库连接成功"); } catch (error) { throw new Error(`数据库连接失败: ${error.message}`); } } // Redis 连接检查 async function checkRedis() { try { const redis = await connectRedis(); await redis.ping(); console.log("✅ Redis 连接成功"); } catch (error) { throw new Error(`Redis 连接失败: ${error.message}`); } } // 启动检查 startCheck( // 前置检查项 [checkDatabase, checkRedis], // 服务启动回调 async () => { // 使用动态导入载入服务模块 const { UserService } = await import("./services/user.ts"); const { OrderService } = await import("./services/order.ts"); const service = new Microservice({ modules: [UserService, OrderService], prefix: "/api", }); service.start(3000); } ); ``` 这种方式的优点: 1. **依赖检查** - 确保所有必要的外部服务都可用 - 避免服务启动后才发现依赖问题 - 提供清晰的错误信息 2. **按需加载** - 使用动态导入延迟加载服务模块 - 避免在检查失败时不必要的资源初始化 - 提高启动性能 3. **优雅失败** - 如果检查失败,服务不会启动 - 适合在容器环境中使用 - 便于问题诊断 ### 目录结构建议 ``` your-service/ ├── main.ts # 入口文件,包含启动检查 ├── config/ │ └── index.ts # 配置文件 ├── services/ │ ├── user.ts # 用户服务模块 │ └── order.ts # 订单服务模块 ├── models/ │ ├── user.ts # 用户数据模型 │ └── order.ts # 订单数据模型 ├── utils/ │ └── db.ts # 数据库连接工具 └── tests/ └── services/ ├── user.test.ts └── order.test.ts ``` ### 配置管理 建议将配置和服务逻辑分离: ```typescript // config/index.ts export const config = { database: { host: process.env.DB_HOST || "localhost", port: parseInt(process.env.DB_PORT || "5432"), // ... }, redis: { url: process.env.REDIS_URL || "redis://localhost:6379", // ... }, service: { port: parseInt(process.env.PORT || "3000"), prefix: process.env.API_PREFIX || "/api", }, }; // main.ts import { config } from "./config/index.ts"; startCheck( [ /* ... */ ], async () => { const service = new Microservice({ modules: [ /* ... */ ], prefix: config.service.prefix, }); service.start(config.service.port); } ); ``` ### 文件上传/二进制数据 框架传输采用 ejson 进行序列化,支持二进制数据传输。只需要在模型中接受 `Uint8Array` 类型即可,并且 Zod 类型需要设置为 `z.instanceof(Uint8Array)`。 ```typescript import * as z from "zod"; @Module("files") export class FileService { @Action({ params: [z.instanceof(Uint8Array)], returns: z.instanceof(Uint8Array), }) reverseBinary(data: Uint8Array): Uint8Array { return data.reverse(); } } ``` ### 定时任务 框架提供了 `@Schedule` 装饰器用于定义定时任务。在分布式环境中,同一个定时任务只会在一个服务实例上执行。 #### 基本用法 ```typescript @Module("tasks") class TaskService { @Schedule({ interval: 5000, // 执行间隔(毫秒) mode: ScheduleMode.FIXED_RATE, // 执行模式 }) async cleanupTask() { // 定时执行的任务代码 } } ``` #### 执行模式 框架支持两种执行模式: - `FIXED_RATE`: 固定频率执行,不考虑任务执行时间 ```typescript @Schedule({ interval: 5000, mode: ScheduleMode.FIXED_RATE, }) async quickTask() { // 每 5 秒执行一次 } ``` - `FIXED_DELAY`: 固定延迟执行,等待任务完成后再计时 ```typescript @Schedule({ interval: 5000, mode: ScheduleMode.FIXED_DELAY, }) async longRunningTask() { // 任务完成后等待 5 秒再执行下一次 } ``` #### 分布式调度 定时任务基于 etcd 实现分布式调度: 1. 自动选主:多个服务实例中只有一个会执行定时任务 2. 故障转移:当执行任务的实例故障时,其他实例会自动接管 3. 服务发现:新加入的实例会自动参与选主 ```typescript const service = new Microservice({ name: "user-service", // 服务名称 modules: [TaskService], etcd: { hosts: ["localhost:2379"], // etcd 服务地址 auth: { // 可选的认证信息 username: "root", password: "password", }, ttl: 10, // 租约 TTL(秒) namespace: "services", // 可选的命名空间 }, }); ``` #### 选举 Key (内部工作机制) 每个定时任务都有唯一的选举 key,格式为: ``` {service-name}/{module-name}/schedules/{method-name} ``` #### 优雅停机 服务停止时会自动清理定时任务和选举信息: ```typescript // 在 k8s 停机信号处理中 await service.stop(); ``` #### 注意事项 1. 使用定时任务需要配置 etcd 2. 建议使用 `FIXED_DELAY` 模式执行耗时任务 3. 任务执行时间不应超过执行间隔 #### Stream 流 服务引擎支持 Stream 流传输,可以在服务端返回 Stream 流,客户端使用 `await iter.next()` 逐个获取数据。或者使用 `for await (const item of iter)` 迭代。 > 注意:服务端返回的流需要使用 `AsyncIterableIterator` 类型,客户端使用 `AsyncIterator` 迭代。 > HTTP 请求方式也支持流式传输,服务端是通过 SSE 实现。 服务端: ```typescript @Module("stream") class StreamService { @Action({ params: [z.number()], returns: z.number, stream: true, }) async *stream(count: number): AsyncIterableIterator<number> { for (let i = 0; i < count; i++) { yield i; await new Promise((resolve) => setTimeout(resolve, 100)); } } } ``` 客户端: ```typescript const client = new MicroserviceClient({ baseUrl: "http://localhost:3000", prefix: "/api", }); const iter = await client.stream.streamNumbers(10); for await (const item of iter) { console.log(item); } ``` ## WebSocket 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势: 1. 保持长连接,减少连接建立的开销 2. 支持双向通信 3. 使用 Brotli 压缩,减少数据传输量 4. 自动重连和心跳检测 服务端配置: ```typescript const service = new Microservice({ modules: [UserService], prefix: "/api", websocket: { pingInterval: 5000, }, }); ``` 客户端配置: ```typescript const client = new MicroserviceClient({ baseUrl: "ws://localhost:3000", prefix: "/api", websocket: { pingInterval: 5000, }, }); ``` 注意:客户端使用 websocket 时,需要安装 brotli-wasm 库。因为服务端使用 brotli 压缩,客户端需要解压。 ### Node.js 环境使用 WebSocket 最新Node.js已经提供了 WebSocket 实现,可以直接使用。如果在较低 Node.js 环境下,可以使用 `isomorphic-ws` 包来提供 WebSocket 实现: ```typescript import WebSocket from "isomorphic-ws"; const client = new MicroserviceClient({ baseUrl: "http://localhost:3000", websocket: { WebSocket, // 传入 WebSocket 实现 timeout: 10000, retryInterval: 3000, maxRetries: 5, pingInterval: 30000, }, }); // 使用方法和浏览器环境完全一样 const result = await client.users.getUser("1"); ``` 安装依赖: ```bash npm install isomorphic-ws brotli-wasm ``` ### 注意事项 1. WebSocket 连接会自动重连,无需手动处理 2. 所有消息都使用 Brotli 压缩,需要安装 brotli-wasm 库 3. 客户端会定期发送心跳消息以保持连接 4. 在不再使用时应调用 `close()` 方法关闭连接 5. Node.js 环境需要安装 `isomorphic-ws` 包