request-iframe 0.0.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 (96) hide show
  1. package/QUICKSTART.CN.md +269 -0
  2. package/QUICKSTART.md +269 -0
  3. package/README.CN.md +1369 -0
  4. package/README.md +1016 -0
  5. package/library/__tests__/interceptors.test.ts +124 -0
  6. package/library/__tests__/requestIframe.test.ts +2216 -0
  7. package/library/__tests__/stream.test.ts +650 -0
  8. package/library/__tests__/utils.test.ts +433 -0
  9. package/library/api/client.d.ts +16 -0
  10. package/library/api/client.d.ts.map +1 -0
  11. package/library/api/client.js +72 -0
  12. package/library/api/server.d.ts +16 -0
  13. package/library/api/server.d.ts.map +1 -0
  14. package/library/api/server.js +44 -0
  15. package/library/constants/index.d.ts +209 -0
  16. package/library/constants/index.d.ts.map +1 -0
  17. package/library/constants/index.js +260 -0
  18. package/library/constants/messages.d.ts +80 -0
  19. package/library/constants/messages.d.ts.map +1 -0
  20. package/library/constants/messages.js +123 -0
  21. package/library/core/client.d.ts +99 -0
  22. package/library/core/client.d.ts.map +1 -0
  23. package/library/core/client.js +440 -0
  24. package/library/core/message-handler.d.ts +110 -0
  25. package/library/core/message-handler.d.ts.map +1 -0
  26. package/library/core/message-handler.js +320 -0
  27. package/library/core/request-response.d.ts +59 -0
  28. package/library/core/request-response.d.ts.map +1 -0
  29. package/library/core/request-response.js +337 -0
  30. package/library/core/request.d.ts +17 -0
  31. package/library/core/request.d.ts.map +1 -0
  32. package/library/core/request.js +34 -0
  33. package/library/core/response.d.ts +51 -0
  34. package/library/core/response.d.ts.map +1 -0
  35. package/library/core/response.js +323 -0
  36. package/library/core/server-base.d.ts +86 -0
  37. package/library/core/server-base.d.ts.map +1 -0
  38. package/library/core/server-base.js +257 -0
  39. package/library/core/server-client.d.ts +99 -0
  40. package/library/core/server-client.d.ts.map +1 -0
  41. package/library/core/server-client.js +256 -0
  42. package/library/core/server.d.ts +82 -0
  43. package/library/core/server.d.ts.map +1 -0
  44. package/library/core/server.js +338 -0
  45. package/library/index.d.ts +16 -0
  46. package/library/index.d.ts.map +1 -0
  47. package/library/index.js +211 -0
  48. package/library/interceptors/index.d.ts +41 -0
  49. package/library/interceptors/index.d.ts.map +1 -0
  50. package/library/interceptors/index.js +126 -0
  51. package/library/message/channel.d.ts +107 -0
  52. package/library/message/channel.d.ts.map +1 -0
  53. package/library/message/channel.js +184 -0
  54. package/library/message/dispatcher.d.ts +119 -0
  55. package/library/message/dispatcher.d.ts.map +1 -0
  56. package/library/message/dispatcher.js +249 -0
  57. package/library/message/index.d.ts +5 -0
  58. package/library/message/index.d.ts.map +1 -0
  59. package/library/message/index.js +25 -0
  60. package/library/stream/file-stream.d.ts +48 -0
  61. package/library/stream/file-stream.d.ts.map +1 -0
  62. package/library/stream/file-stream.js +240 -0
  63. package/library/stream/index.d.ts +15 -0
  64. package/library/stream/index.d.ts.map +1 -0
  65. package/library/stream/index.js +83 -0
  66. package/library/stream/readable-stream.d.ts +83 -0
  67. package/library/stream/readable-stream.d.ts.map +1 -0
  68. package/library/stream/readable-stream.js +249 -0
  69. package/library/stream/types.d.ts +165 -0
  70. package/library/stream/types.d.ts.map +1 -0
  71. package/library/stream/types.js +5 -0
  72. package/library/stream/writable-stream.d.ts +60 -0
  73. package/library/stream/writable-stream.d.ts.map +1 -0
  74. package/library/stream/writable-stream.js +348 -0
  75. package/library/types/index.d.ts +408 -0
  76. package/library/types/index.d.ts.map +1 -0
  77. package/library/types/index.js +5 -0
  78. package/library/utils/cache.d.ts +19 -0
  79. package/library/utils/cache.d.ts.map +1 -0
  80. package/library/utils/cache.js +83 -0
  81. package/library/utils/cookie.d.ts +117 -0
  82. package/library/utils/cookie.d.ts.map +1 -0
  83. package/library/utils/cookie.js +365 -0
  84. package/library/utils/debug.d.ts +11 -0
  85. package/library/utils/debug.d.ts.map +1 -0
  86. package/library/utils/debug.js +162 -0
  87. package/library/utils/index.d.ts +13 -0
  88. package/library/utils/index.d.ts.map +1 -0
  89. package/library/utils/index.js +132 -0
  90. package/library/utils/path-match.d.ts +17 -0
  91. package/library/utils/path-match.d.ts.map +1 -0
  92. package/library/utils/path-match.js +90 -0
  93. package/library/utils/protocol.d.ts +61 -0
  94. package/library/utils/protocol.d.ts.map +1 -0
  95. package/library/utils/protocol.js +169 -0
  96. package/package.json +58 -0
package/README.CN.md ADDED
@@ -0,0 +1,1369 @@
1
+ # request-iframe
2
+
3
+ 像发送 HTTP 请求一样与 iframe 通信!基于 `postMessage` 实现的 iframe 跨域通信库。
4
+
5
+ <p align="center">
6
+ <img src="https://img.shields.io/badge/TypeScript-Ready-blue" alt="TypeScript Ready">
7
+ <img src="https://img.shields.io/badge/API-Express%20Like-green" alt="Express Like API">
8
+ <img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
9
+ <img src="https://img.shields.io/badge/Test%20Coverage-76%25-brightgreen" alt="Test Coverage">
10
+ </p>
11
+
12
+ ## 📑 目录
13
+
14
+ - [为什么选择 request-iframe?](#为什么选择-request-iframe)
15
+ - [特性](#特性)
16
+ - [安装](#安装)
17
+ - [快速开始](#快速开始)
18
+ - [使用场景](#使用场景)
19
+ - [实现原理](#实现原理)
20
+ - [通信协议](#通信协议)
21
+ - [消息类型](#消息类型)
22
+ - [超时机制](#超时机制)
23
+ - [协议版本](#协议版本)
24
+ - [详细功能](#详细功能)
25
+ - [拦截器](#拦截器)
26
+ - [中间件](#中间件)
27
+ - [Headers 和 Cookies](#headers-和-cookies)
28
+ - [文件传输](#文件传输)
29
+ - [流式传输(Stream)](#流式传输stream)
30
+ - [连接检测](#连接检测)
31
+ - [响应确认](#响应确认)
32
+ - [追踪模式](#追踪模式)
33
+ - [多语言支持](#多语言支持)
34
+ - [API 参考](#api-参考)
35
+ - [错误处理](#错误处理)
36
+ - [FAQ](#faq)
37
+ - [开发](#开发)
38
+ - [许可证](#许可证)
39
+
40
+ ## 为什么选择 request-iframe?
41
+
42
+ 在微前端、iframe 嵌套等场景下,父子页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
43
+
44
+ | 痛点 | 传统方式 | request-iframe |
45
+ |------|----------|----------------|
46
+ | 请求-响应关联 | 手动维护 requestId | 自动管理,Promise 风格 |
47
+ | 超时处理 | 手动实现定时器 | 内置多阶段超时机制 |
48
+ | 错误处理 | 各种边界情况 | 标准化错误码 |
49
+ | 消息隔离 | 容易串线 | secretKey 自动隔离 |
50
+ | API 风格 | 事件监听式 | 类 HTTP 请求/Express 风格 |
51
+ | TypeScript | 需要自定义类型 | 完整类型支持 |
52
+ | 测试覆盖 | 无 | 76%+ 测试覆盖率 |
53
+
54
+ **核心优势**:
55
+ - ✅ **零学习成本** - 如果你熟悉 axios 和 Express,立即上手
56
+ - ✅ **类型安全** - 完整的 TypeScript 支持,开发体验友好
57
+ - ✅ **生产就绪** - 高测试覆盖率,经过充分测试
58
+ - ✅ **功能丰富** - 拦截器、中间件、流式传输、文件传输一应俱全
59
+
60
+ ## 特性
61
+
62
+ - 🚀 **类 HTTP 风格** - Client 发送请求,Server 处理并响应,就像 axios + express
63
+ - 🔌 **拦截器支持** - 请求/响应拦截器,轻松实现统一鉴权、日志等
64
+ - 🎭 **中间件机制** - Express 风格的中间件,支持路径匹配
65
+ - ⏱️ **智能超时** - 三阶段超时(连接/同步/异步),自动识别长任务
66
+ - 📦 **TypeScript** - 完整的类型定义和智能提示
67
+ - 🔒 **消息隔离** - secretKey 机制避免多实例消息串线
68
+ - 📁 **文件传输** - 支持 base64 编码的文件发送
69
+ - 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
70
+ - 🌍 **多语言** - 错误消息可自定义,便于国际化
71
+ - ✅ **协议版本** - 内置版本控制,便于升级兼容
72
+
73
+ ## 安装
74
+
75
+ ```bash
76
+ npm install request-iframe
77
+ # 或
78
+ yarn add request-iframe
79
+ # 或
80
+ pnpm add request-iframe
81
+ ```
82
+
83
+ **版本要求**: Node.js >= 14
84
+
85
+ **TypeScript**: 内置完整类型定义,无需安装 `@types/request-iframe`
86
+
87
+ ## 快速开始
88
+
89
+ ### 1. 父页面(Client 端)
90
+
91
+ ```typescript
92
+ import { requestIframeClient } from 'request-iframe';
93
+
94
+ // 获取 iframe 元素
95
+ const iframe = document.querySelector('iframe')!;
96
+
97
+ // 创建 client
98
+ const client = requestIframeClient(iframe, { secretKey: 'my-app' });
99
+
100
+ // 发送请求(就像 axios)
101
+ const response = await client.send('/api/getUserInfo', { userId: 123 });
102
+ console.log(response.data); // { name: 'Tom', age: 18 }
103
+ ```
104
+
105
+ ### 2. iframe 内页面(Server 端)
106
+
107
+ ```typescript
108
+ import { requestIframeServer } from 'request-iframe';
109
+
110
+ // 创建 server
111
+ const server = requestIframeServer({ secretKey: 'my-app' });
112
+
113
+ // 注册处理器(就像 express)
114
+ server.on('/api/getUserInfo', (req, res) => {
115
+ const { userId } = req.body;
116
+ res.send({ name: 'Tom', age: 18 });
117
+ });
118
+ ```
119
+
120
+ 就这么简单!🎉
121
+
122
+ > 💡 **提示**: 更多快速上手指南请查看 [QUICKSTART.CN.md](./QUICKSTART.CN.md) 或 [QUICKSTART.md](./QUICKSTART.md) (English)
123
+
124
+ ---
125
+
126
+ ## 实现原理
127
+
128
+ ### 通信协议
129
+
130
+ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协议:
131
+
132
+ ```
133
+ Client (父页面) Server (iframe)
134
+ │ │
135
+ │ ──── REQUEST ──────────────────────────> │ 发送请求
136
+ │ │
137
+ │ <──── ACK ───────────────────────────── │ 确认收到
138
+ │ │
139
+ │ │ 执行 handler
140
+ │ │
141
+ │ <──── ASYNC (可选) ──────────────────── │ 若 handler 返回 Promise
142
+ │ │
143
+ │ <──── RESPONSE ──────────────────────── │ 返回结果
144
+ │ │
145
+ │ ──── RECEIVED (可选) ──────────────────> │ 确认收到响应
146
+ │ │
147
+ ```
148
+
149
+ ### 消息类型
150
+
151
+ | 类型 | 方向 | 说明 |
152
+ |------|------|------|
153
+ | `request` | Client → Server | 客户端发起请求 |
154
+ | `ack` | Server → Client | 服务端确认收到请求 |
155
+ | `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
156
+ | `response` | Server → Client | 返回响应数据 |
157
+ | `error` | Server → Client | 返回错误信息 |
158
+ | `received` | Client → Server | 客户端确认收到响应(可选,由 `requireAck` 控制) |
159
+ | `ping` | Client → Server | 连接检测(`isConnect()` 方法) |
160
+ | `pong` | Server → Client | 连接检测响应 |
161
+
162
+ ### 超时机制
163
+
164
+ request-iframe 采用三阶段超时策略,智能适应不同场景:
165
+
166
+ ```typescript
167
+ client.send('/api/getData', data, {
168
+ ackTimeout: 500, // 阶段1:等待 ACK 的超时时间(默认 500ms)
169
+ timeout: 5000, // 阶段2:请求超时时间(默认 5s)
170
+ asyncTimeout: 120000 // 阶段3:异步请求超时时间(默认 120s)
171
+ });
172
+ ```
173
+
174
+ **超时切换流程:**
175
+
176
+ ```
177
+ 发送 REQUEST
178
+
179
+
180
+ ┌───────────────────┐ 超时 ┌─────────────────────────────┐
181
+ │ ackTimeout │ ────────> │ 错误: ACK_TIMEOUT │
182
+ │ (等待 ACK) │ │ "连接失败,Server 未响应" │
183
+ └───────────────────┘ └─────────────────────────────┘
184
+
185
+ │ 收到 ACK
186
+
187
+ ┌───────────────────┐ 超时 ┌─────────────────────────────┐
188
+ │ timeout │ ────────> │ 错误: TIMEOUT │
189
+ │ (等待 RESPONSE) │ │ "请求超时" │
190
+ └───────────────────┘ └─────────────────────────────┘
191
+
192
+ │ 收到 ASYNC(可选)
193
+
194
+ ┌───────────────────┐ 超时 ┌─────────────────────────────┐
195
+ │ asyncTimeout │ ────────> │ 错误: ASYNC_TIMEOUT │
196
+ │ (等待 RESPONSE) │ │ "异步请求超时" │
197
+ └───────────────────┘ └─────────────────────────────┘
198
+
199
+ │ 收到 RESPONSE
200
+
201
+ 请求完成 ✓
202
+ ```
203
+
204
+ **为什么这样设计?**
205
+
206
+ | 阶段 | 超时时间 | 场景 |
207
+ |------|----------|------|
208
+ | ackTimeout | 较短(500ms) | 快速检测 Server 是否在线,避免长时间等待不可达的 iframe |
209
+ | timeout | 中等(5s) | 适用于简单的同步处理,如读取数据、参数校验等 |
210
+ | asyncTimeout | 较长(120s) | 适用于复杂异步操作,如文件处理、批量操作、第三方 API 调用等 |
211
+
212
+ ### 协议版本
213
+
214
+ 每条消息都包含 `__requestIframe__` 字段标识协议版本,以及 `timestamp` 字段记录消息创建时间:
215
+
216
+ ```typescript
217
+ {
218
+ __requestIframe__: 1, // 协议版本号
219
+ timestamp: 1704067200000, // 消息创建时间戳(毫秒)
220
+ type: 'request',
221
+ requestId: 'req_xxx',
222
+ path: '/api/getData',
223
+ body: { ... }
224
+ }
225
+ ```
226
+
227
+ 这使得:
228
+ - 不同版本的库可以做兼容处理
229
+ - 新版本 Server 可兼容旧版本 Client
230
+ - 版本过低时会返回明确的错误信息
231
+ - `timestamp` 便于调试消息延迟、分析通信性能
232
+
233
+ ---
234
+
235
+ ## 使用场景
236
+
237
+ ### 微前端通信
238
+
239
+ 在微前端架构中,主应用需要与子应用 iframe 进行数据交互:
240
+
241
+ ```typescript
242
+ // 主应用(父页面)
243
+ const client = requestIframeClient(iframe, { secretKey: 'main-app' });
244
+
245
+ // 获取子应用的用户信息
246
+ const userInfo = await client.send('/api/user/info', {});
247
+
248
+ // 通知子应用更新数据
249
+ await client.send('/api/data/refresh', { timestamp: Date.now() });
250
+ ```
251
+
252
+ ### 第三方组件集成
253
+
254
+ 集成第三方组件时,通过 iframe 隔离,同时保持通信:
255
+
256
+ ```typescript
257
+ // 父页面
258
+ const client = requestIframeClient(thirdPartyIframe, { secretKey: 'widget' });
259
+
260
+ // 配置组件
261
+ await client.send('/config', {
262
+ theme: 'dark',
263
+ language: 'zh-CN'
264
+ });
265
+
266
+ // 监听组件事件(通过反向通信)
267
+ const server = requestIframeServer({ secretKey: 'widget-events' });
268
+ server.on('/event', (req, res) => {
269
+ console.log('组件事件:', req.body);
270
+ res.send({ received: true });
271
+ });
272
+ ```
273
+
274
+ ### 跨域数据获取
275
+
276
+ 当 iframe 与父页面不同域时,使用 request-iframe 安全地获取数据:
277
+
278
+ ```typescript
279
+ // iframe 内(不同域)
280
+ const server = requestIframeServer({ secretKey: 'data-api' });
281
+
282
+ server.on('/api/data', async (req, res) => {
283
+ // 从同域 API 获取数据(iframe 可以访问同域资源)
284
+ const data = await fetch('/api/internal/data').then(r => r.json());
285
+ res.send(data);
286
+ });
287
+
288
+ // 父页面(跨域)
289
+ const client = requestIframeClient(iframe, { secretKey: 'data-api' });
290
+ const data = await client.send('/api/data', {}); // 成功获取跨域数据
291
+ ```
292
+
293
+ ### 文件预览和下载
294
+
295
+ 在 iframe 中处理文件,然后传输给父页面:
296
+
297
+ ```typescript
298
+ // iframe 内:处理文件并返回
299
+ server.on('/api/processFile', async (req, res) => {
300
+ const { fileId } = req.body;
301
+ const processedFile = await processFile(fileId);
302
+
303
+ // 返回处理后的文件
304
+ await res.sendFile(processedFile, {
305
+ mimeType: 'application/pdf',
306
+ fileName: `processed-${fileId}.pdf`
307
+ });
308
+ });
309
+
310
+ // 父页面:下载文件
311
+ const response = await client.send('/api/processFile', { fileId: '123' });
312
+ if (response.fileData) {
313
+ downloadFile(response.fileData);
314
+ }
315
+ ```
316
+
317
+ ---
318
+
319
+ ## 详细功能
320
+
321
+ ### 拦截器
322
+
323
+ #### 请求拦截器
324
+
325
+ ```typescript
326
+ // 添加请求拦截器(统一添加 token)
327
+ client.interceptors.request.use((config) => {
328
+ config.headers = {
329
+ ...config.headers,
330
+ 'Authorization': `Bearer ${getToken()}`
331
+ };
332
+ return config;
333
+ });
334
+
335
+ // 错误处理
336
+ client.interceptors.request.use(
337
+ (config) => config,
338
+ (error) => {
339
+ console.error('请求配置错误:', error);
340
+ return Promise.reject(error);
341
+ }
342
+ );
343
+ ```
344
+
345
+ #### 响应拦截器
346
+
347
+ ```typescript
348
+ // 添加响应拦截器(统一数据转换)
349
+ client.interceptors.response.use((response) => {
350
+ // 假设后端返回 { code: 0, data: {...} } 格式
351
+ if (response.data.code === 0) {
352
+ response.data = response.data.data;
353
+ }
354
+ return response;
355
+ });
356
+
357
+ // 错误处理
358
+ client.interceptors.response.use(
359
+ (response) => response,
360
+ (error) => {
361
+ if (error.code === 'TIMEOUT') {
362
+ message.error('请求超时,请重试');
363
+ }
364
+ return Promise.reject(error);
365
+ }
366
+ );
367
+ ```
368
+
369
+ ### 中间件
370
+
371
+ Server 端支持 Express 风格的中间件:
372
+
373
+ #### 全局中间件
374
+
375
+ ```typescript
376
+ // 日志中间件
377
+ server.use((req, res, next) => {
378
+ console.log(`[${new Date().toISOString()}] ${req.path}`, req.body);
379
+ next();
380
+ });
381
+
382
+ // 权限校验中间件
383
+ server.use((req, res, next) => {
384
+ const token = req.headers['authorization'];
385
+ if (!token) {
386
+ return res.status(401).send({ error: 'Unauthorized' });
387
+ }
388
+ // 验证 token...
389
+ next();
390
+ });
391
+ ```
392
+
393
+ #### 路径匹配中间件
394
+
395
+ ```typescript
396
+ // 只对 /api/* 路径生效
397
+ server.use('/api/*', (req, res, next) => {
398
+ console.log('API 请求:', req.path);
399
+ next();
400
+ });
401
+
402
+ // 正则匹配
403
+ server.use(/^\/admin\//, (req, res, next) => {
404
+ // 管理员接口的特殊处理
405
+ next();
406
+ });
407
+
408
+ // 数组匹配
409
+ server.use(['/user', '/profile'], (req, res, next) => {
410
+ // 用户相关接口
411
+ next();
412
+ });
413
+ ```
414
+
415
+ ### Headers 和 Cookies
416
+
417
+ > **注意**:这里的 `headers` 和 `cookies` 并非浏览器真实的 HTTP Headers 和 Cookies,而是 request-iframe 模拟 HTTP 风格实现的**消息元数据传递机制**。数据通过 `postMessage` 在 iframe 间传递,不会影响浏览器的真实 Cookie 存储。
418
+
419
+ **为什么这样设计?**
420
+
421
+ | 设计目的 | 说明 |
422
+ |----------|------|
423
+ | **API 风格一致** | 与 HTTP 请求(axios/fetch)和服务端(Express)保持一致的使用方式 |
424
+ | **降低学习成本** | 熟悉 HTTP 开发的用户可以快速上手,无需学习新的 API |
425
+ | **三方库兼容** | 便于复用或适配 Express 中间件、认证库等,只需少量修改 |
426
+ | **跨 iframe 状态共享** | 实现不同 iframe 间的登录态传递、权限校验等,解决 iframe 隔离带来的状态同步问题 |
427
+ | **灵活的数据传递** | 在 body 之外提供额外的元数据通道,便于分层处理(如中间件读取 headers,业务逻辑读取 body) |
428
+
429
+ #### Cookies 自动管理
430
+
431
+ request-iframe 模拟了 HTTP 的 cookie 自动管理机制:
432
+
433
+ ```
434
+ ┌─────────────────────────────────────────────────────────────────┐
435
+ │ Cookies 自动管理流程 │
436
+ ├─────────────────────────────────────────────────────────────────┤
437
+ │ │
438
+ │ Client Server │
439
+ │ │ │ │
440
+ │ │ ── REQUEST (自动携带路径匹配的 cookies) │ │
441
+ │ │ cookies: { token: 'abc' } │ │
442
+ │ │ │ │
443
+ │ │ <── RESPONSE ───────────────────── │ │
444
+ │ │ headers: { │ res.cookie(...) │
445
+ │ │ 'Set-Cookie': [ │ │
446
+ │ │ 'token=xyz; Path=/api', │ │
447
+ │ │ 'global=123; Path=/' │ │
448
+ │ │ ] │ │
449
+ │ │ } │ │
450
+ │ │ │ │
451
+ │ │ 【Client 解析 Set-Cookie 并保存】 │ │
452
+ │ │ │ │
453
+ │ │ ── 后续请求 /api/xxx ──────────────> │ │
454
+ │ │ cookies: { token: 'xyz', │ 路径匹配的 cookies │
455
+ │ │ global: '123' } │ │
456
+ │ │ │ │
457
+ └─────────────────────────────────────────────────────────────────┘
458
+ ```
459
+
460
+ **工作机制(类似 HTTP Set-Cookie):**
461
+
462
+ 1. **Server 设置 cookie 时**:通过 `res.cookie(name, value, options)` 生成 `Set-Cookie` 字符串
463
+ 2. **响应返回时**:所有 `Set-Cookie` 存放在 `headers['Set-Cookie']` 数组中
464
+ 3. **Client 收到响应后**:解析 `Set-Cookie` header,根据 Path 等属性保存到 Cookie 存储
465
+ 4. **Client 发送请求时**:只携带**路径匹配**的 cookies(类似浏览器行为)
466
+
467
+ ```typescript
468
+ // Server 端:登录时设置 token(支持完整的 Cookie 选项)
469
+ server.on('/api/login', (req, res) => {
470
+ const { username, password } = req.body;
471
+ // 验证用户...
472
+
473
+ // 设置 cookie(支持 path、expires、maxAge、httpOnly 等选项)
474
+ res.cookie('authToken', 'jwt_xxx', { path: '/api', httpOnly: true });
475
+ res.cookie('userId', '12345', { path: '/' });
476
+ res.send({ success: true });
477
+ });
478
+
479
+ // Server 端:后续接口读取 token(客户端自动携带路径匹配的 cookies)
480
+ server.on('/api/getUserInfo', (req, res) => {
481
+ const token = req.cookies['authToken']; // 路径匹配,自动携带
482
+ const userId = req.cookies['userId']; // 根路径的 cookie,所有请求都携带
483
+ // 验证 token...
484
+ res.send({ name: 'Tom', age: 18 });
485
+ });
486
+
487
+ // Server 端:清除 cookie
488
+ server.on('/api/logout', (req, res) => {
489
+ res.clearCookie('authToken', { path: '/api' });
490
+ res.send({ success: true });
491
+ });
492
+ ```
493
+
494
+ ```typescript
495
+ // Client 端:登录
496
+ await client.send('/api/login', { username: 'tom', password: '123' });
497
+
498
+ // Client 端:后续请求 /api/getUserInfo(自动携带 authToken 和 userId)
499
+ const userInfo = await client.send('/api/getUserInfo', {});
500
+
501
+ // Client 端:请求根路径(只携带 userId,因为 authToken 的 path 是 /api)
502
+ const rootData = await client.send('/other', {});
503
+ ```
504
+
505
+ #### Client Cookie 管理 API
506
+
507
+ Client 提供了手动管理 cookies 的 API,支持**路径隔离**:
508
+
509
+ ```typescript
510
+ // 获取所有 cookies
511
+ client.getCookies(); // { authToken: 'jwt_xxx', userId: '12345' }
512
+
513
+ // 获取匹配指定路径的 cookies
514
+ client.getCookies('/api'); // 只返回路径匹配 /api 的 cookies
515
+
516
+ // 获取指定 cookie
517
+ client.getCookie('authToken'); // 'jwt_xxx'
518
+ client.getCookie('authToken', '/api'); // 指定路径获取
519
+
520
+ // 手动设置 cookie(支持路径选项)
521
+ client.setCookie('theme', 'dark'); // 默认路径 '/'
522
+ client.setCookie('apiConfig', 'v2', { path: '/api' }); // 指定路径
523
+ client.setCookie('temp', 'xxx', { maxAge: 3600 }); // 1 小时后过期
524
+
525
+ // 删除指定 cookie
526
+ client.removeCookie('theme'); // 删除路径为 '/' 的 theme
527
+ client.removeCookie('apiConfig', '/api'); // 删除指定路径的 cookie
528
+
529
+ // 清除所有 cookies(如登出时)
530
+ client.clearCookies();
531
+ ```
532
+
533
+ #### Headers 使用示例
534
+
535
+ ```typescript
536
+ // Client 端发送自定义 headers
537
+ const response = await client.send('/api/data', {}, {
538
+ headers: {
539
+ 'X-Device-Id': 'device-123',
540
+ 'X-Platform': 'web',
541
+ 'Authorization': 'Bearer xxx' // 也可以通过 headers 传递 token
542
+ }
543
+ });
544
+
545
+ // Server 端读取和设置 headers
546
+ server.on('/api/data', (req, res) => {
547
+ // 读取请求 headers
548
+ const deviceId = req.headers['x-device-id'];
549
+ const platform = req.headers['x-platform'];
550
+
551
+ // 设置响应 headers
552
+ res.setHeader('X-Request-Id', req.requestId);
553
+ res.set('X-Custom-Header', 'value'); // 链式调用
554
+
555
+ res.send({ data: 'ok' });
556
+ });
557
+ ```
558
+
559
+ ### 文件传输
560
+
561
+ ```typescript
562
+ // Server 端发送文件
563
+ server.on('/api/download', async (req, res) => {
564
+ // 字符串内容
565
+ await res.sendFile('Hello, World!', {
566
+ mimeType: 'text/plain',
567
+ fileName: 'hello.txt'
568
+ });
569
+
570
+ // 或者 Blob/File 对象
571
+ const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
572
+ await res.sendFile(blob, { fileName: 'data.bin' });
573
+ });
574
+
575
+ // Client 端接收
576
+ const response = await client.send('/api/download', {});
577
+ if (response.fileData) {
578
+ const { content, mimeType, fileName } = response.fileData;
579
+
580
+ // content 是 base64 编码的字符串
581
+ const binaryString = atob(content);
582
+ const blob = new Blob([binaryString], { type: mimeType });
583
+
584
+ // 下载文件
585
+ const url = URL.createObjectURL(blob);
586
+ const a = document.createElement('a');
587
+ a.href = url;
588
+ a.download = fileName || 'download';
589
+ a.click();
590
+ }
591
+ ```
592
+
593
+ ### 流式传输(Stream)
594
+
595
+ 对于大文件或需要分块传输的场景,可以使用流式传输:
596
+
597
+ ```typescript
598
+ import {
599
+ IframeWritableStream,
600
+ IframeFileWritableStream,
601
+ isIframeReadableStream,
602
+ isIframeFileStream
603
+ } from 'request-iframe';
604
+
605
+ // Server 端:使用迭代器发送数据流
606
+ server.on('/api/stream', async (req, res) => {
607
+ const stream = new IframeWritableStream({
608
+ type: 'data',
609
+ chunked: true,
610
+ // 使用异步迭代器生成数据
611
+ iterator: async function* () {
612
+ for (let i = 0; i < 10; i++) {
613
+ yield { chunk: i, data: `Data chunk ${i}` };
614
+ await new Promise(r => setTimeout(r, 100)); // 模拟延迟
615
+ }
616
+ }
617
+ });
618
+
619
+ await res.sendStream(stream);
620
+ });
621
+
622
+ // Server 端:使用 next 函数发送数据流
623
+ server.on('/api/stream2', async (req, res) => {
624
+ let count = 0;
625
+ const stream = new IframeWritableStream({
626
+ next: async () => {
627
+ if (count >= 5) {
628
+ return { data: `Final chunk`, done: true };
629
+ }
630
+ return { data: `Chunk ${count++}`, done: false };
631
+ }
632
+ });
633
+
634
+ await res.sendStream(stream);
635
+ });
636
+
637
+ // Server 端:发送文件流
638
+ server.on('/api/fileStream', async (req, res) => {
639
+ const fileData = new Uint8Array([/* 文件内容 */]);
640
+ const stream = new IframeFileWritableStream({
641
+ filename: 'large-file.bin',
642
+ mimeType: 'application/octet-stream',
643
+ size: fileData.length,
644
+ chunked: true,
645
+ iterator: async function* () {
646
+ const chunkSize = 1024;
647
+ for (let i = 0; i < fileData.length; i += chunkSize) {
648
+ yield fileData.slice(i, i + chunkSize);
649
+ }
650
+ }
651
+ });
652
+
653
+ await res.sendStream(stream);
654
+ });
655
+
656
+ // Client 端:接收流数据
657
+ const response = await client.send('/api/stream', {});
658
+
659
+ // 判断是否是流响应
660
+ if (isIframeReadableStream(response.stream)) {
661
+ // 方式1:一次性读取所有数据
662
+ const allData = await response.stream.read();
663
+
664
+ // 方式2:使用异步迭代器逐块读取
665
+ for await (const chunk of response.stream) {
666
+ console.log('Received chunk:', chunk);
667
+ }
668
+
669
+ // 监听流结束
670
+ response.stream.onEnd(() => {
671
+ console.log('Stream ended');
672
+ });
673
+
674
+ // 监听流错误
675
+ response.stream.onError((error) => {
676
+ console.error('Stream error:', error);
677
+ });
678
+
679
+ // 取消流
680
+ response.stream.cancel('User cancelled');
681
+ }
682
+
683
+ // Client 端:接收文件流
684
+ const fileResponse = await client.send('/api/fileStream', {});
685
+
686
+ if (isIframeFileStream(fileResponse.stream)) {
687
+ // 读取为 Blob
688
+ const blob = await fileResponse.stream.readAsBlob();
689
+
690
+ // 读取为 ArrayBuffer
691
+ const buffer = await fileResponse.stream.readAsArrayBuffer();
692
+
693
+ // 读取为 Data URL
694
+ const dataUrl = await fileResponse.stream.readAsDataURL();
695
+
696
+ // 获取文件信息
697
+ console.log('Filename:', fileResponse.stream.filename);
698
+ console.log('MIME type:', fileResponse.stream.mimeType);
699
+ console.log('Size:', fileResponse.stream.size);
700
+ }
701
+ ```
702
+
703
+ **流类型说明:**
704
+
705
+ | 类型 | 说明 |
706
+ |------|------|
707
+ | `IframeWritableStream` | 服务端可写流,用于发送普通数据 |
708
+ | `IframeFileWritableStream` | 服务端文件可写流,自动处理 base64 编码 |
709
+ | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
710
+ | `IframeFileReadableStream` | 客户端文件可读流,自动处理 base64 解码 |
711
+
712
+ **流选项:**
713
+
714
+ ```typescript
715
+ interface WritableStreamOptions {
716
+ type?: 'data' | 'file'; // 流类型
717
+ chunked?: boolean; // 是否分块传输(默认 true)
718
+ iterator?: () => AsyncGenerator; // 数据生成迭代器
719
+ next?: () => Promise<{ data: any; done: boolean }>; // 数据生成函数
720
+ metadata?: Record<string, any>; // 自定义元数据
721
+ }
722
+ ```
723
+
724
+ ### 连接检测
725
+
726
+ ```typescript
727
+ // 检测 Server 是否可达
728
+ const isConnected = await client.isConnect();
729
+ if (isConnected) {
730
+ console.log('连接正常');
731
+ } else {
732
+ console.log('连接失败');
733
+ }
734
+ ```
735
+
736
+ ### 响应确认
737
+
738
+ Server 可以要求 Client 确认收到响应:
739
+
740
+ ```typescript
741
+ server.on('/api/important', async (req, res) => {
742
+ // requireAck: true 表示需要客户端确认
743
+ const received = await res.send(data, { requireAck: true });
744
+
745
+ if (received) {
746
+ console.log('客户端已确认收到');
747
+ } else {
748
+ console.log('客户端未确认(超时)');
749
+ }
750
+ });
751
+ ```
752
+
753
+ ### 追踪模式
754
+
755
+ 开启追踪模式可以在控制台查看详细的通信日志:
756
+
757
+ ```typescript
758
+ const client = requestIframeClient(iframe, {
759
+ secretKey: 'demo',
760
+ trace: true
761
+ });
762
+
763
+ const server = requestIframeServer({
764
+ secretKey: 'demo',
765
+ trace: true
766
+ });
767
+
768
+ // 控制台输出:
769
+ // [request-iframe] [INFO] 📤 Request Start { path: '/api/getData', ... }
770
+ // [request-iframe] [INFO] 📨 ACK Received { requestId: '...' }
771
+ // [request-iframe] [INFO] ✅ Request Success { status: 200, data: {...} }
772
+ ```
773
+
774
+ ### 多语言支持
775
+
776
+ ```typescript
777
+ import { setMessages } from 'request-iframe';
778
+
779
+ // 切换到中文
780
+ setMessages({
781
+ ACK_TIMEOUT: 'ACK 确认超时,已等待 {0} 毫秒',
782
+ REQUEST_TIMEOUT: '请求超时,已等待 {0} 毫秒',
783
+ REQUEST_FAILED: '请求失败',
784
+ METHOD_NOT_FOUND: '方法未找到',
785
+ MIDDLEWARE_ERROR: '中间件错误',
786
+ IFRAME_NOT_READY: 'iframe 尚未就绪'
787
+ });
788
+ ```
789
+
790
+ ---
791
+
792
+ ## API 参考
793
+
794
+ ### requestIframeClient(target, options?)
795
+
796
+ 创建 Client 实例。
797
+
798
+ **参数:**
799
+
800
+ | 参数 | 类型 | 说明 |
801
+ |------|------|------|
802
+ | `target` | `HTMLIFrameElement \| Window` | 目标 iframe 元素或 window 对象 |
803
+ | `options.secretKey` | `string` | 消息隔离标识(可选) |
804
+ | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
805
+ | `options.ackTimeout` | `number` | 全局默认 ACK 确认超时(ms),默认 500 |
806
+ | `options.timeout` | `number` | 全局默认请求超时(ms),默认 5000 |
807
+ | `options.asyncTimeout` | `number` | 全局默认异步超时(ms),默认 120000 |
808
+
809
+ **返回值:** `RequestIframeClient`
810
+
811
+ **示例:**
812
+
813
+ ```typescript
814
+ // 设置全局超时配置
815
+ const client = requestIframeClient(iframe, {
816
+ secretKey: 'my-app',
817
+ ackTimeout: 1000, // ACK 确认超时 1s
818
+ timeout: 10000, // 请求超时 10s
819
+ asyncTimeout: 300000 // 异步超时 5min
820
+ });
821
+
822
+ // 单次请求可以覆盖全局配置
823
+ await client.send('/api/longTask', {}, {
824
+ asyncTimeout: 600000 // 这个请求使用 10min 超时
825
+ });
826
+ ```
827
+
828
+ ### requestIframeServer(options?)
829
+
830
+ 创建 Server 实例。
831
+
832
+ **参数:**
833
+
834
+ | 参数 | 类型 | 说明 |
835
+ |------|------|------|
836
+ | `options.secretKey` | `string` | 消息隔离标识(可选) |
837
+ | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
838
+ | `options.ackTimeout` | `number` | 等待客户端确认超时(ms),默认 5000 |
839
+
840
+ **返回值:** `RequestIframeServer`
841
+
842
+ ### Client API
843
+
844
+ #### client.send(path, body?, options?)
845
+
846
+ 发送请求。
847
+
848
+ **参数:**
849
+
850
+ | 参数 | 类型 | 说明 |
851
+ |------|------|------|
852
+ | `path` | `string` | 请求路径 |
853
+ | `body` | `object` | 请求数据(可选) |
854
+ | `options.ackTimeout` | `number` | ACK 确认超时(ms),默认 500 |
855
+ | `options.timeout` | `number` | 请求超时(ms),默认 5000 |
856
+ | `options.asyncTimeout` | `number` | 异步超时(ms),默认 120000 |
857
+ | `options.headers` | `object` | 请求 headers(可选) |
858
+ | `options.cookies` | `object` | 请求 cookies(可选,会与内部存储的 cookies 合并,传入的优先级更高) |
859
+ | `options.requestId` | `string` | 自定义请求 ID(可选) |
860
+
861
+ **返回值:** `Promise<Response>`
862
+
863
+ ```typescript
864
+ interface Response<T = any> {
865
+ data: T; // 响应数据
866
+ status: number; // 状态码
867
+ statusText: string; // 状态文本
868
+ requestId: string; // 请求 ID
869
+ headers?: Record<string, string | string[]>; // 响应 headers(Set-Cookie 为数组)
870
+ fileData?: { // 文件数据(如果有)
871
+ content: string; // base64 编码内容
872
+ mimeType?: string;
873
+ fileName?: string;
874
+ };
875
+ stream?: IIframeReadableStream<T>; // 流响应(如果有)
876
+ }
877
+ ```
878
+
879
+ #### client.isConnect()
880
+
881
+ 检测 Server 是否可达。
882
+
883
+ **返回值:** `Promise<boolean>`
884
+
885
+ #### client.interceptors
886
+
887
+ 拦截器管理器。
888
+
889
+ ```typescript
890
+ // 请求拦截器
891
+ client.interceptors.request.use(onFulfilled, onRejected?);
892
+
893
+ // 响应拦截器
894
+ client.interceptors.response.use(onFulfilled, onRejected?);
895
+ ```
896
+
897
+ #### client.getCookies(path?)
898
+
899
+ 获取 cookies。
900
+
901
+ **参数:** `path?: string` - 请求路径(可选,不传返回所有 cookies)
902
+
903
+ **返回值:** `Record<string, string>` - 匹配路径的 cookies
904
+
905
+ #### client.getCookie(name, path?)
906
+
907
+ 获取指定 cookie。
908
+
909
+ **参数:**
910
+ - `name: string` - cookie 名称
911
+ - `path?: string` - 路径(可选)
912
+
913
+ **返回值:** `string | undefined`
914
+
915
+ #### client.setCookie(name, value, options?)
916
+
917
+ 设置 cookie。
918
+
919
+ **参数:**
920
+ - `name: string` - cookie 名称
921
+ - `value: string` - cookie 值
922
+ - `options?: { path?: string; expires?: Date; maxAge?: number }` - cookie 选项
923
+
924
+ #### client.removeCookie(name, path?)
925
+
926
+ 删除指定 cookie。
927
+
928
+ **参数:** `name: string` - cookie 名称
929
+
930
+ #### client.clearCookies()
931
+
932
+ 清除所有 cookies。
933
+
934
+ ### Server API
935
+
936
+ #### server.on(path, handler)
937
+
938
+ 注册路由处理器。
939
+
940
+ **参数:**
941
+
942
+ | 参数 | 类型 | 说明 |
943
+ |------|------|------|
944
+ | `path` | `string` | 请求路径 |
945
+ | `handler` | `ServerHandler` | 处理函数 |
946
+
947
+ ```typescript
948
+ type ServerHandler = (req: ServerRequest, res: ServerResponse) => any | Promise<any>;
949
+ ```
950
+
951
+ #### server.off(path)
952
+
953
+ 移除路由处理器。
954
+
955
+ #### server.map(handlers)
956
+
957
+ 批量注册处理器。
958
+
959
+ ```typescript
960
+ server.map({
961
+ '/api/users': (req, res) => res.send([...]),
962
+ '/api/posts': (req, res) => res.send([...])
963
+ });
964
+ ```
965
+
966
+ #### server.use(middleware)
967
+ #### server.use(path, middleware)
968
+
969
+ 注册中间件。
970
+
971
+ ```typescript
972
+ // 全局中间件
973
+ server.use((req, res, next) => { ... });
974
+
975
+ // 路径匹配中间件
976
+ server.use('/api/*', (req, res, next) => { ... });
977
+ server.use(/^\/admin/, (req, res, next) => { ... });
978
+ server.use(['/a', '/b'], (req, res, next) => { ... });
979
+ ```
980
+
981
+ #### server.destroy()
982
+
983
+ 销毁 Server 实例,移除所有监听器。
984
+
985
+ ### ServerRequest 对象
986
+
987
+ ```typescript
988
+ interface ServerRequest {
989
+ body: any; // 请求 body
990
+ headers: Record<string, string>; // 请求 headers
991
+ cookies: Record<string, string>; // 请求 cookies
992
+ path: string; // 请求路径
993
+ requestId: string; // 请求 ID
994
+ origin: string; // 来源 origin
995
+ source: Window; // 来源 window
996
+ res: ServerResponse; // 关联的 Response 对象
997
+ }
998
+ ```
999
+
1000
+ ### ServerResponse 对象
1001
+
1002
+ ```typescript
1003
+ interface ServerResponse {
1004
+ // 发送响应
1005
+ send(data: any, options?: { requireAck?: boolean }): Promise<boolean>;
1006
+ json(data: any, options?: { requireAck?: boolean }): Promise<boolean>;
1007
+
1008
+ // 发送文件
1009
+ sendFile(content: string | Blob | File, options?: {
1010
+ mimeType?: string;
1011
+ fileName?: string;
1012
+ requireAck?: boolean;
1013
+ }): Promise<boolean>;
1014
+
1015
+ // 发送流
1016
+ sendStream(stream: IframeWritableStream): Promise<void>;
1017
+
1018
+ // 设置状态码(链式调用)
1019
+ status(code: number): ServerResponse;
1020
+
1021
+ // 设置 header
1022
+ setHeader(name: string, value: string | number | string[]): void;
1023
+ set(name: string, value: string | number | string[]): ServerResponse;
1024
+
1025
+ // 设置 cookie(生成 Set-Cookie header)
1026
+ cookie(name: string, value: string, options?: CookieOptions): ServerResponse;
1027
+ clearCookie(name: string, options?: CookieOptions): ServerResponse;
1028
+
1029
+ // 属性
1030
+ statusCode: number;
1031
+ headers: Record<string, string | string[]>; // Set-Cookie 为数组
1032
+ }
1033
+
1034
+ interface CookieOptions {
1035
+ path?: string; // Cookie 路径,默认 '/'
1036
+ expires?: Date; // 过期时间
1037
+ maxAge?: number; // 最大存活时间(秒)
1038
+ httpOnly?: boolean; // HttpOnly 标记
1039
+ secure?: boolean; // Secure 标记
1040
+ sameSite?: 'Strict' | 'Lax' | 'None'; // SameSite 属性
1041
+ }
1042
+ ```
1043
+
1044
+ ### 常量导出
1045
+
1046
+ ```typescript
1047
+ import {
1048
+ // HTTP 状态码
1049
+ HttpStatus,
1050
+ HttpStatusText,
1051
+ getStatusText,
1052
+
1053
+ // 错误码
1054
+ ErrorCode,
1055
+
1056
+ // 消息类型
1057
+ MessageType,
1058
+
1059
+ // 默认超时配置
1060
+ DefaultTimeout,
1061
+
1062
+ // 协议版本
1063
+ ProtocolVersion,
1064
+
1065
+ // 多语言消息
1066
+ Messages,
1067
+ setMessages,
1068
+ formatMessage
1069
+ } from 'request-iframe';
1070
+ ```
1071
+
1072
+ ---
1073
+
1074
+ ## 错误处理
1075
+
1076
+ ### 错误码
1077
+
1078
+ | 错误码 | 说明 |
1079
+ |--------|------|
1080
+ | `ACK_TIMEOUT` | ACK 确认超时(未收到 ACK) |
1081
+ | `TIMEOUT` | 同步请求超时 |
1082
+ | `ASYNC_TIMEOUT` | 异步请求超时 |
1083
+ | `REQUEST_ERROR` | 请求处理错误 |
1084
+ | `METHOD_NOT_FOUND` | 未找到对应的处理器 |
1085
+ | `NO_RESPONSE` | 处理器未发送响应 |
1086
+ | `PROTOCOL_UNSUPPORTED` | 协议版本不支持 |
1087
+ | `IFRAME_NOT_READY` | iframe 未就绪 |
1088
+ | `STREAM_ERROR` | 流传输错误 |
1089
+ | `STREAM_CANCELLED` | 流被取消 |
1090
+ | `STREAM_NOT_BOUND` | 流未绑定到请求上下文 |
1091
+
1092
+ ### 错误处理示例
1093
+
1094
+ ```typescript
1095
+ try {
1096
+ const response = await client.send('/api/getData', { id: 1 });
1097
+ } catch (error) {
1098
+ switch (error.code) {
1099
+ case 'ACK_TIMEOUT':
1100
+ console.error('无法连接到 iframe');
1101
+ break;
1102
+ case 'TIMEOUT':
1103
+ console.error('请求超时');
1104
+ break;
1105
+ case 'METHOD_NOT_FOUND':
1106
+ console.error('接口不存在');
1107
+ break;
1108
+ default:
1109
+ console.error('请求失败:', error.message);
1110
+ }
1111
+ }
1112
+ ```
1113
+
1114
+ ---
1115
+
1116
+ ## FAQ
1117
+
1118
+ ### 1. secretKey 有什么用?
1119
+
1120
+ `secretKey` 用于消息隔离。当页面中有多个 iframe 或多个 request-iframe 实例时,通过不同的 `secretKey` 可以避免消息串线:
1121
+
1122
+ ```typescript
1123
+ // iframe A 的通信
1124
+ const clientA = requestIframeClient(iframeA, { secretKey: 'app-a' });
1125
+ const serverA = requestIframeServer({ secretKey: 'app-a' });
1126
+
1127
+ // iframe B 的通信
1128
+ const clientB = requestIframeClient(iframeB, { secretKey: 'app-b' });
1129
+ const serverB = requestIframeServer({ secretKey: 'app-b' });
1130
+ ```
1131
+
1132
+ ### 2. 为什么需要 ACK 确认?
1133
+
1134
+ ACK 机制类似 TCP 握手,用于:
1135
+ 1. 快速确认 Server 是否在线
1136
+ 2. 区分"连接失败"和"请求超时"
1137
+ 3. 支持异步任务的超时切换
1138
+
1139
+ ### 3. 如何处理 iframe 跨域?
1140
+
1141
+ `postMessage` 本身支持跨域通信,request-iframe 会自动处理:
1142
+
1143
+ ```typescript
1144
+ // 父页面 (https://parent.com)
1145
+ const client = requestIframeClient(iframe);
1146
+
1147
+ // iframe 内 (https://child.com)
1148
+ const server = requestIframeServer();
1149
+ ```
1150
+
1151
+ 只需确保双方使用相同的 `secretKey`。
1152
+
1153
+ ### 4. Server 可以主动推送消息吗?
1154
+
1155
+ request-iframe 是请求-响应模式,Server 不能主动推送。如需双向通信,可以让 iframe 内也创建 Client:
1156
+
1157
+ ```typescript
1158
+ // iframe 内
1159
+ const server = requestIframeServer({ secretKey: 'my-app' });
1160
+ const client = requestIframeClient(window.parent, { secretKey: 'my-app-reverse' });
1161
+
1162
+ // 主动向父页面发送消息
1163
+ await client.send('/notify', { event: 'data-changed' });
1164
+ ```
1165
+
1166
+ ### 5. 如何调试通信问题?
1167
+
1168
+ 1. **开启 trace 模式**:查看详细的通信日志
1169
+ 2. **检查 secretKey**:确保 Client 和 Server 使用相同的 secretKey
1170
+ 3. **检查 iframe 加载**:确保 iframe 已完全加载
1171
+ 4. **检查控制台**:查看是否有跨域错误
1172
+
1173
+ ### 6. 支持哪些浏览器?
1174
+
1175
+ 支持所有现代浏览器,详见 [浏览器兼容性](#浏览器兼容性) 部分。
1176
+
1177
+ ### 7. 如何处理大文件传输?
1178
+
1179
+ 对于大文件(>10MB),建议使用流式传输(Stream)功能,可以分块传输,避免内存占用过大:
1180
+
1181
+ ```typescript
1182
+ // Server 端:使用流式传输大文件
1183
+ server.on('/api/largeFile', async (req, res) => {
1184
+ const stream = new IframeFileWritableStream({
1185
+ filename: 'large-file.zip',
1186
+ mimeType: 'application/zip',
1187
+ chunked: true,
1188
+ iterator: async function* () {
1189
+ // 分块读取文件
1190
+ const chunkSize = 1024 * 1024; // 1MB per chunk
1191
+ for (let i = 0; i < fileSize; i += chunkSize) {
1192
+ yield await readFileChunk(i, chunkSize);
1193
+ }
1194
+ }
1195
+ });
1196
+ await res.sendStream(stream);
1197
+ });
1198
+ ```
1199
+
1200
+ ### 8. 如何实现请求重试?
1201
+
1202
+ 可以通过响应拦截器实现请求重试:
1203
+
1204
+ ```typescript
1205
+ client.interceptors.response.use(
1206
+ (response) => response,
1207
+ async (error) => {
1208
+ if (error.code === 'TIMEOUT' || error.code === 'ACK_TIMEOUT') {
1209
+ // 重试逻辑
1210
+ const maxRetries = 3;
1211
+ for (let i = 0; i < maxRetries; i++) {
1212
+ try {
1213
+ return await client.send(error.config.path, error.config.body, error.config);
1214
+ } catch (retryError) {
1215
+ if (i === maxRetries - 1) throw retryError;
1216
+ await new Promise(r => setTimeout(r, 1000 * (i + 1))); // 递增延迟
1217
+ }
1218
+ }
1219
+ }
1220
+ return Promise.reject(error);
1221
+ }
1222
+ );
1223
+ ```
1224
+
1225
+ ### 9. 如何调试通信问题?
1226
+
1227
+ 1. **开启 trace 模式**:在创建 client/server 时设置 `trace: true`
1228
+ 2. **检查控制台**:查看详细的通信日志
1229
+ 3. **验证 secretKey**:确保 client 和 server 使用相同的 secretKey
1230
+ 4. **检查 iframe 加载**:确保 iframe 已完全加载后再发送请求
1231
+ 5. **使用 `isConnect()`**:先检测连接是否正常
1232
+
1233
+ ```typescript
1234
+ // 开启调试模式
1235
+ const client = requestIframeClient(iframe, {
1236
+ secretKey: 'my-app',
1237
+ trace: true // 开启详细日志
1238
+ });
1239
+
1240
+ // 检测连接
1241
+ const connected = await client.isConnect();
1242
+ if (!connected) {
1243
+ console.error('无法连接到 iframe');
1244
+ }
1245
+ ```
1246
+
1247
+ ### 10. 性能如何?
1248
+
1249
+ - **轻量级**: 核心代码体积小,无外部依赖(除 core-js polyfill)
1250
+ - **高效**: 使用 Promise 和事件机制,避免轮询
1251
+ - **内存友好**: 请求完成后自动清理,支持流式传输处理大文件
1252
+ - **并发支持**: 支持多个并发请求,每个请求独立管理
1253
+
1254
+ ---
1255
+
1256
+ ## 开发
1257
+
1258
+ ### 环境要求
1259
+
1260
+ - Node.js >= 14
1261
+ - npm >= 6 或 yarn >= 1.22
1262
+
1263
+ ### 开发命令
1264
+
1265
+ ```bash
1266
+ # 安装依赖
1267
+ npm install
1268
+ # 或
1269
+ yarn install
1270
+
1271
+ # 运行测试
1272
+ npm test
1273
+ # 或
1274
+ yarn test
1275
+
1276
+ # 运行测试(监听模式)
1277
+ npm run test:watch
1278
+ # 或
1279
+ yarn test:watch
1280
+
1281
+ # 生成测试覆盖率报告
1282
+ npm run test:coverage
1283
+ # 或
1284
+ yarn test:coverage
1285
+
1286
+ # 代码检查
1287
+ npm run lint
1288
+ # 或
1289
+ yarn lint
1290
+
1291
+ # 自动修复代码问题
1292
+ npm run lint:fix
1293
+ # 或
1294
+ yarn lint:fix
1295
+
1296
+ # 构建项目
1297
+ npm run build
1298
+ # 或
1299
+ yarn build
1300
+ ```
1301
+
1302
+ ### 测试覆盖率
1303
+
1304
+ 项目当前测试覆盖率达到 **76.88%**,满足生产环境要求:
1305
+
1306
+ - **语句覆盖率**: 76.88%
1307
+ - **分支覆盖率**: 64.13%
1308
+ - **函数覆盖率**: 75%
1309
+ - **行覆盖率**: 78.71%
1310
+
1311
+ 覆盖率报告生成在 `coverage/` 目录下,可以通过 `coverage/index.html` 查看详细的覆盖率报告。
1312
+
1313
+ ### 项目结构
1314
+
1315
+ ```
1316
+ request-iframe/
1317
+ ├── src/
1318
+ │ ├── api/ # 对外 API(client.ts, server.ts)
1319
+ │ ├── core/ # 核心实现(client, server, request, response)
1320
+ │ ├── message/ # 消息通信层(channel, dispatcher)
1321
+ │ ├── stream/ # 流式传输实现
1322
+ │ ├── interceptors/ # 拦截器实现
1323
+ │ ├── utils/ # 工具函数
1324
+ │ ├── constants/ # 常量定义
1325
+ │ ├── types/ # TypeScript 类型定义
1326
+ │ └── __tests__/ # 测试文件
1327
+ ├── library/ # 构建输出
1328
+ ├── coverage/ # 测试覆盖率报告
1329
+ ├── jest.config.js # Jest 配置
1330
+ ├── tsconfig.json # TypeScript 配置
1331
+ └── package.json
1332
+ ```
1333
+
1334
+ ### 贡献指南
1335
+
1336
+ 欢迎贡献代码!在提交 PR 之前,请确保:
1337
+
1338
+ 1. 代码通过 ESLint 检查(`npm run lint`)
1339
+ 2. 所有测试通过(`npm test`)
1340
+ 3. 测试覆盖率不低于 70%
1341
+ 4. 添加必要的测试用例
1342
+ 5. 更新相关文档
1343
+
1344
+ ### 性能说明
1345
+
1346
+ - **消息大小限制**: `postMessage` 本身没有严格的大小限制,但建议单个消息不超过 10MB,大文件请使用流式传输
1347
+ - **并发请求**: 支持并发请求,每个请求都有独立的 `requestId` 进行管理
1348
+ - **内存占用**: 轻量级实现,核心代码体积小,适合在生产环境使用
1349
+
1350
+ ### 浏览器兼容性
1351
+
1352
+ | 浏览器 | 最低版本 | 说明 |
1353
+ |--------|---------|------|
1354
+ | Chrome | 49+ | 完整支持 |
1355
+ | Firefox | 45+ | 完整支持 |
1356
+ | Safari | 10+ | 完整支持 |
1357
+ | Edge | 12+ | 完整支持 |
1358
+ | IE | 不支持 | 使用 Babel 转译后可能支持 IE 11,但未测试 |
1359
+
1360
+ ### 相关项目
1361
+
1362
+ - [axios](https://github.com/axios/axios) - 灵感来源的 HTTP 客户端库
1363
+ - [Express](https://expressjs.com/) - Server API 设计参考
1364
+
1365
+
1366
+ ## 许可证
1367
+
1368
+ MIT License
1369
+