npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

imean-service-engine

v1.9.0

Published

microservice engine

Downloads

441

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 即可查看服务状态页面。

最佳实践

  1. 页面组织:将页面逻辑与 API 逻辑分离
  2. 组件复用:使用 HtmxLayout 确保一致的页面结构
  3. 渐进增强:优先使用 HTMX 实现交互,必要时使用 Hyperscript
  4. 响应式设计:利用 Tailwind CSS 构建响应式界面
  5. 布局选择
    • 使用 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);
  }
);

这种方式的优点:

  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