request-iframe 0.2.0 → 0.2.2
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/QUICKSTART.CN.md +33 -11
- package/QUICKSTART.md +33 -11
- package/README.CN.md +157 -44
- package/README.md +159 -41
- package/cdn/request-iframe.umd.js +4814 -4026
- package/cdn/request-iframe.umd.js.map +1 -1
- package/cdn/request-iframe.umd.min.js +2 -2
- package/cdn/request-iframe.umd.min.js.map +1 -1
- package/esm/api/client.js +45 -22
- package/esm/api/endpoint.js +30 -13
- package/esm/api/server.js +22 -13
- package/esm/constants/warn-once.js +7 -1
- package/esm/endpoint/index.js +1 -2
- package/esm/endpoint/infra/inbox.js +5 -4
- package/esm/endpoint/infra/outbox.js +8 -8
- package/esm/endpoint/stream/file-auto-resolve.js +9 -8
- package/esm/impl/client.js +3 -2
- package/esm/impl/response.js +4 -2
- package/esm/impl/server.js +8 -6
- package/esm/message/channel.js +15 -3
- package/esm/message/dispatcher.js +27 -0
- package/esm/stream/file-stream.js +311 -72
- package/esm/stream/writable-stream.js +21 -4
- package/esm/utils/blob.js +17 -0
- package/esm/utils/debug-lazy.js +76 -0
- package/esm/utils/logger.js +33 -1
- package/esm/utils/strict-mode.js +85 -0
- package/esm/utils/warn-once.js +30 -0
- package/esm/utils/warnings.js +47 -0
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +45 -22
- package/library/api/endpoint.d.ts.map +1 -1
- package/library/api/endpoint.js +30 -13
- package/library/api/server.d.ts.map +1 -1
- package/library/api/server.js +22 -13
- package/library/constants/warn-once.d.ts +6 -0
- package/library/constants/warn-once.d.ts.map +1 -1
- package/library/constants/warn-once.js +7 -1
- package/library/endpoint/index.d.ts +0 -1
- package/library/endpoint/index.d.ts.map +1 -1
- package/library/endpoint/index.js +1 -8
- package/library/endpoint/infra/inbox.d.ts.map +1 -1
- package/library/endpoint/infra/inbox.js +4 -3
- package/library/endpoint/infra/outbox.d.ts +2 -0
- package/library/endpoint/infra/outbox.d.ts.map +1 -1
- package/library/endpoint/infra/outbox.js +7 -7
- package/library/endpoint/stream/file-auto-resolve.d.ts +1 -1
- package/library/endpoint/stream/file-auto-resolve.d.ts.map +1 -1
- package/library/endpoint/stream/file-auto-resolve.js +8 -8
- package/library/impl/client.d.ts +2 -0
- package/library/impl/client.d.ts.map +1 -1
- package/library/impl/client.js +3 -2
- package/library/impl/response.d.ts.map +1 -1
- package/library/impl/response.js +4 -2
- package/library/impl/server.d.ts.map +1 -1
- package/library/impl/server.js +7 -5
- package/library/message/channel.d.ts +2 -2
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +15 -3
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +27 -0
- package/library/stream/file-stream.d.ts +70 -5
- package/library/stream/file-stream.d.ts.map +1 -1
- package/library/stream/file-stream.js +310 -70
- package/library/stream/types.d.ts +2 -0
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +21 -4
- package/library/types/index.d.ts +38 -0
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/blob.d.ts +7 -0
- package/library/utils/blob.d.ts.map +1 -1
- package/library/utils/blob.js +18 -0
- package/library/utils/debug-lazy.d.ts +26 -0
- package/library/utils/debug-lazy.d.ts.map +1 -0
- package/library/utils/debug-lazy.js +85 -0
- package/library/utils/logger.d.ts +20 -0
- package/library/utils/logger.d.ts.map +1 -1
- package/library/utils/logger.js +34 -1
- package/library/utils/strict-mode.d.ts +37 -0
- package/library/utils/strict-mode.d.ts.map +1 -0
- package/library/utils/strict-mode.js +94 -0
- package/library/utils/warn-once.d.ts +9 -0
- package/library/utils/warn-once.d.ts.map +1 -0
- package/library/utils/warn-once.js +36 -0
- package/library/utils/warnings.d.ts +48 -0
- package/library/utils/warnings.d.ts.map +1 -0
- package/library/utils/warnings.js +54 -0
- package/package.json +1 -1
- package/esm/endpoint/stream/file-writable.js +0 -105
- package/library/endpoint/stream/file-writable.d.ts +0 -33
- package/library/endpoint/stream/file-writable.d.ts.map +0 -1
- package/library/endpoint/stream/file-writable.js +0 -115
package/QUICKSTART.CN.md
CHANGED
|
@@ -29,15 +29,24 @@ yarn add request-iframe
|
|
|
29
29
|
## Step 1: 父页面创建 Client
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
|
-
|
|
32
|
+
/** parent.html */
|
|
33
33
|
import { requestIframeClient } from 'request-iframe';
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/** 获取 iframe 元素 */
|
|
36
36
|
const iframe = document.getElementById('my-iframe') as HTMLIFrameElement;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** 建议等待 iframe load,避免 contentWindow 尚未就绪导致通信失败 */
|
|
39
|
+
await new Promise<void>((resolve) => iframe.addEventListener('load', () => resolve(), { once: true }));
|
|
40
|
+
|
|
41
|
+
/** 创建 client(用于发送请求) */
|
|
39
42
|
const client = requestIframeClient(iframe, {
|
|
40
|
-
secretKey: 'my-app'
|
|
43
|
+
secretKey: 'my-app', /** 消息隔离标识,需要和 iframe 内保持一致 */
|
|
44
|
+
/**
|
|
45
|
+
* strict: true 会把 targetOrigin/allowedOrigins 默认收敛到当前域名(window.location.origin)
|
|
46
|
+
* - 适用于同源 iframe
|
|
47
|
+
* - **注意:strict 不等于跨域安全配置**;若跨域,请显式配置 targetOrigin + allowedOrigins/validateOrigin
|
|
48
|
+
*/
|
|
49
|
+
strict: true
|
|
41
50
|
});
|
|
42
51
|
|
|
43
52
|
// 发送请求并等待响应
|
|
@@ -54,12 +63,18 @@ console.log(user); // { name: 'Tom', age: 18 }
|
|
|
54
63
|
## Step 2: iframe 内创建 Server
|
|
55
64
|
|
|
56
65
|
```typescript
|
|
57
|
-
|
|
66
|
+
/** child.html(iframe 内) */
|
|
58
67
|
import { requestIframeServer } from 'request-iframe';
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 创建 server(用于接收请求)
|
|
71
|
+
* - 生产环境强烈建议配置 allowedOrigins / validateOrigin
|
|
72
|
+
* - 这里使用同源 demo:父页面 origin === iframe 内页面 origin
|
|
73
|
+
* 若跨域,请改成父页面的 origin(例如 'https://parent.example.com')
|
|
74
|
+
*/
|
|
61
75
|
const server = requestIframeServer({
|
|
62
|
-
secretKey: 'my-app'
|
|
76
|
+
secretKey: 'my-app', /** 必须和父页面的 client 保持一致! */
|
|
77
|
+
strict: true
|
|
63
78
|
});
|
|
64
79
|
|
|
65
80
|
// 注册请求处理器
|
|
@@ -224,11 +239,13 @@ import { LogLevel } from 'request-iframe';
|
|
|
224
239
|
|
|
225
240
|
const client = requestIframeClient(iframe, {
|
|
226
241
|
secretKey: 'my-app',
|
|
242
|
+
/** 建议配置 targetOrigin/allowedOrigins(见 Step 1) */
|
|
227
243
|
trace: LogLevel.INFO // 输出 info/warn/error(也可以用 true 开启 TRACE)
|
|
228
244
|
});
|
|
229
245
|
|
|
230
246
|
const server = requestIframeServer({
|
|
231
247
|
secretKey: 'my-app',
|
|
248
|
+
/** 建议配置 allowedOrigins/validateOrigin(见 Step 2) */
|
|
232
249
|
trace: true
|
|
233
250
|
});
|
|
234
251
|
```
|
|
@@ -254,12 +271,17 @@ const server = requestIframeServer({
|
|
|
254
271
|
### Q: 如何在 iframe 内向父页面发送请求?
|
|
255
272
|
|
|
256
273
|
```typescript
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
/**
|
|
275
|
+
* iframe 内
|
|
276
|
+
* - Window 场景必须显式设置 targetOrigin,并把它加入 allowedOrigins
|
|
277
|
+
*/
|
|
278
|
+
const parentOrigin = 'https://parent.example.com';
|
|
279
|
+
const client = requestIframeClient(window.parent, { secretKey: 'reverse', targetOrigin: parentOrigin, allowedOrigins: [parentOrigin] });
|
|
259
280
|
await client.send('/notify', { event: 'ready' });
|
|
260
281
|
|
|
261
|
-
|
|
262
|
-
const
|
|
282
|
+
/** 父页面(allowedOrigins 应配置为 iframe 的 origin) */
|
|
283
|
+
const iframeOrigin = 'https://child.example.com';
|
|
284
|
+
const server = requestIframeServer({ secretKey: 'reverse', allowedOrigins: [iframeOrigin] });
|
|
263
285
|
server.on('/notify', (req, res) => {
|
|
264
286
|
console.log('iframe 已就绪');
|
|
265
287
|
res.send({ ok: true });
|
package/QUICKSTART.md
CHANGED
|
@@ -29,15 +29,24 @@ Suppose you have a parent page that needs to communicate with an embedded iframe
|
|
|
29
29
|
## Step 1: Create Client in Parent Page
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
|
-
|
|
32
|
+
/** parent.html */
|
|
33
33
|
import { requestIframeClient } from 'request-iframe';
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/** Get iframe element */
|
|
36
36
|
const iframe = document.getElementById('my-iframe') as HTMLIFrameElement;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** Prefer waiting iframe load so contentWindow is ready */
|
|
39
|
+
await new Promise<void>((resolve) => iframe.addEventListener('load', () => resolve(), { once: true }));
|
|
40
|
+
|
|
41
|
+
/** Create client (for sending requests) */
|
|
39
42
|
const client = requestIframeClient(iframe, {
|
|
40
|
-
secretKey: 'my-app'
|
|
43
|
+
secretKey: 'my-app', /** Message isolation identifier, must match iframe */
|
|
44
|
+
/**
|
|
45
|
+
* strict: true defaults targetOrigin/allowedOrigins to window.location.origin (same-origin only)
|
|
46
|
+
* - Works for same-origin iframes
|
|
47
|
+
* - **Note: strict is NOT a cross-origin security configuration**; for cross-origin, explicitly configure targetOrigin + allowedOrigins/validateOrigin
|
|
48
|
+
*/
|
|
49
|
+
strict: true
|
|
41
50
|
});
|
|
42
51
|
|
|
43
52
|
// Send request and wait for response
|
|
@@ -54,12 +63,18 @@ console.log(user); // { name: 'Tom', age: 18 }
|
|
|
54
63
|
## Step 2: Create Server in iframe
|
|
55
64
|
|
|
56
65
|
```typescript
|
|
57
|
-
|
|
66
|
+
/** child.html (inside iframe) */
|
|
58
67
|
import { requestIframeServer } from 'request-iframe';
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Create server (for receiving requests)
|
|
71
|
+
* - Strongly recommended to configure allowedOrigins / validateOrigin in production
|
|
72
|
+
* - This snippet assumes a same-origin demo (parent origin === iframe origin)
|
|
73
|
+
* For cross-origin, replace with the real parent origin (e.g. 'https://parent.example.com')
|
|
74
|
+
*/
|
|
61
75
|
const server = requestIframeServer({
|
|
62
|
-
secretKey: 'my-app'
|
|
76
|
+
secretKey: 'my-app', /** Must match parent page's client! */
|
|
77
|
+
strict: true
|
|
63
78
|
});
|
|
64
79
|
|
|
65
80
|
// Register request handler
|
|
@@ -224,11 +239,13 @@ import { LogLevel } from 'request-iframe';
|
|
|
224
239
|
|
|
225
240
|
const client = requestIframeClient(iframe, {
|
|
226
241
|
secretKey: 'my-app',
|
|
242
|
+
/** Recommended: configure targetOrigin/allowedOrigins (see Step 1) */
|
|
227
243
|
trace: LogLevel.INFO // Enable info/warn/error logs (or use true for TRACE)
|
|
228
244
|
});
|
|
229
245
|
|
|
230
246
|
const server = requestIframeServer({
|
|
231
247
|
secretKey: 'my-app',
|
|
248
|
+
/** Recommended: configure allowedOrigins/validateOrigin (see Step 2) */
|
|
232
249
|
trace: true
|
|
233
250
|
});
|
|
234
251
|
```
|
|
@@ -254,12 +271,17 @@ Check the following:
|
|
|
254
271
|
### Q: How to send requests from iframe to parent page?
|
|
255
272
|
|
|
256
273
|
```typescript
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Inside iframe
|
|
276
|
+
* - For Window targets, always set a strict targetOrigin and allowlist it.
|
|
277
|
+
*/
|
|
278
|
+
const parentOrigin = 'https://parent.example.com';
|
|
279
|
+
const client = requestIframeClient(window.parent, { secretKey: 'reverse', targetOrigin: parentOrigin, allowedOrigins: [parentOrigin] });
|
|
259
280
|
await client.send('/notify', { event: 'ready' });
|
|
260
281
|
|
|
261
|
-
|
|
262
|
-
const
|
|
282
|
+
/** Parent page (allowedOrigins should be the iframe origin) */
|
|
283
|
+
const iframeOrigin = 'https://child.example.com';
|
|
284
|
+
const server = requestIframeServer({ secretKey: 'reverse', allowedOrigins: [iframeOrigin] });
|
|
263
285
|
server.on('/notify', (req, res) => {
|
|
264
286
|
console.log('iframe is ready');
|
|
265
287
|
res.send({ ok: true });
|
package/README.CN.md
CHANGED
|
@@ -127,13 +127,20 @@ pnpm add request-iframe
|
|
|
127
127
|
```typescript
|
|
128
128
|
import { requestIframeClient } from 'request-iframe';
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
const iframe = document.querySelector('iframe')
|
|
130
|
+
/** 获取 iframe 元素 */
|
|
131
|
+
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
|
|
132
|
+
|
|
133
|
+
/** 建议等待 iframe load,避免 contentWindow 尚未就绪导致通信失败 */
|
|
134
|
+
await new Promise<void>((resolve) => iframe.addEventListener('load', () => resolve(), { once: true }));
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
/**
|
|
137
|
+
* 创建 client(更安全、更省配置的默认写法)
|
|
138
|
+
* - strict: true 会把 targetOrigin/allowedOrigins 默认收敛到当前域名(window.location.origin)
|
|
139
|
+
* - 适用于同源 iframe;若跨域,请显式配置 targetOrigin + allowedOrigins/validateOrigin
|
|
140
|
+
*/
|
|
141
|
+
const client = requestIframeClient(iframe, { secretKey: 'my-app', strict: true });
|
|
135
142
|
|
|
136
|
-
|
|
143
|
+
/** 发送请求(就像 axios) */
|
|
137
144
|
const response = await client.send('/api/getUserInfo', { userId: 123 });
|
|
138
145
|
console.log(response.data); // { name: 'Tom', age: 18 }
|
|
139
146
|
```
|
|
@@ -143,10 +150,15 @@ console.log(response.data); // { name: 'Tom', age: 18 }
|
|
|
143
150
|
```typescript
|
|
144
151
|
import { requestIframeServer } from 'request-iframe';
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
/**
|
|
154
|
+
* 创建 server
|
|
155
|
+
* - 生产环境强烈建议配置 allowedOrigins / validateOrigin
|
|
156
|
+
* - 这里使用同源 demo:父页面 origin === iframe 内页面 origin
|
|
157
|
+
* 若跨域,请改成父页面的 origin(例如 'https://parent.example.com')
|
|
158
|
+
*/
|
|
159
|
+
const server = requestIframeServer({ secretKey: 'my-app', strict: true });
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
/** 注册处理器(就像 express) */
|
|
150
162
|
server.on('/api/getUserInfo', (req, res) => {
|
|
151
163
|
const { userId } = req.body;
|
|
152
164
|
res.send({ name: 'Tom', age: 18 });
|
|
@@ -284,14 +296,14 @@ client.send('/api/getData', data, {
|
|
|
284
296
|
在微前端架构中,主应用需要与子应用 iframe 进行数据交互:
|
|
285
297
|
|
|
286
298
|
```typescript
|
|
287
|
-
|
|
288
|
-
const client = requestIframeClient(iframe, { secretKey: 'main-app' });
|
|
299
|
+
/** 主应用(父页面,同源默认推荐 strict: true) */
|
|
300
|
+
const client = requestIframeClient(iframe, { secretKey: 'main-app', strict: true });
|
|
289
301
|
|
|
290
|
-
|
|
302
|
+
/** 获取子应用的用户信息 */
|
|
291
303
|
const userInfoResponse = await client.send('/api/user/info', {});
|
|
292
304
|
console.log(userInfoResponse.data); // 用户信息数据
|
|
293
305
|
|
|
294
|
-
|
|
306
|
+
/** 通知子应用更新数据 */
|
|
295
307
|
await client.send('/api/data/refresh', { timestamp: Date.now() });
|
|
296
308
|
```
|
|
297
309
|
|
|
@@ -300,23 +312,25 @@ await client.send('/api/data/refresh', { timestamp: Date.now() });
|
|
|
300
312
|
集成第三方组件时,通过 iframe 隔离,同时保持通信:
|
|
301
313
|
|
|
302
314
|
```typescript
|
|
303
|
-
|
|
304
|
-
const client = requestIframeClient(thirdPartyIframe, { secretKey: 'widget' });
|
|
315
|
+
/** 父页面(同源默认推荐 strict: true) */
|
|
316
|
+
const client = requestIframeClient(thirdPartyIframe, { secretKey: 'widget', strict: true });
|
|
305
317
|
|
|
306
|
-
|
|
318
|
+
/** 配置组件 */
|
|
307
319
|
await client.send('/config', {
|
|
308
320
|
theme: 'dark',
|
|
309
321
|
language: 'zh-CN'
|
|
310
322
|
});
|
|
311
323
|
|
|
312
|
-
|
|
313
|
-
const server = requestIframeServer({ secretKey: 'widget' });
|
|
324
|
+
/** 监听组件事件(通过反向通信) */
|
|
325
|
+
const server = requestIframeServer({ secretKey: 'widget', strict: true });
|
|
314
326
|
server.on('/event', (req, res) => {
|
|
315
327
|
console.log('组件事件:', req.body);
|
|
316
328
|
res.send({ received: true });
|
|
317
329
|
});
|
|
318
330
|
```
|
|
319
331
|
|
|
332
|
+
> 若第三方 iframe **跨域**,请显式配置 `targetOrigin` 与 `allowedOrigins/validateOrigin`(见「安全建议」一节)。
|
|
333
|
+
|
|
320
334
|
### 弹窗 / 新标签页(Window 通信)
|
|
321
335
|
|
|
322
336
|
`request-iframe` 不仅可以与 iframe 通信,也可以把 `target` 直接传 `Window`(例如弹窗/新标签页)。
|
|
@@ -324,19 +338,25 @@ server.on('/event', (req, res) => {
|
|
|
324
338
|
**重要前提**:你必须拿到对方页面的 `Window` 引用(例如 `window.open()` 的返回值,或通过 `window.opener` / `MessageEvent.source` 获取)。**无法**通过 URL 给“任意标签页”发消息。
|
|
325
339
|
|
|
326
340
|
```typescript
|
|
327
|
-
|
|
341
|
+
/** 父页面:打开新标签页/弹窗 */
|
|
328
342
|
const child = window.open('https://child.example.com/page.html', '_blank');
|
|
329
343
|
if (!child) throw new Error('弹窗被拦截');
|
|
330
344
|
|
|
331
|
-
|
|
345
|
+
/** 父 -> 子 */
|
|
346
|
+
const targetOrigin = 'https://child.example.com';
|
|
332
347
|
const client = requestIframeClient(child, {
|
|
333
348
|
secretKey: 'popup-demo',
|
|
334
|
-
targetOrigin
|
|
349
|
+
targetOrigin,
|
|
350
|
+
allowedOrigins: [targetOrigin]
|
|
335
351
|
});
|
|
336
352
|
await client.send('/api/ping', { from: 'parent' });
|
|
337
353
|
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
/**
|
|
355
|
+
* 子页面:创建 server
|
|
356
|
+
* 注意:allowedOrigins 应配置为父页面 origin(这里用示例值,请替换成真实值)
|
|
357
|
+
*/
|
|
358
|
+
const parentOrigin = 'https://parent.example.com';
|
|
359
|
+
const server = requestIframeServer({ secretKey: 'popup-demo', allowedOrigins: [parentOrigin] });
|
|
340
360
|
server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
|
|
341
361
|
```
|
|
342
362
|
|
|
@@ -345,8 +365,12 @@ server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
|
|
|
345
365
|
当 iframe 与父页面不同域时,使用 request-iframe 安全地获取数据:
|
|
346
366
|
|
|
347
367
|
```typescript
|
|
348
|
-
|
|
349
|
-
|
|
368
|
+
/**
|
|
369
|
+
* iframe 内(不同域)
|
|
370
|
+
* 注意:allowedOrigins 应配置为父页面 origin(这里用示例值,请替换成真实值)
|
|
371
|
+
*/
|
|
372
|
+
const parentOrigin = 'https://parent.example.com';
|
|
373
|
+
const server = requestIframeServer({ secretKey: 'data-api', allowedOrigins: [parentOrigin] });
|
|
350
374
|
|
|
351
375
|
server.on('/api/data', async (req, res) => {
|
|
352
376
|
// 从同域 API 获取数据(iframe 可以访问同域资源)
|
|
@@ -354,8 +378,13 @@ server.on('/api/data', async (req, res) => {
|
|
|
354
378
|
res.send(data);
|
|
355
379
|
});
|
|
356
380
|
|
|
357
|
-
|
|
358
|
-
const
|
|
381
|
+
/** 父页面(跨域) */
|
|
382
|
+
const targetOrigin = new URL(iframe.src).origin;
|
|
383
|
+
const client = requestIframeClient(iframe, {
|
|
384
|
+
secretKey: 'data-api',
|
|
385
|
+
targetOrigin,
|
|
386
|
+
allowedOrigins: [targetOrigin]
|
|
387
|
+
});
|
|
359
388
|
const response = await client.send('/api/data', {});
|
|
360
389
|
const data = response.data; // 成功获取跨域数据
|
|
361
390
|
```
|
|
@@ -769,6 +798,17 @@ import {
|
|
|
769
798
|
isIframeFileReadableStream
|
|
770
799
|
} from 'request-iframe';
|
|
771
800
|
|
|
801
|
+
/**
|
|
802
|
+
* 选型建议(数据流 vs 文件/字节流)
|
|
803
|
+
*
|
|
804
|
+
* - IframeWritableStream / IframeReadableStream:
|
|
805
|
+
* 用于“业务数据”(对象/字符串等),依赖 structured clone,适合 JSON/事件/日志等。
|
|
806
|
+
* - IframeFileWritableStream / IframeFileReadableStream:
|
|
807
|
+
* 用于“字节序列”(文件/二进制/UTF-8 文本文件),chunk 语义是 bytes,适合大文件与二进制传输。
|
|
808
|
+
* - 文本文件推荐:IframeFileWritableStream.fromText(...) + fileStream.readAsText()
|
|
809
|
+
* - 二进制推荐:直接传 Uint8Array/ArrayBuffer(可走 transferable)
|
|
810
|
+
*/
|
|
811
|
+
|
|
772
812
|
// Server 端:使用迭代器发送数据流
|
|
773
813
|
server.on('/api/stream', async (req, res) => {
|
|
774
814
|
const stream = new IframeWritableStream({
|
|
@@ -820,6 +860,19 @@ server.on('/api/fileStream', async (req, res) => {
|
|
|
820
860
|
await res.sendStream(stream);
|
|
821
861
|
});
|
|
822
862
|
|
|
863
|
+
// Server 端:方式2(推荐)——用 from() 从 Blob/File 一键创建(自动分片)
|
|
864
|
+
server.on('/api/fileStream2', async (req, res) => {
|
|
865
|
+
const blob = new Blob([/* 文件内容 */], { type: 'application/octet-stream' });
|
|
866
|
+
const stream = await IframeFileWritableStream.from({
|
|
867
|
+
content: blob,
|
|
868
|
+
fileName: 'large-file.bin',
|
|
869
|
+
mimeType: 'application/octet-stream',
|
|
870
|
+
chunked: true,
|
|
871
|
+
chunkSize: 256 * 1024
|
|
872
|
+
});
|
|
873
|
+
await res.sendStream(stream);
|
|
874
|
+
});
|
|
875
|
+
|
|
823
876
|
// Client 端:接收流数据
|
|
824
877
|
const response = await client.send('/api/stream', {});
|
|
825
878
|
|
|
@@ -856,6 +909,9 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
|
|
|
856
909
|
// 读取为 Blob
|
|
857
910
|
const blob = await fileResponse.stream.readAsBlob();
|
|
858
911
|
|
|
912
|
+
// 读取为 UTF-8 文本(适用于“文本文件”)
|
|
913
|
+
const text = await fileResponse.stream.readAsText();
|
|
914
|
+
|
|
859
915
|
// 读取为 ArrayBuffer
|
|
860
916
|
const buffer = await fileResponse.stream.readAsArrayBuffer();
|
|
861
917
|
|
|
@@ -874,11 +930,11 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
|
|
|
874
930
|
| 类型 | 说明 |
|
|
875
931
|
|------|------|
|
|
876
932
|
| `IframeWritableStream` | 写侧(生产者)流:**谁要发送 stream,谁就创建它**;可用于 Server→Client 的响应流,也可用于 Client→Server 的请求流 |
|
|
877
|
-
| `IframeFileWritableStream` |
|
|
933
|
+
| `IframeFileWritableStream` | 文件写侧(生产者)流:用于发送文件(支持 `Uint8Array` / `ArrayBuffer` 分片;会尽量使用 transferable 传输以减少拷贝) |
|
|
878
934
|
| `IframeReadableStream` | 读侧(消费者)流:用于接收普通数据(无论来自 Server 还是 Client) |
|
|
879
|
-
| `IframeFileReadableStream` |
|
|
935
|
+
| `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(支持二进制分片) |
|
|
880
936
|
|
|
881
|
-
>
|
|
937
|
+
> **注意**:文件流的 chunk 语义是“字节序列”。当前版本默认以二进制 chunk(`ArrayBuffer`/`Uint8Array`)传输,并会尽量使用 transferable 来降低拷贝与开销。若你手写 `iterator/next` 产出 `string`,会按 **UTF-8 编码成字节** 后再传输;要传二进制请直接产出 `Uint8Array/ArrayBuffer`。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
|
|
882
938
|
|
|
883
939
|
**流选项:**
|
|
884
940
|
|
|
@@ -1007,6 +1063,7 @@ setMessages({
|
|
|
1007
1063
|
| `target` | `HTMLIFrameElement \| Window` | 目标 iframe 元素或 window 对象 |
|
|
1008
1064
|
| `options.secretKey` | `string` | 消息隔离标识(可选) |
|
|
1009
1065
|
| `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | trace/日志等级(可选)。默认只输出 warn/error |
|
|
1066
|
+
| `options.strict` | `boolean` | 严格模式(推荐同源默认):当你未显式配置 `targetOrigin/allowedOrigins/validateOrigin` 时,默认收敛为 `window.location.origin`(同源 only)。**注意:strict 不等于跨域安全配置**,跨域场景仍需显式设置 `targetOrigin` + `allowedOrigins/validateOrigin`。 |
|
|
1010
1067
|
| `options.targetOrigin` | `string` | 覆盖 postMessage 的 targetOrigin(可选)。当 `target` 是 `Window` 时默认 `*`;当 `target` 是 iframe 时默认取 `iframe.src` 的 origin。 |
|
|
1011
1068
|
| `options.ackTimeout` | `number` | 全局默认 ACK 确认超时(ms),默认 1000 |
|
|
1012
1069
|
| `options.timeout` | `number` | 全局默认请求超时(ms),默认 5000 |
|
|
@@ -1021,7 +1078,8 @@ setMessages({
|
|
|
1021
1078
|
**关于 `target: Window` 的说明:**
|
|
1022
1079
|
- **必须持有对方页面的 `Window` 引用**(例如 `window.open()` 返回值、`window.opener`、或 `MessageEvent.source`)。
|
|
1023
1080
|
- **无法**通过 URL 给“任意标签页”发消息。
|
|
1024
|
-
- 安全起见,建议显式设置 `targetOrigin
|
|
1081
|
+
- 安全起见,建议显式设置 `targetOrigin`(不要用 `*`),并配置 `allowedOrigins` / `validateOrigin`。
|
|
1082
|
+
- `strict: true` 只会把默认值收敛到当前域名(同源 only),并**不会**自动帮你完成跨域场景的安全配置。
|
|
1025
1083
|
|
|
1026
1084
|
**生产环境推荐配置(模板):**
|
|
1027
1085
|
|
|
@@ -1109,6 +1167,7 @@ await client.send('/api/longTask', {}, {
|
|
|
1109
1167
|
|------|------|------|
|
|
1110
1168
|
| `options.secretKey` | `string` | 消息隔离标识(可选) |
|
|
1111
1169
|
| `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | trace/日志等级(可选)。默认只输出 warn/error |
|
|
1170
|
+
| `options.strict` | `boolean` | 严格模式(推荐同源默认):当你未显式配置 `allowedOrigins/validateOrigin` 时,默认 `allowedOrigins: [window.location.origin]`(同源 only)。**注意:strict 不等于跨域安全配置**,跨域场景仍需显式配置父页面 origin 的 allowlist/validator。 |
|
|
1112
1171
|
| `options.ackTimeout` | `number` | 等待客户端确认超时(ms),默认 1000 |
|
|
1113
1172
|
| `options.maxConcurrentRequestsPerClient` | `number` | 每个客户端的最大并发 in-flight 请求数(按 origin + creatorId 维度),默认 Infinity |
|
|
1114
1173
|
| `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
|
|
@@ -1837,13 +1896,34 @@ try {
|
|
|
1837
1896
|
`secretKey` 用于消息隔离。当页面中有多个 iframe 或多个 request-iframe 实例时,通过不同的 `secretKey` 可以避免消息串线:
|
|
1838
1897
|
|
|
1839
1898
|
```typescript
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1899
|
+
/**
|
|
1900
|
+
* iframe A 的通信
|
|
1901
|
+
* - 父页面需要 allowlist iframe A 的 origin
|
|
1902
|
+
* - iframe A 内需要 allowlist 父页面的 origin
|
|
1903
|
+
*/
|
|
1904
|
+
const iframeASrc = iframeA.getAttribute('src');
|
|
1905
|
+
if (!iframeASrc) throw new Error('iframeA src is empty');
|
|
1906
|
+
const iframeAOrigin = new URL(iframeASrc, window.location.href).origin;
|
|
1907
|
+
const clientA = requestIframeClient(iframeA, {
|
|
1908
|
+
secretKey: 'app-a',
|
|
1909
|
+
targetOrigin: iframeAOrigin,
|
|
1910
|
+
allowedOrigins: [iframeAOrigin]
|
|
1911
|
+
});
|
|
1912
|
+
const parentOrigin = 'https://parent.com';
|
|
1913
|
+
const serverA = requestIframeServer({ secretKey: 'app-a', allowedOrigins: [parentOrigin] });
|
|
1843
1914
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1915
|
+
/**
|
|
1916
|
+
* iframe B 的通信(同理)
|
|
1917
|
+
*/
|
|
1918
|
+
const iframeBSrc = iframeB.getAttribute('src');
|
|
1919
|
+
if (!iframeBSrc) throw new Error('iframeB src is empty');
|
|
1920
|
+
const iframeBOrigin = new URL(iframeBSrc, window.location.href).origin;
|
|
1921
|
+
const clientB = requestIframeClient(iframeB, {
|
|
1922
|
+
secretKey: 'app-b',
|
|
1923
|
+
targetOrigin: iframeBOrigin,
|
|
1924
|
+
allowedOrigins: [iframeBOrigin]
|
|
1925
|
+
});
|
|
1926
|
+
const serverB = requestIframeServer({ secretKey: 'app-b', allowedOrigins: [parentOrigin] });
|
|
1847
1927
|
```
|
|
1848
1928
|
|
|
1849
1929
|
### 2. 为什么需要 ACK 确认?
|
|
@@ -1858,14 +1938,28 @@ ACK 机制类似 TCP 握手,用于:
|
|
|
1858
1938
|
`postMessage` 本身支持跨域通信,request-iframe 会自动处理:
|
|
1859
1939
|
|
|
1860
1940
|
```typescript
|
|
1861
|
-
|
|
1862
|
-
|
|
1941
|
+
/**
|
|
1942
|
+
* 父页面 (https://parent.com)
|
|
1943
|
+
* - targetOrigin/allowedOrigins 应配置为 iframe 的 origin
|
|
1944
|
+
*/
|
|
1945
|
+
const iframeSrc = iframe.getAttribute('src');
|
|
1946
|
+
if (!iframeSrc) throw new Error('iframe src is empty');
|
|
1947
|
+
const childOrigin = new URL(iframeSrc, window.location.href).origin;
|
|
1948
|
+
const client = requestIframeClient(iframe, {
|
|
1949
|
+
secretKey: 'my-app',
|
|
1950
|
+
targetOrigin: childOrigin,
|
|
1951
|
+
allowedOrigins: [childOrigin]
|
|
1952
|
+
});
|
|
1863
1953
|
|
|
1864
|
-
|
|
1865
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* iframe 内 (https://child.com)
|
|
1956
|
+
* - allowedOrigins 应配置为父页面的 origin
|
|
1957
|
+
*/
|
|
1958
|
+
const parentOrigin = 'https://parent.com';
|
|
1959
|
+
const server = requestIframeServer({ secretKey: 'my-app', allowedOrigins: [parentOrigin] });
|
|
1866
1960
|
```
|
|
1867
1961
|
|
|
1868
|
-
只需确保双方使用相同的 `secretKey`。
|
|
1962
|
+
只需确保双方使用相同的 `secretKey`,并正确配置 `targetOrigin` / `allowedOrigins`。
|
|
1869
1963
|
|
|
1870
1964
|
### 4. Server 可以主动推送消息吗?
|
|
1871
1965
|
|
|
@@ -1876,9 +1970,17 @@ request-iframe 是请求-响应模式,Server 本身不能“主动推送”。
|
|
|
1876
1970
|
- 双方都使用 `requestIframeEndpoint()`(推荐),一个对象同时具备 **send + handle**
|
|
1877
1971
|
|
|
1878
1972
|
```typescript
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1973
|
+
/**
|
|
1974
|
+
* iframe 内
|
|
1975
|
+
* - Window 场景必须显式设置 targetOrigin,并把它加入 allowedOrigins
|
|
1976
|
+
*/
|
|
1977
|
+
const parentOrigin = 'https://parent.com';
|
|
1978
|
+
const server = requestIframeServer({ secretKey: 'my-app', allowedOrigins: [parentOrigin] });
|
|
1979
|
+
const client = requestIframeClient(window.parent, {
|
|
1980
|
+
secretKey: 'my-app-reverse',
|
|
1981
|
+
targetOrigin: parentOrigin,
|
|
1982
|
+
allowedOrigins: [parentOrigin]
|
|
1983
|
+
});
|
|
1882
1984
|
|
|
1883
1985
|
// 主动向父页面发送消息
|
|
1884
1986
|
await client.send('/notify', { event: 'data-changed' });
|
|
@@ -1911,12 +2013,23 @@ server.on('/api/largeFile', async (req, res) => {
|
|
|
1911
2013
|
// 分块读取文件
|
|
1912
2014
|
const chunkSize = 1024 * 1024; // 1MB per chunk
|
|
1913
2015
|
for (let i = 0; i < fileSize; i += chunkSize) {
|
|
2016
|
+
// 建议产出 Uint8Array / ArrayBuffer(可走 transferable)
|
|
1914
2017
|
yield await readFileChunk(i, chunkSize);
|
|
1915
2018
|
}
|
|
1916
2019
|
}
|
|
1917
2020
|
});
|
|
1918
2021
|
await res.sendStream(stream);
|
|
1919
2022
|
});
|
|
2023
|
+
|
|
2024
|
+
// 也可以用 from() 从 Blob/File 自动分片生成文件流
|
|
2025
|
+
// const { stream } = await IframeFileWritableStream.from({
|
|
2026
|
+
// content: someBlobOrFile,
|
|
2027
|
+
// fileName: 'large-file.zip',
|
|
2028
|
+
// mimeType: 'application/zip',
|
|
2029
|
+
// chunked: true,
|
|
2030
|
+
// chunkSize: 1024 * 1024
|
|
2031
|
+
// });
|
|
2032
|
+
// await res.sendStream(stream);
|
|
1920
2033
|
```
|
|
1921
2034
|
|
|
1922
2035
|
### 8. 如何实现请求重试?
|