Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/message/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const ScriptEnvTag = {
inject: "it",
content: "ct",
} as const;

export type ScriptEnvTag = ValueOf<typeof ScriptEnvTag>;

export const ScriptEnvType = {
inject: 1,
content: 2,
} as const;

export type ScriptEnvType = ValueOf<typeof ScriptEnvType>;

// 避免页面载入后改动全域物件导致消息传递失败
export const MouseEventClone = MouseEvent;
export const CustomEventClone = CustomEvent;
const performanceClone = process.env.VI_TESTING === "true" ? window : performance;

// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone);
export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone);
export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone);
const detailClone = typeof cloneInto === "function" ? cloneInto : null;
export const pageDispatchCustomEvent = (eventType: string, detail: any) => {
if (detailClone && detail) detail = detailClone(detail, performanceClone);
const ev = new CustomEventClone(eventType, {
detail,
cancelable: true,
});
return pageDispatchEvent(ev);
};
128 changes: 76 additions & 52 deletions packages/message/custom_event_message.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { Message, MessageConnect, RuntimeMessageSender, TMessage } from "./types";
import { v4 as uuidv4 } from "uuid";
import { uuidv4 } from "@App/pkg/utils/uuid";
import { type PostMessage, type WindowMessageBody, WindowMessageConnect } from "./window_message";
import LoggerCore from "@App/app/logger/core";
import EventEmitter from "eventemitter3";
import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts";

// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
const pageDispatchEvent = performance.dispatchEvent.bind(performance);
const pageAddEventListener = performance.addEventListener.bind(performance);

// 避免页面载入后改动全域物件导致消息传递失败
const MouseEventClone = MouseEvent;
const CustomEventClone = CustomEvent;
import {
pageDispatchEvent,
pageAddEventListener,
pageRemoveEventListener,
pageDispatchCustomEvent,
MouseEventClone,
CustomEventClone,
} from "@Packages/message/common";

// 避免页面载入后改动 Map.prototype 导致消息传递失败
const relatedTargetMap = new Map<number, EventTarget>();
Expand All @@ -30,28 +29,59 @@ export class CustomEventPostMessage implements PostMessage {
}
}

export type PageMessaging = {
et: string;
bindReceiver?: () => void;
waitReady?: Promise<void>;
waitReadyResolve?: () => any;
onReady?: (callback: () => any) => any;
};

export const createPageMessaging = (et: string) => {
const pageMessaging = { et } as PageMessaging;
pageMessaging.waitReady = new Promise<void>((resolve) => {
pageMessaging.waitReadyResolve = resolve;
});
pageMessaging.onReady = (callback: () => any) => {
if (pageMessaging.et) {
callback();
} else {
pageMessaging.waitReady!.then(callback);
}
};
return pageMessaging;
};

// 使用CustomEvent来进行通讯, 可以在content与inject中传递一些dom对象
export class CustomEventMessage implements Message {
EE = new EventEmitter<string, any>();
readonly receiveFlag: string;
readonly sendFlag: string;
readonly pageMessagingHandler: (event: Event) => any;

// 关联dom目标
relatedTarget: Map<number, EventTarget> = new Map();

constructor(
messageFlag: string,
protected readonly isContent: boolean
private pageMessaging: PageMessaging,
protected readonly isInbound: boolean
) {
this.receiveFlag = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`;
this.sendFlag = `evt${messageFlag}${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`;
pageAddEventListener(this.receiveFlag, (event) => {
this.receiveFlag = `${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`;
this.sendFlag = `${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`;
this.pageMessagingHandler = (event: Event) => {
if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) {
relatedTargetMap.set(event.movementX, event.relatedTarget!);
relatedTargetMap.set(event.movementX, event.relatedTarget);
} else if (event instanceof CustomEventClone) {
this.messageHandle(event.detail, new CustomEventPostMessage(this));
}
});
};
}

bindReceiver() {
if (!this.pageMessaging.et) throw new Error("bindReceiver() failed");
const receiveFlag = `evt_${this.pageMessaging.et}_${this.receiveFlag}`;
pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重复
pageAddEventListener(receiveFlag, this.pageMessagingHandler);
}

messageHandle(data: WindowMessageBody, target: PostMessage) {
Expand Down Expand Up @@ -95,56 +125,49 @@ export class CustomEventMessage implements Message {

connect(data: TMessage): Promise<MessageConnect> {
return new Promise((resolve) => {
const body: WindowMessageBody<TMessage> = {
messageId: uuidv4(),
type: "connect",
data,
};
this.nativeSend(body);
// EventEmitter3 采用同步事件设计,callback会被马上执行而不像传统javascript架构以下一个macrotask 执行
resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this)));
this.pageMessaging.onReady!(() => {
const body: WindowMessageBody<TMessage> = {
messageId: uuidv4(),
type: "connect",
data,
};
this.nativeSend(body);
// EventEmitter3 采用同步事件设计,callback会被马上执行而不像传统javascript架构以下一个macrotask 执行
resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this)));
});
});
}

nativeSend(detail: any) {
if (typeof cloneInto !== "undefined") {
try {
LoggerCore.logger().info("nativeSend");
detail = cloneInto(detail, document.defaultView);
} catch (e) {
console.log(e);
LoggerCore.logger().info("error data");
}
}

const ev = new CustomEventClone(this.sendFlag, {
detail,
});
pageDispatchEvent(ev);
if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed.");
pageDispatchCustomEvent(`evt_${this.pageMessaging.et}_${this.sendFlag}`, detail);
}

sendMessage<T = any>(data: TMessage): Promise<T> {
return new Promise((resolve: ((value: T) => void) | null) => {
const messageId = uuidv4();
const body: WindowMessageBody<TMessage> = {
messageId,
type: "sendMessage",
data,
};
const eventId = `response:${messageId}`;
this.EE.addListener(eventId, (body: WindowMessageBody<TMessage>) => {
this.EE.removeAllListeners(eventId);
resolve!(body.data as T);
resolve = null; // 设为 null 提醒JS引擎可以GC
this.pageMessaging.onReady!(() => {
const messageId = uuidv4();
const body: WindowMessageBody<TMessage> = {
messageId,
type: "sendMessage",
data,
};
const eventId = `response:${messageId}`;
this.EE.addListener(eventId, (body: WindowMessageBody<TMessage>) => {
this.EE.removeAllListeners(eventId);
resolve!(body.data as T);
resolve = null; // 设为 null 提醒JS引擎可以GC
});
this.nativeSend(body);
});
this.nativeSend(body);
});
}

// 同步发送消息
// 与content页的消息通讯实际是同步,此方法不需要经过background
// 但是请注意中间不要有promise
syncSendMessage(data: TMessage): TMessage {
if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed.");
const messageId = uuidv4();
const body: WindowMessageBody<TMessage> = {
messageId,
Expand All @@ -164,11 +187,12 @@ export class CustomEventMessage implements Message {
}

sendRelatedTarget(target: EventTarget): number {
if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed.");
// 特殊处理relatedTarget,返回id进行关联
// 先将relatedTarget转换成id发送过去
const id = (relateId = relateId === maxInteger ? 1 : relateId + 1);
// 可以使用此种方式交互element
const ev = new MouseEventClone(this.sendFlag, {
const ev = new MouseEventClone(`evt_${this.pageMessaging.et}_${this.sendFlag}`, {
movementX: id,
relatedTarget: target,
});
Expand Down
46 changes: 25 additions & 21 deletions packages/message/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { describe, expect, it, beforeEach, vi, afterEach } from "vitest";
import { GetSenderType, SenderConnect, SenderRuntime, Server, type IGetSender } from "./server";
import { CustomEventMessage } from "./custom_event_message";
import { createPageMessaging, CustomEventMessage } from "./custom_event_message";
import type { MessageConnect, RuntimeMessageSender } from "./types";
import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts";
import { uuidv4 } from "@App/pkg/utils/uuid";

let contentMessage: CustomEventMessage;
let injectMessage: CustomEventMessage;
let inboundMessage: CustomEventMessage;
let outboundMessage: CustomEventMessage;
let server: Server;
let client: CustomEventMessage;

const nextTick = () => Promise.resolve().then(() => {});

const setupGlobal = () => {
const flags = "-test.server";
// 创建 content 和 inject 之间的消息通道
contentMessage = new CustomEventMessage(flags, true); // content 端
injectMessage = new CustomEventMessage(flags, false); // inject 端
const testFlag = uuidv4();
const testPageMessaging = createPageMessaging(testFlag);
// 创建 scripting 和 inject / content 之间的消息通道
inboundMessage = new CustomEventMessage(testPageMessaging, true); // scripting 端
outboundMessage = new CustomEventMessage(testPageMessaging, false); // inject / content 端
inboundMessage.bindReceiver();
outboundMessage.bindReceiver();

// 服务端使用 content 消息
server = new Server("api", contentMessage);
// 服务端使用 scripting 消息
server = new Server("api", inboundMessage);

// 客户端使用 inject 消息
client = injectMessage;
// 客户端使用 inject / content 消息
client = outboundMessage;

// 清理 DOM 事件监听器
vi.stubGlobal("window", Object.create(window));
Expand All @@ -33,21 +37,21 @@ const setupGlobal = () => {
vi.fn().mockImplementation((event: Event) => {
if (event instanceof CustomEvent) {
const eventType = event.type;
if (eventType.includes("-test.server")) {
if (eventType.includes(testFlag)) {
let targetEventType: string;
let messageThis: CustomEventMessage;
let messageThat: CustomEventMessage;
// 根据事件类型确定目标消息处理器
if (eventType.includes(DefinedFlags.contentFlag)) {
if (eventType.includes(DefinedFlags.inboundFlag)) {
// inject -> content
targetEventType = eventType.replace(DefinedFlags.contentFlag, DefinedFlags.injectFlag);
messageThis = contentMessage;
messageThat = injectMessage;
} else if (eventType.includes(DefinedFlags.injectFlag)) {
targetEventType = eventType.replace(DefinedFlags.inboundFlag, DefinedFlags.outboundFlag);
messageThis = inboundMessage;
messageThat = outboundMessage;
} else if (eventType.includes(DefinedFlags.outboundFlag)) {
// content -> inject
targetEventType = eventType.replace(DefinedFlags.injectFlag, DefinedFlags.contentFlag);
messageThis = injectMessage;
messageThat = contentMessage;
targetEventType = eventType.replace(DefinedFlags.outboundFlag, DefinedFlags.inboundFlag);
messageThis = outboundMessage;
messageThat = inboundMessage;
} else {
throw new Error("test mock failed");
}
Expand Down Expand Up @@ -642,7 +646,7 @@ describe("Server", () => {
});

it("应该在 enableConnect 为 false 时不处理连接", async () => {
const serverWithoutConnect = new Server("api", contentMessage, false);
const serverWithoutConnect = new Server("api", inboundMessage, false);
const mockHandler = vi.fn();

serverWithoutConnect.on("on-noconnect", mockHandler);
Expand Down
2 changes: 1 addition & 1 deletion packages/message/window_message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage } from "./types";
import { v4 as uuidv4 } from "uuid";
import { uuidv4 } from "@App/pkg/utils/uuid";
import EventEmitter from "eventemitter3";

// 通过 window.postMessage/onmessage 实现通信
Expand Down
16 changes: 15 additions & 1 deletion rspack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core";
import { readFileSync } from "fs";
import { NormalModule } from "@rspack/core";
import { v4 as uuidv4 } from "uuid";

const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));

Expand All @@ -19,7 +20,15 @@ const dist = path.join(dirname, "dist");
const assets = path.join(src, "assets");

// 排除这些文件,不进行分离
const chunkExcludeSet = new Set(["editor.worker", "ts.worker", "linter.worker", "service_worker", "content", "inject"]);
const chunkExcludeSet = new Set([
"editor.worker",
"ts.worker",
"linter.worker",
"service_worker",
"content",
"inject",
"scripting",
]);

export default defineConfig({
...(isDev
Expand All @@ -38,6 +47,7 @@ export default defineConfig({
offscreen: `${src}/offscreen.ts`,
sandbox: `${src}/sandbox.ts`,
content: `${src}/content.ts`,
scripting: `${src}/scripting.ts`,
inject: `${src}/inject.ts`,
popup: `${src}/pages/popup/main.tsx`,
install: `${src}/pages/install/main.tsx`,
Expand Down Expand Up @@ -111,6 +121,10 @@ export default defineConfig({
],
},
plugins: [
new rspack.DefinePlugin({
"process.env.VI_TESTING": "'false'",
"process.env.SC_RANDOM_KEY": `'${uuidv4()}'`,
}),
new rspack.CopyRspackPlugin({
patterns: [
{
Expand Down
Loading
Loading