Skip to main content

latest
This package works with DenoIt is unknown whether this package works with Cloudflare Workers, Node.js, Bun, Browsers
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
This package works with Deno
It is unknown whether this package works with Bun
It is unknown whether this package works with Browsers
JSR Score
64%
Published
2 months ago (1.5.1)

Microservice Framework for Deno

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

JSR

特性

  • 🦕 专为 Deno 设计
  • 📝 完全的 TypeScript 支持
  • 🔄 自动生成类型安全的客户端代码
  • 🛡️ 使用 Zod 进行运行时类型验证
  • 🔁 内置智能重试机制
  • 🎯 支持幂等操作
  • 🌟 优雅的装饰器 API
  • 🚦 优雅停机支持
  • 📡 生成基于 fetch 的客户端代码,可以在 Deno 、Node.js、Bun 以及浏览器中使用
  • 🌟 服务间调用可以利用 Deno 的分布式模块引入快速集成生成的客户端

TODOs

  • 示例项目
  • 微服务高级功能,熔断器、负载均衡等
  • 服务指标统计

安装

import { Action, Microservice, Module } from "jsr:@imean/microservice";

快速开始

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/client.ts?cache=v1",
});
// 创建用户
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; // 是否是幂等操作
}

Microservice

constructor(options: MicroserviceOptions)

创建微服务实例。

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

start(port?: number): void

启动服务器,默认端口为 3000。

clientCode(): Promise

获取生成的客户端代码。

MicroserviceClient

constructor(options: ClientOptions)

创建客户端实例。

interface ClientOptions {
  baseUrl: string; // 服务器地址
  prefix?: string; // API 前缀,默认为 "/api"
  headers?: Record<string, string>; // 自定义请求头
}

错误处理

服务端抛出的错误会被自动转换为标准的错误响应:

interface ServiceResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

类型安全

框架使用 Zod 进行运行时类型验证,确保:

  • 请求参数类型正确
  • 返回值类型符合预期
  • 自动生成的客户端代码类型完整

最佳实践

服务启动前检查

框架提供了 startCheck 方法用于在服务正式启动前进行必要的检查和初始化。这对于确保依赖服务(如数据库)可用非常有用。

// main.ts
import { startCheck } from "jsr:@imean/microservice";

// 数据库连接检查
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: Deno.env.get("DB_HOST") || "localhost",
    port: parseInt(Deno.env.get("DB_PORT") || "5432"),
    // ...
  },
  redis: {
    url: Deno.env.get("REDIS_URL") || "redis://localhost:6379",
    // ...
  },
  service: {
    port: parseInt(Deno.env.get("PORT") || "3000"),
    prefix: Deno.env.get("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. 任务执行时间不应超过执行间隔
  4. 确保不同任务的选举 key 不重复

License

MIT

Add Package

deno add jsr:@imean/microservice

Import symbol

import * as microservice from "@imean/microservice";

---- OR ----

Import directly with a jsr specifier

import * as microservice from "jsr:@imean/microservice";

Add Package

npx jsr add @imean/microservice

Import symbol

import * as microservice from "@imean/microservice";

Add Package

yarn dlx jsr add @imean/microservice

Import symbol

import * as microservice from "@imean/microservice";

Add Package

pnpm dlx jsr add @imean/microservice

Import symbol

import * as microservice from "@imean/microservice";

Add Package

bunx jsr add @imean/microservice

Import symbol

import * as microservice from "@imean/microservice";