# glede-server-bun **Repository Path**: philuo/glede-server-bun ## Basic Information - **Project Name**: glede-server-bun - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 10 - **Forks**: 0 - **Created**: 2025-04-14 - **Last Updated**: 2026-01-29 ## Categories & Tags **Categories**: game-dev **Tags**: None ## README

Logo

## 基于配置启动 - base on [bun@^1.2.7](https://bun.sh) - types at [GledeServerOpts](./types/index.d.ts) - routers tree at `$workspace/logs/routers.txt` - server size: [ungzip(28kb) / gzip(10.5kb)](./dist/index.js) ## 初始化项目 ```bash # 前提是你必须安装了 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 ``` 支持 JSON 配置和 TS 配置, 推荐使用 TS 配置。 **推荐!**[TS 配置案例: app-config.ts](./tests/configs/app-config.ts) ## Benchmark ⚡️ `HTTP: 6~8x faster than` [Express@5.1.0](https://github.com/expressjs/express) ⚡️ `Websocket/SSE: 3~5x faster than` [Nodejs@23.6.0](https://nodejs.org/zh-cn) `MacOS; Intel-i5 2.9GHz; Memory-DDR4(2666Mhz) 32 GB` ![压测数据](https://mall.yinzuo.cn/fsfUrl/group1/M00/C5/ED/CgCFGmgB8zqADjuaAADPaQau44c428.png) ```ts // 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 // 默认: /// │ └── 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开头, 一般用于开放接口对接三方 ``` ## 路由类 ```ts 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: { // ... }, }; } } ``` ## [通用方法](./types/index.d.ts) - [GledeBunRequest](./types/global.ts) - [GledeUtil](./types/export.d.ts) - [GledeStaticUtil](./types/export.d.ts) - [Schema](./types/export.d.ts) - [Model](./types/export.d.ts) - [ObjectId](./types/export.d.ts) ## 路由注册 ### 注册不带前缀的路由 非 index 文件或目录会保持大小写被记录到路由中,例如示例中`./api/user/index.ts`中`user`会被注册到 /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/user`和`localhost:3020/user/` 是不同的路由 ```ts // 目录: 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` ```ts // ./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 })` ```ts // 支持协商压缩,不再需要手动处理数据传输层面的压缩 // 支持心跳机制,不再需要应用层处理心跳,心跳包不再带有数据载体所以开发者工具看不到meesage帧 // @GSD.WS中的第二个参数配置项均为可选参数,可根据需要自行配置 // @GSD.WS配合@NeedAuth装饰器使用,可实现鉴权功能, 小程序端支持使用请求头传递Authorization: 'Bearer ', 浏览器端用url?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来允许/阻止请求升级,允许请求升级时还设置自定义响应头 ```ts 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链接, 它会保证数据传输完成后再断开连接你不用担心。 } } ``` ### 文件上传装饰器 - `@Multer(opts?: MulterOpts, getOpts: MulterGetOpts)` > 处理 `multipart/form-data` 文件上传,基于 `@fastify/busboy` 实现流式处理,支持大文件上传(100MB-2GB),内存效率极高。 **特性:** - **大小限制**:支持文件大小、字段数量、部分数量等多维度限制 - **文件过滤**:通过 `fileFilter` 函数过滤文件类型 - **流式处理**:使用 `pipeline()` 自动处理背压,避免内存爆炸 - **自动清理**:超过限制时自动删除已上传的文件 ```ts export class UploadRouter extends GSD.GledeRouter { // 单文件上传 - 限制10MB @GSD.Multer( { limits: { fileSize: 10 * 1024 * 1024 } }, // 10MB限制 { single: 'file' } // 单个文件字段 ) @GSD.Post('/upload/single') uploadSingle(req: GledeBunRequest, data: GledeReqData) { // data.file 包含上传的文件信息 return { code: 0, data: { filename: data.file.filename, size: data.file.size, path: data.file.path, } }; } // 多文件上传 - 最多5个文件 @GSD.Multer( { dest: 'tmp', limits: { fileSize: 50 * 1024 * 1024 }, // 50MB限制 fileFilter: (req, file, callback) => { // 只允许图片 const allowed = ['image/jpeg', 'image/png', 'image/gif']; if (allowed.includes(file.mimetype)) { callback(null, true); } else { callback(new Error('只允许上传图片'), false); } } }, { array: 'files', maxCount: 5 } ) @GSD.Post('/upload/multiple') uploadMultiple(req: GledeBunRequest, data: GledeReqData) { // data.files 是文件数组 return { code: 0, data: data.files.map(f => ({ filename: f.filename, size: f.size, })) }; } } ``` **配置选项:** ```ts interface MulterOpts { dest?: string; // 保存目录,默认 'tmp' limits?: { fileSize?: number; // 单文件最大大小(字节) fieldNameSize?: number; // 字段名最大大小(字节),默认100 fieldSize?: number; // 非文件字段最大值(字节),默认1MB fields?: number; // 最多非文件字段数 files?: number; // 最多文件字段数 parts?: number; // 最多总部分数(字段+文件) headerSize?: number; // 头部最大大小(字节),默认81920 }; fileFilter?: (req: any, file: any, callback: (err: Error | null, acceptFile: boolean) => void) => void; preservePath?: boolean; // 是否保留文件路径,默认false } type MulterGetOpts = | { single: string } // 单个文件字段 | { array: string; maxCount?: number } // 文件数组字段 | Array<{ name: string; maxCount?: number }>; // 多个文件字段 ``` ### 跨域装饰器 > 设置需要跨域的域名、方法、是否允许携带 cookie。🔔提示:使用跨域后并发降低15%~30%; 开发环境、低并发要求的接口可以使用跨域, - `@Cors(origin: string | string[], method: string, credential?: boolean)` ### 鉴权装饰器 > 身份鉴权(noauth | user | admin | super | root), 是否允许 Handler 处理 > Default: noauth - `@NeedAuth(role: string)` ### 验签装饰器 > 签名验证, 是否允许 Handler 处理 - `@NeedSign()` ```ts /** * 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 工具 - [sql 工具使用文档](https://bun.sh/docs/api/sql) ```ts 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 操作文档](https://mongoosejs.com/docs/api/model.html) ### 定义数据模型 > 📢 参考 DEMO: ./tests/controllers/cat.ts
> cat 对应了数据库中的集合名称 cats, 起名字要使用单数!否则需要指定集合名字 ```ts 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); ``` ### 操作数据模型 ```ts 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 校验 - 手动创建 Schema - [Schema 校验采用 Ajv6](https://www.npmjs.com/package/ajv) ```ts // 新建或修改路由文件 // 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: { // ... } }; } } ``` ## 集成功能 ### 自定义日志输出 ```ts // @/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](https://github.com/philuo/ip2region) ### SMTP 邮件发送 [nodemailer](https://www.npmjs.com/package/nodemailer) ### 黑名单 - `ip blklist` `判黑条件:超管手动添加 / 时间段频率 / 单日访问次数` - `token blklist` `判黑条件:超管手动添加 / 即将过期且验证通过的Token` ### 定时任务 [@yuo/node-cron](https://github.com/philuo/node-cron) ### TODO - 补充用例,给出友好的报错提示。目前一些异常捕获后没有提示,可能会造成疑惑🤔。 - 接Apifox, 自动更新接口文档避免手动维护。 - 丰富装饰器/通用工具。 - 黑名单持久化 - 触发条件: 程序判定新增IP黑名单 - 通知方式: 邮件/机器人通知警告