UNPKG

imean-service-engine

Version:

microservice engine

881 lines (711 loc) 21.5 kB
# Microservice Framework 一个轻量级的 TypeScript 微服务框架提供了类型安全自动客户端生成请求重试等特性 ## 特性 - 📝 完全的 TypeScript 支持 - 🔄 自动生成类型安全的客户端代码 - 🛡️ 使用 Zod 进行运行时类型验证 - 🔁 内置智能重试机制 - 🎯 支持幂等操作 - 🌟 优雅的装饰器 API - 🚦 优雅停机支持 - 📡 生成基于 fetch 的客户端代码,可以在 Deno Node.jsBun 以及浏览器中使用 - 🌟 支持 Stream 流传输,客户端使用 AsyncIterator 迭代 - 🌟 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势: - 保持长连接,减少连接建立的开销 - 支持双向通信 - 使用 Brotli 压缩,减少数据传输量 - 自动重连和心跳检测 - 🌐 内置 PageRenderPlugin 支持服务端渲染页面,集成 HTMXHyperscript ## 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); ``` ## 高级特性 ### PageRenderPlugin - 服务端渲染页面 PageRenderPlugin 为微服务框架提供了服务端渲染页面的能力,集成了 HTMXHyperscript,让你可以轻松构建现代化的 Web 应用 #### 启用 PageRenderPlugin ```typescript import { Microservice, PageRenderPlugin } from "imean-service-engine"; const service = new Microservice({ modules: [UserService], plugins: [new PageRenderPlugin()], }); ``` #### 使用 @Page 装饰器 使用 `@Page` 装饰器可以将模块方法暴露为 Web 页面: ```typescript import { Page, HtmxLayout } from "imean-service-engine"; @Module("web") class WebService { @Page({ path: "/greeting", method: "get", description: "问候页面", }) greetingPage(ctx: Context) { return ( <HtmxLayout title="问候页面"> <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8"> <div class="max-w-4xl mx-auto"> <h1 class="text-4xl font-bold text-center text-gray-800 mb-8"> HTMX 交互示例 </h1> <div class="bg-white rounded-lg shadow-lg p-6"> <h2 class="text-2xl font-semibold mb-4 text-gray-700">问候语</h2> <div id="greeting" class="text-xl p-4 bg-blue-50 rounded-lg"> 欢迎使用微服务框架! </div> <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" hx-post="/api/greeting" hx-target="#greeting" hx-swap="innerHTML" > 更新问候语 </button> </div> </div> </div> </HtmxLayout> ); } @Page({ path: "/greeting", method: "post", description: "更新问候语", }) updateGreeting(ctx: Context) { return "你好,世界!当前时间:" + new Date().toLocaleString(); } } ``` #### JSX 配置 要使用 JSX 语法,需要在 `tsconfig.json` 中配置: ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } } ``` #### HtmxLayout 组件 `HtmxLayout` 提供了预配置的页面布局,包含: - HTMX 库(最新版本) - Hyperscript 库(最新版本) - Tailwind CSS(CDN 版本) - 响应式设计支持 - 默认图标 ```typescript import { HtmxLayout } from "imean-service-engine"; // 基本用法 const page = ( <HtmxLayout title="我的页面"> <div>页面内容</div> </HtmxLayout> ); // 自定义图标 const pageWithCustomIcon = ( <HtmxLayout title="我的页面" favicon={<link rel="icon" href="/custom-icon.ico" />}> <div>页面内容</div> </HtmxLayout> ); ``` #### BaseLayout 组件 如果你不想使用 HTMXHyperscript,而是想使用其他前端框架(如 ReactVue 等),可以使用 `BaseLayout` 组件: ```typescript import { BaseLayout } from "imean-service-engine"; // 使用 BaseLayout 自定义页面 const customPage = ( <BaseLayout title="自定义页面"> <div>页面内容</div> </BaseLayout> ); // 自定义头部内容 const pageWithCustomHead = ( <BaseLayout title="自定义页面" heads={ <> <link rel="stylesheet" href="/custom.css" /> <script src="https://unpkg.com/react@18/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> </> } > <div id="root">React 应用将在这里渲染</div> </BaseLayout> ); ``` `BaseLayout` 提供: - 基本的 HTML 结构 - 可自定义的 `<head>` 内容 - 可自定义的页面标题 - 可自定义的图标 #### HTMX 交互示例 结合 HTMX 可以实现丰富的交互效果: ```typescript @Page({ path: "/users", method: "get", description: "用户列表页面", }) usersPage(ctx: Context) { return ( <HtmxLayout title="用户管理"> <div class="container mx-auto p-8"> <h1 class="text-3xl font-bold mb-6">用户管理</h1> {/* 用户列表 */} <div id="user-list" hx-get="/api/users/list" hx-trigger="load" > 加载中... </div> {/* 添加用户表单 */} <div class="mt-8 bg-white rounded-lg shadow p-6"> <h2 class="text-xl font-semibold mb-4">添加新用户</h2> <form hx-post="/api/users/add" hx-target="#user-list" hx-swap="outerHTML" > <div class="grid grid-cols-2 gap-4"> <input type="text" name="name" placeholder="姓名" class="px-3 py-2 border rounded-md" required /> <input type="number" name="age" placeholder="年龄" class="px-3 py-2 border rounded-md" required /> </div> <button type="submit" class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" > 添加用户 </button> </form> </div> </div> </HtmxLayout> ); } ``` #### Hyperscript 增强交互 使用 Hyperscript 可以实现更复杂的客户端逻辑: ```typescript // 带加载状态的按钮 <button hx-post="/api/users/refresh" hx-target="#user-list" hx-swap="innerHTML" _="on htmx:beforeRequest hide #button-text then show #loading-spinner end on htmx:afterRequest hide #loading-spinner then show #button-text end" > <span id="loading-spinner" class="htmx-indicator"> <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> 加载中... </span> <span id="button-text">刷新用户列表</span> </button> ``` #### 服务状态页面 PageRenderPlugin 自动在服务根路径(`/api`)提供服务的状态页面,显示: - 服务基本信息(名称版本环境) - 模块列表和 API 端点 - 服务健康状态 访问 `http://localhost:3000/api` 即可查看服务状态页面。 #### 最佳实践 1. **页面组织**:将页面逻辑与 API 逻辑分离 2. **组件复用**:使用 HtmxLayout 确保一致的页面结构 3. **渐进增强**:优先使用 HTMX 实现交互,必要时使用 Hyperscript 4. **响应式设计**:利用 Tailwind CSS 构建响应式界面 5. **布局选择**- 使用 `HtmxLayout` 进行快速原型开发和简单交互 - 使用 `BaseLayout` 集成复杂的前端框架(ReactVue 等) - 根据项目需求选择合适的布局组件 ```typescript // 推荐的目录结构 src/ ├── pages/ # 页面组件 ├── users.tsx └── dashboard.tsx ├── services/ # 服务模块 ├── user.ts └── web.ts └── layouts/ # 自定义布局 └── admin.tsx ``` ### 幂等性和重试机制 框架提供了智能的重试机制,但仅对标记为幂等的操作生效: 重试策略: - 仅对标记为 `idempotence: true` 的方法进行重试 - 重试间隔:500ms1000ms3000ms5000ms - 最多重试 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; // 缓存过期时间(秒) } ``` #### @Page(options: PageOptions) 定义一个页面路由(需要启用 PageRenderPlugin) ```typescript interface PageOptions { method: "get" | "post" | "put" | "delete" | "patch" | "options"; path: string; description?: string; } ``` 示例: ```typescript @Page({ path: "/dashboard", method: "get", description: "仪表板页面", }) dashboardPage(ctx: Context) { return ( <HtmxLayout title="仪表板"> <div>仪表板内容</div> </HtmxLayout> ); } ``` ### Microservice #### constructor(options: MicroserviceOptions) 创建微服务实例 ```typescript interface MicroserviceOptions { modules: (new () => any)[]; // 模块类数组 prefix?: string; // API 前缀,默认为 "/api" plugins?: Plugin[]; // 插件数组,如 PageRenderPlugin } ``` #### 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` 包