diff --git a/packages/message/window_message.test.ts b/packages/message/window_message.test.ts new file mode 100644 index 000000000..fcfed77f5 --- /dev/null +++ b/packages/message/window_message.test.ts @@ -0,0 +1,296 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { ServiceWorkerMessageSend, ServiceWorkerClientMessage, type WindowMessageBody } from "./window_message"; +import { Server } from "./server"; +import type { MessageConnect } from "./types"; + +// 模拟 SW 的 postMessage +let swPostMessageMock: ReturnType; +// 捕获 self.addEventListener("message") 注册的 handler +let swMessageHandler: ((e: any) => void) | null; +// 捕获 navigator.serviceWorker.addEventListener("message") 注册的 handler +let clientMessageHandler: ((e: any) => void) | null; + +// 需要在每次测试前设置好 mock,因为构造函数中会访问这些全局对象 +beforeEach(() => { + swMessageHandler = null; + clientMessageHandler = null; + swPostMessageMock = vi.fn(); + + vi.spyOn(self, "addEventListener").mockImplementation(((event: string, handler: any) => { + if (event === "message") swMessageHandler = handler; + }) as any); + + (self as any).clients = { + matchAll: vi.fn().mockResolvedValue([]), + }; + + Object.defineProperty(navigator, "serviceWorker", { + value: { + addEventListener: vi.fn((event: string, handler: any) => { + if (event === "message") clientMessageHandler = handler; + }), + controller: { postMessage: swPostMessageMock }, + ready: Promise.resolve({ active: { postMessage: swPostMessageMock } }), + }, + configurable: true, + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); + delete (self as any).clients; +}); + +describe("ServiceWorkerMessageSend", () => { + describe("messageHandle 处理来自 Offscreen 的请求", () => { + it("处理 sendMessage 类型,调用 onMessage 回调并发送响应", () => { + const swSend = new ServiceWorkerMessageSend(); + + const handler = vi.fn((..._args: any[]) => { + const sendResponse = _args[1] as (data: any) => void; + sendResponse({ code: 0, data: "pong" }); + }); + swSend.onMessage(handler); + + const source = { postMessage: vi.fn() }; + + swSend.messageHandle({ messageId: "msg-1", type: "sendMessage", data: { action: "test", data: "ping" } }, source); + + // 验证传了3个参数: data, sendResponse, sender(空对象) + expect(handler).toHaveBeenCalledWith({ action: "test", data: "ping" }, expect.any(Function), expect.any(Object)); + // sender 应该是空对象,经过 SenderRuntime.getExtMessageSender() 后得到 tabId=-1 等值 + const sender = handler.mock.calls[0]![2]; + expect(sender).toEqual({}); + expect(source.postMessage).toHaveBeenCalledWith({ + messageId: "msg-1", + type: "respMessage", + data: { code: 0, data: "pong" }, + }); + }); + + it("处理 connect 类型,调用 onConnect 回调并创建 WindowMessageConnect", () => { + const swSend = new ServiceWorkerMessageSend(); + + const connectHandler = vi.fn(); + swSend.onConnect(connectHandler); + + const source = { postMessage: vi.fn() }; + + swSend.messageHandle({ messageId: "conn-1", type: "connect", data: { action: "test/connect" } }, source); + + expect(connectHandler).toHaveBeenCalledWith( + { action: "test/connect" }, + expect.objectContaining({ + sendMessage: expect.any(Function), + onMessage: expect.any(Function), + disconnect: expect.any(Function), + onDisconnect: expect.any(Function), + }) + ); + }); + + it("没有 source 时忽略 sendMessage 和 connect", () => { + const swSend = new ServiceWorkerMessageSend(); + + const msgHandler = vi.fn(); + const conHandler = vi.fn(); + swSend.onMessage(msgHandler); + swSend.onConnect(conHandler); + + // 无 source + swSend.messageHandle({ messageId: "x", type: "sendMessage", data: {} }); + swSend.messageHandle({ messageId: "y", type: "connect", data: {} }); + + expect(msgHandler).not.toHaveBeenCalled(); + expect(conHandler).not.toHaveBeenCalled(); + }); + + it("仍然正常处理 respMessage / disconnect / connectMessage", () => { + const swSend = new ServiceWorkerMessageSend(); + + const respHandler = vi.fn(); + const disconnectHandler = vi.fn(); + const connMsgHandler = vi.fn(); + + swSend.EE.addListener("response:resp-1", respHandler); + swSend.EE.addListener("disconnect:disc-1", disconnectHandler); + swSend.EE.addListener("connectMessage:cm-1", connMsgHandler); + + swSend.messageHandle({ messageId: "resp-1", type: "respMessage", data: "r" }); + swSend.messageHandle({ messageId: "disc-1", type: "disconnect", data: null }); + swSend.messageHandle({ messageId: "cm-1", type: "connectMessage", data: "m" }); + + expect(respHandler).toHaveBeenCalled(); + expect(disconnectHandler).toHaveBeenCalled(); + expect(connMsgHandler).toHaveBeenCalledWith("m"); + }); + }); +}); + +describe("ServiceWorkerClientMessage", () => { + it("controller 可用时直接使用", () => { + const clientMsg = new ServiceWorkerClientMessage(); + + expect((clientMsg as any).sw).not.toBeNull(); + expect((clientMsg as any).sw.postMessage).toBe(swPostMessageMock); + }); + + it("controller 为 null 时通过 ready 获取 active SW", async () => { + const readyPostMessage = vi.fn(); + Object.defineProperty(navigator, "serviceWorker", { + value: { + addEventListener: vi.fn((event: string, handler: any) => { + if (event === "message") clientMessageHandler = handler; + }), + controller: null, + ready: Promise.resolve({ active: { postMessage: readyPostMessage } }), + }, + configurable: true, + }); + + const clientMsg = new ServiceWorkerClientMessage(); + + expect((clientMsg as any).sw).toBeNull(); + + // 等待 ready resolve + await new Promise((r) => setTimeout(r, 0)); + + expect((clientMsg as any).sw).not.toBeNull(); + expect((clientMsg as any).sw.postMessage).toBe(readyPostMessage); + }); +}); + +describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", () => { + // 辅助函数: 将两端连接起来,模拟 postMessage 通道 + function createWiredPair() { + const swSend = new ServiceWorkerMessageSend(); + const clientMsg = new ServiceWorkerClientMessage(); + + // 模拟 offscreen client(SW 发送给 offscreen 时的 target) + const offscreenPostMessage = vi.fn((data: WindowMessageBody) => { + // SW → Offscreen: 投递到 clientMsg 的 messageHandle + clientMessageHandler?.({ data }); + }); + + // client.postToServiceWorker → 投递到 SW 的 messageHandle + swPostMessageMock.mockImplementation((data: WindowMessageBody) => { + const source = { postMessage: offscreenPostMessage }; + swMessageHandler?.({ data, source } as any); + }); + + return { swSend, clientMsg }; + } + + it("sendMessage: client→SW 请求并收到响应", async () => { + const { swSend, clientMsg } = createWiredPair(); + + // SW 端注册处理器 + swSend.onMessage((msg: any, sendResponse: any) => { + sendResponse({ code: 0, data: (msg.data as string) + " world" }); + return true; + }); + + const result = await clientMsg.sendMessage({ action: "test/echo", data: "hello" }); + expect(result).toEqual({ code: 0, data: "hello world" }); + }); + + it("connect: 建立连接后双向通信", async () => { + const { swSend, clientMsg } = createWiredPair(); + + const serverReceived: any[] = []; + + // SW 端处理 connect + swSend.onConnect((_msg: any, con: MessageConnect) => { + con.onMessage((data: any) => { + serverReceived.push(data); + // 回复 + con.sendMessage({ action: "reply", data: "got: " + data.data }); + }); + }); + + // Client 端建立连接 + const con = await clientMsg.connect({ action: "test/stream", data: "init" }); + + const clientReceived: any[] = []; + con.onMessage((data: any) => { + clientReceived.push(data); + }); + + // Client → SW + con.sendMessage({ action: "msg1", data: "ping" }); + + // 等待异步消息传递 + await new Promise((r) => setTimeout(r, 10)); + + expect(serverReceived).toHaveLength(1); + expect(serverReceived[0]).toEqual({ action: "msg1", data: "ping" }); + + expect(clientReceived).toHaveLength(1); + expect(clientReceived[0]).toEqual({ action: "reply", data: "got: ping" }); + }); + + it("connect: disconnect 正确清理", async () => { + const { swSend, clientMsg } = createWiredPair(); + + let serverDisconnected = false; + + swSend.onConnect((_msg: any, con: MessageConnect) => { + con.onDisconnect(() => { + serverDisconnected = true; + }); + }); + + const con = await clientMsg.connect({ action: "test/disconnect" }); + + con.disconnect(); + + await new Promise((r) => setTimeout(r, 10)); + + expect(serverDisconnected).toBe(true); + }); + + it("sendMessage: 支持传输复杂对象(模拟结构化克隆场景)", async () => { + const { swSend, clientMsg } = createWiredPair(); + + swSend.onMessage((msg: any, sendResponse: any) => { + // 原样返回,验证数据完整性 + sendResponse({ code: 0, data: msg.data }); + return true; + }); + + const complexData = { + array: [1, 2, 3], + nested: { a: { b: "deep" } }, + nullVal: null, + boolVal: true, + }; + + const result = await clientMsg.sendMessage({ action: "test/complex", data: complexData }); + expect((result as any).data).toEqual(complexData); + }); + + it("与 Server 集成: forwardMessage 路径", async () => { + const swSend = new ServiceWorkerMessageSend(); + const clientMsg = new ServiceWorkerClientMessage(); + + // Wire + const offscreenPostMessage = vi.fn((data: WindowMessageBody) => { + clientMessageHandler?.({ data }); + }); + swPostMessageMock.mockImplementation((data: WindowMessageBody) => { + swMessageHandler?.({ data, source: { postMessage: offscreenPostMessage } } as any); + }); + + // 用 ServiceWorkerMessageSend 作为 Server 的消息源 + const server = new Server("serviceWorker", swSend); + server.on("runtime/gmApi/test", async (params: any) => { + return { result: params.value * 2 }; + }); + + // Client 通过 sendMessage 调用 Server 的 API + const resp = await clientMsg.sendMessage({ action: "serviceWorker/runtime/gmApi/test", data: { value: 21 } }); + + expect((resp as any).code).toBe(0); + expect((resp as any).data).toEqual({ result: 42 }); + }); +}); diff --git a/packages/message/window_message.ts b/packages/message/window_message.ts index 9dee3bafe..44e89e863 100644 --- a/packages/message/window_message.ts +++ b/packages/message/window_message.ts @@ -1,4 +1,12 @@ -import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage } from "./types"; +import type { + Message, + MessageConnect, + MessageSend, + OnConnectCallback, + OnMessageCallback, + RuntimeMessageSender, + TMessage, +} from "./types"; import { uuidv4 } from "@App/pkg/utils/uuid"; import EventEmitter from "eventemitter3"; @@ -205,32 +213,53 @@ export class WindowMessageConnect implements MessageConnect { // service_worker和offscreen同时监听消息,会导致消息被两边同时接收,但是返回结果时会产生问题,导致报错 // 不进行监听的话又无法从service_worker主动发送消息 // 所以service_worker与offscreen使用ServiceWorker的方式进行通信 -export class ServiceWorkerMessageSend implements MessageSend { +// 现在同时支持接收来自offscreen的请求(实现完整Message接口),使双向通道都走postMessage(结构化克隆,支持Blob) +export class ServiceWorkerMessageSend implements Message { EE = new EventEmitter(); private target: PostMessage | undefined = undefined; - constructor() {} - - listened: boolean = false; + constructor() { + // 在构造函数中设置监听,确保能接收来自offscreen的请求 + self.addEventListener("message", (e: MessageEvent) => { + this.messageHandle(e.data, e.source as PostMessage); + }); + } async init() { if (!this.target && self.clients) { - if (!this.listened) { - this.listened = true; - self.addEventListener("message", (e) => { - this.messageHandle(e.data); - }); - } const list = await self.clients.matchAll({ includeUncontrolled: true, type: "window" }); // 找到offscreen.html窗口 this.target = list.find((client) => client.url == chrome.runtime.getURL("src/offscreen.html")) as PostMessage; } } - messageHandle(data: WindowMessageBody) { + messageHandle(data: WindowMessageBody, source?: PostMessage) { // 处理消息 - if (data.type === "respMessage") { + if (data.type === "sendMessage" && source) { + // 接收到来自offscreen的请求消息 + // 第三个参数传空对象作为sender,避免Server中SenderRuntime访问undefined属性 + // 空对象经过getExtMessageSender()会得到tabId=-1等值,表示后台脚本 + this.EE.emit( + "message", + data.data, + (resp: any) => { + if (!data.messageId) { + return; + } + const body: WindowMessageBody = { + messageId: data.messageId, + type: "respMessage", + data: resp, + }; + source.postMessage(body); + }, + {} as RuntimeMessageSender + ); + } else if (data.type === "connect" && source) { + // 接收到来自offscreen的连接请求 + this.EE.emit("connect", data.data, new WindowMessageConnect(data.messageId, this.EE, source)); + } else if (data.type === "respMessage") { // 接收到响应消息 this.EE.emit(`response:${data.messageId}`, data); } else if (data.type === "disconnect") { @@ -240,6 +269,14 @@ export class ServiceWorkerMessageSend implements MessageSend { } } + onMessage(callback: OnMessageCallback): void { + this.EE.addListener("message", callback); + } + + onConnect(callback: OnConnectCallback): void { + this.EE.addListener("connect", callback); + } + async connect(data: TMessage): Promise { await this.init(); const body: WindowMessageBody = { @@ -271,3 +308,81 @@ export class ServiceWorkerMessageSend implements MessageSend { }); } } + +// Offscreen端通过navigator.serviceWorker向SW发送postMessage消息 +// 与ServiceWorkerMessageSend配对使用,实现Offscreen→SW的postMessage通道 +// 注意: 扩展offscreen页面的navigator.serviceWorker.controller通常为null, +// 需要通过navigator.serviceWorker.ready获取registration.active +export class ServiceWorkerClientMessage implements MessageSend { + EE = new EventEmitter(); + + private sw: ServiceWorker | null = null; + private swReady: Promise; + + constructor() { + navigator.serviceWorker.addEventListener("message", (e) => { + this.messageHandle(e.data); + }); + // controller在扩展offscreen页面中通常为null,通过ready获取active + this.sw = navigator.serviceWorker.controller; + if (this.sw) { + this.swReady = Promise.resolve(this.sw); + } else { + this.swReady = navigator.serviceWorker.ready.then((reg) => { + this.sw = reg.active!; + return this.sw; + }); + } + } + + messageHandle(data: WindowMessageBody) { + // 只处理响应类消息,请求类消息由WindowMessage处理 + if (data.type === "respMessage") { + this.EE.emit(`response:${data.messageId}`, data); + } else if (data.type === "disconnect") { + this.EE.emit(`disconnect:${data.messageId}`); + } else if (data.type === "connectMessage") { + this.EE.emit(`connectMessage:${data.messageId}`, data.data); + } + } + + private postToServiceWorker(message: any) { + if (this.sw) { + this.sw.postMessage(message); + } else { + // 初始化期间还没获取到SW引用,等待ready后发送 + this.swReady.then((sw) => sw.postMessage(message)); + } + } + + async connect(data: TMessage): Promise { + const body: WindowMessageBody = { + messageId: uuidv4(), + type: "connect", + data, + }; + const target: PostMessage = { + postMessage: (msg) => this.postToServiceWorker(msg), + }; + this.postToServiceWorker(body); + return new WindowMessageConnect(body.messageId, this.EE, target); + } + + sendMessage(data: TMessage): Promise { + return new Promise((resolve: ((value: T) => void) | null) => { + const messageId = uuidv4(); + const body: WindowMessageBody = { + messageId, + type: "sendMessage", + data, + }; + const eventId = `response:${messageId}`; + this.EE.addListener(eventId, (body: WindowMessageBody) => { + this.EE.removeAllListeners(eventId); + resolve!(body.data as T); + resolve = null; + }); + this.postToServiceWorker(body); + }); + } +} diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index a3a22328d..9dde44955 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -3,7 +3,6 @@ import type { MessageSend } from "@Packages/message/types"; import { ScriptService } from "./script"; import { type Logger } from "@App/app/repo/logger"; import { WindowMessage } from "@Packages/message/window_message"; -import { ServiceWorkerClient } from "../service_worker/client"; import { sendMessage } from "@Packages/message/client"; import GMApi from "./gm_api"; import { MessageQueue } from "@Packages/message/message_queue"; @@ -18,12 +17,9 @@ export class OffscreenManager { private messageQueue = new MessageQueue(); - private serviceWorker: ServiceWorkerClient; - - constructor(private extMsgSender: MessageSend) { + constructor(private msgSender: MessageSend) { this.windowMessage = new WindowMessage(window, sandbox, true); this.windowServer = new Server("offscreen", this.windowMessage); - this.serviceWorker = new ServiceWorkerClient(this.extMsgSender); } logger(data: Logger) { @@ -36,11 +32,11 @@ export class OffscreenManager { preparationSandbox() { // 通知初始化好环境了 - this.serviceWorker.preparationOffscreen(); + sendMessage(this.msgSender, "serviceWorker/preparationOffscreen"); } sendMessageToServiceWorker(data: { action: string; data: any }) { - return sendMessage(this.extMsgSender, `serviceWorker/${data.action}`, data.data); + return sendMessage(this.msgSender, `serviceWorker/${data.action}`, data.data); } async initManager() { @@ -50,20 +46,20 @@ export class OffscreenManager { this.windowServer.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this)); const script = new ScriptService( this.windowServer.group("script"), - this.extMsgSender, + this.msgSender, this.windowMessage, this.messageQueue ); script.init(); - // 转发从sandbox来的gm api请求 - forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extMsgSender); + // 转发从sandbox来的gm api请求,通过postMessage通道传输(支持Blob等结构化克隆) + forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.msgSender); // 转发valueUpdate与emitEvent forwardMessage("sandbox", "runtime/valueUpdate", this.windowServer, this.windowMessage); forwardMessage("sandbox", "runtime/emitEvent", this.windowServer, this.windowMessage); const gmApi = new GMApi(this.windowServer.group("gmApi")); gmApi.init(); - const vscodeConnect = new VSCodeConnect(this.windowServer.group("vscodeConnect"), this.extMsgSender); + const vscodeConnect = new VSCodeConnect(this.windowServer.group("vscodeConnect"), this.msgSender); vscodeConnect.init(); this.windowServer.on("createObjectURL", async (params: { blob: Blob; persistence: boolean }) => { diff --git a/src/app/service/offscreen/script.ts b/src/app/service/offscreen/script.ts index 9e52a6ff8..737c45bec 100644 --- a/src/app/service/offscreen/script.ts +++ b/src/app/service/offscreen/script.ts @@ -24,14 +24,14 @@ export class ScriptService { constructor( private group: Group, - private extMsgSender: MessageSend, + private msgSender: MessageSend, private windowMessage: WindowMessage, private messageQueue: IMessageQueue ) { this.logger = LoggerCore.logger().with({ service: "script" }); - this.scriptClient = new ScriptClient(this.extMsgSender); - this.resourceClient = new ResourceClient(this.extMsgSender); - this.valueClient = new ValueClient(this.extMsgSender); + this.scriptClient = new ScriptClient(this.msgSender); + this.resourceClient = new ResourceClient(this.msgSender); + this.valueClient = new ValueClient(this.msgSender); } runScript(script: ScriptRunResource) { diff --git a/src/offscreen.ts b/src/offscreen.ts index dffec141c..9cc1cb42c 100644 --- a/src/offscreen.ts +++ b/src/offscreen.ts @@ -1,19 +1,19 @@ -import type { Message } from "@Packages/message/types"; import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { OffscreenManager } from "./app/service/offscreen"; -import { ExtensionMessage } from "@Packages/message/extension_message"; +import { ServiceWorkerClientMessage } from "@Packages/message/window_message"; function main() { + // 通过postMessage与SW通信,支持结构化克隆(Blob等) + const swPostMessage = new ServiceWorkerClientMessage(); // 初始化日志组件 - const extMsgSender: Message = new ExtensionMessage(); const loggerCore = new LoggerCore({ - writer: new MessageWriter(extMsgSender, "serviceWorker/logger"), + writer: new MessageWriter(swPostMessage, "serviceWorker/logger"), labels: { env: "offscreen" }, }); loggerCore.logger().debug("offscreen start"); // 初始化管理器 - const manager = new OffscreenManager(extMsgSender); + const manager = new OffscreenManager(swPostMessage); manager.initManager(); } diff --git a/src/service_worker.ts b/src/service_worker.ts index 62ff4d1c4..407a27922 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -69,9 +69,11 @@ function main() { labels: { env: "service_worker" }, }); loggerCore.logger().debug("service worker start"); - const server = new Server("serviceWorker", message); + const swMessage = new ServiceWorkerMessageSend(); + // 同时接收ExtensionMessage(chrome.runtime)和ServiceWorkerMessageSend(postMessage)的消息 + const server = new Server("serviceWorker", [message, swMessage]); const messageQueue = new MessageQueue(); - const manager = new ServiceWorkerManager(server, messageQueue, new ServiceWorkerMessageSend()); + const manager = new ServiceWorkerManager(server, messageQueue, swMessage); manager.initManager(); // 初始化沙盒环境 setupOffscreenDocument();