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.
Files changed (93) hide show
  1. package/QUICKSTART.CN.md +33 -11
  2. package/QUICKSTART.md +33 -11
  3. package/README.CN.md +157 -44
  4. package/README.md +159 -41
  5. package/cdn/request-iframe.umd.js +4814 -4026
  6. package/cdn/request-iframe.umd.js.map +1 -1
  7. package/cdn/request-iframe.umd.min.js +2 -2
  8. package/cdn/request-iframe.umd.min.js.map +1 -1
  9. package/esm/api/client.js +45 -22
  10. package/esm/api/endpoint.js +30 -13
  11. package/esm/api/server.js +22 -13
  12. package/esm/constants/warn-once.js +7 -1
  13. package/esm/endpoint/index.js +1 -2
  14. package/esm/endpoint/infra/inbox.js +5 -4
  15. package/esm/endpoint/infra/outbox.js +8 -8
  16. package/esm/endpoint/stream/file-auto-resolve.js +9 -8
  17. package/esm/impl/client.js +3 -2
  18. package/esm/impl/response.js +4 -2
  19. package/esm/impl/server.js +8 -6
  20. package/esm/message/channel.js +15 -3
  21. package/esm/message/dispatcher.js +27 -0
  22. package/esm/stream/file-stream.js +311 -72
  23. package/esm/stream/writable-stream.js +21 -4
  24. package/esm/utils/blob.js +17 -0
  25. package/esm/utils/debug-lazy.js +76 -0
  26. package/esm/utils/logger.js +33 -1
  27. package/esm/utils/strict-mode.js +85 -0
  28. package/esm/utils/warn-once.js +30 -0
  29. package/esm/utils/warnings.js +47 -0
  30. package/library/api/client.d.ts.map +1 -1
  31. package/library/api/client.js +45 -22
  32. package/library/api/endpoint.d.ts.map +1 -1
  33. package/library/api/endpoint.js +30 -13
  34. package/library/api/server.d.ts.map +1 -1
  35. package/library/api/server.js +22 -13
  36. package/library/constants/warn-once.d.ts +6 -0
  37. package/library/constants/warn-once.d.ts.map +1 -1
  38. package/library/constants/warn-once.js +7 -1
  39. package/library/endpoint/index.d.ts +0 -1
  40. package/library/endpoint/index.d.ts.map +1 -1
  41. package/library/endpoint/index.js +1 -8
  42. package/library/endpoint/infra/inbox.d.ts.map +1 -1
  43. package/library/endpoint/infra/inbox.js +4 -3
  44. package/library/endpoint/infra/outbox.d.ts +2 -0
  45. package/library/endpoint/infra/outbox.d.ts.map +1 -1
  46. package/library/endpoint/infra/outbox.js +7 -7
  47. package/library/endpoint/stream/file-auto-resolve.d.ts +1 -1
  48. package/library/endpoint/stream/file-auto-resolve.d.ts.map +1 -1
  49. package/library/endpoint/stream/file-auto-resolve.js +8 -8
  50. package/library/impl/client.d.ts +2 -0
  51. package/library/impl/client.d.ts.map +1 -1
  52. package/library/impl/client.js +3 -2
  53. package/library/impl/response.d.ts.map +1 -1
  54. package/library/impl/response.js +4 -2
  55. package/library/impl/server.d.ts.map +1 -1
  56. package/library/impl/server.js +7 -5
  57. package/library/message/channel.d.ts +2 -2
  58. package/library/message/channel.d.ts.map +1 -1
  59. package/library/message/channel.js +15 -3
  60. package/library/message/dispatcher.d.ts.map +1 -1
  61. package/library/message/dispatcher.js +27 -0
  62. package/library/stream/file-stream.d.ts +70 -5
  63. package/library/stream/file-stream.d.ts.map +1 -1
  64. package/library/stream/file-stream.js +310 -70
  65. package/library/stream/types.d.ts +2 -0
  66. package/library/stream/types.d.ts.map +1 -1
  67. package/library/stream/writable-stream.d.ts.map +1 -1
  68. package/library/stream/writable-stream.js +21 -4
  69. package/library/types/index.d.ts +38 -0
  70. package/library/types/index.d.ts.map +1 -1
  71. package/library/utils/blob.d.ts +7 -0
  72. package/library/utils/blob.d.ts.map +1 -1
  73. package/library/utils/blob.js +18 -0
  74. package/library/utils/debug-lazy.d.ts +26 -0
  75. package/library/utils/debug-lazy.d.ts.map +1 -0
  76. package/library/utils/debug-lazy.js +85 -0
  77. package/library/utils/logger.d.ts +20 -0
  78. package/library/utils/logger.d.ts.map +1 -1
  79. package/library/utils/logger.js +34 -1
  80. package/library/utils/strict-mode.d.ts +37 -0
  81. package/library/utils/strict-mode.d.ts.map +1 -0
  82. package/library/utils/strict-mode.js +94 -0
  83. package/library/utils/warn-once.d.ts +9 -0
  84. package/library/utils/warn-once.d.ts.map +1 -0
  85. package/library/utils/warn-once.js +36 -0
  86. package/library/utils/warnings.d.ts +48 -0
  87. package/library/utils/warnings.d.ts.map +1 -0
  88. package/library/utils/warnings.js +54 -0
  89. package/package.json +1 -1
  90. package/esm/endpoint/stream/file-writable.js +0 -105
  91. package/library/endpoint/stream/file-writable.d.ts +0 -33
  92. package/library/endpoint/stream/file-writable.d.ts.map +0 -1
  93. 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
- // parent.html
32
+ /** parent.html */
33
33
  import { requestIframeClient } from 'request-iframe';
34
34
 
35
- // 获取 iframe 元素
35
+ /** 获取 iframe 元素 */
36
36
  const iframe = document.getElementById('my-iframe') as HTMLIFrameElement;
37
37
 
38
- // 创建 client(用于发送请求)
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' // 消息隔离标识,需要和 iframe 内保持一致
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
- // child.html (iframe 内)
66
+ /** child.htmliframe 内) */
58
67
  import { requestIframeServer } from 'request-iframe';
59
68
 
60
- // 创建 server(用于接收请求)
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' // 必须和父页面的 client 保持一致!
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
- // iframe 内
258
- const client = requestIframeClient(window.parent, { secretKey: 'reverse' });
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 server = requestIframeServer({ secretKey: 'reverse' });
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
- // parent.html
32
+ /** parent.html */
33
33
  import { requestIframeClient } from 'request-iframe';
34
34
 
35
- // Get iframe element
35
+ /** Get iframe element */
36
36
  const iframe = document.getElementById('my-iframe') as HTMLIFrameElement;
37
37
 
38
- // Create client (for sending requests)
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' // Message isolation identifier, must match iframe
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
- // child.html (inside iframe)
66
+ /** child.html (inside iframe) */
58
67
  import { requestIframeServer } from 'request-iframe';
59
68
 
60
- // Create server (for receiving requests)
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' // Must match parent page's client!
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
- // Inside iframe
258
- const client = requestIframeClient(window.parent, { secretKey: 'reverse' });
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
- // Parent page
262
- const server = requestIframeServer({ secretKey: 'reverse' });
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
- // 获取 iframe 元素
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
- // 创建 client
134
- const client = requestIframeClient(iframe, { secretKey: 'my-app' });
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
- // 发送请求(就像 axios)
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
- // 创建 server
147
- const server = requestIframeServer({ secretKey: 'my-app' });
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
- // 注册处理器(就像 express)
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: 'https://child.example.com' // 强烈建议不要用 '*'
349
+ targetOrigin,
350
+ allowedOrigins: [targetOrigin]
335
351
  });
336
352
  await client.send('/api/ping', { from: 'parent' });
337
353
 
338
- // 子页面:创建 server
339
- const server = requestIframeServer({ secretKey: 'popup-demo' });
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
- // iframe 内(不同域)
349
- const server = requestIframeServer({ secretKey: 'data-api' });
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 client = requestIframeClient(iframe, { secretKey: 'data-api' });
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` | 文件写侧(生产者)流:用于发送文件(底层会做 Base64 编码) |
933
+ | `IframeFileWritableStream` | 文件写侧(生产者)流:用于发送文件(支持 `Uint8Array` / `ArrayBuffer` 分片;会尽量使用 transferable 传输以减少拷贝) |
878
934
  | `IframeReadableStream` | 读侧(消费者)流:用于接收普通数据(无论来自 Server 还是 Client) |
879
- | `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(底层会做 Base64 解码) |
935
+ | `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(支持二进制分片) |
880
936
 
881
- > **注意**:文件流内部会进行 Base64 编/解码。Base64 会带来约 33% 的体积膨胀,并且在超大文件场景下可能会有较高的内存/CPU 开销。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
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`,并配置 `allowedOrigins` / `validateOrigin`。
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
- // iframe A 的通信
1841
- const clientA = requestIframeClient(iframeA, { secretKey: 'app-a' });
1842
- const serverA = requestIframeServer({ secretKey: 'app-a' });
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
- // iframe B 的通信
1845
- const clientB = requestIframeClient(iframeB, { secretKey: 'app-b' });
1846
- const serverB = requestIframeServer({ secretKey: 'app-b' });
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
- // 父页面 (https://parent.com)
1862
- const client = requestIframeClient(iframe);
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
- // iframe 内 (https://child.com)
1865
- const server = requestIframeServer();
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
- // iframe 内
1880
- const server = requestIframeServer({ secretKey: 'my-app' });
1881
- const client = requestIframeClient(window.parent, { secretKey: 'my-app-reverse' });
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. 如何实现请求重试?