@lzpong/httpm 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +23 -20
  2. package/httpm.js +157 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,6 +66,9 @@ app.all('/any', (req, res) => res.json({ method: req.method }));
66
66
 
67
67
  路由匹配优先级:精准静态路由 > 动态参数路由 > ALL 通用路由 > 静态文件服务。
68
68
 
69
+ - **HEAD 请求**:自动匹配 GET 路由,仅返回响应头(Express 兼容)
70
+ - **OPTIONS 请求**:自动返回 `Allow` 头和 CORS 预检响应,动态查询该路径支持的方法
71
+
69
72
  路由处理器返回 `false` 时,请求进入静态文件兜底:
70
73
 
71
74
  ```javascript
@@ -119,10 +122,10 @@ app.use((err, req, res, next) => {
119
122
  | `req.body` | 请求体(JSON/URL-encoded 自动解析) |
120
123
  | `req.formData` | 表单数据(multipart 解析后) |
121
124
  | `req.cookies` | Cookie 对象 |
125
+ | `req.get(name)` | 获取请求头(不区分大小写) |
122
126
  | `req.headers` | 请求头对象 |
123
127
  | `req.ip` | 客户端 IP |
124
128
  | `req.protocol` | 协议(http/https) |
125
- | `req.get(name)` | 获取请求头 |
126
129
 
127
130
  ## 响应对象 (res)
128
131
 
@@ -130,15 +133,16 @@ app.use((err, req, res, next) => {
130
133
  |------|------|
131
134
  | `res.status(code)` | 设置状态码,链式调用 |
132
135
  | `res.json(obj)` | 发送 JSON 响应 |
133
- | `res.send(data)` | 发送响应(自动识别类型) |
136
+ | `res.send(data)` | 发送响应(自动识别类型;`null`→`"null"`,`undefined`→空响应) |
134
137
  | `res.sendFile(path, opts)` | 发送文件 |
135
138
  | `res.download(path, name)` | 下载文件 |
136
- | `res.redirect([code,] url)` | 重定向(默认 302,可指定状态码) |
139
+ | `res.redirect([code,] url)` | 重定向,兼容 Express 签名:`redirect(url)` 默认 302,`redirect(status, url)` 指定状态码 |
137
140
  | `res.cookie(name, value, opts)` | 设置 Cookie |
138
141
  | `res.clearCookie(name, opts)` | 清除 Cookie |
139
- | `res.set(name, value)` / `res.setHeader()` | 设置响应头 |
142
+ | `res.set(name, value)` / `res.setHeader()` | 设置响应头(set 支持对象批量) |
140
143
  | `res.get(name)` / `res.getHeader()` | 获取响应头 |
141
- | `res.type(type)` | 设置 Content-Type |
144
+ | `res.type(type)` | 设置 Content-Type(支持简写:html→text/html) |
145
+ | `res.on(event, fn)` | 监听响应事件(finish/close 等) |
142
146
  | `res.sse()` | 创建 SSE 实例 |
143
147
 
144
148
  ### Cookie 签名
@@ -199,31 +203,28 @@ app.post('/upload', (req, res) => {
199
203
  ### 简化注册
200
204
 
201
205
  ```javascript
206
+ // 静态路径
202
207
  app.ws('/chat', (ws, req) => {
203
208
  ws.send('Welcome!');
204
209
 
205
210
  ws.on('text', msg => {
206
- app.wsServer.broadcast('/chat', msg, ws.id);
207
- });
208
-
209
- ws.on('binary', data => {
210
- // 处理二进制数据
211
+ app.wss.broadcast('/chat', msg, ws);
211
212
  });
213
+ });
212
214
 
213
- ws.on('close', (code, reason) => {
214
- console.log('Client disconnected', code, reason);
215
- });
215
+ // 动态参数路径
216
+ app.ws('/chat/:room', (ws, req) => {
217
+ console.log('Room:', req.params.room);
218
+ ws.send(`Welcome to room ${req.params.room}!`);
216
219
  });
217
220
  ```
218
221
 
219
222
  ### WebSocketServer API
220
223
 
221
224
  ```javascript
222
- const wss = app.wsServer;
223
-
224
- wss.broadcast('/chat', 'Hello everyone'); // 按路径分组广播
225
- wss.broadcast('/chat', 'Hello', excludeWsId); // 排除指定连接
226
- wss.getConnections(); // 获取所有连接
225
+ app.wss.broadcast('/chat', 'Hello everyone'); // 按路径分组广播
226
+ app.wss.broadcast('/chat', 'Hello', ws); // 排除指定连接(传 ws 对象)
227
+ app.wss.getConnections(); // 获取所有连接
227
228
  ```
228
229
 
229
230
  ### WebSocket 事件
@@ -233,7 +234,7 @@ wss.getConnections(); // 获取所有连接
233
234
  | `data` | 接收消息(`{ type: 'text'/'binary', data }`) |
234
235
  | `text` | 接收文本消息 |
235
236
  | `binary` | 接收二进制消息 |
236
- | `close` | 连接关闭(回调参数:code, reason |
237
+ | `close` | 连接关闭,回调参数 `(code, reason)` |
237
238
  | `error` | 连接错误 |
238
239
 
239
240
  ### data 事件使用
@@ -357,6 +358,8 @@ const app = httpm({ svrPort: 3000 }); // svrPort=3000, enableGzip=true(来自app
357
358
  | `cookieParserSecret` | `null` | Cookie 签名密钥 |
358
359
  | `wsMaxPayload` | `104857600` | WebSocket 最大帧负载(100MB) |
359
360
  | `wsAllowedOrigins` | `null` | WebSocket 允许的 Origin 列表 |
361
+ | `wsHeartbeatInterval` | `30000` | WebSocket 心跳检测间隔(毫秒) |
362
+ | `wsHeartbeatTimeout` | `30000` | WebSocket 心跳超时时间(毫秒) |
360
363
 
361
364
  ## HTTPS / HTTP2
362
365
 
@@ -410,7 +413,7 @@ httpm.isPathSafe
410
413
  httpm.generateETag
411
414
  httpm.parseRange
412
415
  httpm.escapeHtml
413
- httpm.WebSocketHandShark
416
+ httpm.WebSocketHandShak
414
417
  ```
415
418
 
416
419
  ## 运行要求
package/httpm.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
3
3
  *
4
4
  * @name httpm
5
- * @version 1.1.0
5
+ * @version 1.2.0
6
6
  * @description 兼容 Express API,内置路由、中间件、静态文件服务、
7
7
  * WebSocket、SSE、流式上传、日志系统等功能
8
8
  * @license MIT
@@ -301,7 +301,6 @@ class Logger {
301
301
  const year = now.getFullYear().toString();
302
302
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
303
303
  const day = now.getDate().toString().padStart(2, '0');
304
- const dateStr = `${day}`;
305
304
 
306
305
  // 同一天复用同一个流
307
306
  const streamKey = `${year}-${month}-${day}`;
@@ -543,6 +542,20 @@ class Request {
543
542
  return (this._req.socket?.encrypted || this._req.connection?.encrypted) ? 'https' : 'http';
544
543
  }
545
544
 
545
+ /**
546
+ * 获取请求头(Express 兼容),不区分大小写
547
+ * req.get('Content-Type') / req.get('content-type')
548
+ */
549
+ get(name) {
550
+ if (!name) return undefined;
551
+ const lower = name.toLowerCase();
552
+ // 特殊别名
553
+ if (lower === 'referrer' || lower === 'referer') {
554
+ return this._req.headers['referer'] || this._req.headers['referrer'];
555
+ }
556
+ return this._req.headers[lower];
557
+ }
558
+
546
559
  /**
547
560
  * 读取请求体原始数据(带超时保护和大小限制)
548
561
  */
@@ -621,6 +634,59 @@ class Response {
621
634
  return this;
622
635
  }
623
636
 
637
+ /**
638
+ * 设置响应头(Express 兼容),支持单键和对象批量设置
639
+ * res.set('Content-Type', 'text/html')
640
+ * res.set({ 'Content-Type': 'text/html', 'X-Custom': 'value' })
641
+ */
642
+ set(name, value) {
643
+ if (typeof name === 'object') {
644
+ for (const key of Object.keys(name)) {
645
+ this._res.setHeader(key, name[key]);
646
+ }
647
+ } else {
648
+ this._res.setHeader(name, value);
649
+ }
650
+ return this;
651
+ }
652
+
653
+ /**
654
+ * 设置 Content-Type(Express 兼容),支持简写
655
+ * res.type('html') → text/html
656
+ * res.type('.html') → text/html
657
+ * res.type('text/html') → text/html
658
+ */
659
+ type(contentType) {
660
+ // 已是完整 MIME 类型,直接设置
661
+ if (contentType.includes('/')) {
662
+ this.setHeader('Content-Type', contentType);
663
+ return this;
664
+ }
665
+ // 简写或带点号扩展名,通过 getMimeType 解析
666
+ const ext = contentType.startsWith('.') ? contentType : '.' + contentType;
667
+ const mime = getMimeType(ext);
668
+ this.setHeader('Content-Type', mime);
669
+ return this;
670
+ }
671
+
672
+ /**
673
+ * 代理原生 ServerResponse 事件监听(Express 兼容)
674
+ * res.on('finish', fn) / res.on('close', fn)
675
+ */
676
+ on(event, listener) {
677
+ this._res.on(event, listener);
678
+ return this;
679
+ }
680
+
681
+ /**
682
+ * 获取响应头(Express 兼容),不区分大小写
683
+ * res.get('Content-Type')
684
+ */
685
+ get(name) {
686
+ if (!name) return undefined;
687
+ return this._res.getHeader(name);
688
+ }
689
+
624
690
  /**
625
691
  * 设置 HTTP 状态码,支持链式调用
626
692
  */
@@ -643,11 +709,18 @@ class Response {
643
709
  * 通用输出,支持字符串、HTML、Buffer、对象
644
710
  */
645
711
  send(data) {
646
- if (data === undefined || data === null) {
712
+ // Express 兼容:null 序列化为 "null",undefined 返回空响应
713
+ if (data === undefined) {
647
714
  this.setHeader('Content-Length', 0);
648
715
  this._send('');
649
716
  return;
650
717
  }
718
+ if (data === null) {
719
+ this.setHeader('Content-Type', 'text/html; charset=utf-8');
720
+ this.setHeader('Content-Length', 4);
721
+ this._send('null');
722
+ return;
723
+ }
651
724
  if (Buffer.isBuffer(data)) {
652
725
  this.setHeader('Content-Type', 'application/octet-stream');
653
726
  this.setHeader('Content-Length', data.length);
@@ -890,6 +963,7 @@ class SSE {
890
963
  const onClose = () => {
891
964
  this.connected = false;
892
965
  };
966
+ this._onClose = onClose;
893
967
  res.on('close', onClose);
894
968
  // HTTP/1.1 兼容:aborted 事件在请求被客户端中断时触发
895
969
  res.on('aborted', onClose);
@@ -934,11 +1008,13 @@ class SSE {
934
1008
  }
935
1009
 
936
1010
  /**
937
- * 主动关闭 SSE 连接
1011
+ * 主动关闭 SSE 连接,移除监听器防止内存泄漏
938
1012
  */
939
1013
  close() {
940
1014
  if (!this.connected) return;
941
1015
  this.connected = false;
1016
+ this._res.removeListener('close', this._onClose);
1017
+ this._res.removeListener('aborted', this._onClose);
942
1018
  this._res.end();
943
1019
  }
944
1020
  }
@@ -1109,7 +1185,8 @@ class WebSocket {
1109
1185
  return;
1110
1186
  }
1111
1187
  // 非关闭握手状态:回复 Close 帧后关闭
1112
- this._sendCloseFrame(code);
1188
+ // RFC 6455 Section 7.4.1: 1005/1006 状态码不得在 Close 帧中发送
1189
+ this._sendCloseFrame(code === 1005 || code === 1006 ? undefined : code);
1113
1190
  this.connected = false;
1114
1191
  this._emitClose(code, reason);
1115
1192
  return;
@@ -1212,6 +1289,8 @@ class WebSocket {
1212
1289
  }
1213
1290
  } catch (e) {
1214
1291
  this.connected = false;
1292
+ // 发送失败时触发 error 事件,便于用户感知和处理
1293
+ this._emit('error', e);
1215
1294
  }
1216
1295
  }
1217
1296
 
@@ -1309,7 +1388,7 @@ class WebSocket {
1309
1388
  /**
1310
1389
  * WebSocket 握手辅助函数:计算 Sec-WebSocket-Accept 值
1311
1390
  */
1312
- function WebSocketHandShark(key) {
1391
+ function WebSocketHandShak(key) {
1313
1392
  return crypto.createHash('sha1')
1314
1393
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
1315
1394
  .digest('base64');
@@ -1350,7 +1429,7 @@ class WebSocketServer {
1350
1429
  }
1351
1430
  }
1352
1431
 
1353
- const accept = WebSocketHandShark(key);
1432
+ const accept = WebSocketHandShak(key);
1354
1433
 
1355
1434
  // 发送握手响应
1356
1435
  const responseHeaders = [
@@ -1444,7 +1523,7 @@ class WebSocketServer {
1444
1523
  /**
1445
1524
  * 按路径广播消息
1446
1525
  */
1447
- broadcastTo(pathStr, data, exclude = null) {
1526
+ broadcast(pathStr, data, exclude = null) {
1448
1527
  const group = this.groups.get(pathStr);
1449
1528
  if (!group) return;
1450
1529
  for (const ws of group) {
@@ -1457,7 +1536,7 @@ class WebSocketServer {
1457
1536
  /**
1458
1537
  * 全局广播消息
1459
1538
  */
1460
- broadcast(data, exclude = null) {
1539
+ broadcastAll(data, exclude = null) {
1461
1540
  for (const ws of this.connections.values()) {
1462
1541
  if (ws.connected && ws !== exclude) {
1463
1542
  ws.send(data);
@@ -1526,8 +1605,8 @@ function bodyParser(options = {}) {
1526
1605
  const boundary = _extractBoundary(contentType);
1527
1606
  if (boundary) {
1528
1607
  _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next);
1529
- // 仅 multipart 需要临时文件清理
1530
- res._res.on('finish', () => {
1608
+ // 仅 multipart 需要临时文件清理(使用 res.on 保持封装一致性)
1609
+ res.on('finish', () => {
1531
1610
  _cleanupTempFiles(req._tempFiles);
1532
1611
  });
1533
1612
  } else {
@@ -1762,7 +1841,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1762
1841
  currentFile.stream = fs.createWriteStream(currentFile.path);
1763
1842
  }
1764
1843
  currentFile.stream.write(trimmedData);
1765
- // 结束写入并等待刷盘完成
1844
+ // 结束写入(注意:stream.end 为异步操作,极端情况如磁盘满时可能写入不完整,
1845
+ // 但 fileInfo 仍会被记录。若需严格保证写入完整性,需改为异步流程)
1766
1846
  currentFile.stream.end();
1767
1847
  fileSize += trimmedData.length;
1768
1848
  currentFile.size = fileSize;
@@ -2130,7 +2210,7 @@ class Application extends Router {
2130
2210
  const status = err.status || 500;
2131
2211
  const msg = err.message || 'Internal Server Error';
2132
2212
  this._logger.error(`[${status}] ${req.method} ${req.path} - ${msg}`);
2133
- if (!res.headersSent && !res._res.headersSent) {
2213
+ if (!res.headersSent) {
2134
2214
  res.status(status).json({ error: msg, status });
2135
2215
  }
2136
2216
  }
@@ -2148,8 +2228,9 @@ class Application extends Router {
2148
2228
  // CORS 预检响应
2149
2229
  this._handleCORS(req, res);
2150
2230
  } else if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') {
2151
- // 已知方法但无匹配路由,返回 405 Method Not Allowed
2152
- res.status(405).json({ error: 'Method Not Allowed', status: 405 });
2231
+ // 已知方法但无匹配路由,返回 405 Method Not Allowed(RFC 7231 要求必须包含 Allow 头)
2232
+ const allowed = this._getAllowedMethods(req.path);
2233
+ res.set('Allow', allowed.join(', ')).status(405).json({ error: 'Method Not Allowed', status: 405 });
2153
2234
  } else {
2154
2235
  // 其他未知方法返回 404
2155
2236
  res.status(404).json({ error: 'Not Found', status: 404 });
@@ -2157,7 +2238,7 @@ class Application extends Router {
2157
2238
  }
2158
2239
 
2159
2240
  /**
2160
- * CORS 预检响应
2241
+ * CORS 预检响应(动态查询该路径支持的 HTTP 方法)
2161
2242
  */
2162
2243
  _handleCORS(req, res) {
2163
2244
  const cors = this.settings.cors;
@@ -2165,17 +2246,53 @@ class Application extends Router {
2165
2246
  res.status(204)._send('');
2166
2247
  return;
2167
2248
  }
2249
+ // 动态查找该路径匹配的所有 HTTP 方法
2250
+ const allowedMethods = this._getAllowedMethods(req.path);
2168
2251
  const origin = typeof cors.origin === 'string' ? cors.origin : '*';
2169
2252
  res.setHeader('Access-Control-Allow-Origin', origin);
2170
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
2253
+ // 动态设置允许的方法列表,而非硬编码
2254
+ res.setHeader('Access-Control-Allow-Methods', allowedMethods.join(', '));
2171
2255
  res.setHeader('Access-Control-Allow-Headers', cors.headers || 'Content-Type, Authorization');
2172
2256
  res.setHeader('Access-Control-Max-Age', parseInt(cors.maxAge, 10) || 86400);
2257
+ // Allow 头告知客户端该路径实际支持的方法
2258
+ res.setHeader('Allow', allowedMethods.join(', '));
2173
2259
  if (cors.credentials) {
2174
2260
  res.setHeader('Access-Control-Allow-Credentials', 'true');
2175
2261
  }
2176
2262
  res.status(204)._send('');
2177
2263
  }
2178
2264
 
2265
+ /**
2266
+ * 查询指定路径支持的所有 HTTP 方法(含隐式 HEAD)
2267
+ * 用于 OPTIONS Allow 头和 405 Method Not Allowed 响应
2268
+ */
2269
+ _getAllowedMethods(pathname) {
2270
+ const methods = new Set();
2271
+ // 遍历所有已注册方法的路由,查找匹配该路径的方法
2272
+ for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) {
2273
+ const routes = this.routes[method] || [];
2274
+ for (const route of routes) {
2275
+ if (route.pattern.exec(pathname)) {
2276
+ methods.add(method);
2277
+ break;
2278
+ }
2279
+ }
2280
+ }
2281
+ // 检查 ALL 中间件路由
2282
+ const allRoutes = this.routes['ALL'] || [];
2283
+ for (const route of allRoutes) {
2284
+ if (route.pattern.exec(pathname)) {
2285
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].forEach(m => methods.add(m));
2286
+ break;
2287
+ }
2288
+ }
2289
+ // GET 路由隐式支持 HEAD
2290
+ if (methods.has('GET')) methods.add('HEAD');
2291
+ // OPTIONS 始终可用(CORS 预检)
2292
+ methods.add('OPTIONS');
2293
+ return [...methods].sort();
2294
+ }
2295
+
2179
2296
  /**
2180
2297
  * 静态文件服务
2181
2298
  */
@@ -2267,10 +2384,14 @@ class Application extends Router {
2267
2384
  */
2268
2385
  _renderDirectoryHTML(requestPath, items) {
2269
2386
  // requestPath 已去掉前导 /,空字符串表示根目录
2270
- const parentPath = requestPath ? path.dirname(requestPath) : '/';
2271
- let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(requestPath)}</title>`;
2387
+ // path.dirname('subdir') 返回 '.',应映射为根目录 ''
2388
+ let parentPath = requestPath ? path.dirname(requestPath) : '/';
2389
+ if (parentPath === '.') parentPath = '';
2390
+ // 根目录时显示 '/',否则显示请求路径
2391
+ const displayPath = requestPath || '/';
2392
+ let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
2272
2393
  html += `<style>body{font-family:-apple-system,sans-serif;margin:20px;background:#f5f5f5}h1{font-size:18px;color:#333}table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.1)}th{text-align:left;padding:10px 12px;background:#f8f8f8;border-bottom:2px solid #ddd;font-size:13px;color:#666}td{padding:8px 12px;border-bottom:1px solid #eee;font-size:13px}a{color:#0066cc;text-decoration:none}a:hover{text-decoration:underline}.dir{font-weight:bold}.size{color:#999}</style>`;
2273
- html += `</head><body><h1>Directory: ${escapeHtml(requestPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
2394
+ html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
2274
2395
 
2275
2396
  // 父目录链接(根目录时不显示)
2276
2397
  if (requestPath && requestPath !== '/') {
@@ -2278,7 +2399,8 @@ class Application extends Router {
2278
2399
  }
2279
2400
 
2280
2401
  for (const item of items) {
2281
- const href = path.join(requestPath, item.name);
2402
+ // 使用 '/' 拼接路径,防止 Windows 上 path.join 产生反斜杠导致 href 失效
2403
+ const href = requestPath ? requestPath + '/' + item.name : item.name;
2282
2404
  const name = item.isDirectory ? item.name + '/' : item.name;
2283
2405
  const size = item.isDirectory ? '-' : fmtSize(item.size);
2284
2406
  const cls = item.isDirectory ? 'dir' : '';
@@ -2297,28 +2419,23 @@ class Application extends Router {
2297
2419
  if (!ws) return;
2298
2420
 
2299
2421
  // 匹配 app.ws() 注册的处理器(支持动态参数路径)
2422
+ // _compilePath 始终返回 pattern,静态和动态路径均通过正则精确匹配
2300
2423
  if (this._wsHandlers) {
2301
2424
  const pathname = parseUrl(req.url).pathname;
2302
2425
  for (const entry of this._wsHandlers) {
2303
2426
  let matched = false;
2304
2427
  let params = {};
2305
- if (entry.pattern) {
2306
- // 动态路径:正则匹配
2307
- const m = entry.pattern.exec(pathname);
2308
- if (m) {
2309
- matched = true;
2310
- entry.params.forEach((name, i) => {
2311
- try {
2312
- params[name] = decodeURIComponent(m[i + 1]);
2313
- } catch (e) {
2314
- // 非法 URI 编码,保留原始值
2315
- params[name] = m[i + 1];
2316
- }
2317
- });
2318
- }
2319
- } else {
2320
- // 静态路径:精确或前缀匹配
2321
- matched = pathname === entry.path || pathname.startsWith(entry.path + '/');
2428
+ const m = entry.pattern.exec(pathname);
2429
+ if (m) {
2430
+ matched = true;
2431
+ entry.params.forEach((name, i) => {
2432
+ try {
2433
+ params[name] = decodeURIComponent(m[i + 1]);
2434
+ } catch (e) {
2435
+ // 非法 URI 编码,保留原始值
2436
+ params[name] = m[i + 1];
2437
+ }
2438
+ });
2322
2439
  }
2323
2440
  if (matched) {
2324
2441
  // 将动态参数挂载到 req 上
@@ -2506,7 +2623,8 @@ httpm.fmtTime = fmtTime;
2506
2623
  httpm.isPathSafe = isPathSafe;
2507
2624
  httpm.generateETag = generateETag;
2508
2625
  httpm.parseRange = parseRange;
2509
- httpm.WebSocketHandShark = WebSocketHandShark;
2626
+ httpm.WebSocketHandShak = WebSocketHandShak;
2627
+ httpm.escapeHtml = escapeHtml;
2510
2628
 
2511
2629
  /**
2512
2630
  * parseQuery:独立导出的 Query 解析函数(复用内部 _parseQueryString)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpong/httpm",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库,兼容 Express API",
5
5
  "main": "httpm.js",
6
6
  "files": [