request-iframe 0.0.6 → 0.1.1

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 (95) hide show
  1. package/README.CN.md +220 -21
  2. package/README.md +221 -24
  3. package/esm/api/client.js +80 -0
  4. package/esm/api/server.js +61 -0
  5. package/esm/constants/index.js +289 -0
  6. package/esm/constants/messages.js +157 -0
  7. package/esm/core/client-server.js +294 -0
  8. package/esm/core/client.js +873 -0
  9. package/esm/core/request.js +27 -0
  10. package/esm/core/response.js +459 -0
  11. package/esm/core/server.js +776 -0
  12. package/esm/index.js +21 -0
  13. package/esm/interceptors/index.js +122 -0
  14. package/esm/message/channel.js +182 -0
  15. package/esm/message/dispatcher.js +418 -0
  16. package/esm/message/index.js +2 -0
  17. package/esm/stream/file-stream.js +289 -0
  18. package/esm/stream/index.js +44 -0
  19. package/esm/stream/readable-stream.js +539 -0
  20. package/esm/stream/stream-core.js +204 -0
  21. package/esm/stream/types.js +1 -0
  22. package/esm/stream/writable-stream.js +836 -0
  23. package/esm/types/index.js +1 -0
  24. package/esm/utils/ack.js +36 -0
  25. package/esm/utils/cache.js +147 -0
  26. package/esm/utils/cookie.js +352 -0
  27. package/esm/utils/debug.js +521 -0
  28. package/esm/utils/error.js +27 -0
  29. package/esm/utils/index.js +180 -0
  30. package/esm/utils/origin.js +30 -0
  31. package/esm/utils/path-match.js +148 -0
  32. package/esm/utils/protocol.js +157 -0
  33. package/library/api/client.d.ts.map +1 -1
  34. package/library/api/client.js +13 -5
  35. package/library/api/server.d.ts.map +1 -1
  36. package/library/api/server.js +6 -1
  37. package/library/constants/index.d.ts +59 -4
  38. package/library/constants/index.d.ts.map +1 -1
  39. package/library/constants/index.js +67 -9
  40. package/library/constants/messages.d.ts +8 -1
  41. package/library/constants/messages.d.ts.map +1 -1
  42. package/library/constants/messages.js +8 -1
  43. package/library/core/client-server.d.ts +7 -15
  44. package/library/core/client-server.d.ts.map +1 -1
  45. package/library/core/client-server.js +56 -44
  46. package/library/core/client.d.ts +4 -1
  47. package/library/core/client.d.ts.map +1 -1
  48. package/library/core/client.js +74 -31
  49. package/library/core/response.d.ts +21 -3
  50. package/library/core/response.d.ts.map +1 -1
  51. package/library/core/response.js +55 -7
  52. package/library/core/server.d.ts +34 -3
  53. package/library/core/server.d.ts.map +1 -1
  54. package/library/core/server.js +191 -21
  55. package/library/message/channel.d.ts +6 -0
  56. package/library/message/channel.d.ts.map +1 -1
  57. package/library/message/channel.js +2 -1
  58. package/library/message/dispatcher.d.ts +32 -0
  59. package/library/message/dispatcher.d.ts.map +1 -1
  60. package/library/message/dispatcher.js +131 -1
  61. package/library/stream/file-stream.d.ts +4 -0
  62. package/library/stream/file-stream.d.ts.map +1 -1
  63. package/library/stream/file-stream.js +61 -33
  64. package/library/stream/index.d.ts.map +1 -1
  65. package/library/stream/index.js +2 -0
  66. package/library/stream/readable-stream.d.ts +30 -11
  67. package/library/stream/readable-stream.d.ts.map +1 -1
  68. package/library/stream/readable-stream.js +368 -73
  69. package/library/stream/stream-core.d.ts +65 -0
  70. package/library/stream/stream-core.d.ts.map +1 -0
  71. package/library/stream/stream-core.js +211 -0
  72. package/library/stream/types.d.ts +203 -3
  73. package/library/stream/types.d.ts.map +1 -1
  74. package/library/stream/writable-stream.d.ts +59 -13
  75. package/library/stream/writable-stream.d.ts.map +1 -1
  76. package/library/stream/writable-stream.js +647 -197
  77. package/library/types/index.d.ts +70 -4
  78. package/library/types/index.d.ts.map +1 -1
  79. package/library/utils/ack.d.ts +2 -0
  80. package/library/utils/ack.d.ts.map +1 -0
  81. package/library/utils/ack.js +44 -0
  82. package/library/utils/debug.js +1 -1
  83. package/library/utils/index.d.ts +1 -0
  84. package/library/utils/index.d.ts.map +1 -1
  85. package/library/utils/index.js +19 -2
  86. package/library/utils/origin.d.ts +14 -0
  87. package/library/utils/origin.d.ts.map +1 -0
  88. package/library/utils/origin.js +35 -0
  89. package/package.json +30 -7
  90. package/react/README.md +16 -0
  91. package/react/esm/index.js +284 -0
  92. package/react/library/index.d.ts +1 -1
  93. package/react/library/index.d.ts.map +1 -1
  94. package/react/library/index.js +3 -3
  95. package/react/package.json +24 -2
package/README.CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # request-iframe
2
2
 
3
- 像发送 HTTP 请求一样与 iframe 通信!基于 `postMessage` 实现的 iframe 跨域通信库。
3
+ 像发送 HTTP 请求一样与 iframe / Window 通信!基于 `postMessage` 实现的浏览器跨页面通信库。
4
4
 
5
5
  > 🌐 **Languages**: [English](./README.md) | [中文](./README.CN.md)
6
6
 
@@ -8,7 +8,7 @@
8
8
  <img src="https://img.shields.io/badge/TypeScript-Ready-blue" alt="TypeScript Ready">
9
9
  <img src="https://img.shields.io/badge/API-Express%20Like-green" alt="Express Like API">
10
10
  <img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
11
- <img src="https://img.shields.io/badge/Test%20Coverage-76%25-brightgreen" alt="Test Coverage">
11
+ <img src="https://coveralls.io/repos/github/gxlmyacc/request-iframe/badge.svg?branch=main" alt="Coverage Status">
12
12
  </p>
13
13
 
14
14
  ## 📑 目录
@@ -48,7 +48,7 @@
48
48
 
49
49
  ## 为什么选择 request-iframe?
50
50
 
51
- 在微前端、iframe 嵌套等场景下,父子页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
51
+ 在微前端、iframe 嵌套、弹窗(window.open)等场景下,跨页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
52
52
 
53
53
  | 痛点 | 传统方式 | request-iframe |
54
54
  |------|----------|----------------|
@@ -74,7 +74,7 @@
74
74
  - ⏱️ **智能超时** - 三阶段超时(连接/同步/异步),自动识别长任务
75
75
  - 📦 **TypeScript** - 完整的类型定义和智能提示
76
76
  - 🔒 **消息隔离** - secretKey 机制避免多实例消息串线
77
- - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
77
+ - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
78
78
  - 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
79
79
  - 🌍 **多语言** - 错误消息可自定义,便于国际化
80
80
  - ✅ **协议版本** - 内置版本控制,便于升级兼容
@@ -151,7 +151,7 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
151
151
  │ │
152
152
  │ <──── RESPONSE ──────────────────────── │ 返回结果
153
153
  │ │
154
- │ ──── RECEIVED (可选) ──────────────────> │ 确认收到响应
154
+ │ ──── ACK (可选) ───────────────────────> │ 确认收到响应
155
155
  │ │
156
156
  ```
157
157
 
@@ -160,12 +160,11 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
160
160
  | 类型 | 方向 | 说明 |
161
161
  |------|------|------|
162
162
  | `request` | Client → Server | 客户端发起请求 |
163
- | `ack` | ServerClient | 服务端确认收到请求 |
163
+ | `ack` | 接收方发送方 | 回执确认(当消息 `requireAck: true` 且被接管/处理时发送;ACK-only) |
164
164
  | `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
165
165
  | `response` | Server → Client | 返回响应数据 |
166
166
  | `error` | Server → Client | 返回错误信息 |
167
- | `received` | Client → Server | 客户端确认收到响应(可选,由 `requireAck` 控制) |
168
- | `ping` | Client → Server | 连接检测(`isConnect()` 方法) |
167
+ | `ping` | Client → Server | 连接检测(`isConnect()`;可使用 `requireAck` 确认投递) |
169
168
  | `pong` | Server → Client | 连接检测响应 |
170
169
 
171
170
  ### 超时机制
@@ -176,7 +175,8 @@ request-iframe 采用三阶段超时策略,智能适应不同场景:
176
175
  client.send('/api/getData', data, {
177
176
  ackTimeout: 1000, // 阶段1:等待 ACK 的超时时间(默认 1000ms)
178
177
  timeout: 5000, // 阶段2:请求超时时间(默认 5s)
179
- asyncTimeout: 120000 // 阶段3:异步请求超时时间(默认 120s)
178
+ asyncTimeout: 120000, // 阶段3:异步请求超时时间(默认 120s)
179
+ requireAck: true // 是否需要服务端 ACK(默认 true;为 false 则跳过 ACK 阶段,直接进入 timeout)
180
180
  });
181
181
  ```
182
182
 
@@ -218,13 +218,17 @@ client.send('/api/getData', data, {
218
218
  | timeout | 中等(5s) | 适用于简单的同步处理,如读取数据、参数校验等 |
219
219
  | asyncTimeout | 较长(120s) | 适用于复杂异步操作,如文件处理、批量操作、第三方 API 调用等 |
220
220
 
221
+ **补充说明:**
222
+ - `requireAck: false` 会跳过 ACK 阶段,直接以 `timeout` 作为第一阶段计时。
223
+ - 流(Stream)有独立的可选空闲超时:`streamTimeout`(见「流式传输(Stream)」)。
224
+
221
225
  ### 协议版本
222
226
 
223
227
  每条消息都包含 `__requestIframe__` 字段标识协议版本,以及 `timestamp` 字段记录消息创建时间:
224
228
 
225
229
  ```typescript
226
230
  {
227
- __requestIframe__: 1, // 协议版本号
231
+ __requestIframe__: 2, // 协议版本号
228
232
  timestamp: 1704067200000, // 消息创建时间戳(毫秒)
229
233
  type: 'request',
230
234
  requestId: 'req_xxx',
@@ -235,7 +239,7 @@ client.send('/api/getData', data, {
235
239
 
236
240
  这使得:
237
241
  - 不同版本的库可以做兼容处理
238
- - 新版本 Server 可兼容旧版本 Client
242
+ - 当前协议版本为 `2`;对于新的 stream pull/ack 流程,建议双方保持一致版本
239
243
  - 版本过低时会返回明确的错误信息
240
244
  - `timestamp` 便于调试消息延迟、分析通信性能
241
245
 
@@ -281,6 +285,29 @@ server.on('/event', (req, res) => {
281
285
  });
282
286
  ```
283
287
 
288
+ ### 弹窗 / 新标签页(Window 通信)
289
+
290
+ `request-iframe` 不仅可以与 iframe 通信,也可以把 `target` 直接传 `Window`(例如弹窗/新标签页)。
291
+
292
+ **重要前提**:你必须拿到对方页面的 `Window` 引用(例如 `window.open()` 的返回值,或通过 `window.opener` / `MessageEvent.source` 获取)。**无法**通过 URL 给“任意标签页”发消息。
293
+
294
+ ```typescript
295
+ // 父页面:打开新标签页/弹窗
296
+ const child = window.open('https://child.example.com/page.html', '_blank');
297
+ if (!child) throw new Error('弹窗被拦截');
298
+
299
+ // 父 -> 子
300
+ const client = requestIframeClient(child, {
301
+ secretKey: 'popup-demo',
302
+ targetOrigin: 'https://child.example.com' // 强烈建议不要用 '*'
303
+ });
304
+ await client.send('/api/ping', { from: 'parent' });
305
+
306
+ // 子页面:创建 server
307
+ const server = requestIframeServer({ secretKey: 'popup-demo' });
308
+ server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
309
+ ```
310
+
284
311
  ### 跨域数据获取
285
312
 
286
313
  当 iframe 与父页面不同域时,使用 request-iframe 安全地获取数据:
@@ -578,6 +605,8 @@ server.on('/api/data', (req, res) => {
578
605
 
579
606
  ### 文件传输
580
607
 
608
+ > 说明:文件传输(无论 Client→Server 还是 Server→Client)底层都会通过 stream 协议承载;你只需要使用 `client.sendFile()` / `res.sendFile()` 这一层 API 即可。
609
+
581
610
  ```typescript
582
611
  // Server 端发送文件
583
612
  server.on('/api/download', async (req, res) => {
@@ -610,7 +639,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
610
639
 
611
640
  #### Client → Server(Client 向 Server 发送文件)
612
641
 
613
- Client 端发送文件**仅走流式**。使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
642
+ Client 端发送文件使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
614
643
 
615
644
  ```typescript
616
645
  // Client 端:发送文件(stream,autoResolve 默认 true)
@@ -634,14 +663,74 @@ server.on('/api/upload', async (req, res) => {
634
663
  });
635
664
  ```
636
665
 
637
- **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`(走流式)。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
666
+ **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
638
667
 
639
668
  ### 流式传输(Stream)
640
669
 
641
- 对于大文件或需要分块传输的场景,可以使用流式传输:
670
+ Stream 除了用于大文件/分块传输,也可以用于“长连接 / 订阅式交互”(类似 SSE / WebSocket,但基于 `postMessage`)。常见用法有两类:
671
+
672
+ - **长连接/订阅**:Client 发起一次请求拿到 `response.stream`,然后用 `for await` 持续消费事件;需要结束时调用 `stream.cancel()`。
673
+ - **分块/文件流**:按 chunk 传输数据或文件(下方示例)。
674
+
675
+ > 长连接注意事项:
676
+ > - `IframeWritableStream` 默认会使用 `asyncTimeout` 作为 `expireTimeout`(避免泄露)。如果你的订阅需要更久,请显式设置更大的 `expireTimeout`,或设置 `expireTimeout: 0` 关闭自动过期(建议配合业务侧取消/重连,避免泄露)。
677
+ > - Server 端的 `res.sendStream(stream)` 会一直等待到流结束;如果你需要在后续主动 `write()` 推送数据,请不要直接 `await` 它,可以用 `void res.sendStream(stream)` 或保存返回的 Promise。
678
+ > - 如果启用了 `maxConcurrentRequestsPerClient`,一个长连接 stream 会占用一个并发槽,需要按业务场景调整。
679
+ > - **事件订阅**:stream 支持 `stream.on(event, listener)`(返回取消订阅函数),可用于埋点/进度/调试(如监听 `start/data/read/write/cancel/end/error/timeout/expired`)。主消费仍建议使用 `for await`。
680
+
681
+ #### 长连接 / 订阅式交互(push 模式示例)
642
682
 
643
683
  ```typescript
644
- import {
684
+ /**
685
+ * Server 端:订阅(长连接)
686
+ * - mode: 'push':由写侧主动 write()
687
+ * - expireTimeout: 0:关闭自动过期(谨慎使用,建议结合业务取消/重连)
688
+ */
689
+ server.on('/api/subscribe', (req, res) => {
690
+ const stream = new IframeWritableStream({
691
+ type: 'data',
692
+ chunked: true,
693
+ mode: 'push',
694
+ expireTimeout: 0,
695
+ /** 可选:写侧空闲检测(等待 pull/ack 太久会做心跳并失败) */
696
+ streamTimeout: 15000
697
+ });
698
+
699
+ /** 注意:不要 await,否则会一直等到流结束 */
700
+ void res.sendStream(stream);
701
+
702
+ const timer = setInterval(() => {
703
+ try {
704
+ stream.write({ type: 'tick', ts: Date.now() });
705
+ } catch {
706
+ clearInterval(timer);
707
+ }
708
+ }, 1000);
709
+ });
710
+
711
+ /**
712
+ * Client 端:持续读取(长连接建议用 for await,而不是 readAll())
713
+ */
714
+ const resp = await client.send('/api/subscribe', {});
715
+ if (isIframeReadableStream(resp.stream)) {
716
+ /** 事件订阅示例(可选) */
717
+ const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
718
+ console.error('stream error:', error);
719
+ });
720
+
721
+ for await (const evt of resp.stream) {
722
+ console.log('event:', evt);
723
+ }
724
+
725
+ off();
726
+ }
727
+ ```
728
+
729
+ #### 分块 / 文件流示例
730
+
731
+ ```typescript
732
+ import {
733
+ StreamEvent,
645
734
  IframeWritableStream,
646
735
  IframeFileWritableStream,
647
736
  isIframeReadableStream,
@@ -706,6 +795,8 @@ const response = await client.send('/api/stream', {});
706
795
  if (isIframeReadableStream(response.stream)) {
707
796
  // 方式1:一次性读取所有数据
708
797
  const allData = await response.stream.read();
798
+ // 如果希望返回类型稳定(始终是 chunk 数组),可使用 readAll()
799
+ const allChunks = await response.stream.readAll();
709
800
 
710
801
  // 方式2:使用异步迭代器逐块读取
711
802
  for await (const chunk of response.stream) {
@@ -750,10 +841,12 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
750
841
 
751
842
  | 类型 | 说明 |
752
843
  |------|------|
753
- | `IframeWritableStream` | 服务端可写流,用于发送普通数据 |
754
- | `IframeFileWritableStream` | 服务端文件可写流(文件流) |
755
- | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
756
- | `IframeFileReadableStream` | 客户端文件可读流(文件流) |
844
+ | `IframeWritableStream` | 写侧(生产者)流:**谁要发送 stream,谁就创建它**;可用于 Server→Client 的响应流,也可用于 Client→Server 的请求流 |
845
+ | `IframeFileWritableStream` | 文件写侧(生产者)流:用于发送文件(底层会做 Base64 编码) |
846
+ | `IframeReadableStream` | 读侧(消费者)流:用于接收普通数据(无论来自 Server 还是 Client) |
847
+ | `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(底层会做 Base64 解码) |
848
+
849
+ > **注意**:文件流内部会进行 Base64 编/解码。Base64 会带来约 33% 的体积膨胀,并且在超大文件场景下可能会有较高的内存/CPU 开销。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
757
850
 
758
851
  **流选项:**
759
852
 
@@ -761,12 +854,32 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
761
854
  interface WritableStreamOptions {
762
855
  type?: 'data' | 'file'; // 流类型
763
856
  chunked?: boolean; // 是否分块传输(默认 true)
857
+ mode?: 'pull' | 'push'; // 流模式:pull(默认,按需拉取) / push(主动写入)
858
+ expireTimeout?: number; // 流过期时间(ms,可选;默认约等于 asyncTimeout)
859
+ streamTimeout?: number; // 写侧空闲超时(ms,可选):长时间未收到对端 pull/ack 时会做心跳确认并失败
764
860
  iterator?: () => AsyncGenerator; // 数据生成迭代器
765
861
  next?: () => Promise<{ data: any; done: boolean }>; // 数据生成函数
862
+ maxPendingChunks?: number; // 写侧待发送缓冲上限(可选;push/长连接场景建议配置,避免 pendingQueue 无限增长)
863
+ maxPendingBytes?: number; // 写侧待发送字节上限(可选;避免单次 write 超大 chunk 导致内存暴涨)
766
864
  metadata?: Record<string, any>; // 自定义元数据
767
865
  }
768
866
  ```
769
867
 
868
+ **流超时/保活:**
869
+ - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认(默认使用 `client.isConnect()`),失败则认为流已断开并报错。
870
+ - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull 协议下,若长时间未收到对端 `pull`,会做心跳确认并失败(避免长时间无效占用)。
871
+ - `expireTimeout`(流参数):写侧有效期;过期后会发送 `stream_error`,读侧会收到明确的“已过期”错误。
872
+ - `maxPendingChunks`(流参数):写侧待发送缓冲上限(可选)。对 `push` / 长连接场景很重要:当对端停止 pull 时,继续 `write()` 会在写侧积压,建议设置上限防止内存无限增长。
873
+ - `maxPendingBytes`(流参数):写侧待发送字节上限(可选)。用于防止单次写入超大 chunk(例如大字符串/大 Blob 包装)导致内存占用过高。
874
+
875
+ **pull/ack 协议(新增,默认启用):**
876
+ - 读侧会自动发送 `stream_pull` 请求更多 chunk;写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
877
+ - 断连检测不依赖 `stream_ack`,而是通过 `streamTimeout + 心跳(isConnect)` 来实现。
878
+
879
+ **consume 默认行为(变更):**
880
+ - `for await (const chunk of stream)` 默认会 **消费并丢弃已迭代过的 chunk**(`consume: true`),避免长流场景内存无限增长。
881
+ - 如果你希望后续还能 `read()/readAll()` 拿到历史数据,可在创建流时传 `consume: false`(或在业务上自行缓存)。
882
+
770
883
  ### 连接检测
771
884
 
772
885
  ```typescript
@@ -786,9 +899,9 @@ Server 可以要求 Client 确认收到响应:
786
899
  ```typescript
787
900
  server.on('/api/important', async (req, res) => {
788
901
  // requireAck: true 表示需要客户端确认
789
- const received = await res.send(data, { requireAck: true });
902
+ const acked = await res.send(data, { requireAck: true });
790
903
 
791
- if (received) {
904
+ if (acked) {
792
905
  console.log('客户端已确认收到');
793
906
  } else {
794
907
  console.log('客户端未确认(超时)');
@@ -796,6 +909,8 @@ server.on('/api/important', async (req, res) => {
796
909
  });
797
910
  ```
798
911
 
912
+ > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `ack`,无需业务侧手动发送。
913
+
799
914
  ### 追踪模式
800
915
 
801
916
  开启追踪模式可以在控制台查看详细的通信日志:
@@ -848,12 +963,81 @@ setMessages({
848
963
  | `target` | `HTMLIFrameElement \| Window` | 目标 iframe 元素或 window 对象 |
849
964
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
850
965
  | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
966
+ | `options.targetOrigin` | `string` | 覆盖 postMessage 的 targetOrigin(可选)。当 `target` 是 `Window` 时默认 `*`;当 `target` 是 iframe 时默认取 `iframe.src` 的 origin。 |
851
967
  | `options.ackTimeout` | `number` | 全局默认 ACK 确认超时(ms),默认 1000 |
852
968
  | `options.timeout` | `number` | 全局默认请求超时(ms),默认 5000 |
853
969
  | `options.asyncTimeout` | `number` | 全局默认异步超时(ms),默认 120000 |
970
+ | `options.requireAck` | `boolean` | 全局默认请求投递 ACK(默认 true)。为 false 时请求跳过 ACK 阶段,直接进入 timeout |
971
+ | `options.streamTimeout` | `number` | 全局默认流空闲超时(ms,可选),用于消费 `response.stream` |
972
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
973
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | 自定义 origin 校验函数(可选,优先级高于 `allowedOrigins`) |
854
974
 
855
975
  **返回值:** `RequestIframeClient`
856
976
 
977
+ **关于 `target: Window` 的说明:**
978
+ - **必须持有对方页面的 `Window` 引用**(例如 `window.open()` 返回值、`window.opener`、或 `MessageEvent.source`)。
979
+ - **无法**通过 URL 给“任意标签页”发消息。
980
+ - 安全起见,建议显式设置 `targetOrigin`,并配置 `allowedOrigins` / `validateOrigin`。
981
+
982
+ **生产环境推荐配置(模板):**
983
+
984
+ ```typescript
985
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
986
+
987
+ /**
988
+ * 建议:明确限定 3 件事
989
+ * - secretKey:隔离不同业务/不同实例,避免消息串线
990
+ * - targetOrigin:发送时的 targetOrigin(Window 场景强烈建议不要用 '*')
991
+ * - allowedOrigins / validateOrigin:接收时的 origin 白名单校验
992
+ */
993
+ const secretKey = 'my-app';
994
+ const targetOrigin = 'https://child.example.com';
995
+ const allowedOrigins = [targetOrigin];
996
+
997
+ // Client(父页)
998
+ const client = requestIframeClient(window.open(targetOrigin)!, {
999
+ secretKey,
1000
+ targetOrigin,
1001
+ allowedOrigins
1002
+ });
1003
+
1004
+ // Server(子页/iframe 内)
1005
+ const server = requestIframeServer({
1006
+ secretKey,
1007
+ allowedOrigins,
1008
+ // 防止异常/攻击导致消息爆炸(按需设置)
1009
+ maxConcurrentRequestsPerClient: 50
1010
+ });
1011
+ ```
1012
+
1013
+ **生产环境推荐配置(iframe 场景模板):**
1014
+
1015
+ ```typescript
1016
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
1017
+
1018
+ /**
1019
+ * iframe 场景通常可以从 iframe.src 推导 targetOrigin,并用它作为 allowedOrigins 白名单。
1020
+ */
1021
+ const iframe = document.querySelector('iframe')!;
1022
+ const targetOrigin = new URL(iframe.src).origin;
1023
+ const secretKey = 'my-app';
1024
+
1025
+ // Client(父页)
1026
+ const client = requestIframeClient(iframe, {
1027
+ secretKey,
1028
+ // 可显式写出来(即使库内部也会默认推导),便于审计/避免误用 '*'
1029
+ targetOrigin,
1030
+ allowedOrigins: [targetOrigin]
1031
+ });
1032
+
1033
+ // Server(iframe 内)
1034
+ const server = requestIframeServer({
1035
+ secretKey,
1036
+ allowedOrigins: [targetOrigin],
1037
+ maxConcurrentRequestsPerClient: 50
1038
+ });
1039
+ ```
1040
+
857
1041
  **示例:**
858
1042
 
859
1043
  ```typescript
@@ -882,6 +1066,9 @@ await client.send('/api/longTask', {}, {
882
1066
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
883
1067
  | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
884
1068
  | `options.ackTimeout` | `number` | 等待客户端确认超时(ms),默认 1000 |
1069
+ | `options.maxConcurrentRequestsPerClient` | `number` | 每个客户端的最大并发 in-flight 请求数(按 origin + creatorId 维度),默认 Infinity |
1070
+ | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
1071
+ | `options.validateOrigin` | `(origin, data, context) => boolean` | 自定义 origin 校验函数(可选,优先级高于 `allowedOrigins`) |
885
1072
 
886
1073
  **返回值:** `RequestIframeServer`
887
1074
 
@@ -902,6 +1089,8 @@ await client.send('/api/longTask', {}, {
902
1089
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
903
1090
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
904
1091
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
1092
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
1093
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
905
1094
  | `options.headers` | `object` | 请求 headers(可选) |
906
1095
  | `options.cookies` | `object` | 请求 cookies(可选,会与内部存储的 cookies 合并,传入的优先级更高) |
907
1096
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -953,6 +1142,8 @@ await client.send('/api/uploadStream', stream);
953
1142
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
954
1143
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
955
1144
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
1145
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
1146
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
956
1147
  | `options.headers` | `object` | 请求 headers(可选) |
957
1148
  | `options.cookies` | `object` | 请求 cookies(可选) |
958
1149
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -974,6 +1165,8 @@ await client.send('/api/uploadStream', stream);
974
1165
  | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 1000 |
975
1166
  | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
976
1167
  | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
1168
+ | `options.requireAck` | `boolean` | 是否需要服务端 ACK(默认 true)。为 false 时跳过 ACK 阶段 |
1169
+ | `options.streamTimeout` | `number` | 流空闲超时(ms,可选),用于消费 `response.stream` |
977
1170
  | `options.headers` | `object` | 请求 headers(可选) |
978
1171
  | `options.cookies` | `object` | 请求 cookies(可选) |
979
1172
  | `options.requestId` | `string` | 自定义请求 ID(可选) |
@@ -1127,6 +1320,8 @@ server.use(['/a', '/b'], (req, res, next) => { ... });
1127
1320
 
1128
1321
  request-iframe 提供了 React hooks,方便在 React 应用中使用。从 `request-iframe/react` 导入 hooks:
1129
1322
 
1323
+ > 注意:只有在使用 `request-iframe/react` 时才需要安装 React;单独安装 `request-iframe` 不依赖 React。
1324
+
1130
1325
  ```typescript
1131
1326
  import { useClient, useServer, useServerHandler, useServerHandlerMap } from 'request-iframe/react';
1132
1327
  ```
@@ -1498,8 +1693,12 @@ import {
1498
1693
  | `PROTOCOL_UNSUPPORTED` | 协议版本不支持 |
1499
1694
  | `IFRAME_NOT_READY` | iframe 未就绪 |
1500
1695
  | `STREAM_ERROR` | 流传输错误 |
1696
+ | `STREAM_TIMEOUT` | 流空闲超时 |
1697
+ | `STREAM_EXPIRED` | 流已过期(可写流超过有效期) |
1501
1698
  | `STREAM_CANCELLED` | 流被取消 |
1502
1699
  | `STREAM_NOT_BOUND` | 流未绑定到请求上下文 |
1700
+ | `STREAM_START_TIMEOUT` | 流启动超时(请求体 stream_start 未按时到达) |
1701
+ | `TOO_MANY_REQUESTS` | 请求过多(服务端并发限制) |
1503
1702
 
1504
1703
  ### 错误处理示例
1505
1704