优化消息

This commit is contained in:
王一之 2024-12-19 18:03:32 +08:00
parent 84261e22bd
commit 177594b638
12 changed files with 269 additions and 391 deletions

View File

@ -0,0 +1,10 @@
# 消息
对扩展内消息交互的抽象
主要会有以下几种类型的消息:
- 从脚本发起的GM请求需要层层传递到service_worker/offscreen进行处理有的GM只需要进行一次调用获取一次结果有的需要进行
多次调用获取多次结果使用connect的方式实现
- 从service_woker/offscreen发起的请求类似消息队列其它页面进行监听触发后广播给所有页面使用connect方式实现
- 从扩展页面发起的请求需要传递到service_worker/offscreen进行处理如果只是单次调用获取一次结果使用message方式实现

View File

@ -1,54 +0,0 @@
import EventEmitter from "eventemitter3";
import { IConnect, IServer } from ".";
export class ExtServer implements IServer {
private EE: EventEmitter;
constructor() {
this.EE = new EventEmitter();
chrome.runtime.onConnect.addListener((port) => {
this.EE.emit("connect", new ExtConnect(port));
});
}
onConnect(callback: (con: IConnect) => void) {
this.EE.on("connect", callback);
}
}
export function extConnect() {
return new ExtConnect(chrome.runtime.connect());
}
export class ExtConnect implements IConnect {
private EE: EventEmitter;
private port: chrome.runtime.Port;
constructor(port: chrome.runtime.Port) {
this.EE = new EventEmitter();
this.port = port;
port.onMessage.addListener((message) => {
this.EE.emit("message", message);
});
port.onDisconnect.addListener(() => {
this.EE.emit("disconnect");
this.EE.removeAllListeners();
});
}
postMessage(message: unknown) {
this.port.postMessage(message);
}
onMessage(callback: (message: unknown) => void) {
this.EE.on("message", callback);
}
onDisconnect(callback: () => void) {
this.EE.on("disconnect", callback);
}
disconnect() {
this.port.disconnect();
}
}

View File

@ -1,85 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import EventEmitter from "eventemitter3";
import { v4 as uuidv4 } from "uuid";
export interface IServer {
onConnect: (callback: (con: IConnect) => void) => void;
}
export interface IConnect {
postMessage: (message: unknown) => void;
onMessage: (callback: (message: unknown) => void) => void;
onDisconnect: (callback: () => void) => void;
disconnect: () => void;
}
// 消息通道, 通过连接封装消息通道
export class Server {
private EE: EventEmitter;
constructor(private connect: IServer) {
this.EE = new EventEmitter();
this.connect.onConnect((con) => {
this.EE.emit("connection", con);
});
}
on(eventName: "connection", callback: (con: IConnect) => void): void;
on(eventName: string, callback: (con: IConnect) => void) {
this.EE.on(eventName, callback);
}
}
export class Connect {
private EE: EventEmitter;
constructor(private con: IConnect) {
this.EE = new EventEmitter();
this.con.onMessage((message) => {
this.messageHandler(message);
});
this.con.onDisconnect(() => {
this.EE.emit("disconnect");
this.EE.removeAllListeners();
});
}
private callbackFunc(msgId: string): (...data: unknown[]) => void {
return (...data: unknown[]) => {
this.con.postMessage({ eventName: "callback", data, messageId: msgId });
};
}
private messageHandler(data: unknown) {
const subData = data as { eventName: string; data: unknown[]; messageId: string; conType: string; id: string };
if (subData.eventName === "callback") {
this.EE.emit(subData.eventName + subData.messageId, ...subData.data);
return;
}
subData.data.push(this.callbackFunc(subData.messageId));
this.EE.emit(subData.eventName, ...subData.data);
}
on(eventName: string, callback: (...args: any[]) => void) {
this.EE.on(eventName, callback);
}
send(eventName: string, ...data: unknown[]) {
this.con.postMessage({ eventName, data });
}
emit(eventName: string, ...data: any[]) {
// 判断最后一个参数是否为函数
const callback = data.pop();
const messageId = uuidv4();
if (typeof callback !== "function") {
data.push(callback);
} else {
this.EE.on("callback" + messageId, (...args) => {
callback(...args);
});
}
const sendData = { eventName, data, messageId };
this.con.postMessage(sendData);
}
}

View File

@ -1,65 +0,0 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
import { Server, Connect } from ".";
import { windowConnect, WindowServer } from "./window";
describe("server", () => {
it("hello", async () => {
const myFunc = vi.fn();
const server = new Server(new WindowServer(global.window));
server.on("connection", (con) => {
myFunc();
con.onMessage((message) => {
myFunc(message);
});
});
const client = windowConnect(window, window);
client.postMessage("hello");
await new Promise((resolve) => setTimeout(resolve, 10));
expect(myFunc).toHaveBeenCalledTimes(2);
expect(myFunc).toHaveBeenCalledWith("hello");
});
});
describe("connect", async () => {
it("hello", async () => {
const server = new Server(new WindowServer(global.window));
const myFunc = vi.fn();
server.on("connection", (con) => {
myFunc();
const wrapCon = new Connect(con);
wrapCon.on("hello", (message) => {
myFunc(message);
wrapCon.emit("world", "world");
});
});
const client = new Connect(windowConnect(window, window));
client.on("world", (message) => {
myFunc(message);
});
client.emit("hello", "hello");
await new Promise((resolve) => setTimeout(resolve, 10));
expect(myFunc).toHaveBeenCalledTimes(3);
expect(myFunc).toHaveBeenCalledWith("hello");
expect(myFunc).toHaveBeenCalledWith("world");
});
it("response", async () => {
const server = new Server(new WindowServer(global.window));
const myFunc = vi.fn();
server.on("connection", (con) => {
const wrapCon = new Connect(con);
wrapCon.on("ping", (message, response) => {
myFunc(message);
response("pong");
});
});
const client = new Connect(windowConnect(window, window));
client.emit("ping", "ping", (message: string) => {
myFunc(message);
});
await new Promise((resolve) => setTimeout(resolve, 10));
expect(myFunc).toHaveBeenCalledTimes(2);
expect(myFunc).toHaveBeenCalledWith("ping");
expect(myFunc).toHaveBeenCalledWith("pong");
});
});

View File

@ -0,0 +1,66 @@
import { ApiFunction } from "./server";
export class Broker {
constructor() {}
// 订阅
subscribe(topic: string, handler: (message: any) => void) {
const con = chrome.runtime.connect({ name: topic });
con.postMessage({ action: "subscribe", topic });
con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => {
if (msg.action === "message") {
handler(msg.message);
}
});
}
// 发布
publish(topic: string, message: any) {
chrome.runtime.sendMessage({ action: "publish", topic, message });
}
}
// 消息队列
export class MessageQueue {
topicConMap: Map<string, { name: string; con: chrome.runtime.Port }[]> = new Map();
handler(): ApiFunction {
return ({ action, topic, message }: { action: string; topic: string; message: any }, con) => {
if (!con) {
throw new Error("con is required");
}
if (!topic) {
throw new Error("topic is required");
}
switch (action) {
case "subscribe":
this.subscribe(topic, con as chrome.runtime.Port);
break;
case "publish":
this.publish(topic, message);
break;
default:
throw new Error("action not found");
}
};
}
private subscribe(topic: string, con: chrome.runtime.Port) {
let list = this.topicConMap.get(topic);
if (!list) {
list = [];
this.topicConMap.set(topic, list);
}
list.push({ name: topic, con });
con.onDisconnect.addListener(() => {
list = list!.filter((item) => item.con !== con);
});
}
publish(topic: string, message: any) {
const list = this.topicConMap.get(topic);
list?.forEach((item) => {
item.con.postMessage({ action: "message", topic, message });
});
}
}

View File

@ -0,0 +1,45 @@
export type ApiFunction = (params: any, con: chrome.runtime.Port | chrome.runtime.MessageSender) => any;
export class Server {
apiFunctionMap: Map<string, ApiFunction> = new Map();
constructor() {
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg: { action: string }) => {
this.connectHandle(msg.action, msg, port);
});
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
this.messageHandle(msg.action, msg, sender, sendResponse);
});
}
on(name: string, func: ApiFunction) {
this.apiFunctionMap.set(name, func);
}
private connectHandle(msg: string, params: any, con: chrome.runtime.Port) {
const func = this.apiFunctionMap.get(msg);
if (func) {
func(params, con);
}
}
private messageHandle(
msg: string,
params: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
) {
const func = this.apiFunctionMap.get(msg);
if (func) {
try {
const ret = func(params, sender);
sendResponse({ code: 0, data: ret });
} catch (e: any) {
sendResponse({ code: -1, message: e.message });
}
}
}
}

View File

@ -1,61 +0,0 @@
import EventEmitter from "eventemitter3";
import { IConnect, IServer } from ".";
import { v4 as uuidv4 } from "uuid";
export class WindowServer implements IServer {
private EE: EventEmitter;
constructor(win: Window) {
this.EE = new EventEmitter();
win.addEventListener("message", (event) => {
if (event.data.type === "connect") {
this.EE.emit("connection", new WindowConnect(event.data.connectId, win, event.source as Window));
}
});
}
onConnect(callback: (con: IConnect) => void) {
this.EE.on("connection", callback);
}
}
export function windowConnect(source: Window, target: Window) {
const connectId = uuidv4();
target.postMessage({ type: "connect", connectId }, "*");
const con = new WindowConnect(connectId, source, target);
return con;
}
export class WindowConnect implements IConnect {
private EE: EventEmitter;
constructor(
private id: string,
private source: Window,
private target: Window
) {
this.EE = new EventEmitter();
this.source.addEventListener("message", (event) => {
if (event.data.eventName === "message" && event.data.id === id) {
this.EE.emit("message", event.data.data);
}
});
}
postMessage(data: unknown) {
this.target.postMessage({ eventName: "message", id: this.id, data }, "*");
}
onMessage(callback: (message: unknown) => void) {
this.EE.on("message", callback);
}
onDisconnect(callback: () => void) {
this.EE.on("disconnect", callback);
}
disconnect() {
this.EE.emit("disconnect");
this.EE.removeAllListeners();
}
}

View File

@ -1,124 +1,11 @@
import { fetchScriptInfo } from "@App/pkg/utils/script";
import { v4 as uuidv4 } from "uuid";
import { Connect } from "@Packages/message";
import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key";
import { openInCurrentTab } from "@App/pkg/utils/utils";
import LoggerCore from "@App/app/logger/core";
import { Server } from "@Packages/message/server";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
export default class Manager {
constructor(private connect: Connect) {}
listenerScriptInstall() {
// 初始化脚本安装监听
chrome.webRequest.onCompleted.addListener(
(req: chrome.webRequest.WebResponseCacheDetails) => {
// 处理url, 实现安装脚本
if (req.method !== "GET") {
return;
}
const url = new URL(req.url);
// 判断是否有hash
if (!url.hash) {
return;
}
// 判断是否有url参数
if (!url.hash.includes("url=")) {
return;
}
// 获取url参数
const targetUrl = url.hash.split("url=")[1];
// 读取脚本url内容, 进行安装
LoggerCore.getInstance().logger().debug("install script", { url: targetUrl });
this.openInstallPageByUrl(targetUrl).catch(() => {
// 如果打开失败, 则重定向到安装页
chrome.scripting.executeScript({
target: { tabId: req.tabId },
func: function () {
history.back();
},
});
// 并不再重定向当前url
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [2],
addRules: [
{
id: 2,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
},
condition: {
regexFilter: targetUrl,
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
});
},
{
urls: [
"https://docs.scriptcat.org/docs/script_installation",
"https://www.tampermonkey.net/script_installation.php",
],
types: ["main_frame"],
}
);
// 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [1],
addRules: [
{
id: 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
},
},
condition: {
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
// 排除常见的复合上述条件的域名
excludedRequestDomains: ["github.com"],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
}
public openInstallPageByUrl(url: string) {
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
Cache.getInstance().set(CacheKey.scriptInfo(info.uuid), info);
setTimeout(() => {
// 清理缓存
Cache.getInstance().del(CacheKey.scriptInfo(info.uuid));
}, 60 * 1000);
openInCurrentTab(`/src/install.html?uuid=${info.uuid}`);
});
}
// offscreen环境的管理器
export class Manager {
private api: Server = new Server();
initManager() {
this.listenerScriptInstall();
// 监听消息
}
}

View File

@ -0,0 +1,139 @@
import { fetchScriptInfo } from "@App/pkg/utils/script";
import { v4 as uuidv4 } from "uuid";
import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key";
import { openInCurrentTab } from "@App/pkg/utils/utils";
import LoggerCore from "@App/app/logger/core";
import { Server } from "@Packages/message/server";
import { MessageQueue } from "@Packages/message/message_queue";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
// service worker的管理器
export default class ServiceWorkerManager {
constructor() {}
listenerScriptInstall() {
// 初始化脚本安装监听
chrome.webRequest.onCompleted.addListener(
(req: chrome.webRequest.WebResponseCacheDetails) => {
// 处理url, 实现安装脚本
if (req.method !== "GET") {
return;
}
const url = new URL(req.url);
// 判断是否有hash
if (!url.hash) {
return;
}
// 判断是否有url参数
if (!url.hash.includes("url=")) {
return;
}
// 获取url参数
const targetUrl = url.hash.split("url=")[1];
// 读取脚本url内容, 进行安装
LoggerCore.getInstance().logger().debug("install script", { url: targetUrl });
this.openInstallPageByUrl(targetUrl).catch(() => {
// 如果打开失败, 则重定向到安装页
chrome.scripting.executeScript({
target: { tabId: req.tabId },
func: function () {
history.back();
},
});
// 并不再重定向当前url
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [2],
addRules: [
{
id: 2,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
},
condition: {
regexFilter: targetUrl,
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
});
},
{
urls: [
"https://docs.scriptcat.org/docs/script_installation",
"https://www.tampermonkey.net/script_installation.php",
],
types: ["main_frame"],
}
);
// 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [1],
addRules: [
{
id: 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
},
},
condition: {
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
// 排除常见的复合上述条件的域名
excludedRequestDomains: ["github.com"],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
}
public openInstallPageByUrl(url: string) {
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
Cache.getInstance().set(CacheKey.scriptInfo(info.uuid), info);
setTimeout(() => {
// 清理缓存
Cache.getInstance().del(CacheKey.scriptInfo(info.uuid));
}, 60 * 1000);
openInCurrentTab(`/src/install.html?uuid=${info.uuid}`);
});
}
private api: Server = new Server();
private mq: MessageQueue = new MessageQueue();
// 获取安装信息
getInstallInfo(params: { uuid: string }) {
const info = Cache.getInstance().get(CacheKey.scriptInfo(params.uuid));
return info;
}
initManager() {
// 监听消息
this.api.on("getInstallInfo", this.getInstallInfo);
this.listenerScriptInstall();
}
}

View File

@ -11,7 +11,7 @@ import {
ScriptDAO,
UserConfig,
} from "@App/app/repo/scripts";
import { InstallSource } from "@App/app/service/manager";
import { InstallSource } from "@App/app/service/service_worker";
import YAML from "yaml";
import { Subscribe, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
import { nextTime } from "./utils";

View File

@ -1,6 +1,4 @@
import { extConnect } from "@Packages/message/extension";
import Manager from "./app/service/manager";
import { Connect } from "@Packages/message";
import ServiceWorkerManager from "./app/service/service_worker";
import migrate from "./app/migrate";
import LoggerCore from "./app/logger/core";
import DBWriter from "./app/logger/db_writer";
@ -55,10 +53,8 @@ async function main() {
loggerCore.logger().debug("background start");
// 初始化沙盒环境
await setupOffscreenDocument();
// 初始化连接
const extClient = new Connect(extConnect());
// 初始化管理器
const manager = new Manager(extClient);
const manager = new ServiceWorkerManager();
manager.initManager();
}