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 🙏

© 2026 – Pkg Stats / Ryan Hefner

glede-server-bun

v1.0.22

Published

Web业务服务器

Readme

基于配置启动

初始化项目

# 前提是你必须安装了 bun
bun --version

# 初始化项目(MacOS/Linux)
bunx glede-init

# 初始化项目(Windows)
bun x glede-init
# 或者
npx glede-init

# 如果你想使用最新的脚手架
bunx glede-init@latest
# 或者
npx glede-init@latest

# 进入你的项目目录
cd <your-project>

支持 JSON 配置和 TS 配置, 推荐使用 TS 配置。 推荐!TS 配置案例: app-config.ts

Benchmark

⚡️ HTTP: 6~8x faster than [email protected]

⚡️ Websocket/SSE: 3~5x faster than [email protected]

MacOS; Intel-i5 2.9GHz; Memory-DDR4(2666Mhz) 32 GB

压测数据

// app.ts
import { Server } from "glede-server-bun";

Server({ conf: "configs/app-config.ts" }, (err, address) => {
  if (err) {
    console.log(err);
  }
  else {
    console.log(`GledeServer is running at ${address}`);
  }
});

模版目录结构

  • 在您的项目目录下执行 npm install glede-server 后参考本项目的tests目录创建即可
  • 注意引包时使用 import {...} from 'glede-server-bun';
  • 注意引用类型使用 import type {...} from 'glede-server-bun';
├── app.ts                      // 服务器启动入口
├── configs                     // 服务器配置
│   ├── app-config.ts           // 服务器配置文件 支持ts和json格式, 可配置多个用于区分运行时环境
│   ├── app.json
│   └── lua                     // lua脚本目录
│       ├── index.ts            // lua脚本导出口
│       └── statList.lua        // 自定义redis lua脚本
├── tsconfig.json               // ts编译配置
├── types                       // ts类型描述
│   ├── server.d.ts             // 默认: /// <reference types="glede-server/types" />
│   └── redis-lua.ts            // 拓展redis指令类型描述
├── controllers                 // DAO, 数据操作对象
│   └── cat.ts
├── demos                       // 基础使用方式
├── crons                       // 定时事务
│   └── test.ts
├── logs                        // 日志目录
│   ├── apis.json               // 配置开启swagger, 在运行时执行生成覆盖接口文档
│   ├── error.log               // 服务运行异常日志
│   └── routers.txt             // 最新的路由信息, 服务器的路由树
└── routers                     // 接口目录
    ├── api                     // /api开头, 一般用于业务接口
    ├── common                  // /开头, 一般用于通用接口
    └── openapi                 // /openapi开头, 一般用于开放接口对接三方

路由类

import { GledeRouter, Get, Post } from "glede-server-bun";

export class Router extends GledeRouter {
  // 注意方法不要使用箭头函数
  // 1. 依赖原型处理逻辑; 2. 注入依赖工具方便处理请求

  getAllUser(req: GledeBunRequest, data: GledeReqData) {
    // doSomething.

    if (noPass) {
      return {
        // 1 客户端参数校验未通过, 业务无需关心
        // >= 2 自定义
        code: 2,
        data: null,
        msg: "描述错误原因",
      };
    }

    // 以下情景等价于返回 {code:0, data: null}
    // 1. 无return语句
    // 2. return null
    // 3. return;
    // 4. return undefined

    return {
      code: 0, // 0 处理成功
      data: {
        // ...
      },
    };
  }
}

通用方法

路由注册

注册不带前缀的路由

非 index 文件或目录会保持大小写被记录到路由中,例如示例中./api/user/index.tsuser会被注册到 /api/user/$subpath。一下示例中 index 是不会注册到路由中的,若注册/index则需装饰器完成需求:@Get('/index')。

routers/open?api|common/index/index.ts

routers/open?api|common/index.ts

严格注册模式

  • 除 '/' 路由外,是否携带 / 需注册不同的 RouterHandler

@Get('')@Get('/')监听的是不同的路由,

localhost:3020/userlocalhost:3020/user/ 是不同的路由

// 目录: routers/api/post
import { Poster } from "../controllers";

export default class extends GSD.GledeRouter {
  @GSD.NeedAuth("user")
  @GSD.Get("/del/:id", { schema: schema.delPost })
  async delPost(req: GledeBunRequest, data: GledeReqData) {
    const { token, payload } = req.token;
    console.log(payload.role, payload.uid, payload.exp);

    // 指定身份 root 0 | super 1 | admin 2 可下架用户文章
    if (payload.role < ROLE_USER) {
      Poster.deleteOne({ postId: data.params.id });
    } else {
      Poster.deleteOne({ postId: data.params.id }); // 非管理员, 只能删除自己的文章
    }
  }
}

最佳实践

  • 为了您能便捷使用 GledeServer 的装饰器, 装饰器GledeRouter被挂在了全局变量GSD上。
  • 日志打印、数据库模型、高复用代码块儿等挂载到global上或统一导出在 GledeServer 初始化前或其他合适时机执行一次。具体操作参考tests/app.ts & tests/components/service
// ./routers/api/xx
export class Router extends GSD.GledeRouter {
  @GSD.Get("/test")
  test(req: GledeBunRequest, data: GledeReqData) {
    // do sth
  }
}

装饰器介绍

方法装饰器

将 Handler 装载至路由

  • @Get(url: string, { schema?: GledeGetSchema, version?: string })
  • @Post(url: string, { schema?: GledePostSchema, version?: string })
  • @WS(url: string, { schema?: GledePostSchema, version?: string })
// 支持协商压缩,不再需要手动处理数据传输层面的压缩
// 支持心跳机制,不再需要应用层处理心跳,心跳包不再带有数据载体所以开发者工具看不到meesage帧
// @GSD.WS中的第二个参数配置项均为可选参数,可根据需要自行配置
// @GSD.WS配合@NeedAuth装饰器使用,可实现鉴权功能, 小程序端支持使用请求头传递Authorization: 'Bearer <token>', 浏览器端用url?token=<token>,后续可支持cookie处理,建议使用url传递token
export class WSRouter extends GSD.GledeRouter {
  @GSD.NeedAuth("user")
  @GSD.WS("/ws", {
    schema: schema.wsSchema, // ajv对?search校验/自动转类型, 不用业务再手动处理性能优越 & 安全可靠
    upgrade(req) {
      /** 协议升级 */
      return {
        headers: {
          /** 自定义HTTP响应头 */
        },
        data: {
          /** 自定义传递给websocket实例的数据 */
        },
      };
    },
    // drain() {/** 处理背压 */},
    open(ws) {
      ws.id = ws.data.query.sessionId;
    },
    close(ws) {
      /** 处理链接关闭 */
    },
    ping(ws) {
      /** 接收客户端ping帧 */
    },
    pong(ws) {
      /** 接收客户端pong帧 */
    },
  })
  handler(ws: ServerWebSocket, message: string | Buffer) {
    // 接收消息
    const data = client.ClientDirective.decode(message);
    // 处理消息
    agent[data.option](ws, data);
  }
}

SSE装饰器

  • @SSE({ mode: 'direct', upgrade: (req, server) => { headers: Headers; } })

设置该装饰器后, GledeReqData 中会注入 controller实例, 通过它发送消息, 并自动关闭连接。若发生异常建议手动调用 controller.close() 关闭链接更加优雅。它仍然可以和其它装饰器一起工作。

  • mode默认为direct模式对应 controller.write, web对应controller.enqueue
  • 通过传入upgrade来允许/阻止请求升级,允许请求升级时还设置自定义响应头
export class SSERouter extends GSD.GledeRouter {
  @CORS() @SSE()
  @GSD.NeedAuth("user")
  @POST('/chat/sse', { schema: schema.chatSSE  })
  chatSSE(req: GledeBunRequest, data: GledeReqData) {
    const { controller } = data;

    controller.write(`data: 你的数据\n\n`);
    // doSomething(controller); // 把键盘⌨️给你,你来写!
    await asyncFunction(controlelr); // 请注意必须在方法退出前或contoller关闭前等待编排处理结束, 一旦路由处理执行结束会中断与客户端的连接
    controller.close(); // 完事儿后, 断开SSE链接, 它会保证数据传输完成后再断开连接你不用担心。
  }
}

跨域装饰器

设置需要跨域的域名、方法、是否允许携带 cookie。🔔提示:使用跨域后并发降低15%~30%; 开发环境、低并发要求的接口可以使用跨域,

  • @Cors(origin: string | string[], method: string, credential?: boolean)

鉴权装饰器

身份鉴权(noauth | user | admin | super | root), 是否允许 Handler 处理 Default: noauth

  • @NeedAuth(role: string)

验签装饰器

签名验证, 是否允许 Handler 处理

  • @NeedSign()
/**
 * 1. 客户端 摘要过程
 */

// 通过登陆等鉴权接口拿到 'MTcwMjE0MTE0Mzg5M183ODk4.BGZh4oyyHMWAWkiVSJptV5yNb7w'

// 切割取第二部分缓存
const signKey = "BGZh4oyyHMWAWkiVSJptV5yNb7w";

// 切割取第一部分, 需要随请求报文发送到服务端
const content = "MTcwMjE0MTE0Mzg5M183ODk4";

// 要发送的报文体
const payload = JSON.stringify({ name: "Kitty" });

// 同服务端约定的本项目的key
const baseKey = "007";

// 请求方法 uppercase
const method = "POST" as "POST" | "GET";

// /开头的url上的query
const query = "/?test=001";

// 一个空格分割method 和 query
const head = method + " " + query;

function stringify(content) {
  if (method === "GET") {
    return "";
  }
  if (method === "POST") {
    return typeof content === "string" ? content : JSON.stringify(content);
  }

  return "";
}

function getSign(head, payload) {
  return content + "." + sha1(signKey + baseKey + head + stringify(payload));
}

function sendRequest() {
  return fetch("http://localhost:3020/?test=001", {
    method: "POST",
    headers: {
      signature: getSign(head, payload),
    },
    body: stringify(payload),
  }).then((res) => res.json());
}

sendRequest().then((res) => {
  console.log(res);
});

数据库操作

sql 工具

import { GledeStaticUtil } from "glede-server-bun";

const sql = GledeStaticUtil.getPgInstance();
const data = await sql`SELECT * FROM "Job" LIMIT 10`;

// 建议挂载到全局变量上, 方便使用。
global.sql = sql;

// 开启事务
await sql.begin(async (tx) => {
  // All queries in this function run in a transaction
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
  await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`;

  // Transaction automatically commits if no errors are thrown
  // Rolls back if any error occurs
});

await sql.begin(async (tx) => {
  return [
    tx`INSERT INTO users (name) VALUES (${"Alice"})`,
    tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`,
  ];
});

await sql.begin(async (tx) => {
  await tx`INSERT INTO users (name) VALUES (${"Alice"})`;

  await tx.savepoint(async (sp) => {
    // This part can be rolled back separately
    await sp`UPDATE users SET status = 'active'`;
    if (someCondition) {
      throw new Error("Rollback to savepoint");
    }
  });

  // Continue with transaction even if savepoint rolled back
  await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});

mongoose 操作文档

定义数据模型

📢 参考 DEMO: ./tests/controllers/cat.ts cat 对应了数据库中的集合名称 cats, 起名字要使用单数!否则需要指定集合名字

import { Model } from "@/index";

// 模型数据结构
const CatSchema = {};

// 模型自定义
const CatOpts = {
  // 指定集合名, 此时集合链接到了cat, 默认是cats
  collection: "cat",

  // 添加便捷方法, 注意不要使用箭头函数!
  // 可以这样使用:Cat.findByName('^cool').then(res => {});
  statics: {
    findByName(name: string) {
      return this.find({ name: new RegExp(name, "i") });
    },
  },
};

export default Model("cat", CatSchema, CatOpts);

操作数据模型

import Cat from "@/tests/controllers/cat";

// 1. 在Cat表中插入一条数据, 后面Demo默认包裹在try-catch中
try {
  await Cat.create({
    // 插入数据格式必须是CatSchema中定义, 否则字段会被忽略
  });
} catch (err) {
  /* Handle Error */
}

// 2. 在Cat表中查找一条数据, 随便找一只名叫 cool_xx 且小于2岁的🐱
// 非常不推荐正则, 除非搜索过滤等场景。一般在任何语言中的实现都是最慢最耗性能的模式匹配。
// 不过有的语言实现了正则的缓存, 可能在某些场景下会快。尽量不用吧!
Cat.findOne({
  name: new RegExp("^cool_", "i"),
  age: { $lt: 2 },
});

// 3. 在Cat表中找到一条匹配的数据,删除
Cat.deleteOne({});

// 4. 在Cat表中找到所有可以匹配删除的数据
Cat.deleteMany({});

// 5. 在Cat表中找到数据并更新, upsert默认为false, 设置为true不存在就插入
// 注意原子操作, filter, { $set: { name: '小小明' } }, options
Cat.updateOne({ name: "明" }, { $set: { name: "小明" } }, { upsert: true });
Cat.updateOne({ name: "小明" }, {}, { upsert: true });

// 所有男生, 分数 +1
Cat.updateMany({ sex: "male" }, { $inc: { score: 1 } });

// 6. 多种操作, 一次通信。性能upup!
// [https://mongoosejs.com/docs/api/model.html#model_Model.bulkWrite]
Cat.bulkWrite([
  {
    insertOne: {
      document: {
        name: "Eddard Stark",
        title: "Warden of the North",
      },
    },
  },
  {
    updateOne: {
      filter: { name: "Eddard Stark" },
      update: { title: "Hand of the King" },
    },
  },
  {
    deleteOne: {
      filter: { name: "Eddard Stark" },
    },
  },
]).then(({ insertedCount, modifiedCount, deletedCount }) => {
  // 1 1 1
  console.log(insertedCount, modifiedCount, deletedCount);
});

默认记录错误日志

  • 默认记录日志, 需要创建对应的目录路径
  • 根目录创建文件: logs/error.log

请求需通过 Schema 校验

// 新建或修改路由文件
// mkdir routers/${api | openapi}/${router | routerDir/index.ts}
// api|openapi目录下存放路由可以是ts文件或目录, 文件内和目录内的Schema定义可相互引用
// 示例 /routers/api/user/index.ts
import { getAllUsersSchema, getAllUsersSchemaV2 } from './schema';

export Router extends GledeRouter {
  // version是接口的版本用于线上并行, 可选:默认 '', 如果出现版本区分可填写 v1, v2, ...
  // schema是参数的拦截校验, 必选:1. 客户端字段安全拦截 2. 增加序列化的性能10%~15% 3. 生成接口文档协同开发
  // match: /api/v1/:id
  @Get('/:id', { version: 'v1', schema: getAllUsersSchema }) @Cors()
  getAllUsers(req: GledeBunRequest, data: GledeReqData): GledeResData {
    return {
      code: 0,
      data: {
          // ...
      }
    };
  }

  @NeedAuth('super') @Cors('https://philuo.com', 'GET,POST')
  @Get('/:id', { version: 'v2', schema: getAllUsersSchemaV2 })
  @Post('/:id', { schema })
  getAllUsersV2(req: GledeBunRequest, data: GledeReqData): GledeResData {
    return {
      code: 0,
      data: {
          // ...
      }
    };
  }
}

集成功能

自定义日志输出

// @/utils/log.ts
import { GledeStaticUtil } from "glede-server-bun";
import { join } from "path";

export const logger = new GledeStaticUtil.Logger({
  // 输出位置, 默认[1]输出到日志文件; [0]输出到控制台, [0, 1]输出到控制台和文件
  target: [1],
  // 日志输出的目录, 默认存储在运行node的路径下的logs路径下
  // !import 注意服务运行中不可以删除 dir目录
  dir: join(__dirname, "logs"),
  // 日志文件名 默认 glede-server.log 如果开启轮转会自动补充后缀
  // !import 注意服务运行中不可以删除 filename文件, 其他轮转生成的文件可以移动或删除
  filename: "glede-server.log",
  // 日志轮转, 到期生成新的日志文件格式如下 20231210-1411-03-glede-server.log
  interval: "30d",
  // 日志大小, 超限生成新的日志文件格式如下 20231210-1411-03-glede-server.log
  size: "10M",
  // 控制单个文件大小, 注意开启压缩再使用 超过限制后旧文件会被压缩
  // maxSize: '10M',
  // 是否开启压缩, 默认关闭 不允许设置false, 关闭不设置该属性即可
  // compress: true
  // 最多保留的最近的日志文件和压缩包数量, 默认全部保留不设置即可
  // maxFiles: 30
});

logger.error("123"); // level === 0
logger.warn("123"); // level === 1
logger.info("123"); // level === 2
logger.log("123", 2); // 仅输出到控制台, 不干扰日志文件(level可选默认2 INFO级别)

Token 签发与验证

  • 实现分发(sign, unsign)

  • 实现校验(verify)

if not 过期 -> if not 快过期 -> if match 身份 -> if not 是否篡改 -> if not blklist -> ok
else fail -> else -> else fail -> else fail -> else fail
             if ok then blklist and return data with new token
             if not ok then fail

数据库驱动

  • mongoose

  • ioredis

  • sqlite

  • postgres

  • mysql

区域检测

@yuo/ip2region

SMTP 邮件发送

nodemailer

黑名单

  • ip blklist

判黑条件:超管手动添加 / 时间段频率 / 单日访问次数

  • token blklist

判黑条件:超管手动添加 / 即将过期且验证通过的Token

定时任务

@yuo/node-cron

TODO

  • 补充用例,给出友好的报错提示。目前一些异常捕获后没有提示,可能会造成疑惑🤔。
  • 接Apifox, 自动更新接口文档避免手动维护。
  • 丰富装饰器/通用工具。
  • 黑名单持久化
    • 触发条件: 程序判定新增IP黑名单
    • 通知方式: 邮件/机器人通知警告