Skip to content

Commit d990045

Browse files
authored
♻️ Offscreen 与 ServiceWorker 通信统一走 postMessage 通道 (#1299)
* 🐛 Offscreen→SW 消息通道改用 postMessage 支持 Blob 传输 Offscreen 转发 GM API 请求到 ServiceWorker 时,原先走 ExtensionMessage (chrome.runtime, JSON 序列化),导致 Blob 等结构化数据丢失。 改为通过 postMessage 通道(结构化克隆)双向传输,所有 GM API 自动受益。 * ♻️ 移除 Offscreen 对 ExtensionMessage 的依赖 所有 Offscreen→SW 通信统一走 ServiceWorkerClientMessage(postMessage), 不再需要 ExtensionMessage(chrome.runtime)。 * 🐛 修复 offscreen 页面 navigator.serviceWorker.controller 为 null 的问题 扩展 offscreen 页面的 controller 通常为 null,改用 navigator.serviceWorker.ready 获取 registration.active 作为 SW 引用。 * ✅ 补齐 ServiceWorkerMessageSend sender 参数并添加单元测试 修复 messageHandle 处理 sendMessage 时未传 sender 导致 SenderRuntime 访问 undefined 属性崩溃的问题,传入空对象表示后台脚本来源。 新增 11 个单元测试覆盖双向 postMessage 通道的核心链路。
1 parent f9dd5ea commit d990045

6 files changed

Lines changed: 444 additions & 35 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2+
import { ServiceWorkerMessageSend, ServiceWorkerClientMessage, type WindowMessageBody } from "./window_message";
3+
import { Server } from "./server";
4+
import type { MessageConnect } from "./types";
5+
6+
// 模拟 SW 的 postMessage
7+
let swPostMessageMock: ReturnType<typeof vi.fn>;
8+
// 捕获 self.addEventListener("message") 注册的 handler
9+
let swMessageHandler: ((e: any) => void) | null;
10+
// 捕获 navigator.serviceWorker.addEventListener("message") 注册的 handler
11+
let clientMessageHandler: ((e: any) => void) | null;
12+
13+
// 需要在每次测试前设置好 mock,因为构造函数中会访问这些全局对象
14+
beforeEach(() => {
15+
swMessageHandler = null;
16+
clientMessageHandler = null;
17+
swPostMessageMock = vi.fn();
18+
19+
vi.spyOn(self, "addEventListener").mockImplementation(((event: string, handler: any) => {
20+
if (event === "message") swMessageHandler = handler;
21+
}) as any);
22+
23+
(self as any).clients = {
24+
matchAll: vi.fn().mockResolvedValue([]),
25+
};
26+
27+
Object.defineProperty(navigator, "serviceWorker", {
28+
value: {
29+
addEventListener: vi.fn((event: string, handler: any) => {
30+
if (event === "message") clientMessageHandler = handler;
31+
}),
32+
controller: { postMessage: swPostMessageMock },
33+
ready: Promise.resolve({ active: { postMessage: swPostMessageMock } }),
34+
},
35+
configurable: true,
36+
});
37+
});
38+
39+
afterEach(() => {
40+
vi.restoreAllMocks();
41+
delete (self as any).clients;
42+
});
43+
44+
describe("ServiceWorkerMessageSend", () => {
45+
describe("messageHandle 处理来自 Offscreen 的请求", () => {
46+
it("处理 sendMessage 类型,调用 onMessage 回调并发送响应", () => {
47+
const swSend = new ServiceWorkerMessageSend();
48+
49+
const handler = vi.fn((..._args: any[]) => {
50+
const sendResponse = _args[1] as (data: any) => void;
51+
sendResponse({ code: 0, data: "pong" });
52+
});
53+
swSend.onMessage(handler);
54+
55+
const source = { postMessage: vi.fn() };
56+
57+
swSend.messageHandle({ messageId: "msg-1", type: "sendMessage", data: { action: "test", data: "ping" } }, source);
58+
59+
// 验证传了3个参数: data, sendResponse, sender(空对象)
60+
expect(handler).toHaveBeenCalledWith({ action: "test", data: "ping" }, expect.any(Function), expect.any(Object));
61+
// sender 应该是空对象,经过 SenderRuntime.getExtMessageSender() 后得到 tabId=-1 等值
62+
const sender = handler.mock.calls[0]![2];
63+
expect(sender).toEqual({});
64+
expect(source.postMessage).toHaveBeenCalledWith({
65+
messageId: "msg-1",
66+
type: "respMessage",
67+
data: { code: 0, data: "pong" },
68+
});
69+
});
70+
71+
it("处理 connect 类型,调用 onConnect 回调并创建 WindowMessageConnect", () => {
72+
const swSend = new ServiceWorkerMessageSend();
73+
74+
const connectHandler = vi.fn();
75+
swSend.onConnect(connectHandler);
76+
77+
const source = { postMessage: vi.fn() };
78+
79+
swSend.messageHandle({ messageId: "conn-1", type: "connect", data: { action: "test/connect" } }, source);
80+
81+
expect(connectHandler).toHaveBeenCalledWith(
82+
{ action: "test/connect" },
83+
expect.objectContaining({
84+
sendMessage: expect.any(Function),
85+
onMessage: expect.any(Function),
86+
disconnect: expect.any(Function),
87+
onDisconnect: expect.any(Function),
88+
})
89+
);
90+
});
91+
92+
it("没有 source 时忽略 sendMessage 和 connect", () => {
93+
const swSend = new ServiceWorkerMessageSend();
94+
95+
const msgHandler = vi.fn();
96+
const conHandler = vi.fn();
97+
swSend.onMessage(msgHandler);
98+
swSend.onConnect(conHandler);
99+
100+
// 无 source
101+
swSend.messageHandle({ messageId: "x", type: "sendMessage", data: {} });
102+
swSend.messageHandle({ messageId: "y", type: "connect", data: {} });
103+
104+
expect(msgHandler).not.toHaveBeenCalled();
105+
expect(conHandler).not.toHaveBeenCalled();
106+
});
107+
108+
it("仍然正常处理 respMessage / disconnect / connectMessage", () => {
109+
const swSend = new ServiceWorkerMessageSend();
110+
111+
const respHandler = vi.fn();
112+
const disconnectHandler = vi.fn();
113+
const connMsgHandler = vi.fn();
114+
115+
swSend.EE.addListener("response:resp-1", respHandler);
116+
swSend.EE.addListener("disconnect:disc-1", disconnectHandler);
117+
swSend.EE.addListener("connectMessage:cm-1", connMsgHandler);
118+
119+
swSend.messageHandle({ messageId: "resp-1", type: "respMessage", data: "r" });
120+
swSend.messageHandle({ messageId: "disc-1", type: "disconnect", data: null });
121+
swSend.messageHandle({ messageId: "cm-1", type: "connectMessage", data: "m" });
122+
123+
expect(respHandler).toHaveBeenCalled();
124+
expect(disconnectHandler).toHaveBeenCalled();
125+
expect(connMsgHandler).toHaveBeenCalledWith("m");
126+
});
127+
});
128+
});
129+
130+
describe("ServiceWorkerClientMessage", () => {
131+
it("controller 可用时直接使用", () => {
132+
const clientMsg = new ServiceWorkerClientMessage();
133+
134+
expect((clientMsg as any).sw).not.toBeNull();
135+
expect((clientMsg as any).sw.postMessage).toBe(swPostMessageMock);
136+
});
137+
138+
it("controller 为 null 时通过 ready 获取 active SW", async () => {
139+
const readyPostMessage = vi.fn();
140+
Object.defineProperty(navigator, "serviceWorker", {
141+
value: {
142+
addEventListener: vi.fn((event: string, handler: any) => {
143+
if (event === "message") clientMessageHandler = handler;
144+
}),
145+
controller: null,
146+
ready: Promise.resolve({ active: { postMessage: readyPostMessage } }),
147+
},
148+
configurable: true,
149+
});
150+
151+
const clientMsg = new ServiceWorkerClientMessage();
152+
153+
expect((clientMsg as any).sw).toBeNull();
154+
155+
// 等待 ready resolve
156+
await new Promise((r) => setTimeout(r, 0));
157+
158+
expect((clientMsg as any).sw).not.toBeNull();
159+
expect((clientMsg as any).sw.postMessage).toBe(readyPostMessage);
160+
});
161+
});
162+
163+
describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", () => {
164+
// 辅助函数: 将两端连接起来,模拟 postMessage 通道
165+
function createWiredPair() {
166+
const swSend = new ServiceWorkerMessageSend();
167+
const clientMsg = new ServiceWorkerClientMessage();
168+
169+
// 模拟 offscreen client(SW 发送给 offscreen 时的 target)
170+
const offscreenPostMessage = vi.fn((data: WindowMessageBody) => {
171+
// SW → Offscreen: 投递到 clientMsg 的 messageHandle
172+
clientMessageHandler?.({ data });
173+
});
174+
175+
// client.postToServiceWorker → 投递到 SW 的 messageHandle
176+
swPostMessageMock.mockImplementation((data: WindowMessageBody) => {
177+
const source = { postMessage: offscreenPostMessage };
178+
swMessageHandler?.({ data, source } as any);
179+
});
180+
181+
return { swSend, clientMsg };
182+
}
183+
184+
it("sendMessage: client→SW 请求并收到响应", async () => {
185+
const { swSend, clientMsg } = createWiredPair();
186+
187+
// SW 端注册处理器
188+
swSend.onMessage((msg: any, sendResponse: any) => {
189+
sendResponse({ code: 0, data: (msg.data as string) + " world" });
190+
return true;
191+
});
192+
193+
const result = await clientMsg.sendMessage({ action: "test/echo", data: "hello" });
194+
expect(result).toEqual({ code: 0, data: "hello world" });
195+
});
196+
197+
it("connect: 建立连接后双向通信", async () => {
198+
const { swSend, clientMsg } = createWiredPair();
199+
200+
const serverReceived: any[] = [];
201+
202+
// SW 端处理 connect
203+
swSend.onConnect((_msg: any, con: MessageConnect) => {
204+
con.onMessage((data: any) => {
205+
serverReceived.push(data);
206+
// 回复
207+
con.sendMessage({ action: "reply", data: "got: " + data.data });
208+
});
209+
});
210+
211+
// Client 端建立连接
212+
const con = await clientMsg.connect({ action: "test/stream", data: "init" });
213+
214+
const clientReceived: any[] = [];
215+
con.onMessage((data: any) => {
216+
clientReceived.push(data);
217+
});
218+
219+
// Client → SW
220+
con.sendMessage({ action: "msg1", data: "ping" });
221+
222+
// 等待异步消息传递
223+
await new Promise((r) => setTimeout(r, 10));
224+
225+
expect(serverReceived).toHaveLength(1);
226+
expect(serverReceived[0]).toEqual({ action: "msg1", data: "ping" });
227+
228+
expect(clientReceived).toHaveLength(1);
229+
expect(clientReceived[0]).toEqual({ action: "reply", data: "got: ping" });
230+
});
231+
232+
it("connect: disconnect 正确清理", async () => {
233+
const { swSend, clientMsg } = createWiredPair();
234+
235+
let serverDisconnected = false;
236+
237+
swSend.onConnect((_msg: any, con: MessageConnect) => {
238+
con.onDisconnect(() => {
239+
serverDisconnected = true;
240+
});
241+
});
242+
243+
const con = await clientMsg.connect({ action: "test/disconnect" });
244+
245+
con.disconnect();
246+
247+
await new Promise((r) => setTimeout(r, 10));
248+
249+
expect(serverDisconnected).toBe(true);
250+
});
251+
252+
it("sendMessage: 支持传输复杂对象(模拟结构化克隆场景)", async () => {
253+
const { swSend, clientMsg } = createWiredPair();
254+
255+
swSend.onMessage((msg: any, sendResponse: any) => {
256+
// 原样返回,验证数据完整性
257+
sendResponse({ code: 0, data: msg.data });
258+
return true;
259+
});
260+
261+
const complexData = {
262+
array: [1, 2, 3],
263+
nested: { a: { b: "deep" } },
264+
nullVal: null,
265+
boolVal: true,
266+
};
267+
268+
const result = await clientMsg.sendMessage({ action: "test/complex", data: complexData });
269+
expect((result as any).data).toEqual(complexData);
270+
});
271+
272+
it("与 Server 集成: forwardMessage 路径", async () => {
273+
const swSend = new ServiceWorkerMessageSend();
274+
const clientMsg = new ServiceWorkerClientMessage();
275+
276+
// Wire
277+
const offscreenPostMessage = vi.fn((data: WindowMessageBody) => {
278+
clientMessageHandler?.({ data });
279+
});
280+
swPostMessageMock.mockImplementation((data: WindowMessageBody) => {
281+
swMessageHandler?.({ data, source: { postMessage: offscreenPostMessage } } as any);
282+
});
283+
284+
// 用 ServiceWorkerMessageSend 作为 Server 的消息源
285+
const server = new Server("serviceWorker", swSend);
286+
server.on("runtime/gmApi/test", async (params: any) => {
287+
return { result: params.value * 2 };
288+
});
289+
290+
// Client 通过 sendMessage 调用 Server 的 API
291+
const resp = await clientMsg.sendMessage({ action: "serviceWorker/runtime/gmApi/test", data: { value: 21 } });
292+
293+
expect((resp as any).code).toBe(0);
294+
expect((resp as any).data).toEqual({ result: 42 });
295+
});
296+
});

0 commit comments

Comments
 (0)