1.3.0 • Published 3 months ago

imean-service-engine v1.3.0

Weekly downloads
-
License
MIT
Repository
-
Last release
3 months ago

Microservice Framework

一个轻量级的 TypeScript 微服务框架。提供了类型安全、自动客户端生成、请求重试等特性。

特性

  • 📝 完全的 TypeScript 支持
  • 🔄 自动生成类型安全的客户端代码
  • 🛡️ 使用 Zod 进行运行时类型验证
  • 🔁 内置智能重试机制
  • 🎯 支持幂等操作
  • 🌟 优雅的装饰器 API
  • 🚦 优雅停机支持
  • 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用
  • 🌟 支持 Stream 流传输,客户端使用 AsyncIterator 迭代
  • 🌟 服务引擎支持通过 WebSocket 进行实时通信,相比 HTTP 请求具有以下优势:
    • 保持长连接,减少连接建立的开销
    • 支持双向通信
    • 使用 Brotli 压缩,减少数据传输量
    • 自动重连和心跳检测

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);

高级特性

幂等性和重试机制

框架提供了智能的重试机制,但仅对标记为幂等的操作生效:

重试策略:

  • 仅对标记为 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; // 缓存过期时间(秒)
}

Microservice

constructor(options: MicroserviceOptions)

创建微服务实例。

interface MicroserviceOptions {
  modules: (new () => any)[]; // 模块类数组
  prefix?: string; // API 前缀,默认为 "/api"
}

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);
  }
);

这种方式的优点:

  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

配置管理

建议将配置和服务逻辑分离:

// 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 实现分布式调度:

  1. 自动选主:多个服务实例中只有一个会执行定时任务
  2. 故障转移:当执行任务的实例故障时,其他实例会自动接管
  3. 服务发现:新加入的实例会自动参与选主
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();

注意事项

  1. 使用定时任务需要配置 etcd
  2. 建议使用 FIXED_DELAY 模式执行耗时任务
  3. 任务执行时间不应超过执行间隔

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 请求具有以下优势:

  1. 保持长连接,减少连接建立的开销
  2. 支持双向通信
  3. 使用 Brotli 压缩,减少数据传输量
  4. 自动重连和心跳检测

服务端配置:

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

注意事项

  1. WebSocket 连接会自动重连,无需手动处理
  2. 所有消息都使用 Brotli 压缩,需要安装 brotli-wasm 库
  3. 客户端会定期发送心跳消息以保持连接
  4. 在不再使用时应调用 close() 方法关闭连接
  5. Node.js 环境需要安装 isomorphic-ws
1.3.0

3 months ago

1.2.3

3 months ago

1.2.2

4 months ago

1.2.1

5 months ago

1.2.0

5 months ago

1.1.0

5 months ago

1.0.0

5 months ago