knight-server
v1.2.4
Published
Knight server architecture library — Express/WebSocket server core with database, auth, cache support
Downloads
2,036
Maintainers
Readme
Knight-Server 服务插件使用手册
概述
Knight-Server 是一个基于 Express + WebSocket 的 Node.js 服务端框架,集成了 MySQL、Redis 两大一等模块,通过装饰器 @OnRegister 实现路由自动注册,提供开箱即用的 HTTP/WebSocket/Database/Redis 全栈开发体验。
- 包名:
knight-server - 入口:
dist/index.js - 类型声明:
dist/index.d.ts
一、安装与启动
安装
npm install knight-server最小启动示例
import { Server } from "knight-server";
const server = Server.Instance;
await server.OnStart({
database: { /* ... */ },
redis: { /* ... */ },
http: { port: 3000 },
websocket: {
port: 6666,
heartbeat_timeout_interval: 30000,
router_timeout_interval: 60000,
},
serverHost: "127.0.0.1",
});Server 是单例类,构造函数私有,通过 Server.Instance 访问。OnStart 按 database → redis → http → websocket 顺序启动各模块,重复调用会被忽略。
二、服务配置 (ServerConfig)
export interface ServerConfig {
database: DataBaseConfig;
redis: RedisConfig;
http: HttpConfig;
websocket: WebSocketConfig;
token?: TokenConfig; // 可选,JWT 配置
serverHost: string; // 服务器对外地址,如 "127.0.0.1" 或域名
}2.1 DataBaseConfig
interface DataBaseConfig {
host: string;
port: number;
user: string;
password: string;
database: string;
connectionLimit: number; // 连接池大小
tableRelativePath: string; // .sql 表定义文件目录(相对于项目根目录)
}模块启动时会自动校验必填字段,并调用 SmartTable 根据 .sql 文件同步数据库表结构。
2.2 RedisConfig
interface RedisConfig {
host: string;
port: number;
password?: string;
db?: number;
url?: string; // 连接 URL,优先级高于 host/port
keyPrefix?: string; // 键前缀
connectTimeout?: number; // 连接超时(ms)
heartbeatInterval?: number; // 心跳间隔(ms),默认 240000(4分钟)
}模块启动后维护两个客户端:主客户端执行命令,订阅客户端处理 Pub/Sub。内置心跳机制,每隔 heartbeatInterval 毫秒向主客户端和订阅客户端发送 PING 命令,防止服务端因空闲超时断开连接(如 Redis timeout 配置)。重连时自动恢复所有频道和模式订阅。
2.3 HttpConfig
interface HttpConfig {
port: number;
}2.4 WebSocketConfig
interface WebSocketConfig {
port: number;
heartbeat_timeout_interval: number; // 心跳超时间隔(ms),超时客户端会被关闭(code 1001)
router_timeout_interval: number; // 非持久路由闲置检测间隔(ms),无客户端时自动注销
}2.5 TokenConfig (可选)
interface TokenConfig {
secret: string;
accessExpiresIn?: string; // 默认 "15m"
refreshExpiresIn?: string; // 默认 "7d"
}此配置存放在 server.config.token 中供业务代码使用,框架本身不自动接入鉴权逻辑——你需要在 Router 的 OnCheck 中自行调用 TokenUtility.verify()。
三、路由注册机制
3.1 @OnRegister 装饰器
import { OnRegister } from "knight-server";
@OnRegister("/api/users")
export class UserRouter extends HttpRouter { /* ... */ }行为:
- 将路由元数据
{ path, type, permanent: true }推入全局RouteRegistry数组 path自动规范化为小写且以/开头- 各 Module 在
OnStart时通过instanceof过滤出归属自己的 Router 并实例化 - 装饰器标记的路由
permanent默认为true,不会被自动清理
3.2 手动注册 / 注销
// 动态注册
server.http.OnRegistRouter({
path: "/api/dynamic",
type: DynamicRouter,
permanent: false,
});
// 动态注销
server.http.OnDegistRouter("/api/dynamic");四、HTTP 模块
4.1 HttpRouter 抽象类
继承 HttpRouter 后需实现以下方法:
| 方法 | 签名 | 说明 |
|------|------|------|
| OnCheck | (server, request: Request, response: Response): Promise<CheckResult> | 鉴权/校验 |
| OnGetHandler | (server, checkResult, request, response): Promise<void> | GET 请求 |
| OnPostHandler | 同上 | POST 请求 |
| OnPutHandler | 同上 | PUT 请求 |
| OnDeleteHandler | 同上 | DELETE 请求 |
| OnOptionsHandler | 同上 | OPTIONS 请求 |
| OnHeadHandler | 同上 | HEAD 请求 |
| OnPatchHandler | 同上 | PATCH 请求 |
| OnTraceHandler | 同上 | TRACE 请求 |
4.2 调度流程
HTTP 请求 → express.all(path) 代理层 → 查找 router → OnCheck
├─ valid=false → 返回 401
└─ valid=true → 按 method 分发到对应 OnXxxHandler4.3 示例
import { HttpRouter, OnRegister, CheckResult, TokenUtility } from "knight-server";
@OnRegister("/api/user/info")
export class UserInfoRouter extends HttpRouter {
async OnCheck(server, request, response): Promise<CheckResult> {
const token = request.headers.authorization?.replace("Bearer ", "");
if (!token) return { valid: false, error: "请先登录" };
try {
const payload = TokenUtility.verify(token, server.config.token.secret);
return { valid: true, payload };
} catch {
return { valid: false, error: "令牌无效" };
}
}
async OnGetHandler(server, checkResult, request, response) {
const users = await server.database.find("users", "id = ?", [checkResult.payload.uid]);
response.json({ success: true, data: users.data });
}
async OnPostHandler(server, checkResult, request, response) {
const result = await server.database.insert("users", request.body);
response.json(result);
}
// 未实现的 method handler 会导致请求无响应,建议至少实现 OnOptionsHandler
async OnOptionsHandler(server, checkResult, request, response) {
response.status(204).end();
}
}五、WebSocket 模块
5.1 WebSocketRouter 抽象类
abstract class WebSocketRouter {
// 鉴权检查
abstract OnCheck(server: Server, url: URL): Promise<CheckResult>;
// 客户端连接
abstract OnConnect(server: Server, checkResult: CheckResult, client: WebSocketClient): Promise<void>;
// 客户端断开
abstract OnDisconnect(server: Server, checkResult: CheckResult, client: WebSocketClient): Promise<void>;
// 收到消息
abstract OnMessage(server: Server, checkResult: CheckResult, client: WebSocketClient, data: string): Promise<void>;
}5.2 WebSocketClient 关键方法
| 方法 | 说明 |
|------|------|
| OnSend(data) | 向该客户端发送消息 |
| OnBroadcast(data, excludeSelf?) | 向同路由下所有客户端广播 |
| OnUpdateHeartbeat() | 刷新心跳时间戳(收到消息时调用;协议层 Ping 帧会自动刷新) |
| OnChangeRouter(router) | 将客户端迁移到另一个路由 |
| OnGetSearchParams(key) | 获取连接 URL 上的查询参数 |
| OnGetCheckResultPayload<T>() | 获取鉴权时写入的 payload |
5.3 关键属性
| 属性 | 说明 |
|------|------|
| client.key | "ip:port" 格式的唯一标识 |
| client.checkResult | 鉴权结果 |
5.4 WebSocketRouter 客户端管理
| 方法 | 说明 |
|------|------|
| OnGetClient(key) | 按 key 获取客户端 |
| OnGetClients() | 获取所有客户端 |
| OnGetClientBySearchParams(key, value) | 按 URL 参数查找 |
| OnGetClientByCheckResultPayload(key, value) | 按鉴权 payload 字段查找 |
5.5 连接流程
WebSocket 连接 → 按 pathname 找到 router → router.OnHandler
→ 去重检查(ip:port)
→ OnCheck(鉴权)
→ 创建 WebSocketClient → 注册到 client pool
→ OnConnect
→ 启动心跳检测定时器客户端发送 Ping 帧时,服务端自动回复 Pong 并刷新心跳时间戳;此外在 OnMessage 中调用 client.OnUpdateHeartbeat() 也可维持心跳。
5.6 示例
import { WebSocketRouter, WebSocketClient, OnRegister, CheckResult, Server } from "knight-server";
@OnRegister("/ws/chat")
export class ChatRouter extends WebSocketRouter {
async OnCheck(server: Server, url: URL): Promise<CheckResult> {
const token = url.searchParams.get("token");
if (!token) return { valid: false, error: "缺少令牌" };
// ... 验证 token
return { valid: true, payload: { userId: "xxx" } };
}
async OnConnect(server, checkResult, client) {
client.OnBroadcast(JSON.stringify({
type: "join",
userId: (checkResult.payload as any).userId,
}));
}
async OnDisconnect(server, checkResult, client) {
client.OnBroadcast(JSON.stringify({
type: "leave",
userId: (checkResult.payload as any).userId,
}));
}
async OnMessage(server, checkResult, client, data) {
client.OnUpdateHeartbeat(); // 刷新心跳
client.OnBroadcast(data, true); // 广播给同路由其他人
}
}六、Database 模块
6.1 访问方式
// 直接通过 server.database 调用
const result = await server.database.find("users", "age > ?", [18]);6.2 DataBaseRouter 钩子
@OnRegister("/users")
export class UsersRouter extends DataBaseRouter {
// 访问控制
async OnCheck(server): Promise<CheckResult> {
if (!server.database.isConnected()) return { valid: false, error: "数据库未连接" };
return { valid: true };
}
// 插入前钩子
async OnBeforeInsert(data: any): Promise<HookResult> {
return { proceed: true, data };
}
// 更新前钩子
async OnBeforeUpdate(data: any, where: string, params: any[]): Promise<HookResult> {
return { proceed: true, data };
}
// 删除前钩子
async OnBeforeDelete(where: string, params: any[]): Promise<HookResult> {
return { proceed: true };
}
// 查询后钩子(脱敏、转换等)
async OnAfterSelect(rows: any[]): Promise<any[]> {
return rows.map(({ password, ...rest }) => rest);
}
}6.3 HookResult
interface HookResult<T = any> {
proceed: boolean; // true=继续, false=阻止
data?: T; // 修改后的数据
error?: string; // 阻止时的错误信息
}6.4 CRUD 方法一览
所有方法返回 Promise<QueryResult<T>>:
interface QueryResult<T = any> {
success: boolean;
data?: T[]; // 多行结果
item?: T; // 单行结果 (findOne, findById)
affectedRows?: number; // insert/update/delete
insertId?: number; // insert
error?: string; // 失败时的错误信息
}查询:
| 方法 | 说明 |
|------|------|
| find<T>(table, where?, params?) | 查询多条 |
| findOne<T>(table, where, params?) | 查询单条 (含 .item) |
| findById<T>(table, id, idField?) | 按 ID 查询 |
| paginate<T>(table, page?, pageSize?, where?, params?, orderBy?) | 分页查询 |
写入:
| 方法 | 说明 |
|------|------|
| insert<T>(table, data) | 插入单条 |
| insertBatch<T>(table, dataArray) | 批量插入 |
| update<T>(table, data, where, whereParams?) | 条件更新 |
| updateById<T>(table, id, data, idField?) | 按 ID 更新 |
| delete(table, where, params?) | 条件删除 |
| deleteById(table, id, idField?) | 按 ID 删除 |
高级:
| 方法 | 说明 |
|------|------|
| query<T>(sql, params?) | 执行自定义 SQL(绕过钩子与访问控制) |
| transaction<T>(callback) | 事务执行,自动 begin/commit/rollback/release |
6.5 事务示例
const result = await server.database.transaction(async (connection) => {
await connection.execute("UPDATE accounts SET balance = balance - 100 WHERE id = ?", [1]);
await connection.execute("UPDATE accounts SET balance = balance + 100 WHERE id = ?", [2]);
return { msg: "转账成功" };
});
if (!result.success) {
console.error(result.error); // 事务已自动回滚
}七、Redis 模块
7.1 访问方式
await server.redis.set("key", "value");
const val = await server.redis.get("key");7.2 心跳保活
模块启动后自动对主客户端和订阅客户端定时发送 PING 保活,防止 Redis 服务端因空闲超时(timeout)断开连接。心跳间隔由 RedisConfig.heartbeatInterval 控制(默认 4 分钟)。连接意外断开重连后,自动恢复所有频道和模式订阅,心跳也会自动恢复。
7.3 KV 操作
| 方法 | Redis 命令 | 返回值 |
|------|------------|--------|
| get(key) | GET | Promise<string \| {}> |
| set(key, value) | SET | Promise<string \| {}> |
| deleteKeys(...keys) | DEL | Promise<number> |
| expire(key, seconds) | EXPIRE | Promise<number> |
| exists(...keys) | EXISTS | Promise<number> |
| increment(key) | INCR | Promise<number> |
| incrementBy(key, n) | INCRBY | Promise<number> |
| decrement(key) | DECR | Promise<number> |
| decrementBy(key, n) | DECRBY | Promise<number> |
| timeToLive(key) | TTL | Promise<number> |
| keys(pattern) | KEYS | Promise<string[]> |
| setIfNotExists(key, value) | SETNX | Promise<number> |
| getAndSet(key, value) | GETSET | Promise<string \| {}> |
| multiGet(...keys) | MGET | Promise<(string \| {})[]> |
| multiSet(keyValues) | MSET | Promise<string \| null> |
7.4 Hash 操作
| 方法 | Redis 命令 |
|------|------------|
| hashSet(key, field, value) | HSET |
| hashGet(key, field) | HGET |
| hashGetAll(key) | HGETALL |
| hashDelete(key, ...fields) | HDEL |
| hashExists(key, field) | HEXISTS |
| hashKeys(key) | HKEYS |
| hashValues(key) | HVALS |
| hashLength(key) | HLEN |
| hashMultiSet(key, fieldValues) | HSET (批量) |
7.5 List 操作
| 方法 | Redis 命令 |
|------|------------|
| listLeftPush(key, ...elements) | LPUSH |
| listRightPush(key, ...elements) | RPUSH |
| listLeftPop(key) | LPOP |
| listRightPop(key) | RPOP |
| listRange(key, start, stop) | LRANGE |
| listLength(key) | LLEN |
| listRemove(key, count, element) | LREM |
7.6 Set 操作
| 方法 | Redis 命令 |
|------|------------|
| setAdd(key, ...members) | SADD |
| setRemove(key, ...members) | SREM |
| setMembers(key) | SMEMBERS |
| setIsMember(key, member) | SISMEMBER |
| setCardinality(key) | SCARD |
| setIntersect(...keys) | SINTER |
| setUnion(...keys) | SUNION |
| setDifference(...keys) | SDIFF |
7.7 SortedSet 操作
| 方法 | Redis 命令 |
|------|------------|
| sortedSetAdd(key, ...{score, value}[]) | ZADD |
| sortedSetRange(key, start, stop, withScores?) | ZRANGE |
| sortedSetRangeByScore(key, min, max, withScores?) | ZRANGEBYSCORE |
| sortedSetRemove(key, ...members) | ZREM |
| sortedSetCardinality(key) | ZCARD |
| sortedSetScore(key, member) | ZSCORE |
| sortedSetRank(key, member) | ZRANK |
| sortedSetReverseRank(key, member) | ZREVRANK |
7.8 Pub/Sub 操作
// 订阅频道
await server.redis.subscribe("game:events", (message, channel) => {
console.log(`[${channel}] ${message}`);
});
// 发布消息
await server.redis.publish("game:events", JSON.stringify({ type: "start" }));
// 退订
await server.redis.unsubscribe("game:events");
// 模式订阅(通配符)
await server.redis.patternSubscribe("game:*", (message, channel) => { /* ... */ });
await server.redis.patternUnsubscribe("game:*");7.9 RedisRouter 自动订阅
@OnRegister 的路径即频道名,RedisModule 启动后自动订阅,消息到达时调用 OnHandler:
@OnRegister("/channel/notifications")
export class NotificationRouter extends RedisRouter {
// 有人 publish 到 "/channel/notifications" 时自动触发
async OnHandler(channel: string, message: string): Promise<void> {
const data = JSON.parse(message);
console.log(`收到频道 ${channel} 的消息:`, data);
}
}7.10 Pipeline 操作
const pipeline = server.redis.pipeline();
pipeline.set("key1", "val1")
.get("key1")
.deleteKeys("key2")
.listLeftPush("queue", "item1", "item2");
const results = await pipeline.exec(server.redis.getNativeClient());7.11 Lua 脚本
// 执行脚本
const result = await server.redis.evalScript("return redis.call('GET', KEYS[1])", ["mykey"]);
// 加载 + 缓存执行
const sha = await server.redis.scriptLoad("return redis.call('INCR', KEYS[1])");
const count = await server.redis.evalSha(sha, ["counter"]);7.12 底层客户端访问
const native = server.redis.getNativeClient(); // 主客户端
const sub = server.redis.getNativeSubscriber(); // 订阅客户端八、工具集 (Utility)
8.1 LogUtility
import { LogUtility } from "knight-server";
LogUtility.LogTip("提示信息");
LogUtility.LogInfo("信息");
LogUtility.LogWarning("警告");
LogUtility.LogError("错误");
LogUtility.LogSystem("系统信息");
LogUtility.Switch = false; // 关闭日志日志格式:[KNIGHT][标签][调用类名][yyyy-MM-dd HH:mm:ss.SSS] 消息...
调用类名通过解析 Error.stack 自动获取,无需手动传入。
8.2 TokenUtility (JWT)
import { TokenUtility } from "knight-server";
// 签发
const accessToken = TokenUtility.sign({ uid: 1 }, server.config.token.secret, "2h");
const refreshToken = TokenUtility.signRefresh({ uid: 1 }, server.config.token.secret, "7d");
// 验证(抛出异常时视为无效)
const payload = TokenUtility.verify<{ uid: number }>(accessToken, server.config.token.secret);
// 解码(不验证签名)
const decoded = TokenUtility.decode<{ uid: number }>(accessToken);8.3 StringUtility
| 方法 | 说明 |
|------|------|
| empty(str) | null/undefined/"" → true |
| isBlank(str) | 空或纯空白 → true |
| trimToEmpty(str) | 安全 trim,null → "" |
| uuid() | 生成 UUID |
| random(length?) | 随机字符串,默认 16 位 |
| randomNumber(length?) | 随机数字串,默认 6 位 |
| format(template, params) | "Hello {name}" 模板替换 |
| camelToSnake(str) | 驼峰转下划线 |
| snakeToCamel(str) | 下划线转驼峰 |
| joinUrl(...parts) | URL 路径拼接 |
8.4 TimeUtility
| 方法 | 说明 |
|------|------|
| nowTimestamp() | Date.now() |
| nowUnix() | 秒级时间戳 |
| formatDate(date?) | yyyy-MM-dd |
| formatDateTime(date?) | yyyy-MM-dd HH:mm:ss |
| sleep(ms) | 异步等待 |
| startOfDay(date?) | 当天 00:00:00 |
| endOfDay(date?) | 当天 23:59:59 |
| addDays(date?, n) | 加/减天数 |
| diffInDays(d1, d2) | 日期差 |
| isExpired(date) | 是否已过期 |
| timer(fn) | 测量异步执行耗时 |
8.5 NetworkUtility
import { NetworkUtility } from "knight-server";
// HTTP 请求
const res = await NetworkUtility.HTTP.OnRequest<{ data: any }>("https://api.example.com", {
method: "post",
data: { key: "value" },
params: { page: 1 },
timeout: 5000,
});
// SSE 流式请求
for await (const chunk of NetworkUtility.HTTP.OnStreamRequest("https://api.example.com/stream")) {
console.log(chunk);
}
// 客户端 WebSocket
const ws = new NetworkUtility.Websocket();
ws.On("message", (event) => console.log(event.data));
ws.OnConnect("ws://localhost:6666/ws/chat?token=xxx");
ws.OnSend("hello");
ws.OnDisConnect();常用枚举:
NetworkUtility.WebSocketCloseCode.CONFLICT // 4000
NetworkUtility.WebSocketCloseCode.AUTH_FAILED // 4001
NetworkUtility.ResponseCode.UNAUTHORIZED // 401
NetworkUtility.ResponseCode.TOO_MANY_REQUESTS // 429
NetworkUtility.ResponseCode.BUSINESS_ERROR // 1000九、CheckResult 与 CheckPayload
interface CheckResult<T extends CheckPayload = CheckPayload> {
valid: boolean;
error?: string; // valid=false 时的错误信息
payload?: T; // 携带的业务数据(用户信息等)
}
interface CheckPayload { }在 OnCheck 中返回 payload,后续可通过 checkResult.payload 获取,避免重复查询。
扩展 Payload 类型:
interface ChatPayload extends CheckPayload {
userId: string;
roomId: string;
}
// 在 OnCheck 中
return { valid: true, payload: { userId: "xxx", roomId: "room1" } as ChatPayload };十、完整示例
import {
Server,
HttpRouter, WebSocketRouter, WebSocketClient,
DataBaseRouter, RedisRouter,
OnRegister, CheckResult, CheckPayload,
TokenUtility, TimeUtility, LogUtility,
} from "knight-server";
// ────────── HTTP 路由 ──────────
@OnRegister("/api/user/info")
class UserInfoRouter extends HttpRouter {
async OnCheck(server, request, response): Promise<CheckResult> {
const token = request.headers.authorization?.replace("Bearer ", "");
if (!token) return { valid: false, error: "请先登录" };
try {
const payload = TokenUtility.verify(token, server.config.token.secret);
return { valid: true, payload };
} catch {
return { valid: false, error: "令牌无效" };
}
}
async OnGetHandler(server, checkResult, request, response) {
const uid = (checkResult.payload as any).uid;
const result = await server.database.findOne("users", "id = ?", [uid]);
if (!result.item) {
response.status(404).json({ success: false, error: "用户不存在" });
return;
}
const { password, ...safe } = result.item;
response.json({ success: true, data: safe });
}
}
// ────────── WebSocket 路由 ──────────
@OnRegister("/ws/chat")
class ChatRouter extends WebSocketRouter {
async OnCheck(server, url): Promise<CheckResult> {
const token = url.searchParams.get("token");
if (!token) return { valid: false, error: "缺少令牌" };
try {
const payload = TokenUtility.verify(token, server.config.token.secret);
return { valid: true, payload };
} catch {
return { valid: false, error: "令牌无效" };
}
}
async OnConnect(server, checkResult, client) {
const uid = (checkResult.payload as any).uid;
await server.database.update("users", { online: 1 }, "id = ?", [uid]);
client.OnBroadcast(JSON.stringify({ type: "join", uid }));
}
async OnDisconnect(server, checkResult, client) {
const uid = (checkResult.payload as any).uid;
await server.database.update("users", { online: 0, last_seen: TimeUtility.formatDateTime() }, "id = ?", [uid]);
client.OnBroadcast(JSON.stringify({ type: "leave", uid }));
}
async OnMessage(server, checkResult, client, data) {
client.OnUpdateHeartbeat();
client.OnBroadcast(data, true);
}
}
// ────────── DB 路由 (钩子) ──────────
@OnRegister("/users")
class UsersRouter extends DataBaseRouter {
async OnBeforeInsert(data) {
data.created_at = TimeUtility.formatDateTime();
return { proceed: true, data };
}
async OnAfterSelect(rows) {
return rows.map(({ password, ...rest }) => rest);
}
}
// ────────── Redis 路由 (Pub/Sub) ──────────
@OnRegister("/channel/notifications")
class NotificationRouter extends RedisRouter {
async OnHandler(channel, message) {
LogUtility.LogInfo(`[${channel}] 收到消息:`, message);
}
}
// ────────── 启动 ──────────
(async () => {
const server = Server.Instance;
await server.OnStart({
database: {
host: "localhost",
port: 3306,
user: "root",
password: "your_password",
database: "myapp",
connectionLimit: 10,
tableRelativePath: "./src/database/tables",
},
redis: {
host: "localhost",
port: 6379,
},
http: {
port: 3000,
},
websocket: {
port: 6666,
heartbeat_timeout_interval: 30000,
router_timeout_interval: 60000,
},
token: {
secret: "your-jwt-secret-key",
accessExpiresIn: "2h",
},
serverHost: "127.0.0.1",
});
console.log("[KNIGHT] 服务启动完成");
})();十一、设计要点
- 所有业务 Router 必须用
@OnRegister装饰,否则 Module 无法发现和注册它 - Router 归属由
instanceof判断,继承正确的基类(HttpRouter / WebSocketRouter / DataBaseRouter / RedisRouter)即可 - WebSocket 心跳由协议层 Ping/Pong + 业务层共同维持:客户端发送 Ping 帧时服务端自动回复 Pong 并刷新心跳;在
OnMessage中调用client.OnUpdateHeartbeat()也是备选方案。超时客户端会被自动断开 - Database 的钩子可以阻止或改写数据,返回
{ proceed: false }可拦截操作 server.config在所有 Router 中都可通过this.server访问(继承自AbstractObject)- Express 中间件已内置:CORS、JSON body 解析(50MB)、URL-encoded 解析(50MB)
