request-iframe 0.1.0 → 0.2.0

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 (223) hide show
  1. package/QUICKSTART.CN.md +4 -2
  2. package/QUICKSTART.md +4 -2
  3. package/README.CN.md +302 -54
  4. package/README.md +281 -36
  5. package/cdn/request-iframe-react.umd.js +3354 -0
  6. package/cdn/request-iframe-react.umd.js.map +1 -0
  7. package/cdn/request-iframe-react.umd.min.js +2 -0
  8. package/cdn/request-iframe-react.umd.min.js.map +1 -0
  9. package/cdn/request-iframe.umd.js +19735 -0
  10. package/cdn/request-iframe.umd.js.map +1 -0
  11. package/cdn/request-iframe.umd.min.js +4 -0
  12. package/cdn/request-iframe.umd.min.js.map +1 -0
  13. package/esm/api/client.js +31 -22
  14. package/esm/api/endpoint.js +229 -0
  15. package/esm/api/server.js +19 -9
  16. package/esm/constants/debug.js +17 -0
  17. package/esm/constants/index.js +115 -66
  18. package/esm/constants/log.js +11 -0
  19. package/esm/constants/messages.js +6 -1
  20. package/esm/constants/warn-once.js +15 -0
  21. package/esm/endpoint/facade.js +390 -0
  22. package/esm/endpoint/heartbeat/heartbeat.js +60 -0
  23. package/esm/endpoint/heartbeat/ping.js +20 -0
  24. package/esm/endpoint/index.js +13 -0
  25. package/esm/endpoint/infra/hub.js +316 -0
  26. package/esm/endpoint/infra/inbox.js +232 -0
  27. package/esm/endpoint/infra/outbox.js +408 -0
  28. package/esm/endpoint/stream/dispatcher.js +58 -0
  29. package/esm/endpoint/stream/errors.js +27 -0
  30. package/esm/endpoint/stream/factory.js +76 -0
  31. package/esm/endpoint/stream/file-auto-resolve.js +34 -0
  32. package/esm/endpoint/stream/file-writable.js +105 -0
  33. package/esm/endpoint/stream/handler.js +26 -0
  34. package/esm/{core → impl}/client.js +243 -320
  35. package/esm/{core → impl}/response.js +120 -154
  36. package/esm/impl/server.js +568 -0
  37. package/esm/index.js +13 -6
  38. package/esm/message/ack.js +27 -0
  39. package/esm/message/channel-cache.js +108 -0
  40. package/esm/message/channel.js +92 -5
  41. package/esm/message/dispatcher.js +149 -98
  42. package/esm/stream/error.js +22 -0
  43. package/esm/stream/index.js +3 -1
  44. package/esm/stream/readable-stream.js +101 -26
  45. package/esm/stream/stream-core.js +121 -3
  46. package/esm/stream/writable-stream.js +368 -43
  47. package/esm/utils/ack.js +36 -0
  48. package/esm/utils/blob.js +16 -0
  49. package/esm/utils/cache.js +25 -76
  50. package/esm/utils/content-type.js +81 -0
  51. package/esm/utils/debug.js +157 -180
  52. package/esm/utils/hooks.js +130 -0
  53. package/esm/utils/id.js +14 -0
  54. package/esm/utils/iframe.js +20 -0
  55. package/esm/utils/index.js +12 -162
  56. package/esm/utils/is.js +3 -0
  57. package/esm/utils/logger.js +55 -0
  58. package/esm/utils/origin.js +3 -1
  59. package/esm/utils/promise.js +3 -0
  60. package/esm/utils/window.js +31 -0
  61. package/library/api/client.d.ts.map +1 -1
  62. package/library/api/client.js +32 -23
  63. package/library/api/endpoint.d.ts +23 -0
  64. package/library/api/endpoint.d.ts.map +1 -0
  65. package/library/api/endpoint.js +235 -0
  66. package/library/api/server.d.ts +4 -1
  67. package/library/api/server.d.ts.map +1 -1
  68. package/library/api/server.js +19 -9
  69. package/library/constants/debug.d.ts +18 -0
  70. package/library/constants/debug.d.ts.map +1 -0
  71. package/library/constants/debug.js +23 -0
  72. package/library/constants/index.d.ts +58 -7
  73. package/library/constants/index.d.ts.map +1 -1
  74. package/library/constants/index.js +143 -67
  75. package/library/constants/log.d.ts +12 -0
  76. package/library/constants/log.d.ts.map +1 -0
  77. package/library/constants/log.js +17 -0
  78. package/library/constants/messages.d.ts +6 -1
  79. package/library/constants/messages.d.ts.map +1 -1
  80. package/library/constants/messages.js +6 -1
  81. package/library/constants/warn-once.d.ts +12 -0
  82. package/library/constants/warn-once.d.ts.map +1 -0
  83. package/library/constants/warn-once.js +22 -0
  84. package/library/endpoint/facade.d.ts +238 -0
  85. package/library/endpoint/facade.d.ts.map +1 -0
  86. package/library/endpoint/facade.js +398 -0
  87. package/library/endpoint/heartbeat/heartbeat.d.ts +34 -0
  88. package/library/endpoint/heartbeat/heartbeat.d.ts.map +1 -0
  89. package/library/endpoint/heartbeat/heartbeat.js +67 -0
  90. package/library/endpoint/heartbeat/ping.d.ts +18 -0
  91. package/library/endpoint/heartbeat/ping.d.ts.map +1 -0
  92. package/library/endpoint/heartbeat/ping.js +26 -0
  93. package/library/endpoint/index.d.ts +16 -0
  94. package/library/endpoint/index.d.ts.map +1 -0
  95. package/library/endpoint/index.js +114 -0
  96. package/library/endpoint/infra/hub.d.ts +170 -0
  97. package/library/endpoint/infra/hub.d.ts.map +1 -0
  98. package/library/endpoint/infra/hub.js +323 -0
  99. package/library/endpoint/infra/inbox.d.ts +73 -0
  100. package/library/endpoint/infra/inbox.d.ts.map +1 -0
  101. package/library/endpoint/infra/inbox.js +239 -0
  102. package/library/endpoint/infra/outbox.d.ts +149 -0
  103. package/library/endpoint/infra/outbox.d.ts.map +1 -0
  104. package/library/endpoint/infra/outbox.js +415 -0
  105. package/library/endpoint/stream/dispatcher.d.ts +33 -0
  106. package/library/endpoint/stream/dispatcher.d.ts.map +1 -0
  107. package/library/endpoint/stream/dispatcher.js +66 -0
  108. package/library/endpoint/stream/errors.d.ts +20 -0
  109. package/library/endpoint/stream/errors.d.ts.map +1 -0
  110. package/library/endpoint/stream/errors.js +32 -0
  111. package/library/endpoint/stream/factory.d.ts +44 -0
  112. package/library/endpoint/stream/factory.d.ts.map +1 -0
  113. package/library/endpoint/stream/factory.js +82 -0
  114. package/library/endpoint/stream/file-auto-resolve.d.ts +26 -0
  115. package/library/endpoint/stream/file-auto-resolve.d.ts.map +1 -0
  116. package/library/endpoint/stream/file-auto-resolve.js +41 -0
  117. package/library/endpoint/stream/file-writable.d.ts +33 -0
  118. package/library/endpoint/stream/file-writable.d.ts.map +1 -0
  119. package/library/endpoint/stream/file-writable.js +115 -0
  120. package/library/endpoint/stream/handler.d.ts +20 -0
  121. package/library/endpoint/stream/handler.d.ts.map +1 -0
  122. package/library/endpoint/stream/handler.js +32 -0
  123. package/library/{core → impl}/client.d.ts +16 -13
  124. package/library/impl/client.d.ts.map +1 -0
  125. package/library/{core → impl}/client.js +254 -333
  126. package/library/{core → impl}/request.d.ts.map +1 -1
  127. package/library/{core → impl}/response.d.ts +7 -12
  128. package/library/impl/response.d.ts.map +1 -0
  129. package/library/{core → impl}/response.js +120 -154
  130. package/library/{core → impl}/server.d.ts +26 -55
  131. package/library/impl/server.d.ts.map +1 -0
  132. package/library/impl/server.js +575 -0
  133. package/library/index.d.ts +13 -6
  134. package/library/index.d.ts.map +1 -1
  135. package/library/index.js +16 -16
  136. package/library/message/ack.d.ts +15 -0
  137. package/library/message/ack.d.ts.map +1 -0
  138. package/library/message/ack.js +33 -0
  139. package/library/message/channel-cache.d.ts +26 -0
  140. package/library/message/channel-cache.d.ts.map +1 -0
  141. package/library/message/channel-cache.js +115 -0
  142. package/library/message/channel.d.ts +53 -6
  143. package/library/message/channel.d.ts.map +1 -1
  144. package/library/message/channel.js +96 -9
  145. package/library/message/dispatcher.d.ts +17 -0
  146. package/library/message/dispatcher.d.ts.map +1 -1
  147. package/library/message/dispatcher.js +149 -98
  148. package/library/stream/error.d.ts +24 -0
  149. package/library/stream/error.d.ts.map +1 -0
  150. package/library/stream/error.js +29 -0
  151. package/library/stream/index.d.ts +4 -1
  152. package/library/stream/index.d.ts.map +1 -1
  153. package/library/stream/index.js +7 -4
  154. package/library/stream/readable-stream.d.ts.map +1 -1
  155. package/library/stream/readable-stream.js +102 -27
  156. package/library/stream/stream-core.d.ts +22 -1
  157. package/library/stream/stream-core.d.ts.map +1 -1
  158. package/library/stream/stream-core.js +120 -2
  159. package/library/stream/types.d.ts +115 -2
  160. package/library/stream/types.d.ts.map +1 -1
  161. package/library/stream/writable-stream.d.ts +20 -2
  162. package/library/stream/writable-stream.d.ts.map +1 -1
  163. package/library/stream/writable-stream.js +366 -41
  164. package/library/types/index.d.ts +17 -22
  165. package/library/types/index.d.ts.map +1 -1
  166. package/library/utils/ack.d.ts +2 -0
  167. package/library/utils/ack.d.ts.map +1 -0
  168. package/library/utils/ack.js +44 -0
  169. package/library/utils/blob.d.ts +3 -0
  170. package/library/utils/blob.d.ts.map +1 -0
  171. package/library/utils/blob.js +22 -0
  172. package/library/utils/cache.d.ts +10 -20
  173. package/library/utils/cache.d.ts.map +1 -1
  174. package/library/utils/cache.js +25 -79
  175. package/library/utils/content-type.d.ts +13 -0
  176. package/library/utils/content-type.d.ts.map +1 -0
  177. package/library/utils/content-type.js +87 -0
  178. package/library/utils/debug.d.ts.map +1 -1
  179. package/library/utils/debug.js +156 -178
  180. package/library/utils/hooks.d.ts +30 -0
  181. package/library/utils/hooks.d.ts.map +1 -0
  182. package/library/utils/hooks.js +139 -0
  183. package/library/utils/id.d.ts +9 -0
  184. package/library/utils/id.d.ts.map +1 -0
  185. package/library/utils/id.js +21 -0
  186. package/library/utils/iframe.d.ts +5 -0
  187. package/library/utils/iframe.d.ts.map +1 -0
  188. package/library/utils/iframe.js +25 -0
  189. package/library/utils/index.d.ts +7 -34
  190. package/library/utils/index.d.ts.map +1 -1
  191. package/library/utils/index.js +58 -193
  192. package/library/utils/is.d.ts +2 -0
  193. package/library/utils/is.d.ts.map +1 -0
  194. package/library/utils/is.js +9 -0
  195. package/library/utils/logger.d.ts +13 -0
  196. package/library/utils/logger.d.ts.map +1 -0
  197. package/library/utils/logger.js +63 -0
  198. package/library/utils/origin.d.ts.map +1 -1
  199. package/library/utils/origin.js +2 -1
  200. package/library/utils/promise.d.ts +2 -0
  201. package/library/utils/promise.d.ts.map +1 -0
  202. package/library/utils/promise.js +9 -0
  203. package/library/utils/window.d.ts +2 -0
  204. package/library/utils/window.d.ts.map +1 -0
  205. package/library/utils/window.js +38 -0
  206. package/package.json +49 -2
  207. package/react/package.json +2 -1
  208. package/esm/core/client-server.js +0 -329
  209. package/esm/core/server.js +0 -767
  210. package/esm/utils/ack-meta.js +0 -53
  211. package/library/core/client-server.d.ts +0 -106
  212. package/library/core/client-server.d.ts.map +0 -1
  213. package/library/core/client-server.js +0 -336
  214. package/library/core/client.d.ts.map +0 -1
  215. package/library/core/response.d.ts.map +0 -1
  216. package/library/core/server.d.ts.map +0 -1
  217. package/library/core/server.js +0 -772
  218. package/library/utils/ack-meta.d.ts +0 -2
  219. package/library/utils/ack-meta.d.ts.map +0 -1
  220. package/library/utils/ack-meta.js +0 -59
  221. /package/esm/{core → impl}/request.js +0 -0
  222. /package/library/{core → impl}/request.d.ts +0 -0
  223. /package/library/{core → impl}/request.js +0 -0
package/QUICKSTART.CN.md CHANGED
@@ -220,9 +220,11 @@ server.on('/api/users/:id', (req, res) => {
220
220
  开启 trace 模式查看详细日志:
221
221
 
222
222
  ```typescript
223
+ import { LogLevel } from 'request-iframe';
224
+
223
225
  const client = requestIframeClient(iframe, {
224
226
  secretKey: 'my-app',
225
- trace: true // 开启调试日志
227
+ trace: LogLevel.INFO // 输出 info/warn/error(也可以用 true 开启 TRACE)
226
228
  });
227
229
 
228
230
  const server = requestIframeServer({
@@ -293,4 +295,4 @@ console.log(response.data.name); // TypeScript 知道这是 string
293
295
 
294
296
  - 查看 [README.CN.md](./README.CN.md) 了解完整 API(中文)
295
297
  - 查看 [README.md](./README.md) 了解完整 API(English)
296
- - 查看 [src/__tests__](./src/__tests__) 目录下的测试用例获取更多示例
298
+ - 查看 [`__tests__/`](./__tests__) 与 [`react/__tests__/`](./react/__tests__) 下的测试用例获取更多示例
package/QUICKSTART.md CHANGED
@@ -220,9 +220,11 @@ server.on('/api/users/:id', (req, res) => {
220
220
  Enable trace mode to view detailed logs:
221
221
 
222
222
  ```typescript
223
+ import { LogLevel } from 'request-iframe';
224
+
223
225
  const client = requestIframeClient(iframe, {
224
226
  secretKey: 'my-app',
225
- trace: true // Enable debug logs
227
+ trace: LogLevel.INFO // Enable info/warn/error logs (or use true for TRACE)
226
228
  });
227
229
 
228
230
  const server = requestIframeServer({
@@ -293,4 +295,4 @@ console.log(response.data.name); // TypeScript knows this is string
293
295
 
294
296
  - View [README.md](./README.md) for complete API documentation (English)
295
297
  - View [README.CN.md](./README.CN.md) for complete API documentation (中文)
296
- - Check [src/__tests__](./src/__tests__) directory for more examples from test cases
298
+ - Check [`__tests__/`](./__tests__) and [`react/__tests__/`](./react/__tests__) for more examples from test cases
package/README.CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # request-iframe
2
2
 
3
- 像发送 HTTP 请求一样与 iframe 通信!基于 `postMessage` 实现的 iframe 跨域通信库。
3
+ 像发送 HTTP 请求一样与 iframe / Window 通信!基于 `postMessage` 实现的浏览器跨页面通信库。
4
4
 
5
5
  > 🌐 **Languages**: [English](./README.md) | [中文](./README.CN.md)
6
6
 
@@ -8,7 +8,7 @@
8
8
  <img src="https://img.shields.io/badge/TypeScript-Ready-blue" alt="TypeScript Ready">
9
9
  <img src="https://img.shields.io/badge/API-Express%20Like-green" alt="Express Like API">
10
10
  <img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
11
- <img src="https://img.shields.io/badge/Test%20Coverage-76%25-brightgreen" alt="Test Coverage">
11
+ <img src="https://coveralls.io/repos/github/gxlmyacc/request-iframe/badge.svg?branch=main" alt="Coverage Status">
12
12
  </p>
13
13
 
14
14
  ## 📑 目录
@@ -48,7 +48,7 @@
48
48
 
49
49
  ## 为什么选择 request-iframe?
50
50
 
51
- 在微前端、iframe 嵌套等场景下,父子页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
51
+ 在微前端、iframe 嵌套、弹窗(window.open)等场景下,跨页面通信是常见需求。传统的 `postMessage` 通信存在以下痛点:
52
52
 
53
53
  | 痛点 | 传统方式 | request-iframe |
54
54
  |------|----------|----------------|
@@ -74,8 +74,9 @@
74
74
  - ⏱️ **智能超时** - 三阶段超时(连接/同步/异步),自动识别长任务
75
75
  - 📦 **TypeScript** - 完整的类型定义和智能提示
76
76
  - 🔒 **消息隔离** - secretKey 机制避免多实例消息串线
77
- - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
77
+ - 📁 **文件传输** - 支持文件通过流方式传输(ClientServer)
78
78
  - 🌊 **流式传输** - 支持大文件分块传输,支持异步迭代器
79
+ - 🧾 **分级日志** - 默认只输出 warn/error,可通过 `trace` 设置日志等级与调试日志
79
80
  - 🌍 **多语言** - 错误消息可自定义,便于国际化
80
81
  - ✅ **协议版本** - 内置版本控制,便于升级兼容
81
82
 
@@ -93,6 +94,32 @@ pnpm add request-iframe
93
94
 
94
95
  **TypeScript**: 内置完整类型定义,无需安装 `@types/request-iframe`
95
96
 
97
+ ## CDN(UMD bundle)
98
+
99
+ 本项目也支持构建 **可直接用 `<script>` 引入的 UMD bundle**(核心 + React hooks),方便放到 CDN 上。
100
+
101
+ - 核心 bundle 输出:`cdn/request-iframe.umd(.min).js` → 全局变量 `RequestIframe`
102
+ - React bundle 输出:`cdn/request-iframe-react.umd(.min).js` → 全局变量 `RequestIframeReact`
103
+ - 依赖 `React` 全局变量(即 `react` 的 UMD 版本)
104
+ - 依赖 `RequestIframe` 全局变量(先加载核心 bundle)
105
+
106
+ 示例(使用 unpkg):
107
+
108
+ ```html
109
+ <!-- 核心 -->
110
+ <script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe.umd.min.js"></script>
111
+
112
+ <!-- React(可选) -->
113
+ <script src="https://unpkg.com/react@latest/umd/react.production.min.js"></script>
114
+ <script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe-react.umd.min.js"></script>
115
+
116
+ <script>
117
+ const { requestIframeClient, requestIframeServer, requestIframeEndpoint } = RequestIframe;
118
+ // React hooks 在 RequestIframeReact 上(例如 RequestIframeReact.useClient)
119
+ console.log(!!requestIframeClient, !!requestIframeServer, !!requestIframeEndpoint);
120
+ <\/script>
121
+ ```
122
+
96
123
  ## 快速开始
97
124
 
98
125
  ### 1. 父页面(Client 端)
@@ -130,6 +157,11 @@ server.on('/api/getUserInfo', (req, res) => {
130
157
 
131
158
  > 💡 **提示**: 更多快速上手指南请查看 [QUICKSTART.CN.md](./QUICKSTART.CN.md) 或 [QUICKSTART.md](./QUICKSTART.md) (English)
132
159
 
160
+ ## 该用哪个 API?
161
+
162
+ - **优先使用 `requestIframeClient()` + `requestIframeServer()`**:适用于主要是单向通信(父页 → iframe),并且你希望把“发送请求”和“处理请求”职责明确分开。
163
+ - **优先使用 `requestIframeEndpoint()`**:适用于需要 **双向通信**(双方都需要 `send()` + `on()/use()/map()`),或者你希望用一个门面对象更方便地串起全链路做调试。
164
+
133
165
  ---
134
166
 
135
167
  ## 实现原理
@@ -151,7 +183,7 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
151
183
  │ │
152
184
  │ <──── RESPONSE ──────────────────────── │ 返回结果
153
185
  │ │
154
- │ ──── RECEIVED (可选) ──────────────────> │ 确认收到响应
186
+ │ ──── ACK (可选) ───────────────────────> │ 确认收到响应
155
187
  │ │
156
188
  ```
157
189
 
@@ -160,11 +192,10 @@ request-iframe 在 `postMessage` 基础上实现了一套类 HTTP 的通信协
160
192
  | 类型 | 方向 | 说明 |
161
193
  |------|------|------|
162
194
  | `request` | Client → Server | 客户端发起请求 |
163
- | `ack` | ServerClient | 服务端确认收到请求(当请求 `requireAck` 开启时) |
195
+ | `ack` | 接收方发送方 | 回执确认(当消息 `requireAck: true` 且被接管/处理时发送;ACK-only) |
164
196
  | `async` | Server → Client | 通知客户端这是异步任务(handler 返回 Promise 时发送) |
165
197
  | `response` | Server → Client | 返回响应数据 |
166
198
  | `error` | Server → Client | 返回错误信息 |
167
- | `received` | Client → Server | 客户端确认收到响应/错误(可选,由响应的 `requireAck` 控制) |
168
199
  | `ping` | Client → Server | 连接检测(`isConnect()`;可使用 `requireAck` 确认投递) |
169
200
  | `pong` | Server → Client | 连接检测响应 |
170
201
 
@@ -286,6 +317,29 @@ server.on('/event', (req, res) => {
286
317
  });
287
318
  ```
288
319
 
320
+ ### 弹窗 / 新标签页(Window 通信)
321
+
322
+ `request-iframe` 不仅可以与 iframe 通信,也可以把 `target` 直接传 `Window`(例如弹窗/新标签页)。
323
+
324
+ **重要前提**:你必须拿到对方页面的 `Window` 引用(例如 `window.open()` 的返回值,或通过 `window.opener` / `MessageEvent.source` 获取)。**无法**通过 URL 给“任意标签页”发消息。
325
+
326
+ ```typescript
327
+ // 父页面:打开新标签页/弹窗
328
+ const child = window.open('https://child.example.com/page.html', '_blank');
329
+ if (!child) throw new Error('弹窗被拦截');
330
+
331
+ // 父 -> 子
332
+ const client = requestIframeClient(child, {
333
+ secretKey: 'popup-demo',
334
+ targetOrigin: 'https://child.example.com' // 强烈建议不要用 '*'
335
+ });
336
+ await client.send('/api/ping', { from: 'parent' });
337
+
338
+ // 子页面:创建 server
339
+ const server = requestIframeServer({ secretKey: 'popup-demo' });
340
+ server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));
341
+ ```
342
+
289
343
  ### 跨域数据获取
290
344
 
291
345
  当 iframe 与父页面不同域时,使用 request-iframe 安全地获取数据:
@@ -583,6 +637,8 @@ server.on('/api/data', (req, res) => {
583
637
 
584
638
  ### 文件传输
585
639
 
640
+ > 说明:文件传输(无论 Client→Server 还是 Server→Client)底层都会通过 stream 协议承载;你只需要使用 `client.sendFile()` / `res.sendFile()` 这一层 API 即可。
641
+
586
642
  ```typescript
587
643
  // Server 端发送文件
588
644
  server.on('/api/download', async (req, res) => {
@@ -615,7 +671,7 @@ if (response.data instanceof File || response.data instanceof Blob) {
615
671
 
616
672
  #### Client → Server(Client 向 Server 发送文件)
617
673
 
618
- Client 端发送文件**仅走流式**。使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
674
+ Client 端发送文件使用 `sendFile()`(或直接 `send(path, file)`);Server 端在 `autoResolve: true`(默认)时会把文件自动解析成 `File/Blob` 放到 `req.body`,当 `autoResolve: false` 时则通过 `req.stream` / `req.body` 暴露为 `IframeFileReadableStream`。
619
675
 
620
676
  ```typescript
621
677
  // Client 端:发送文件(stream,autoResolve 默认 true)
@@ -639,14 +695,74 @@ server.on('/api/upload', async (req, res) => {
639
695
  });
640
696
  ```
641
697
 
642
- **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`(走流式)。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
698
+ **提示**:当 `client.send()` 的 `body` 是 `File/Blob` 时,会自动分发到 `client.sendFile()`。`autoResolve` 为 true(默认)时 Server 拿到 `req.body`(File/Blob),为 false 时拿到 `req.stream` / `req.body`(`IframeFileReadableStream`)。
643
699
 
644
700
  ### 流式传输(Stream)
645
701
 
646
- 对于大文件或需要分块传输的场景,可以使用流式传输:
702
+ Stream 除了用于大文件/分块传输,也可以用于“长连接 / 订阅式交互”(类似 SSE / WebSocket,但基于 `postMessage`)。常见用法有两类:
703
+
704
+ - **长连接/订阅**:Client 发起一次请求拿到 `response.stream`,然后用 `for await` 持续消费事件;需要结束时调用 `stream.cancel()`。
705
+ - **分块/文件流**:按 chunk 传输数据或文件(下方示例)。
706
+
707
+ > 长连接注意事项:
708
+ > - `IframeWritableStream` 默认会使用 `asyncTimeout` 作为 `expireTimeout`(避免泄露)。如果你的订阅需要更久,请显式设置更大的 `expireTimeout`,或设置 `expireTimeout: 0` 关闭自动过期(建议配合业务侧取消/重连,避免泄露)。
709
+ > - Server 端的 `res.sendStream(stream)` 会一直等待到流结束;如果你需要在后续主动 `write()` 推送数据,请不要直接 `await` 它,可以用 `void res.sendStream(stream)` 或保存返回的 Promise。
710
+ > - 如果启用了 `maxConcurrentRequestsPerClient`,一个长连接 stream 会占用一个并发槽,需要按业务场景调整。
711
+ > - **事件订阅**:stream 支持 `stream.on(event, listener)`(返回取消订阅函数),可用于埋点/进度/调试(如监听 `start/data/read/write/cancel/end/error/timeout/expired`)。主消费仍建议使用 `for await`。
712
+
713
+ #### 长连接 / 订阅式交互(push 模式示例)
647
714
 
648
715
  ```typescript
649
- import {
716
+ /**
717
+ * Server 端:订阅(长连接)
718
+ * - mode: 'push':由写侧主动 write()
719
+ * - expireTimeout: 0:关闭自动过期(谨慎使用,建议结合业务取消/重连)
720
+ */
721
+ server.on('/api/subscribe', (req, res) => {
722
+ const stream = new IframeWritableStream({
723
+ type: 'data',
724
+ chunked: true,
725
+ mode: 'push',
726
+ expireTimeout: 0,
727
+ /** 可选:写侧空闲检测(等待 pull/ack 太久会做心跳并失败) */
728
+ streamTimeout: 15000
729
+ });
730
+
731
+ /** 注意:不要 await,否则会一直等到流结束 */
732
+ void res.sendStream(stream);
733
+
734
+ const timer = setInterval(() => {
735
+ try {
736
+ stream.write({ type: 'tick', ts: Date.now() });
737
+ } catch {
738
+ clearInterval(timer);
739
+ }
740
+ }, 1000);
741
+ });
742
+
743
+ /**
744
+ * Client 端:持续读取(长连接建议用 for await,而不是 readAll())
745
+ */
746
+ const resp = await client.send('/api/subscribe', {});
747
+ if (isIframeReadableStream(resp.stream)) {
748
+ /** 事件订阅示例(可选) */
749
+ const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
750
+ console.error('stream error:', error);
751
+ });
752
+
753
+ for await (const evt of resp.stream) {
754
+ console.log('event:', evt);
755
+ }
756
+
757
+ off();
758
+ }
759
+ ```
760
+
761
+ #### 分块 / 文件流示例
762
+
763
+ ```typescript
764
+ import {
765
+ StreamEvent,
650
766
  IframeWritableStream,
651
767
  IframeFileWritableStream,
652
768
  isIframeReadableStream,
@@ -757,10 +873,10 @@ if (isIframeFileReadableStream(fileResponse.stream)) {
757
873
 
758
874
  | 类型 | 说明 |
759
875
  |------|------|
760
- | `IframeWritableStream` | 服务端可写流,用于发送普通数据 |
761
- | `IframeFileWritableStream` | 服务端文件可写流(文件流) |
762
- | `IframeReadableStream` | 客户端可读流,用于接收普通数据 |
763
- | `IframeFileReadableStream` | 客户端文件可读流(文件流) |
876
+ | `IframeWritableStream` | 写侧(生产者)流:**谁要发送 stream,谁就创建它**;可用于 Server→Client 的响应流,也可用于 Client→Server 的请求流 |
877
+ | `IframeFileWritableStream` | 文件写侧(生产者)流:用于发送文件(底层会做 Base64 编码) |
878
+ | `IframeReadableStream` | 读侧(消费者)流:用于接收普通数据(无论来自 Server 还是 Client) |
879
+ | `IframeFileReadableStream` | 文件读侧(消费者)流:用于接收文件(底层会做 Base64 解码) |
764
880
 
765
881
  > **注意**:文件流内部会进行 Base64 编/解码。Base64 会带来约 33% 的体积膨胀,并且在超大文件场景下可能会有较高的内存/CPU 开销。大文件建议使用 **分块** 文件流(`chunked: true`),并控制 chunk 大小(例如 256KB–1MB)。
766
882
 
@@ -775,18 +891,22 @@ interface WritableStreamOptions {
775
891
  streamTimeout?: number; // 写侧空闲超时(ms,可选):长时间未收到对端 pull/ack 时会做心跳确认并失败
776
892
  iterator?: () => AsyncGenerator; // 数据生成迭代器
777
893
  next?: () => Promise<{ data: any; done: boolean }>; // 数据生成函数
894
+ maxPendingChunks?: number; // 写侧待发送缓冲上限(可选;push/长连接场景建议配置,避免 pendingQueue 无限增长)
895
+ maxPendingBytes?: number; // 写侧待发送字节上限(可选;避免单次 write 超大 chunk 导致内存暴涨)
778
896
  metadata?: Record<string, any>; // 自定义元数据
779
897
  }
780
898
  ```
781
899
 
782
900
  **流超时/保活:**
783
- - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认,失败则认为流已断开并报错。
784
- - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull/ack 协议下,若长时间未收到对端 `pull/ack`,会做心跳确认并失败(避免长时间无效占用)。
901
+ - `streamTimeout`(请求参数):读侧空闲超时(ms,可选)。消费 `response.stream` 时超过该时间未收到新的 chunk,会先做一次心跳确认(默认使用 `client.isConnect()`),失败则认为流已断开并报错。
902
+ - `streamTimeout`(流参数):写侧空闲超时(ms,可选)。写侧在 pull 协议下,若长时间未收到对端 `pull`,会做心跳确认并失败(避免长时间无效占用)。
785
903
  - `expireTimeout`(流参数):写侧有效期;过期后会发送 `stream_error`,读侧会收到明确的“已过期”错误。
904
+ - `maxPendingChunks`(流参数):写侧待发送缓冲上限(可选)。对 `push` / 长连接场景很重要:当对端停止 pull 时,继续 `write()` 会在写侧积压,建议设置上限防止内存无限增长。
905
+ - `maxPendingBytes`(流参数):写侧待发送字节上限(可选)。用于防止单次写入超大 chunk(例如大字符串/大 Blob 包装)导致内存占用过高。
786
906
 
787
907
  **pull/ack 协议(新增,默认启用):**
788
- - 读侧会自动发送 `stream_pull` 请求更多 chunk,并对每个收到的 chunk 自动发送 `stream_ack`。
789
- - 写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
908
+ - 读侧会自动发送 `stream_pull` 请求更多 chunk;写侧只会在收到 `stream_pull` 后才继续发送 `stream_data`,实现真正的背压(按需拉取)。
909
+ - 断连检测不依赖“逐帧确认专用消息类型”,而是通过 `streamTimeout + 心跳(isConnect)` 来实现。
790
910
 
791
911
  **consume 默认行为(变更):**
792
912
  - `for await (const chunk of stream)` 默认会 **消费并丢弃已迭代过的 chunk**(`consume: true`),避免长流场景内存无限增长。
@@ -811,9 +931,9 @@ Server 可以要求 Client 确认收到响应:
811
931
  ```typescript
812
932
  server.on('/api/important', async (req, res) => {
813
933
  // requireAck: true 表示需要客户端确认
814
- const received = await res.send(data, { requireAck: true });
934
+ const acked = await res.send(data, { requireAck: true });
815
935
 
816
- if (received) {
936
+ if (acked) {
817
937
  console.log('客户端已确认收到');
818
938
  } else {
819
939
  console.log('客户端未确认(超时)');
@@ -821,21 +941,25 @@ server.on('/api/important', async (req, res) => {
821
941
  });
822
942
  ```
823
943
 
824
- > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `received`,无需业务侧手动发送。
944
+ > **说明**:当响应/错误被客户端“接管”(即存在对应的 pending request)时,库会自动发送 `ack`,无需业务侧手动发送。
825
945
 
826
946
  ### 追踪模式
827
947
 
828
- 开启追踪模式可以在控制台查看详细的通信日志:
948
+ 默认情况下,request-iframe 只会输出 **warn/error** 日志(避免生产环境控制台过于吵闹)。
949
+
950
+ 开启追踪模式(或设置日志等级)可以在控制台查看更详细的通信日志:
829
951
 
830
952
  ```typescript
953
+ import { LogLevel } from 'request-iframe';
954
+
831
955
  const client = requestIframeClient(iframe, {
832
956
  secretKey: 'demo',
833
- trace: true
957
+ trace: true // 等价于 LogLevel.TRACE
834
958
  });
835
959
 
836
960
  const server = requestIframeServer({
837
961
  secretKey: 'demo',
838
- trace: true
962
+ trace: LogLevel.INFO // 输出 info/warn/error(比 trace 更克制)
839
963
  });
840
964
 
841
965
  // 控制台输出:
@@ -844,6 +968,14 @@ const server = requestIframeServer({
844
968
  // [request-iframe] [INFO] ✅ Request Success { status: 200, data: {...} }
845
969
  ```
846
970
 
971
+ `trace` 支持:
972
+ - `true` / `false`
973
+ - `'trace' | 'info' | 'warn' | 'error' | 'silent'`(或 `LogLevel.*`)
974
+
975
+ 说明:
976
+ - 当 `trace` 为 `LogLevel.TRACE` / `LogLevel.INFO` 时,库会额外挂载内置的调试拦截器/监听器,以输出更丰富的 request/response 日志。
977
+ - 当 `trace` 为 `LogLevel.WARN` / `LogLevel.ERROR` / `LogLevel.SILENT` 时,只影响日志输出等级(不会额外挂载调试拦截器)。
978
+
847
979
  ### 多语言支持
848
980
 
849
981
  ```typescript
@@ -874,7 +1006,7 @@ setMessages({
874
1006
  |------|------|------|
875
1007
  | `target` | `HTMLIFrameElement \| Window` | 目标 iframe 元素或 window 对象 |
876
1008
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
877
- | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
1009
+ | `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | trace/日志等级(可选)。默认只输出 warn/error |
878
1010
  | `options.targetOrigin` | `string` | 覆盖 postMessage 的 targetOrigin(可选)。当 `target` 是 `Window` 时默认 `*`;当 `target` 是 iframe 时默认取 `iframe.src` 的 origin。 |
879
1011
  | `options.ackTimeout` | `number` | 全局默认 ACK 确认超时(ms),默认 1000 |
880
1012
  | `options.timeout` | `number` | 全局默认请求超时(ms),默认 5000 |
@@ -886,6 +1018,70 @@ setMessages({
886
1018
 
887
1019
  **返回值:** `RequestIframeClient`
888
1020
 
1021
+ **关于 `target: Window` 的说明:**
1022
+ - **必须持有对方页面的 `Window` 引用**(例如 `window.open()` 返回值、`window.opener`、或 `MessageEvent.source`)。
1023
+ - **无法**通过 URL 给“任意标签页”发消息。
1024
+ - 安全起见,建议显式设置 `targetOrigin`,并配置 `allowedOrigins` / `validateOrigin`。
1025
+
1026
+ **生产环境推荐配置(模板):**
1027
+
1028
+ ```typescript
1029
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
1030
+
1031
+ /**
1032
+ * 建议:明确限定 3 件事
1033
+ * - secretKey:隔离不同业务/不同实例,避免消息串线
1034
+ * - targetOrigin:发送时的 targetOrigin(Window 场景强烈建议不要用 '*')
1035
+ * - allowedOrigins / validateOrigin:接收时的 origin 白名单校验
1036
+ */
1037
+ const secretKey = 'my-app';
1038
+ const targetOrigin = 'https://child.example.com';
1039
+ const allowedOrigins = [targetOrigin];
1040
+
1041
+ // Client(父页)
1042
+ const client = requestIframeClient(window.open(targetOrigin)!, {
1043
+ secretKey,
1044
+ targetOrigin,
1045
+ allowedOrigins
1046
+ });
1047
+
1048
+ // Server(子页/iframe 内)
1049
+ const server = requestIframeServer({
1050
+ secretKey,
1051
+ allowedOrigins,
1052
+ // 防止异常/攻击导致消息爆炸(按需设置)
1053
+ maxConcurrentRequestsPerClient: 50
1054
+ });
1055
+ ```
1056
+
1057
+ **生产环境推荐配置(iframe 场景模板):**
1058
+
1059
+ ```typescript
1060
+ import { requestIframeClient, requestIframeServer } from 'request-iframe';
1061
+
1062
+ /**
1063
+ * iframe 场景通常可以从 iframe.src 推导 targetOrigin,并用它作为 allowedOrigins 白名单。
1064
+ */
1065
+ const iframe = document.querySelector('iframe')!;
1066
+ const targetOrigin = new URL(iframe.src).origin;
1067
+ const secretKey = 'my-app';
1068
+
1069
+ // Client(父页)
1070
+ const client = requestIframeClient(iframe, {
1071
+ secretKey,
1072
+ // 可显式写出来(即使库内部也会默认推导),便于审计/避免误用 '*'
1073
+ targetOrigin,
1074
+ allowedOrigins: [targetOrigin]
1075
+ });
1076
+
1077
+ // Server(iframe 内)
1078
+ const server = requestIframeServer({
1079
+ secretKey,
1080
+ allowedOrigins: [targetOrigin],
1081
+ maxConcurrentRequestsPerClient: 50
1082
+ });
1083
+ ```
1084
+
889
1085
  **示例:**
890
1086
 
891
1087
  ```typescript
@@ -912,7 +1108,7 @@ await client.send('/api/longTask', {}, {
912
1108
  | 参数 | 类型 | 说明 |
913
1109
  |------|------|------|
914
1110
  | `options.secretKey` | `string` | 消息隔离标识(可选) |
915
- | `options.trace` | `boolean` | 是否开启追踪模式(可选) |
1111
+ | `options.trace` | `boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent'` | trace/日志等级(可选)。默认只输出 warn/error |
916
1112
  | `options.ackTimeout` | `number` | 等待客户端确认超时(ms),默认 1000 |
917
1113
  | `options.maxConcurrentRequestsPerClient` | `number` | 每个客户端的最大并发 in-flight 请求数(按 origin + creatorId 维度),默认 Infinity |
918
1114
  | `options.allowedOrigins` | `string \| RegExp \| Array<string \| RegExp>` | 接收消息的 origin 白名单(可选,生产环境强烈建议配置) |
@@ -920,6 +1116,65 @@ await client.send('/api/longTask', {}, {
920
1116
 
921
1117
  **返回值:** `RequestIframeServer`
922
1118
 
1119
+ ### requestIframeEndpoint(target, options?)
1120
+
1121
+ 创建一个 **endpoint 门面**(client + server)并绑定到某个对端窗口/iframe。
1122
+
1123
+ 它可以:
1124
+ - **向对端发送请求**:`endpoint.send(...)`
1125
+ - **处理对端发来的请求**:`endpoint.on(...)` / `endpoint.use(...)` / `endpoint.map(...)`
1126
+
1127
+ 说明:
1128
+ - 内部的 client/server 是 **懒创建** 的(只有首次使用发送/注册 handler 等能力时才会创建)。
1129
+ - 如果传了 `options.id`,它会作为 client+server 的共享 id;不传则会自动生成一个。
1130
+ - `options.trace` 与 client/server 一致,推荐用 `LogLevel.*` 来配置日志等级。
1131
+
1132
+ 示例(使用 endpoint 做双向通信,推荐):
1133
+
1134
+ ```typescript
1135
+ import { requestIframeEndpoint, LogLevel } from 'request-iframe';
1136
+
1137
+ // 父页面(持有 iframe 元素)
1138
+ const iframe = document.querySelector('iframe')!;
1139
+ const parentEndpoint = requestIframeEndpoint(iframe, {
1140
+ secretKey: 'demo',
1141
+ trace: LogLevel.INFO
1142
+ });
1143
+ parentEndpoint.on('/notify', (req, res) => res.send({ ok: true, echo: req.body }));
1144
+
1145
+ // iframe 页面(持有 window.parent)
1146
+ const iframeEndpoint = requestIframeEndpoint(window.parent, {
1147
+ secretKey: 'demo',
1148
+ targetOrigin: 'https://parent.example.com',
1149
+ trace: true
1150
+ });
1151
+ iframeEndpoint.on('/api/ping', (req, res) => res.send({ ok: true }));
1152
+
1153
+ // 任意一侧都可以 send + handle
1154
+ await parentEndpoint.send('/api/ping', { from: 'parent' });
1155
+ await iframeEndpoint.send('/notify', { from: 'iframe' });
1156
+ ```
1157
+
1158
+ 生产环境推荐配置(模板):
1159
+
1160
+ ```typescript
1161
+ import { requestIframeEndpoint, LogLevel } from 'request-iframe';
1162
+
1163
+ const secretKey = 'my-app';
1164
+ const iframe = document.querySelector('iframe')!;
1165
+ const targetOrigin = new URL(iframe.src).origin;
1166
+
1167
+ const endpoint = requestIframeEndpoint(iframe, {
1168
+ secretKey,
1169
+ targetOrigin,
1170
+ allowedOrigins: [targetOrigin],
1171
+ // 防止异常/攻击导致消息爆炸(按需设置)
1172
+ maxConcurrentRequestsPerClient: 50,
1173
+ // 日志:默认只输出 warn/error;调试时可切到 LogLevel.INFO / LogLevel.TRACE
1174
+ trace: LogLevel.WARN
1175
+ });
1176
+ ```
1177
+
923
1178
  ### Client API
924
1179
 
925
1180
  #### client.send(path, body?, options?)
@@ -1520,7 +1775,10 @@ import {
1520
1775
  // 多语言消息
1521
1776
  Messages,
1522
1777
  setMessages,
1523
- formatMessage
1778
+ formatMessage,
1779
+
1780
+ // 日志等级
1781
+ LogLevel
1524
1782
  } from 'request-iframe';
1525
1783
  ```
1526
1784
 
@@ -1611,7 +1869,11 @@ const server = requestIframeServer();
1611
1869
 
1612
1870
  ### 4. Server 可以主动推送消息吗?
1613
1871
 
1614
- request-iframe 是请求-响应模式,Server 不能主动推送。如需双向通信,可以让 iframe 内也创建 Client:
1872
+ request-iframe 是请求-响应模式,Server 本身不能“主动推送”。
1873
+
1874
+ 如需双向通信,有两种做法:
1875
+ - iframe 内创建一个反向的 Client(传统做法)
1876
+ - 双方都使用 `requestIframeEndpoint()`(推荐),一个对象同时具备 **send + handle**
1615
1877
 
1616
1878
  ```typescript
1617
1879
  // iframe 内
@@ -1624,10 +1886,11 @@ await client.send('/notify', { event: 'data-changed' });
1624
1886
 
1625
1887
  ### 5. 如何调试通信问题?
1626
1888
 
1627
- 1. **开启 trace 模式**:查看详细的通信日志
1628
- 2. **检查 secretKey**:确保 Client 和 Server 使用相同的 secretKey
1889
+ 1. **按日志等级开启输出**:默认只输出 warn/error;建议设置 `trace: LogLevel.INFO`(或 `trace: true`)来输出更详细的通信日志
1890
+ 2. **检查 secretKey**:确保双方使用相同的 `secretKey`
1629
1891
  3. **检查 iframe 加载**:确保 iframe 已完全加载
1630
- 4. **检查控制台**:查看是否有跨域错误
1892
+ 4. **检查 origin 约束**:尽量设置严格的 `targetOrigin`,并配置 `allowedOrigins` / `validateOrigin`,避免因为校验失败导致消息被忽略
1893
+ 5. **考虑使用 `requestIframeEndpoint()`**:把双向(send + handle)能力合在一个对象里,更容易串起完整链路做排查
1631
1894
 
1632
1895
  ### 6. 支持哪些浏览器?
1633
1896
 
@@ -1681,27 +1944,10 @@ client.interceptors.response.use(
1681
1944
  );
1682
1945
  ```
1683
1946
 
1684
- ### 9. 如何调试通信问题?
1685
-
1686
- 1. **开启 trace 模式**:在创建 client/server 时设置 `trace: true`
1687
- 2. **检查控制台**:查看详细的通信日志
1688
- 3. **验证 secretKey**:确保 client 和 server 使用相同的 secretKey
1689
- 4. **检查 iframe 加载**:确保 iframe 已完全加载后再发送请求
1690
- 5. **使用 `isConnect()`**:先检测连接是否正常
1691
-
1692
- ```typescript
1693
- // 开启调试模式
1694
- const client = requestIframeClient(iframe, {
1695
- secretKey: 'my-app',
1696
- trace: true // 开启详细日志
1697
- });
1947
+ ### 9. trace/日志等级怎么用?
1698
1948
 
1699
- // 检测连接
1700
- const connected = await client.isConnect();
1701
- if (!connected) {
1702
- console.error('无法连接到 iframe');
1703
- }
1704
- ```
1949
+ - 推荐优先使用常量:`trace: LogLevel.INFO` / `trace: LogLevel.TRACE`
1950
+ - 如果你在做双向排查,推荐使用 `requestIframeEndpoint()` 并把 trace 打开(这样 send/handle 都在同一对象上更直观)
1705
1951
 
1706
1952
  ### 10. 性能如何?
1707
1953
 
@@ -1775,14 +2021,16 @@ yarn build
1775
2021
  request-iframe/
1776
2022
  ├── src/
1777
2023
  │ ├── api/ # 对外 API(client.ts, server.ts)
1778
- │ ├── core/ # 核心实现(client, server, request, response)
2024
+ │ ├── impl/ # 实现层(client, server, request, response)
2025
+ │ ├── endpoint/ # endpoint 基础设施(hub/inbox/outbox + stream/heartbeat 等)
1779
2026
  │ ├── message/ # 消息通信层(channel, dispatcher)
1780
2027
  │ ├── stream/ # 流式传输实现
1781
2028
  │ ├── interceptors/ # 拦截器实现
1782
2029
  │ ├── utils/ # 工具函数
1783
2030
  │ ├── constants/ # 常量定义
1784
2031
  │ ├── types/ # TypeScript 类型定义
1785
- │ └── __tests__/ # 测试文件
2032
+ ├── __tests__/ # 测试文件(Jest)
2033
+ ├── react/__tests__/ # React hooks 测试
1786
2034
  ├── library/ # 构建输出
1787
2035
  ├── coverage/ # 测试覆盖率报告
1788
2036
  ├── jest.config.js # Jest 配置