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.
- package/README.CN.md +220 -21
- package/README.md +221 -24
- package/esm/api/client.js +80 -0
- package/esm/api/server.js +61 -0
- package/esm/constants/index.js +289 -0
- package/esm/constants/messages.js +157 -0
- package/esm/core/client-server.js +294 -0
- package/esm/core/client.js +873 -0
- package/esm/core/request.js +27 -0
- package/esm/core/response.js +459 -0
- package/esm/core/server.js +776 -0
- package/esm/index.js +21 -0
- package/esm/interceptors/index.js +122 -0
- package/esm/message/channel.js +182 -0
- package/esm/message/dispatcher.js +418 -0
- package/esm/message/index.js +2 -0
- package/esm/stream/file-stream.js +289 -0
- package/esm/stream/index.js +44 -0
- package/esm/stream/readable-stream.js +539 -0
- package/esm/stream/stream-core.js +204 -0
- package/esm/stream/types.js +1 -0
- package/esm/stream/writable-stream.js +836 -0
- package/esm/types/index.js +1 -0
- package/esm/utils/ack.js +36 -0
- package/esm/utils/cache.js +147 -0
- package/esm/utils/cookie.js +352 -0
- package/esm/utils/debug.js +521 -0
- package/esm/utils/error.js +27 -0
- package/esm/utils/index.js +180 -0
- package/esm/utils/origin.js +30 -0
- package/esm/utils/path-match.js +148 -0
- package/esm/utils/protocol.js +157 -0
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +13 -5
- package/library/api/server.d.ts.map +1 -1
- package/library/api/server.js +6 -1
- package/library/constants/index.d.ts +59 -4
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +67 -9
- package/library/constants/messages.d.ts +8 -1
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +8 -1
- package/library/core/client-server.d.ts +7 -15
- package/library/core/client-server.d.ts.map +1 -1
- package/library/core/client-server.js +56 -44
- package/library/core/client.d.ts +4 -1
- package/library/core/client.d.ts.map +1 -1
- package/library/core/client.js +74 -31
- package/library/core/response.d.ts +21 -3
- package/library/core/response.d.ts.map +1 -1
- package/library/core/response.js +55 -7
- package/library/core/server.d.ts +34 -3
- package/library/core/server.d.ts.map +1 -1
- package/library/core/server.js +191 -21
- package/library/message/channel.d.ts +6 -0
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +2 -1
- package/library/message/dispatcher.d.ts +32 -0
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +131 -1
- package/library/stream/file-stream.d.ts +4 -0
- package/library/stream/file-stream.d.ts.map +1 -1
- package/library/stream/file-stream.js +61 -33
- package/library/stream/index.d.ts.map +1 -1
- package/library/stream/index.js +2 -0
- package/library/stream/readable-stream.d.ts +30 -11
- package/library/stream/readable-stream.d.ts.map +1 -1
- package/library/stream/readable-stream.js +368 -73
- package/library/stream/stream-core.d.ts +65 -0
- package/library/stream/stream-core.d.ts.map +1 -0
- package/library/stream/stream-core.js +211 -0
- package/library/stream/types.d.ts +203 -3
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts +59 -13
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +647 -197
- package/library/types/index.d.ts +70 -4
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/ack.d.ts +2 -0
- package/library/utils/ack.d.ts.map +1 -0
- package/library/utils/ack.js +44 -0
- package/library/utils/debug.js +1 -1
- package/library/utils/index.d.ts +1 -0
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +19 -2
- package/library/utils/origin.d.ts +14 -0
- package/library/utils/origin.d.ts.map +1 -0
- package/library/utils/origin.js +35 -0
- package/package.json +30 -7
- package/react/README.md +16 -0
- package/react/esm/index.js +284 -0
- package/react/library/index.d.ts +1 -1
- package/react/library/index.d.ts.map +1 -1
- package/react/library/index.js +3 -3
- 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`
|
|
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://
|
|
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
|
|
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
|
-
- 📁 **文件传输** - 支持文件通过流方式传输(Client
|
|
77
|
+
- 📁 **文件传输** - 支持文件通过流方式传输(Client↔Server)
|
|
78
78
|
- 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
|
|
79
79
|
- 🌍 **多语言** - 错误消息可自定义,便于国际化
|
|
80
80
|
- ✅ **协议版本** - 内置版本控制,便于升级兼容
|
|
@@ -151,7 +151,7 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
|
|
|
151
151
|
│ │
|
|
152
152
|
│ <──── RESPONSE ──────────────────────── │ 返回结果
|
|
153
153
|
│ │
|
|
154
|
-
│ ────
|
|
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` |
|
|
163
|
+
| `ack` | 接收方 → 发送方 | 回执确认(当消息 `requireAck: true` 且被接管/处理时发送;ACK-only) |
|
|
164
164
|
| `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
|
|
165
165
|
| `response` | Server → Client | 返回响应数据 |
|
|
166
166
|
| `error` | Server → Client | 返回错误信息 |
|
|
167
|
-
| `
|
|
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
|
|
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__:
|
|
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
|
-
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
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
|
|
902
|
+
const acked = await res.send(data, { requireAck: true });
|
|
790
903
|
|
|
791
|
-
if (
|
|
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
|
|