Skip to content

Commit

Permalink
fix: 修复浏览器的兼容性(polyfill)问题,request + getValue
Browse files Browse the repository at this point in the history
  • Loading branch information
SSmJaE committed Mar 19, 2023
1 parent 1e6988d commit 18eeda3
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Store {

/** 因为subscribe了这个key,如果直接替换(=),会导致subscribe失效 */
setUserSettings(userSettings: Partial<IWELearnSettings & ICommonSettings>) {
for (const [key, value] of Object.entries(userSettings)) {
for (const [key, value] of Object.entries(userSettings || {})) {
// @ts-ignore
this.userSettings[key] = value;
}
Expand Down
16 changes: 11 additions & 5 deletions src/utils/polyfill/extension/background.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RequestMessagePayload } from "../request/types";
import { IExtensionMessage } from "./types";

async function handleContentMessage(
Expand All @@ -9,13 +10,13 @@ async function handleContentMessage(
payload: { url, init },
} = message;

const messageToContent: IExtensionMessage = {
const messageToContent: IExtensionMessage<RequestMessagePayload> = {
extensionName: message.extensionName,
sessionId: message.sessionId,
sessionSource: "background",
sessionTarget: "content",
type: "request",
payload: undefined,
payload: undefined as any,
};

try {
Expand All @@ -35,9 +36,14 @@ async function handleContentMessage(
};
}

sendResponse(messageToContent);
console.log("[background] : messageToContent");
console.log(messageToContent);

return true;
sendResponse(messageToContent);
}

chrome.runtime.onMessage.addListener(handleContentMessage);
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleContentMessage(message, sender, sendResponse);

return true;
});
36 changes: 25 additions & 11 deletions src/utils/polyfill/extension/content.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { RequestMessagePayload } from "../request/types";
import { IExtensionMessage } from "./types";

const EXTENSION_NAME = "eocs-helper";
// 会和inject中出现const冲突,懒得折腾rollup了
const EXTENSION_NAME_CONTENT = "eocs-helper";

/** 消息来自inject */
async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>) {
const message = messageEvent.data;

if (!message.extensionName || message.extensionName !== EXTENSION_NAME) {
if (!message.extensionName || message.extensionName !== EXTENSION_NAME_CONTENT) {
return;
}

Expand Down Expand Up @@ -34,17 +36,26 @@ async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>
payload: message.payload,
};

returnPayload = await new Promise<IExtensionMessage>((resolve) =>
returnPayload = await new Promise<RequestMessagePayload>((resolve, reject) => {
// const messageFromBackground = await chrome.runtime.sendMessage<
// IExtensionMessage<any>,
// IExtensionMessage<any>
// >(messageToBackground);

// 原来可以直接获取到response啊,我以为得自己维护一套事件监听
chrome.runtime.sendMessage(
messageToBackground,
(messageFromBackground: IExtensionMessage) => {
(messageFromBackground: IExtensionMessage<RequestMessagePayload>) => {
if (!messageFromBackground) {
reject("[content] : messageFromBackground is undefined");
}

console.log("[content] : messageFromBackground");
console.log(messageFromBackground);
resolve(messageFromBackground);
resolve(messageFromBackground.payload);
},
),
);
);
});

break;
case "setValue":
Expand All @@ -53,6 +64,7 @@ async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>

returnPayload = await new Promise<void>((resolve) => {
// 只有sync api,并不是非要用sync
// value可以是object,但是必须是jsonable的
chrome.storage.sync.set({ [key]: value }, function () {
console.log(`[content] : ${key} is set to ${value}`);
resolve();
Expand All @@ -65,7 +77,7 @@ async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>
{
const { key, defaultValue } = payload;

returnPayload = await new Promise((resolve) => {
returnPayload = await new Promise<any>((resolve) => {
chrome.storage.sync.get(key, function (result) {
// 提前序列化,不用在每次使用时序列化
let temp: any;
Expand All @@ -75,12 +87,14 @@ async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>
temp = result[key];
}

console.log(`[content] : ${key} is ${temp}`);

if (!temp) {
chrome.storage.sync.set({ [key]: defaultValue });
temp = defaultValue;
}

console.log(`[content] : ${key} is ${temp}`);
console.log(`[content] : ${key} is set to ${defaultValue}`);
}

resolve(temp);
});
Expand All @@ -106,7 +120,7 @@ async function fromInjectToContent(messageEvent: MessageEvent<IExtensionMessage>
payload: returnPayload,
};

window.postMessage(messageToInject, "*");
window.postMessage(JSON.parse(JSON.stringify(messageToInject)), "*");
}

// /** 消息来(返回)自background
Expand Down
8 changes: 5 additions & 3 deletions src/utils/polyfill/extension/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ const EXTENSION_NAME = "eocs-helper";
*/
const sessions = new Map<string, ExtensionMessageCallback>();

export function injectToContent(
export async function injectToContent<T = any>(
type: ExtensionMessageType,
payload: any,
callback: ExtensionMessageCallback = async () => {},
callback: ExtensionMessageCallback<T> = async () => {},
) {
// 只能发送jsonable的数据,Promise不行
// 如果无法序列化,让其直接在inject中报错,方便调试
const jsonablePayload = JSON.parse(JSON.stringify(payload));

// const isEqual = (await import("lodash/isEqual")).default;

if (!isEqual(jsonablePayload, payload)) {
throw new Error("payload is not jsonable");
}
Expand All @@ -52,7 +54,7 @@ export function injectToContent(
payload,
};

window.postMessage(messageToContent, "*");
window.postMessage(JSON.parse(JSON.stringify(messageToContent)), "*");

sessions.set(sessionId, callback);
}
Expand Down
4 changes: 3 additions & 1 deletion src/utils/polyfill/extension/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface IExtensionMessage<T = any> {
payload: T;
}

export type ExtensionMessageCallback = (extensionMessage: IExtensionMessage) => Promise<void>;
export type ExtensionMessageCallback<T = any> = (
extensionMessage: IExtensionMessage<T>,
) => Promise<void>;
64 changes: 55 additions & 9 deletions src/utils/polyfill/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import { GM_getValue, GM_setValue } from "$";

import logger from "../logger";
import { injectToContent } from "./extension/inject";

// 避免在浏览器环境(非脚本管理器)下报错
// typeof GM_setValue === "function" || function GM_setValue() {};
// const GM_setValue = await import("$").then((module) => module.GM_setValue);

let hasInitializeSetValue = false;
let GM_setValue: any;

async function initializeSetValue() {
if (process.env.CRX) {
GM_setValue = (key: string, value: any) => {
logger.debug("should not invoke placeholder function GM_setValue");
};
} else {
GM_setValue = await import("$").then((module) => module.GM_setValue);
}
}

/**
* 调用GM_setValue或者chrome.storage
*/
export async function setValue(key: string, value: any) {
// 避免在浏览器环境(非脚本管理器)下报错
typeof GM_setValue === "function" || function GM_setValue() {};
// 确保只初始化一次GM_setValue,并且在调用GM_setValue之前初始化
if (!hasInitializeSetValue) {
await initializeSetValue();
hasInitializeSetValue = true;
}

if (process.env.CRX) {
injectToContent("setValue", {
Expand All @@ -20,19 +38,47 @@ export async function setValue(key: string, value: any) {
}
}

// 避免在浏览器环境(非脚本管理器)下报错
// typeof GM_getValue === "function" || function GM_getValue() {};
let hasInitializeGetValue = false;
let GM_getValue: any;

async function initializeGetValue() {
if (process.env.CRX) {
GM_getValue = (key: string, defaultValue?: any): any => {
logger.debug("should not invoke placeholder function GM_getValue");
};
} else {
GM_getValue = await import("$").then((module) => module.GM_getValue);
}
}

/**
* 调用GM_getValue或者chrome.storage
*
* 如果调用的是GM_getValue,返回JSON.parse后的结果 */
export async function getValue(key: string, defaultValue?: any) {
// 避免在浏览器环境(非脚本管理器)下报错
typeof GM_getValue === "function" || function GM_getValue() {};
// 确保只初始化一次GM_getValue,并且在调用GM_getValue之前初始化
if (!hasInitializeGetValue) {
await initializeGetValue();
hasInitializeGetValue = true;
}

let returnValue: any = undefined;
if (process.env.CRX) {
returnValue = injectToContent("getValue", {
key: key,
defaultValue: defaultValue,
returnValue = await new Promise((resolve) => {
injectToContent<any>(
"getValue",
{
key: key,
defaultValue: defaultValue,
},
async (extensionMessage) => {
const { payload } = extensionMessage;

resolve(payload);
},
);
});
} else {
const temp = await GM_getValue(key, defaultValue);
Expand Down
43 changes: 38 additions & 5 deletions src/utils/polyfill/request/implement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GM_xmlhttpRequest } from "$";
import queryString from "query-string";

import metadata from "@/metadata.json";
Expand All @@ -9,7 +8,9 @@ import {
CustomRequestMethod,
CustomRequestResponse,
GM_xmlhttpResponse,
RequestMessagePayload,
} from "./types";
import logger from "../../logger";

/**
* 如果url以/开头,则自动拼接BASE_URL,比如 /query => [apiServer]/[platform]/xxx
Expand Down Expand Up @@ -41,7 +42,7 @@ export async function requestForExtension<T = any>(
},
): Promise<CustomRequestResponse<T>> {
return new Promise(async (resolve, reject) => {
injectToContent(
injectToContent<RequestMessagePayload>(
"request",
{
url: getFullUrl(url, query),
Expand All @@ -68,6 +69,31 @@ export async function requestForExtension<T = any>(
});
}

// 避免在浏览器环境(非脚本管理器)下报错
// typeof GM_xmlhttpRequest === "function" || function GM_xmlhttpRequest() {};
let hasInitializeXhr = false;
let GM_xmlhttpRequest: any;

async function initializeXhr() {
if (process.env.CRX) {
GM_xmlhttpRequest = (options: any) => {
logger.debug("should not invoke placeholder function GM_xmlhttpRequest");
};
} else {
GM_xmlhttpRequest = await import("$").then((module) => module.GM_xmlhttpRequest);
}
}

// 不想在requestForUserscript里面再套一层Promise,所以在这里初始化
// 因为并不像setValue和getValue那样一启动app就会调用,这里稍微有点不一致性也没关系
// 可以保证用得上request的时候,已经初始化完毕了
// (async () => {
// if (!hasInitializeXhr) {
// await initializeXhr();
// hasInitializeXhr = true;
// }
// })();

/**对GM_xmlhttpRequest的封装,以实现一致的fetch风格的request通用接口 */
export function requestForUserscript<T = any>(
url: string,
Expand All @@ -78,10 +104,17 @@ export function requestForUserscript<T = any>(
query: undefined,
},
) {
// 避免在浏览器环境(非脚本管理器)下报错
typeof GM_xmlhttpRequest === "function" || function GM_xmlhttpRequest() {};
// promise的executor本身,可以是异步的,所以这里不用套一层Promise
// https://eslint.org/docs/latest/rules/no-async-promise-executor
// 文档说捕获不到错误,但是实测,可以捕获到
return new Promise<CustomRequestResponse<T>>(async (resolve, reject) => {
if (!hasInitializeXhr) {
await initializeXhr();
hasInitializeXhr = true;
}

// throw new Error("GM_xmlhttpRequest is xxxxxxxxxx");

return new Promise<CustomRequestResponse<T>>((resolve, reject) => {
GM_xmlhttpRequest({
url: getFullUrl(url, query),
method: method as any,
Expand Down
5 changes: 5 additions & 0 deletions src/utils/polyfill/request/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export type CustomRequestInit = {
body?: any;
};

export interface RequestMessagePayload {
text: string;
ok: boolean;
}

export interface CustomRequestResponse<T = any> {
json(): Promise<T>;
text(): Promise<string>;
Expand Down

0 comments on commit 18eeda3

Please sign in to comment.