imean-service-engine
v1.9.0
Published
microservice engine
Downloads
441
Maintainers
Readme
Microservice Framework
一个轻量级的 TypeScript 微服务框架。提供了类型安全、自动客户端生成、请求重试等特性。
特性
- 📝 完全的 TypeScript 支持
- 🔄 自动生成类型安全的客户端代码
- 🛡️ 使用 Zod 进行运行时类型验证
- 🔁 内置智能重试机制
- 🎯 支持幂等操作
- 🌟 优雅的装饰器 API
- 🚦 优雅停机支持
- 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用
- 🌟 支持 Stream 流传输,客户端使用 AsyncIterator 迭代
- 🌟 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势:
- 保持长连接,减少连接建立的开销
- 支持双向通信
- 使用 Brotli 压缩,减少数据传输量
- 自动重连和心跳检测
- 🌐 内置 PageRenderPlugin 支持服务端渲染页面,集成 HTMX 和 Hyperscript
TODOs
- [ ] 示例项目
- [ ] 微服务高级功能,熔断器、负载均衡等
安装
import { Action, Microservice, Module } from "imean-service-engine";快速开始
1. 定义数据模型
使用 Zod 定义你的数据模型:
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. 创建服务模块
使用装饰器定义你的服务模块和方法:
@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. 启动服务
const service = new Microservice({
modules: [UserService],
prefix: "/api",
});
await service.init();
// 启动在 3000 端口
service.start(3000);4. 使用生成的客户端
访问服务根路径(如 http://localhost:3000/client.ts)会自动下载生成的
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 为微服务框架提供了服务端渲染页面的能力,集成了 HTMX 和 Hyperscript,让你可以轻松构建现代化的 Web 应用。
启用 PageRenderPlugin
import { Microservice, PageRenderPlugin } from "imean-service-engine";
const service = new Microservice({
modules: [UserService],
plugins: [new PageRenderPlugin()],
});使用 @Page 装饰器
使用 @Page 装饰器可以将模块方法暴露为 Web 页面:
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 中配置:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}HtmxLayout 组件
HtmxLayout 提供了预配置的页面布局,包含:
- HTMX 库(最新版本)
- Hyperscript 库(最新版本)
- Tailwind CSS(CDN 版本)
- 响应式设计支持
- 默认图标
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 组件
如果你不想使用 HTMX 和 Hyperscript,而是想使用其他前端框架(如 React、Vue 等),可以使用 BaseLayout 组件:
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 可以实现丰富的交互效果:
@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 可以实现更复杂的客户端逻辑:
// 带加载状态的按钮
<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 即可查看服务状态页面。
最佳实践
- 页面组织:将页面逻辑与 API 逻辑分离
- 组件复用:使用 HtmxLayout 确保一致的页面结构
- 渐进增强:优先使用 HTMX 实现交互,必要时使用 Hyperscript
- 响应式设计:利用 Tailwind CSS 构建响应式界面
- 布局选择:
- 使用
HtmxLayout进行快速原型开发和简单交互 - 使用
BaseLayout集成复杂的前端框架(React、Vue 等) - 根据项目需求选择合适的布局组件
- 使用
// 推荐的目录结构
src/
├── pages/ # 页面组件
│ ├── users.tsx
│ └── dashboard.tsx
├── services/ # 服务模块
│ ├── user.ts
│ └── web.ts
└── layouts/ # 自定义布局
└── admin.tsx幂等性和重试机制
框架提供了智能的重试机制,但仅对标记为幂等的操作生效:
重试策略:
- 仅对标记为
idempotence: true的方法进行重试 - 重试间隔:500ms、1000ms、3000ms、5000ms
- 最多重试 4 次
优雅停机
在需要停止服务时,可以等待所有重试请求完成:
API 参考
装饰器
@Module(name: string, options: ModuleOptions)
定义一个服务模块。
interface ModuleOptions {
description?: string;
version?: string;
}@Action(options: ActionOptions)
定义一个模块方法。
interface ActionOptions {
description?: string;
params: z.ZodType<any>[]; // 参数类型定义
returns: z.ZodType<any>; // 返回值类型定义
idempotence?: boolean; // 是否是幂等操作
stream?: boolean; // 是否是流式操作
cache?: boolean; // 是否开启缓存
cacheTTL?: number; // 缓存过期时间(秒)
}@Page(options: PageOptions)
定义一个页面路由(需要启用 PageRenderPlugin)。
interface PageOptions {
method: "get" | "post" | "put" | "delete" | "patch" | "options";
path: string;
description?: string;
}示例:
@Page({
path: "/dashboard",
method: "get",
description: "仪表板页面",
})
dashboardPage(ctx: Context) {
return (
<HtmxLayout title="仪表板">
<div>仪表板内容</div>
</HtmxLayout>
);
}Microservice
constructor(options: MicroserviceOptions)
创建微服务实例。
interface MicroserviceOptions {
modules: (new () => any)[]; // 模块类数组
prefix?: string; // API 前缀,默认为 "/api"
plugins?: Plugin[]; // 插件数组,如 PageRenderPlugin
}start(port?: number): void
启动服务器,默认端口为 3000。
MicroserviceClient
constructor(options: ClientOptions)
创建客户端实例。
interface ClientOptions {
baseUrl: string; // 服务器地址
prefix?: string; // API 前缀,默认为 "/api"
headers?: Record<string, string>; // 自定义请求头
}类型安全
框架使用 Zod 进行运行时类型验证,确保:
- 请求参数类型正确
- 返回值类型符合预期
- 自动生成的客户端代码类型完整
最佳实践
服务启动前检查
框架提供了 startCheck 方法用于在服务正式启动前进行必要的检查和初始化。这对于确保依赖服务(如数据库)可用非常有用。
// 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);
}
);这种方式的优点:
依赖检查
- 确保所有必要的外部服务都可用
- 避免服务启动后才发现依赖问题
- 提供清晰的错误信息
按需加载
- 使用动态导入延迟加载服务模块
- 避免在检查失败时不必要的资源初始化
- 提高启动性能
优雅失败
- 如果检查失败,服务不会启动
- 适合在容器环境中使用
- 便于问题诊断
目录结构建议
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配置管理
建议将配置和服务逻辑分离:
// 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)。
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 装饰器用于定义定时任务。在分布式环境中,同一个定时任务只会在一个服务实例上执行。
基本用法
@Module("tasks")
class TaskService {
@Schedule({
interval: 5000, // 执行间隔(毫秒)
mode: ScheduleMode.FIXED_RATE, // 执行模式
})
async cleanupTask() {
// 定时执行的任务代码
}
}执行模式
框架支持两种执行模式:
FIXED_RATE: 固定频率执行,不考虑任务执行时间@Schedule({ interval: 5000, mode: ScheduleMode.FIXED_RATE, }) async quickTask() { // 每 5 秒执行一次 }FIXED_DELAY: 固定延迟执行,等待任务完成后再计时@Schedule({ interval: 5000, mode: ScheduleMode.FIXED_DELAY, }) async longRunningTask() { // 任务完成后等待 5 秒再执行下一次 }
分布式调度
定时任务基于 etcd 实现分布式调度:
- 自动选主:多个服务实例中只有一个会执行定时任务
- 故障转移:当执行任务的实例故障时,其他实例会自动接管
- 服务发现:新加入的实例会自动参与选主
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}优雅停机
服务停止时会自动清理定时任务和选举信息:
// 在 k8s 停机信号处理中
await service.stop();注意事项
- 使用定时任务需要配置 etcd
- 建议使用
FIXED_DELAY模式执行耗时任务 - 任务执行时间不应超过执行间隔
Stream 流
服务引擎支持 Stream 流传输,可以在服务端返回 Stream 流,客户端使用 await iter.next() 逐个获取数据。或者使用 for await (const item of iter) 迭代。
注意:服务端返回的流需要使用
AsyncIterableIterator类型,客户端使用AsyncIterator迭代。 HTTP 请求方式也支持流式传输,服务端是通过 SSE 实现。
服务端:
@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));
}
}
}客户端:
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 请求具有以下优势:
- 保持长连接,减少连接建立的开销
- 支持双向通信
- 使用 Brotli 压缩,减少数据传输量
- 自动重连和心跳检测
服务端配置:
const service = new Microservice({
modules: [UserService],
prefix: "/api",
websocket: {
pingInterval: 5000,
},
});客户端配置:
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 实现:
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");安装依赖:
npm install isomorphic-ws brotli-wasm注意事项
- WebSocket 连接会自动重连,无需手动处理
- 所有消息都使用 Brotli 压缩,需要安装 brotli-wasm 库
- 客户端会定期发送心跳消息以保持连接
- 在不再使用时应调用
close()方法关闭连接 - Node.js 环境需要安装
isomorphic-ws包
