优化消息
This commit is contained in:
10
packages/message/README.md
Normal file
10
packages/message/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# 消息
|
||||
|
||||
对扩展内消息交互的抽象
|
||||
|
||||
主要会有以下几种类型的消息:
|
||||
|
||||
- 从脚本发起的GM请求,需要层层传递到service_worker/offscreen进行处理,有的GM只需要进行一次调用获取一次结果,有的需要进行
|
||||
多次调用获取多次结果,使用connect的方式实现
|
||||
- 从service_woker/offscreen发起的请求,类似消息队列,其它页面进行监听,触发后广播给所有页面,使用connect方式实现
|
||||
- 从扩展页面发起的请求,需要传递到service_worker/offscreen进行处理,如果只是单次调用,获取一次结果,使用message方式实现
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
});
|
66
packages/message/message_queue.ts
Normal file
66
packages/message/message_queue.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
}
|
45
packages/message/server.ts
Normal file
45
packages/message/server.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user