基于
net模块的高性能轻量 Node.js Web 框架。
全链路可控 · TypeScript 原生 · Express 风格 API
| 特性 | 说明 |
|---|---|
| 零依赖 | 生产环境零第三方依赖,仅使用 Node.js 内置模块 |
| 内置 HTTP 解析 | 基于 net 模块 + 9 状态机,全链路可控 |
| TypeScript 原生 | 源码即 TypeScript,完整类型导出,无需 @types/nova-http |
| Radix Tree 路由 | O(k) 路由查找(k=路径分段数),支持 :param 和 * 通配符 |
| Keep-Alive 多路复用 | 单 TCP 连接处理多请求,支持流水线,内置 Slowloris 防御 |
| 全链路钩子 | 10 个生命周期钩子,支持异步,覆盖 连接→解析→路由→响应→断开 全链路 |
| 内置中间件 | bodyParser(JSON/urlencoded)、staticFiles(ETag/Range/流式) |
| 流式响应 | sendFile() 支持 HTTP Range 206、ETag 缓存、背压(drain)感知 |
| Express 兼容风格 | app.get/post/use/route(),中间件签名 (req, res, next) |
import { createApp, bodyParser } from 'nova-http';
const app = createApp();
app.use(bodyParser());
app.get('/', (_req, res) => {
res.json({ hello: 'Nova!' });
});
app.get('/hello/:name', (req, res) => {
res.json({ greeting: `你好,${req.params.name}!` });
});
app.listen(3000, '0.0.0.0', () => {
console.log('服务已在 http://localhost:3000 启动');
});依赖要求:
- Node.js >= 18.0.0
- TypeScript >= 5.0(dev)
# npm
npm install nova-http
# 本地开发
npm install
npm run buildTCP 连接到达
│
▼
ConnectionHandler ← 超时管理 / Keep-Alive / 背压
│
▼
BufferReader ← 滑动窗口 Buffer,零拷贝追加
│
▼
HttpParser (9状态机) ← 请求行 / 头部 / 定长/分块 Body
│
▼
Nova._dispatch() ← 全局中间件链
│
▼
Router.find() ← Radix Tree,O(k) 匹配
│
▼
路由处理器 + 局部中间件
│
▼
NovaResponse._flush() ← socket.cork() 聚合写入
│
▼
TCP 响应 / Keep-Alive 复用
IDLE
└=> REQUEST_LINE 解析 "GET /path HTTP/1.1\r\n"
└=> HEADERS 逐行解析请求头,检测 Content-Length / Transfer-Encoding
└=> BODY_DETECT
├=> BODY_FIXED 定长 Body(Content-Length)
├=> CHUNK_SIZE 分块传输第一个 chunk-size 行
│ └=> CHUNK_DATA → CHUNK_SIZE(循环)
└=> DONE 请求解析完成,回调 Nova._dispatch()
function createApp(config?: NovaConfig): NovaNovaConfig 选项:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
maxBodySize |
number |
1048576 (1MB) |
请求体最大字节数,超出则返回 413 |
keepAliveTimeout |
number |
65000 |
Keep-Alive 空闲超时(毫秒) |
headersTimeout |
number |
60000 |
接收完整请求头的超时(毫秒),防 Slowloris |
requestTimeout |
number |
600000 |
单请求最大处理时间(毫秒) |
trustProxy |
boolean |
false |
信任 X-Forwarded-For 头,影响 req.ip |
app.get(path, ...handlers)
app.post(path, ...handlers)
app.put(path, ...handlers)
app.patch(path, ...handlers)
app.delete(path, ...handlers)
app.head(path, ...handlers)
app.options(path, ...handlers)
app.all(path, ...handlers) // 匹配所有 HTTP 方法路径语法:
| 模式 | 示例 | 说明 |
|---|---|---|
| 静态路径 | /users/profile |
精确匹配 |
| 参数路径 | /users/:id |
匹配单段,值存入 req.params.id |
| 通配符 | /static/* |
匹配剩余所有路径段,值存入 req.params['*'] |
优先级: 静态 > 参数 > 通配符
app.route('/users/:id')
.get((req, res) => { /* 查询 */ })
.put((req, res) => { /* 更新 */ })
.delete((req, res) => { /* 删除 */ });// 全局中间件
app.use(middleware)
// 路径前缀中间件
app.use('/api', middleware)
// 多个中间件
app.use('/api', authMiddleware(), logMiddleware())中间件签名:
// 普通中间件
type Middleware = (req: NovaRequest, res: NovaResponse, next: NextFunction) => void | Promise<void>
// 错误处理中间件(4 个参数,必须放在所有普通中间件之后)
type ErrorMiddleware = (err: Error, req: NovaRequest, res: NovaResponse, next: NextFunction) => void | Promise<void>示例:
// 请求日志
app.use((req, _res, next) => {
console.log(`${req.method} ${req.pathname}`);
next();
});
// 错误处理
app.use((err: Error, _req, res, _next) => {
res.status(500).json({ error: err.message });
});| 属性 | 类型 | 说明 |
|---|---|---|
method |
HttpMethod |
HTTP 方法,如 'GET' |
path |
string |
原始路径字符串(含查询字符串) |
pathname |
string |
不含查询字符串的路径 |
httpVersion |
string |
'1.0' 或 '1.1' |
headers |
Map<string, string> |
请求头(键已小写化) |
body |
Buffer |
原始请求体 Buffer |
bodyParsed |
unknown |
bodyParser() 解析后的结构化数据 |
params |
Record<string, string> |
路径参数,如 { id: '42' } |
query |
URLSearchParams |
查询字符串(惰性解析) |
cookies |
Record<string, string> |
Cookie 键值对(惰性解析) |
ip |
string |
客户端 IP(trustProxy 时读 X-Forwarded-For) |
context |
Record<string, unknown> |
中间件间共享的请求上下文 |
keepAlive |
boolean |
是否为 Keep-Alive 连接 |
socket |
net.Socket |
底层 TCP socket |
// 状态码
res.status(404)
// 响应头
res.setHeader('X-Custom', 'value')
res.getHeader('content-type')
res.removeHeader('x-powered-by')
// 发送响应
res.send(data: string | Buffer) // 自动推断 Content-Type
res.json(data: unknown) // application/json
res.html(html: string) // text/html
res.end() // 无 Body
// 重定向
res.redirect(location: string, status?: 301 | 302 | 307 | 308)
// 文件发送(支持 Range 206、ETag 缓存、流式传输)
await res.sendFile(absolutePath: string)app.addHook(hookName, handler)
app.removeHook(hookName, handler)可用钩子:
| 钩子名 | 触发时机 | Handler 签名 |
|---|---|---|
onConnect |
TCP 连接建立 | (socket: net.Socket) => void |
onDisconnect |
TCP 连接断开 | (socket: net.Socket) => void |
onRequest |
HTTP 请求解析完成,进入中间件前 | (req: NovaRequest) => void |
onRoute |
路由匹配成功后 | (req: NovaRequest, match: RouteMatch) => void |
onBodyParsed |
bodyParser() 完成解析后 |
(req: NovaRequest) => void |
onResponse |
响应发送完成 | (req: NovaRequest, res: NovaResponse) => void |
onError |
中间件链抛出未捕获异常 | (err: Error, req: NovaRequest) => void |
onNotFound |
路由未命中 | (req: NovaRequest) => void |
onListen |
服务开始监听 | (info: { host: string; port: number }) => void |
onClose |
服务关闭 | () => void |
示例:
// 全链路耗时统计
app.addHook('onRequest', (req) => {
req.context['_start'] = process.hrtime.bigint();
});
app.addHook('onResponse', (req) => {
const ns = process.hrtime.bigint() - (req.context['_start'] as bigint);
console.log(`${req.pathname} 耗时 ${Number(ns) / 1e6}ms`);
});
// 或直接使用内置插件
const timer = createRequestTimer();
app.addHook('onRequest', timer.onRequest);
app.addHook('onResponse', timer.onResponse);解析 application/json 和 application/x-www-form-urlencoded 请求体。
app.use(bodyParser({
maxBodySize: 1 * 1024 * 1024, // 1MB,默认与 app 配置相同
strict: true, // JSON 根值必须是 object/array
}));解析结果写入 req.bodyParsed。
静态文件服务,支持 ETag、Range 206、Gzip(由 Content-Negotiation 决定)。
app.use('/static', staticFiles('./public', {
dotfiles: 'ignore', // 'ignore' | 'allow' | 'deny'
maxAge: 3600, // Cache-Control: max-age=3600(秒)
index: 'index.html', // 目录默认索引文件
}));# 创建最小化项目
npx nova-http create my-app
# 创建完整 API 项目
npx nova-http create my-api --template api
# 创建 JavaScript 项目
npx nova-http create my-app --lang js
# 强制覆盖已存在目录
npx nova-http create my-app --force
# initializer 入口
npx create-nova-http my-app可用模板:
| 模板 | 描述 |
|---|---|
minimal |
最小化 Hello World,适合快速体验 |
api |
完整 CRUD API + 路由/中间件/身份验证示例 |
本地前验:
npm run verify:create这个脚本会验证:
- 打包后的 CLI 是否仍可执行
nova-http create ...与create-nova-http ...两种入口是否都能生成项目minimal/api模板是否都能正确替换变量并通过本地 TypeScript 构建- 重复创建时不带
--force会失败,带--force可覆盖
说明:运行时包名为 nova-http,对应 initializer 包名为 create-nova-http,因此用户侧命令应为 npm create nova-http。
nova/
├== src/
│ ├== core/
│ │ ├== BufferReader.ts 滑动窗口 TCP Buffer 读取器
│ │ ├== HttpParser.ts 9 状态机 HTTP/1.1 解析器
│ │ ├== NovaRequest.ts 请求对象(惰性属性)
│ │ ├== NovaResponse.ts 响应对象(直写 net.Socket)
│ │ ├== ConnectionHandler.ts TCP 连接生命周期管理器
│ │ ├== Router.ts Radix Tree 路由器
│ │ ├== MiddlewareChain.ts 异步中间件组合器
│ │ ├== Hooks.ts EventEmitter 生命周期钩子
│ │ └== Nova.ts 主应用类
│ ├== middlewares/
│ │ ├== bodyParser.ts 请求体解析中间件
│ │ ├== staticFiles.ts 静态文件中间件
│ │ └== index.ts 中间件桶导出
│ └== index.ts 包主入口(公共 API)
├== cli/
│ ├== nova.ts CLI 工具入口(nova-http create)
│ ├== create-nova.ts initializer 入口(create-nova-http)
│ ├== shared.ts CLI 共享实现
│ └== templates/
│ ├== minimal/ 最小化项目模板
│ └== api/ 完整 API 项目模板
├== package.json
├== tsconfig.json
└== tsconfig.build.json
为什么使用 net 模块而非 http 模块?
Node.js http 模块基于 llhttp(C++ 解析器),无法从 JavaScript 层控制解析细节。net 模块提供原始 TCP 流,让 Nova 对以下层面拥有完整控制权:
- 超时粒度:可区分 "接收头部超时"(防 Slowloris)和 "请求处理超时"
- Keep-Alive 策略:自定义空闲超时、连接复用策略
- 请求走私防御:主动检测 CL+TE 冲突,立即返回 400
- 背压感知:直接监听
socket.drain事件,流式传输时无内存堆积 - 性能调优:
socket.cork()/uncork()减少系统调用,TCP_NODELAY消除 Nagle 延迟
const app = createApp({
headersTimeout: 30_000, // 降低以更快丢弃慢连接
keepAliveTimeout: 30_000, // 根据客户端行为调整
requestTimeout: 120_000, // 接口最长处理时间
});// 上传接口设较大限制
app.post('/upload', bodyParser({ maxBodySize: 50 * 1024 * 1024 }), handler);
// 默认限制 1MB 防止 OOM
const app = createApp({ maxBodySize: 1 * 1024 * 1024 });钩子为 fire-and-forget(非阻塞),中间件会阻塞请求链路。对于日志、指标采集,优先使用钩子:
// 推荐:用钩子做指标采集
app.addHook('onResponse', (req, res) => {
metrics.record(req.method, req.pathname, res._statusCode);
});
// 谨慎:用中间件做阻塞式日志(会增加 P99 延迟)
app.use(async (req, res, next) => {
await writeToLogFile(...); // I/O 操作
next();
});app.use(staticFiles('./public', {
maxAge: 86400, // 强缓存 1 天(生产环境)
dotfiles: 'ignore',
}));app.get('/download/:file', async (req, res) => {
// sendFile 自动处理 Range、ETag、drain 背压
await res.sendFile(path.join(STORAGE_DIR, req.params['file']!));
});import type { Middleware } from 'nova-http';
export function rateLimiter(maxRpm: number): Middleware {
const counts = new Map<string, number>();
setInterval(() => counts.clear(), 60_000);
return (req, res, next) => {
const ip = req.ip;
const count = (counts.get(ip) ?? 0) + 1;
counts.set(ip, count);
if (count > maxRpm) {
res.status(429).json({ error: '请求过于频繁,请稍后重试' });
return;
}
next();
};
}import type { Nova } from 'nova-http';
export function metricsPlugin(app: Nova): void {
const counters = { total: 0, errors: 0 };
app.addHook('onRequest', () => { counters.total++; });
app.addHook('onError', () => { counters.errors++; });
// 暴露指标端点
app.get('/metrics', (_req, res) => {
res.json(counters);
});
}
// 使用
metricsPlugin(app);const usersApp = createApp();
usersApp.get('/', listUsers);
usersApp.post('/', createUser);
usersApp.get('/:id', getUser);
// 挂载到 /api/users
app.use('/api/users', usersApp);MIT © Owl23007