request-iframe 0.0.3 → 0.0.5

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 (69) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +177 -24
  4. package/README.md +237 -19
  5. package/library/__tests__/channel.test.ts +16 -4
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +22 -0
  8. package/library/__tests__/dispatcher.test.ts +8 -4
  9. package/library/__tests__/requestIframe.test.ts +1243 -87
  10. package/library/__tests__/stream.test.ts +92 -16
  11. package/library/__tests__/utils.test.ts +41 -1
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +1 -0
  14. package/library/constants/index.d.ts +2 -0
  15. package/library/constants/index.d.ts.map +1 -1
  16. package/library/constants/index.js +3 -1
  17. package/library/constants/messages.d.ts +3 -0
  18. package/library/constants/messages.d.ts.map +1 -1
  19. package/library/constants/messages.js +3 -0
  20. package/library/core/client-server.d.ts +4 -0
  21. package/library/core/client-server.d.ts.map +1 -1
  22. package/library/core/client-server.js +45 -22
  23. package/library/core/client.d.ts +36 -4
  24. package/library/core/client.d.ts.map +1 -1
  25. package/library/core/client.js +508 -285
  26. package/library/core/request.d.ts +3 -1
  27. package/library/core/request.d.ts.map +1 -1
  28. package/library/core/request.js +2 -1
  29. package/library/core/response.d.ts +26 -4
  30. package/library/core/response.d.ts.map +1 -1
  31. package/library/core/response.js +192 -112
  32. package/library/core/server.d.ts +13 -0
  33. package/library/core/server.d.ts.map +1 -1
  34. package/library/core/server.js +221 -6
  35. package/library/index.d.ts +2 -1
  36. package/library/index.d.ts.map +1 -1
  37. package/library/index.js +39 -3
  38. package/library/message/channel.d.ts +2 -2
  39. package/library/message/channel.d.ts.map +1 -1
  40. package/library/message/channel.js +5 -1
  41. package/library/message/dispatcher.d.ts +2 -2
  42. package/library/message/dispatcher.d.ts.map +1 -1
  43. package/library/message/dispatcher.js +6 -5
  44. package/library/stream/index.d.ts +11 -1
  45. package/library/stream/index.d.ts.map +1 -1
  46. package/library/stream/index.js +21 -3
  47. package/library/stream/types.d.ts +2 -2
  48. package/library/stream/types.d.ts.map +1 -1
  49. package/library/stream/writable-stream.d.ts +1 -1
  50. package/library/stream/writable-stream.d.ts.map +1 -1
  51. package/library/stream/writable-stream.js +87 -47
  52. package/library/types/index.d.ts +29 -5
  53. package/library/types/index.d.ts.map +1 -1
  54. package/library/utils/debug.d.ts.map +1 -1
  55. package/library/utils/debug.js +6 -2
  56. package/library/utils/error.d.ts +21 -0
  57. package/library/utils/error.d.ts.map +1 -0
  58. package/library/utils/error.js +34 -0
  59. package/library/utils/index.d.ts +21 -0
  60. package/library/utils/index.d.ts.map +1 -1
  61. package/library/utils/index.js +141 -2
  62. package/library/utils/path-match.d.ts +16 -0
  63. package/library/utils/path-match.d.ts.map +1 -1
  64. package/library/utils/path-match.js +65 -0
  65. package/package.json +2 -1
  66. package/react/library/__tests__/index.test.tsx +44 -22
  67. package/react/library/index.d.ts.map +1 -1
  68. package/react/library/index.js +81 -23
  69. package/react/package.json +7 -0
package/QUICKSTART.CN.md CHANGED
@@ -172,22 +172,49 @@ server.on('/api/download', async (req, res) => {
172
172
 
173
173
  // Client 端
174
174
  const response = await client.send('/api/download', {});
175
- if (response.fileData) {
175
+ if (response.data instanceof File || response.data instanceof Blob) {
176
+ const file = response.data instanceof File ? response.data : null;
177
+ const fileName = file?.name || 'download';
178
+
176
179
  // 创建下载链接
177
- const blob = new Blob(
178
- [atob(response.fileData.content)],
179
- { type: response.fileData.mimeType }
180
- );
181
- const url = URL.createObjectURL(blob);
182
-
180
+ const url = URL.createObjectURL(response.data);
181
+
183
182
  // 触发下载
184
183
  const a = document.createElement('a');
185
184
  a.href = url;
186
- a.download = response.fileData.fileName || 'download';
185
+ a.download = fileName;
187
186
  a.click();
187
+ URL.revokeObjectURL(url);
188
188
  }
189
189
  ```
190
190
 
191
+ ### 文件上传(Client → Server)
192
+
193
+ Client 向 Server 发送文件仅走**流式**。默认 `autoResolve: true`,Server 会在进入 handler 前把文件解析成 `File/Blob` 放到 `req.body`。
194
+
195
+ ```typescript
196
+ // Client 端
197
+ const file = new File(['Hello Upload'], 'upload.txt', { type: 'text/plain' });
198
+ await client.send('/api/upload', file); // File/Blob 会自动分发到 sendFile(走流式)
199
+
200
+ // Server 端
201
+ server.on('/api/upload', async (req, res) => {
202
+ const blob = req.body as Blob;
203
+ const text = await blob.text();
204
+ res.send({ ok: true, text });
205
+ });
206
+ ```
207
+
208
+ ### 路由参数(req.params)
209
+
210
+ 支持 Express 风格的 `:param` 路由参数,解析结果在 `req.params`。
211
+
212
+ ```typescript
213
+ server.on('/api/users/:id', (req, res) => {
214
+ res.send({ userId: req.params.id });
215
+ });
216
+ ```
217
+
191
218
  ### 调试模式
192
219
 
193
220
  开启 trace 模式查看详细日志:
package/QUICKSTART.md CHANGED
@@ -172,22 +172,49 @@ server.on('/api/download', async (req, res) => {
172
172
 
173
173
  // Client side
174
174
  const response = await client.send('/api/download', {});
175
- if (response.fileData) {
175
+ if (response.data instanceof File || response.data instanceof Blob) {
176
+ const file = response.data instanceof File ? response.data : null;
177
+ const fileName = file?.name || 'download';
178
+
176
179
  // Create download link
177
- const blob = new Blob(
178
- [atob(response.fileData.content)],
179
- { type: response.fileData.mimeType }
180
- );
181
- const url = URL.createObjectURL(blob);
182
-
180
+ const url = URL.createObjectURL(response.data);
181
+
183
182
  // Trigger download
184
183
  const a = document.createElement('a');
185
184
  a.href = url;
186
- a.download = response.fileData.fileName || 'download';
185
+ a.download = fileName;
187
186
  a.click();
187
+ URL.revokeObjectURL(url);
188
188
  }
189
189
  ```
190
190
 
191
+ ### File Upload (Client → Server)
192
+
193
+ Client sends files via stream only. By default `autoResolve: true`, server will resolve the file to `File/Blob` and put it into `req.body` before calling your handler.
194
+
195
+ ```typescript
196
+ // Client side
197
+ const file = new File(['Hello Upload'], 'upload.txt', { type: 'text/plain' });
198
+ await client.send('/api/upload', file); // File/Blob auto-dispatches to sendFile (stream)
199
+
200
+ // Server side
201
+ server.on('/api/upload', async (req, res) => {
202
+ const blob = req.body as Blob;
203
+ const text = await blob.text();
204
+ res.send({ ok: true, text });
205
+ });
206
+ ```
207
+
208
+ ### Route Params (req.params)
209
+
210
+ Supports Express-style `:param` route parameters. Extracted parameters are available in `req.params`.
211
+
212
+ ```typescript
213
+ server.on('/api/users/:id', (req, res) => {
214
+ res.send({ userId: req.params.id });
215
+ });
216
+ ```
217
+
191
218
  ### Debug Mode
192
219
 
193
220
  Enable trace mode to view detailed logs:
package/README.CN.md CHANGED
@@ -74,7 +74,7 @@
74
74
  - ⏱️ **智能超时** - 三阶段超时(连接/同步/异步),自动识别长任务
75
75
  - 📦 **TypeScript** - 完整的类型定义和智能提示
76
76
  - 🔒 **消息隔离** - secretKey 机制避免多实例消息串线
77
- - 📁 **文件传输** - 支持 base64 编码的文件发送
77
+ - 📁 **文件传输** - 支持文件通过流方式传输(Client→Server)
78
78
  - 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
79
79
  - 🌍 **多语言** - 错误消息可自定义,便于国际化
80
80
  - ✅ **协议版本** - 内置版本控制,便于升级兼容
@@ -252,7 +252,8 @@ client.send('/api/getData', data, {
252
252
  const client = requestIframeClient(iframe, { secretKey: 'main-app' });
253
253
 
254
254
  // 获取子应用的用户信息
255
- const userInfo = await client.send('/api/user/info', {});
255
+ const userInfoResponse = await client.send('/api/user/info', {});
256
+ console.log(userInfoResponse.data); // 用户信息数据
256
257
 
257
258
  // 通知子应用更新数据
258
259
  await client.send('/api/data/refresh', { timestamp: Date.now() });
@@ -296,7 +297,8 @@ server.on('/api/data', async (req, res) => {
296
297
 
297
298
  // 父页面(跨域)
298
299
  const client = requestIframeClient(iframe, { secretKey: 'data-api' });
299
- const data = await client.send('/api/data', {}); // 成功获取跨域数据
300
+ const response = await client.send('/api/data', {});
301
+ const data = response.data; // 成功获取跨域数据
300
302
  ```
301
303
 
302
304
  ### 文件预览和下载
@@ -318,8 +320,8 @@ server.on('/api/processFile', async (req, res) => {
318
320
 
319
321
  // 父页面:下载文件
320
322
  const response = await client.send('/api/processFile', { fileId: '123' });
321
- if (response.fileData) {
322
- downloadFile(response.fileData);
323
+ if (response.data instanceof File || response.data instanceof Blob) {
324
+ downloadFile(response.data);
323
325
  }
324
326
  ```
325
327
 
@@ -439,6 +441,13 @@ server.use(['/user', '/profile'], (req, res, next) => {
439
441
 
440
442
  request-iframe 模拟了 HTTP 的 cookie 自动管理机制:
441
443
 
444
+ **Cookie 有效期与生命周期(重要):**
445
+
446
+ - **仅内存存储**:cookies 存在于 Client 实例内部的 `CookieStore`(不会写入浏览器真实 Cookie)。
447
+ - **生命周期**:默认从 `requestIframeClient()` 创建开始,直到 `client.destroy()` 为止。
448
+ - **`open()` / `close()`**:只控制消息监听的开启/关闭,**不会清空**内部 cookies。
449
+ - **过期处理**:会遵循 `Expires` / `Max-Age`。已过期的 cookie 在读取/发送时会被自动过滤(也可以用 `client.clearCookies()` / `client.removeCookie()` 手动清理)。
450
+
442
451
  ```
443
452
  ┌─────────────────────────────────────────────────────────────────┐
444
453
  │ Cookies 自动管理流程 │
@@ -505,10 +514,12 @@ server.on('/api/logout', (req, res) => {
505
514
  await client.send('/api/login', { username: 'tom', password: '123' });
506
515
 
507
516
  // Client 端:后续请求 /api/getUserInfo(自动携带 authToken 和 userId)
508
- const userInfo = await client.send('/api/getUserInfo', {});
517
+ const userInfoResponse = await client.send('/api/getUserInfo', {});
518
+ const userInfo = userInfoResponse.data;
509
519
 
510
520
  // Client 端:请求根路径(只携带 userId,因为 authToken 的 path 是 /api)
511
- const rootData = await client.send('/other', {});
521
+ const rootResponse = await client.send('/other', {});
522
+ const rootData = rootResponse.data;
512
523
  ```
513
524
 
514
525
  #### Client Cookie 管理 API
@@ -583,22 +594,48 @@ server.on('/api/download', async (req, res) => {
583
594
 
584
595
  // Client 端接收
585
596
  const response = await client.send('/api/download', {});
586
- if (response.fileData) {
587
- const { content, mimeType, fileName } = response.fileData;
588
-
589
- // content 是 base64 编码的字符串
590
- const binaryString = atob(content);
591
- const blob = new Blob([binaryString], { type: mimeType });
597
+ if (response.data instanceof File || response.data instanceof Blob) {
598
+ const file = response.data instanceof File ? response.data : null;
599
+ const fileName = file?.name || 'download';
592
600
 
593
- // 下载文件
594
- const url = URL.createObjectURL(blob);
601
+ // 直接使用 File/Blob 下载文件
602
+ const url = URL.createObjectURL(response.data);
595
603
  const a = document.createElement('a');
596
604
  a.href = url;
597
- a.download = fileName || 'download';
605
+ a.download = fileName;
598
606
  a.click();
607
+ URL.revokeObjectURL(url);
599
608
  }
600
609
  ```
601
610
 
611
+ #### Client → Server(Client 向 Server 发送文件)
612
+
613
+ Client 端发送文件**仅走流式**。使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
614
+
615
+ ```typescript
616
+ // Client 端:发送文件(stream,autoResolve 默认 true)
617
+ const file = new File(['Hello Upload'], 'upload.txt', { type: 'text/plain' });
618
+ const response = await client.send('/api/upload', file);
619
+
620
+ // 或显式使用 sendFile
621
+ const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
622
+ const response2 = await client.sendFile('/api/upload', blob, {
623
+ fileName: 'data.bin',
624
+ mimeType: 'application/octet-stream',
625
+ autoResolve: true // 可选,默认 true:Server 在 req.body 里拿到 File/Blob
626
+ });
627
+
628
+ // Server 端:接收文件(autoResolve true → req.body 是 File/Blob)
629
+ server.on('/api/upload', async (req, res) => {
630
+ const blob = req.body as Blob; // 如果 client 发送的是 File,这里也可能是 File
631
+ const text = await blob.text();
632
+ console.log('Received file content:', text);
633
+ res.send({ success: true, size: blob.size });
634
+ });
635
+ ```
636
+
637
+ **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`(走流式)。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
638
+
602
639
  ### 流式传输(Stream)
603
640
 
604
641
  对于大文件或需要分块传输的场景,可以使用流式传输:
@@ -608,7 +645,7 @@ import {
608
645
  IframeWritableStream,
609
646
  IframeFileWritableStream,
610
647
  isIframeReadableStream,
611
- isIframeFileStream
648
+ isIframeFileReadableStream
612
649
  } from 'request-iframe';
613
650
 
614
651
  // Server 端:使用迭代器发送数据流
@@ -692,7 +729,7 @@ if (isIframeReadableStream(response.stream)) {
692
729
  // Client 端:接收文件流
693
730
  const fileResponse = await client.send('/api/fileStream', {});
694
731
 
695
- if (isIframeFileStream(fileResponse.stream)) {
732
+ if (isIframeFileReadableStream(fileResponse.stream)) {
696
733
  // 读取为 Blob
697
734
  const blob = await fileResponse.stream.readAsBlob();
698
735
 
@@ -714,9 +751,9 @@ if (isIframeFileStream(fileResponse.stream)) {
714
751
  | 类型 | 说明 |
715
752
  |------|------|
716
753
  | `IframeWritableStream` | 服务端可写流,用于发送普通数据 |
717
- | `IframeFileWritableStream` | 服务端文件可写流,自动处理 base64 编码 |
754
+ | `IframeFileWritableStream` | 服务端文件可写流(文件流) |
718
755
  | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
719
- | `IframeFileReadableStream` | 客户端文件可读流,自动处理 base64 解码 |
756
+ | `IframeFileReadableStream` | 客户端文件可读流(文件流) |
720
757
 
721
758
  **流选项:**
722
759
 
@@ -852,14 +889,16 @@ await client.send('/api/longTask', {}, {
852
889
 
853
890
  #### client.send(path, body?, options?)
854
891
 
855
- 发送请求。
892
+ 发送请求。会根据 body 类型自动分发到 `sendFile()` 或 `sendStream()`:
893
+ - `File/Blob` → `sendFile()`
894
+ - `IframeWritableStream` → `sendStream()`
856
895
 
857
896
  **参数:**
858
897
 
859
898
  | 参数 | 类型 | 说明 |
860
899
  |------|------|------|
861
900
  | `path` | `string` | 请求路径 |
862
- | `body` | `object` | 请求数据(可选) |
901
+ | `body` | `any` | 请求数据(可选)。可以是普通对象、File、Blob、或 IframeWritableStream;会自动分发:File/Blob → `sendFile()`,IframeWritableStream → `sendStream()` |
863
902
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
864
903
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
865
904
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
@@ -880,6 +919,69 @@ interface Response<T = any> {
880
919
  }
881
920
  ```
882
921
 
922
+ **示例:**
923
+
924
+ ```typescript
925
+ // 发送普通对象(自动 Content-Type: application/json)
926
+ await client.send('/api/data', { name: 'test' });
927
+
928
+ // 发送字符串(自动 Content-Type: text/plain)
929
+ await client.send('/api/text', 'Hello');
930
+
931
+ // 发送 File/Blob(自动分发到 sendFile)
932
+ const file = new File(['content'], 'test.txt');
933
+ await client.send('/api/upload', file);
934
+
935
+ // 发送流(自动分发到 sendStream)
936
+ const stream = new IframeWritableStream({ iterator: async function* () { yield 'data'; } });
937
+ await client.send('/api/uploadStream', stream);
938
+ ```
939
+
940
+ #### client.sendFile(path, content, options?)
941
+
942
+ 发送文件作为请求体(通过流传输;当 `autoResolve` 为 true 时,Server 在 `req.body` 中拿到 File/Blob)。
943
+
944
+ **参数:**
945
+
946
+ | 参数 | 类型 | 说明 |
947
+ |------|------|------|
948
+ | `path` | `string` | 请求路径 |
949
+ | `content` | `string \| Blob \| File` | 文件内容 |
950
+ | `options.mimeType` | `string` | MIME 类型(可选,优先使用 content.type) |
951
+ | `options.fileName` | `string` | 文件名(可选) |
952
+ | `options.autoResolve` | `boolean` | 为 true(默认)时 Server 在 `req.body` 中拿到 File/Blob;为 false 时 Server 在 `req.stream` / `req.body` 中拿到 `IframeFileReadableStream` |
953
+ | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
954
+ | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
955
+ | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
956
+ | `options.headers` | `object` | 请求 headers(可选) |
957
+ | `options.cookies` | `object` | 请求 cookies(可选) |
958
+ | `options.requestId` | `string` | 自定义请求 ID(可选) |
959
+
960
+ **返回值:** `Promise<Response>`
961
+
962
+ **说明:** 文件通过流发送。`autoResolve` 为 true(默认)时 Server 收到 `req.body`(File/Blob);为 false 时 Server 收到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
963
+
964
+ #### client.sendStream(path, stream, options?)
965
+
966
+ 发送流作为请求体(Server 端收到可读流)。
967
+
968
+ **参数:**
969
+
970
+ | 参数 | 类型 | 说明 |
971
+ |------|------|------|
972
+ | `path` | `string` | 请求路径 |
973
+ | `stream` | `IframeWritableStream` | 要发送的可写流 |
974
+ | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
975
+ | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
976
+ | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
977
+ | `options.headers` | `object` | 请求 headers(可选) |
978
+ | `options.cookies` | `object` | 请求 cookies(可选) |
979
+ | `options.requestId` | `string` | 自定义请求 ID(可选) |
980
+
981
+ **返回值:** `Promise<Response>`
982
+
983
+ **说明:** Server 端的流在 `req.stream`(`IIframeReadableStream`)中,可用 `for await (const chunk of req.stream)` 迭代读取。
984
+
883
985
  #### client.isConnect()
884
986
 
885
987
  检测 Server 是否可达。
@@ -952,6 +1054,39 @@ client.interceptors.response.use(onFulfilled, onRejected?);
952
1054
  type ServerHandler = (req: ServerRequest, res: ServerResponse) => any | Promise<any>;
953
1055
  ```
954
1056
 
1057
+ **ServerRequest 接口:**
1058
+
1059
+ ```typescript
1060
+ interface ServerRequest {
1061
+ body: any; // 请求 body(普通数据;或 autoResolve=true 时的 File/Blob)
1062
+ stream?: IIframeReadableStream; // 请求流(sendStream;或 sendFile 且 autoResolve=false)
1063
+ headers: Record<string, string>; // 请求 headers
1064
+ cookies: Record<string, string>; // 请求 cookies
1065
+ path: string; // 请求路径(实际请求路径)
1066
+ params: Record<string, string>; // 路由参数(由 server.on 注册的路径模式解析得出,如 /api/users/:id)
1067
+ requestId: string; // 请求 ID
1068
+ origin: string; // 来源 origin
1069
+ source: Window; // 来源 window
1070
+ res: ServerResponse; // 关联的 Response 对象
1071
+ }
1072
+ ```
1073
+
1074
+ **说明:**
1075
+ - Client 通过 `sendFile()`(或 `send(path, file)`)发送文件时:文件通过流传输;`autoResolve` 为 true(默认)时 Server 在 `req.body` 中拿到 File/Blob;为 false 时在 `req.stream` / `req.body` 中拿到 `IframeFileReadableStream`。
1076
+ - Client 通过 `sendStream()` 发送流时:Server 在 `req.stream` 中拿到 `IIframeReadableStream`,可用 `for await` 迭代读取。
1077
+ - **路径参数(类似 Express)**:支持 `/api/users/:id` 形式的路由参数,解析结果在 `req.params` 中。
1078
+
1079
+ ```typescript
1080
+ server.on('/api/users/:id', (req, res) => {
1081
+ res.send({ userId: req.params.id });
1082
+ });
1083
+
1084
+ server.on('/api/users/:userId/posts/:postId', (req, res) => {
1085
+ const { userId, postId } = req.params;
1086
+ res.send({ userId, postId });
1087
+ });
1088
+ ```
1089
+
955
1090
  #### server.off(path)
956
1091
 
957
1092
  移除路由处理器。
@@ -1245,10 +1380,12 @@ const IframeComponent = () => {
1245
1380
 
1246
1381
  ```typescript
1247
1382
  interface ServerRequest {
1248
- body: any; // 请求 body
1383
+ body: any; // 请求 body(普通数据;或 autoResolve=true 时的 File/Blob)
1384
+ stream?: IIframeReadableStream; // 请求流(sendStream;或 sendFile 且 autoResolve=false)
1249
1385
  headers: Record<string, string>; // 请求 headers
1250
1386
  cookies: Record<string, string>; // 请求 cookies
1251
- path: string; // 请求路径
1387
+ path: string; // 请求路径(实际请求路径)
1388
+ params: Record<string, string>; // 路由参数(由 server.on 注册的路径模式解析得出,如 /api/users/:id)
1252
1389
  requestId: string; // 请求 ID
1253
1390
  origin: string; // 来源 origin
1254
1391
  source: Window; // 来源 window
@@ -1256,6 +1393,22 @@ interface ServerRequest {
1256
1393
  }
1257
1394
  ```
1258
1395
 
1396
+ **路径参数(类似 Express)**:
1397
+
1398
+ 支持使用 `:param` 形式声明路由参数,解析结果在 `req.params` 中。
1399
+
1400
+ ```typescript
1401
+ server.on('/api/users/:id', (req, res) => {
1402
+ // 请求 /api/users/123 时:req.params.id === '123'
1403
+ res.send({ userId: req.params.id });
1404
+ });
1405
+
1406
+ server.on('/api/users/:userId/posts/:postId', (req, res) => {
1407
+ const { userId, postId } = req.params;
1408
+ res.send({ userId, postId });
1409
+ });
1410
+ ```
1411
+
1259
1412
  ### ServerResponse 对象
1260
1413
 
1261
1414
  ```typescript