Skip to content

Commit 5b01c10

Browse files
cyfung1031CodFrm
andauthored
♻️ 重写UrlMatch (#637)
* 重写UrlMatch * 整理代码 * 添加单元测试 * 兼容 .tld * 修正 match pattern ** 问题 * 代码及测试修正 * 补充单元测试 * 调整单元测试 * 将单元测试url换成更实际的url * 修改单元测试 * 修改01 * file应该是三个斜杠 * 修正02 * 修正03 * 修正04 * add -> addInclude * customUrlCovering -> customMUP * urlCovering -> scriptMUP * metaUMatchAnalyze -> extractMUP * 修正05 * lint fix * 注释修正 * 注释修正 * 内部测试补充 * glob *? -> ?* * 單元測試 glob-test-3 * 修正06 * MUP -> UrlPatterns * 修正07 * 加注释:GM 的 magic tld 说明 * glob * 修正 * 代码优化,注释补充 * 修正特殊處理,修正單元測試 * Update match.test.ts * 处理lint问题 * 代码优化 * lint * sorter 相关代码优化 * 变数名统一 strBlacklist * 修正 regex pattern 没有匹配到页面问题 * wildcard處理 * lint * 注釋修正 * 把glob的指定网域取出来 * 效能修正 * 处理review问题 * 单元测试优化,加强错误处理 * 改进代码处理 * lint * extractSchemesOfGlobs, 单元测试调整 * ⚡ runtime.ts 代码优化 (#642) * runtime.ts 代码优化 * 优化 * 🎨 getEnableScript 优化 (#645) * runtime.ts 代码优化 * 优化 * getEnableScript 优化 * lint * 誤字 * 加注釋 * lint * 微调 regex pattern ruleContent * RuleType * 增加单元测试 (extractMatchPatternsFromGlobs, extractSchemesOfGlobs) * 配合使用情景,增强 regexToGlob处理 * vitest 测试顺序 * url_matcher.test.js -> url_matcher.test.ts * 🐛 兼容较低的浏览器内核 #647 * 移除 vitest 测试顺序 * 增强 regexToGlob处理 * 刪去不必要code * 单元测试調整 * 单元测试調整 * regexToGlob 规范化连续 修正 * 刪去不必要code * 補充注釋 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent cbe880e commit 5b01c10

13 files changed

Lines changed: 3471 additions & 722 deletions

File tree

src/app/service/service_worker/popup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class PopupService {
161161
runNum: script.type === SCRIPT_TYPE_NORMAL ? 0 : script.runStatus === SCRIPT_RUN_STATUS_RUNNING ? 1 : 0,
162162
runNumByIframe: 0,
163163
menus: [],
164-
customExclude: (script as ScriptMatchInfo).customizeExcludeMatches || [],
164+
customUrlPatterns: (script as ScriptMatchInfo).customUrlPatterns || null,
165165
};
166166
}
167167

@@ -182,7 +182,7 @@ export class PopupService {
182182
if (run) {
183183
// 如果脚本已经存在,则不添加,更新信息
184184
run.enable = script.status === SCRIPT_STATUS_ENABLE;
185-
run.customExclude = script.customizeExcludeMatches || run.customExclude;
185+
run.customUrlPatterns = script.customUrlPatterns || run.customUrlPatterns;
186186
run.hasUserConfig = !!script.config;
187187
} else {
188188
run = this.scriptToMenu(script);
@@ -198,9 +198,9 @@ export class PopupService {
198198
}
199199
const scriptMenu = [...scriptMenuMap.values()];
200200
// 检查是否在黑名单中
201-
const isBlack = this.runtime.blackMatch.match(req.url).length > 0;
201+
const isBlacklist = this.runtime.isUrlBlacklist(req.url);
202202
// 后台脚本只显示开启或者运行中的脚本
203-
return { isBlacklist: isBlack, scriptList: scriptMenu, backScriptList };
203+
return { isBlacklist, scriptList: scriptMenu, backScriptList };
204204
}
205205

206206
async getScriptMenu(tabId: number): Promise<ScriptMenu[]> {

src/app/service/service_worker/runtime.ts

Lines changed: 86 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { runScript, stopScript } from "../offscreen/client";
1212
import { getRunAt } from "./utils";
1313
import { isUserScriptsAvailable, randomMessageFlag } from "@App/pkg/utils/utils";
1414
import { cacheInstance } from "@App/app/cache";
15-
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
15+
import { UrlMatch } from "@App/pkg/utils/match";
1616
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
1717
import { sendMessage } from "@Packages/message/client";
1818
import { compileInjectScript, compileScriptCode } from "../content/utils";
@@ -22,26 +22,23 @@ import { type SystemConfig } from "@App/pkg/config/config";
2222
import { type ResourceService } from "./resource";
2323
import { LocalStorageDAO } from "@App/app/repo/localStorage";
2424
import Logger from "@App/app/logger/logger";
25-
import { getMetadataStr, getUserConfigStr } from "@App/pkg/utils/utils";
25+
import { getMetadataStr, getUserConfigStr, obtainBlackList } from "@App/pkg/utils/utils";
2626
import type { GMInfoEnv } from "../content/types";
2727
import { localePath } from "@App/locales/locales";
2828
import { DocumentationSite } from "@App/app/const";
2929
import { CACHE_KEY_REGISTRY_SCRIPT } from "@App/app/cache_key";
30-
31-
const obtainBlackList = (strBlacklist: string | null | undefined) => {
32-
const blacklist = strBlacklist
33-
? strBlacklist
34-
.split("\n")
35-
.map((item) => item.trim())
36-
.filter((item) => item)
37-
: [];
38-
return blacklist;
39-
};
30+
import {
31+
getApiMatchesAndGlobs,
32+
extractUrlPatterns,
33+
RuleType,
34+
toUniquePatternStrings,
35+
type URLRuleEntry,
36+
} from "@App/pkg/utils/url_matcher";
4037

4138
export class RuntimeService {
4239
scriptMatch: UrlMatch<string> = new UrlMatch<string>();
4340
scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>();
44-
blackMatch: UrlMatch<boolean> = new UrlMatch<boolean>();
41+
blackMatch: UrlMatch<string> = new UrlMatch<string>();
4542
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
4643

4744
logger: Logger;
@@ -181,7 +178,8 @@ export class RuntimeService {
181178
// 监听脚本排序
182179
this.mq.subscribe<TSortScript>("sortScript", async (scripts) => {
183180
const uuidSort = Object.fromEntries(scripts.map(({ uuid, sort }) => [uuid, sort]));
184-
this.scriptMatch.sort((a, b) => uuidSort[a] - uuidSort[b]);
181+
this.scriptMatch.setupSorter(uuidSort);
182+
this.scriptCustomizeMatch.setupSorter(uuidSort);
185183
// 更新缓存
186184
const scriptMatchCache = await cacheInstance.get<{ [key: string]: ScriptMatchInfo }>("scriptMatch");
187185
if (!scriptMatchCache) {
@@ -272,15 +270,14 @@ export class RuntimeService {
272270
private loadBlacklist() {
273271
// 设置黑名单match
274272
const blacklist = this.blacklist; // 重用cache的blacklist阵列 (immutable)
275-
const result = dealPatternMatches(blacklist, {
276-
exclude: true,
277-
});
278-
this.blackMatch.forEach((uuid) => {
279-
this.blackMatch.del(uuid);
280-
});
281-
result.result.forEach((match) => {
282-
this.blackMatch.add(match, true);
283-
});
273+
274+
const scriptUrlPatterns = extractUrlPatterns([...blacklist.map((e) => `@include ${e}`)]);
275+
this.blackMatch.clearRules("BK");
276+
this.blackMatch.addRules("BK", scriptUrlPatterns);
277+
}
278+
279+
public isUrlBlacklist(url: string) {
280+
return this.blackMatch.urlMatch(url)[0] === "BK";
284281
}
285282

286283
// 取消脚本注册
@@ -412,10 +409,10 @@ export class RuntimeService {
412409
async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) {
413410
await this.loadScriptMatchInfo();
414411
// 匹配当前页面的脚本
415-
let matchScriptUuid = this.scriptMatch.match(url!);
412+
let matchScriptUuid = this.scriptMatch.urlMatch(url!);
416413
// 包含自定义排除的脚本
417414
if (includeCustomize) {
418-
const excludeScriptUuid = this.scriptCustomizeMatch.match(url!);
415+
const excludeScriptUuid = this.scriptCustomizeMatch.urlMatch(url!);
419416
// 自定义排除的脚本优化显示
420417
matchScriptUuid = [...new Set<string>([...excludeScriptUuid, ...matchScriptUuid])];
421418
}
@@ -432,12 +429,10 @@ export class RuntimeService {
432429
if (!this.isLoadScripts) {
433430
return { flag: "", scripts: [] };
434431
}
435-
436432
const chromeSender = sender.getSender() as MessageSender;
437433

438434
// 判断是否黑名单(针对网址,与个别脚本设定无关)
439-
const isBlack = this.blackMatch.match(chromeSender.url!);
440-
if (isBlack.length > 0) {
435+
if (this.isUrlBlacklist(chromeSender.url!)) {
441436
// 如果在黑名单中, 则不加载脚本
442437
return { flag: "", scripts: [] };
443438
}
@@ -608,13 +603,19 @@ export class RuntimeService {
608603
let messageFlag = await this.getMessageFlag();
609604
if (!messageFlag) {
610605
// 黑名单排除
606+
611607
const blacklist = this.blacklist;
612608
const excludeMatches = [];
613-
if (blacklist.length) {
614-
const result = dealPatternMatches(blacklist, {
615-
exclude: true,
616-
});
617-
excludeMatches.push(...result.patternResult);
609+
const excludeGlobs = [];
610+
const rules = extractUrlPatterns([...blacklist.map((e) => `@include ${e}`)]);
611+
for (const rule of rules) {
612+
if (rule.ruleType === RuleType.MATCH_INCLUDE) {
613+
// matches -> excludeMatches
614+
excludeMatches.push(rule.patternString);
615+
} else if (rule.ruleType === RuleType.GLOB_INCLUDE) {
616+
// includeGlobs -> excludeGlobs
617+
excludeGlobs.push(rule.patternString);
618+
}
618619
}
619620

620621
messageFlag = await this.getAndGenMessageFlag();
@@ -634,6 +635,7 @@ export class RuntimeService {
634635
world: "MAIN",
635636
runAt: "document_start",
636637
excludeMatches,
638+
excludeGlobs,
637639
},
638640
// 注册content
639641
{
@@ -644,6 +646,7 @@ export class RuntimeService {
644646
runAt: "document_start",
645647
world: "USER_SCRIPT",
646648
excludeMatches,
649+
excludeGlobs,
647650
},
648651
];
649652
try {
@@ -722,18 +725,13 @@ export class RuntimeService {
722725

723726
syncAddScriptMatch(item: ScriptMatchInfo) {
724727
// 清理一下老数据
725-
this.scriptMatch.del(item.uuid);
726-
this.scriptCustomizeMatch.del(item.uuid);
728+
this.scriptMatch.clearRules(item.uuid);
729+
this.scriptCustomizeMatch.clearRules(item.uuid);
727730
// 添加新的数据
728-
item.matches.forEach((match) => {
729-
this.scriptMatch.add(match, item.uuid);
730-
});
731-
item.excludeMatches.forEach((match) => {
732-
this.scriptMatch.exclude(match, item.uuid);
733-
});
734-
item.customizeExcludeMatches.forEach((match) => {
735-
this.scriptCustomizeMatch.add(match, item.uuid);
736-
});
731+
this.scriptMatch.addRules(item.uuid, item.scriptUrlPatterns);
732+
if (item.customUrlPatterns?.length) {
733+
this.scriptCustomizeMatch.addRules(item.uuid, item.customUrlPatterns!);
734+
}
737735
}
738736

739737
async updateScriptStatus(uuid: string, status: SCRIPT_STATUS) {
@@ -752,76 +750,74 @@ export class RuntimeService {
752750
await this.loadScriptMatchInfo();
753751
}
754752
this.scriptMatchCache!.delete(uuid);
755-
this.scriptMatch.del(uuid);
756-
this.scriptCustomizeMatch.del(uuid);
753+
this.scriptMatch.clearRules(uuid);
754+
this.scriptCustomizeMatch.clearRules(uuid);
757755
this.saveScriptMatchInfo();
758756
}
759757

760758
// 构建userScript注册信息
761759
async getAndSetUserScriptRegister(script: Script) {
762760
const scriptRes = await this.script.buildScriptRunResource(script);
763-
// concat 浅拷贝是为了避免修改原数组
764-
const matches = (scriptRes.metadata["match"] || []).concat();
765-
matches.push(...(scriptRes.metadata["include"] || []));
766-
if (!matches.length) {
761+
const metaMatch = scriptRes.metadata["match"];
762+
const metaInclude = scriptRes.metadata["include"];
763+
const metaExclude = scriptRes.metadata["exclude"];
764+
if ((metaMatch?.length ?? 0) + (metaInclude?.length ?? 0) === 0) {
767765
return undefined;
768766
}
769767

768+
// 黑名单排除
769+
const strBlacklist = (await this.systemConfig.getBlacklist()) as string | undefined;
770+
const blacklist = obtainBlackList(strBlacklist);
771+
772+
const scriptUrlPatterns = extractUrlPatterns([
773+
...(metaMatch || []).map((e) => `@match ${e}`),
774+
...(metaInclude || []).map((e) => `@include ${e}`),
775+
...(metaExclude || []).map((e) => `@exclude ${e}`),
776+
...(blacklist || []).map((e) => `@exclude ${e}`),
777+
]);
778+
779+
let customUrlPatterns: URLRuleEntry[] | null = null;
780+
781+
// 自定义排除
782+
if (script.selfMetadata && script.selfMetadata.exclude) {
783+
customUrlPatterns = extractUrlPatterns([...(script.selfMetadata.exclude || []).map((e) => `@exclude ${e}`)]);
784+
if (customUrlPatterns.length === 0) customUrlPatterns = null;
785+
}
786+
770787
scriptRes.code = compileInjectScript(scriptRes, scriptRes.code);
771788

772-
const patternMatches = dealPatternMatches(matches);
773-
const scriptMatchInfo: ScriptMatchInfo = Object.assign(
774-
{ matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] },
775-
scriptRes
789+
const { matches, includeGlobs } = getApiMatchesAndGlobs(scriptUrlPatterns);
790+
791+
const excludeMatches = toUniquePatternStrings(
792+
scriptUrlPatterns.filter((e) => e.ruleType === RuleType.MATCH_EXCLUDE)
776793
);
794+
const excludeGlobs = toUniquePatternStrings(scriptUrlPatterns.filter((e) => e.ruleType === RuleType.GLOB_EXCLUDE));
777795

778796
const registerScript: chrome.userScripts.RegisteredUserScript = {
779797
id: scriptRes.uuid,
780798
js: [{ code: scriptRes.code }],
781-
matches: patternMatches.patternResult,
799+
matches: matches, // primary
800+
includeGlobs: includeGlobs, // includeGlobs applied after matches
801+
excludeMatches: excludeMatches,
802+
excludeGlobs: excludeGlobs,
782803
allFrames: !scriptRes.metadata["noframes"],
783804
world: "MAIN",
784-
excludeMatches: [],
785805
};
786806

787-
// 排除由loadPage时决定, 不使用userScript的excludeMatches处理
788-
if (script.metadata["exclude"]) {
789-
// concat 浅拷贝是为了避免修改原数组
790-
const excludeMatches = script.metadata["exclude"].concat();
791-
const result = dealPatternMatches(excludeMatches, {
792-
exclude: true,
793-
});
794-
795-
// registerScript.excludeMatches = result.patternResult;
796-
scriptMatchInfo.excludeMatches = result.result;
797-
}
798-
// 自定义排除
799-
if (script.selfMetadata && script.selfMetadata.exclude) {
800-
const excludeMatches = script.selfMetadata.exclude;
801-
const result = dealPatternMatches(excludeMatches, {
802-
exclude: true,
803-
});
804-
805-
// registerScript.excludeMatches.push(...result.patternResult);
806-
scriptMatchInfo.customizeExcludeMatches = result.result;
807+
if (scriptRes.metadata["run-at"]) {
808+
registerScript.runAt = getRunAt(scriptRes.metadata["run-at"]);
807809
}
808810

809-
// 黑名单排除
810-
const blacklist = this.blacklist;
811-
if (blacklist.length) {
812-
const result = dealPatternMatches(blacklist, {
813-
exclude: true,
814-
});
815-
// scriptMatchInfo.excludeMatches.push(...result.result);
816-
registerScript.excludeMatches!.push(...result.patternResult);
817-
}
811+
const scriptMatchInfo = Object.assign(
812+
{
813+
scriptUrlPatterns: scriptUrlPatterns,
814+
customUrlPatterns: customUrlPatterns,
815+
},
816+
scriptRes
817+
) as ScriptMatchInfo;
818818

819819
// 将脚本match信息放入缓存中
820-
await this.addScriptMatch(scriptMatchInfo);
821-
822-
if (scriptRes.metadata["run-at"]) {
823-
registerScript.runAt = getRunAt(scriptRes.metadata["run-at"]);
824-
}
820+
this.addScriptMatch(scriptMatchInfo);
825821

826822
return {
827823
scriptMatchInfo,

src/app/service/service_worker/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { Script, ScriptRunResource, SCRIPT_RUN_STATUS, SCMetadata } from "@App/app/repo/scripts";
2+
import { type URLRuleEntry } from "@App/pkg/utils/url_matcher";
23
import { type GetSender } from "@Packages/message/server";
34

45
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
56

67
// 为了优化性能,存储到缓存时删除了code、value与resource
78
export interface ScriptMatchInfo extends ScriptRunResource {
8-
matches: string[];
9-
excludeMatches: string[];
10-
customizeExcludeMatches: string[];
9+
scriptUrlPatterns: URLRuleEntry[];
10+
customUrlPatterns: URLRuleEntry[] | null;
1111
}
1212

1313
export interface ScriptLoadInfo extends ScriptMatchInfo {
@@ -86,5 +86,5 @@ export type ScriptMenu = {
8686
runNum: number; // 脚本运行次数
8787
runNumByIframe: number; // iframe运行次数
8888
menus: ScriptMenuItem[]; // 脚本菜单
89-
customExclude: string[]; // 自定义排除
89+
customUrlPatterns: URLRuleEntry[] | null; // 自定义排除
9090
};

src/pages/components/ScriptMenuList/index.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@ import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/featu
2929
import { messageQueue, systemConfig } from "@App/pages/store/global";
3030
import { i18nName } from "@App/locales/locales";
3131
import { type TScriptRunStatus } from "@App/app/service/queue";
32+
import { isUrlMatch, RuleTypeBit } from "@App/pkg/utils/url_matcher";
3233

3334
const CollapseItem = Collapse.Item;
3435

35-
function isExclude(script: ScriptMenu, host: string) {
36-
if (!script.customExclude) {
36+
function isExclude(script: ScriptMenu, url: URL) {
37+
const rules = script.customUrlPatterns;
38+
const href = url.href;
39+
if (!rules) {
3740
return false;
3841
}
39-
for (let i = 0; i < script.customExclude.length; i += 1) {
40-
if (script.customExclude[i] === `*://${host}/*`) {
41-
return true;
42+
for (const rule of rules) {
43+
if (!(rule.ruleType & RuleTypeBit.INCLUSION)) {
44+
// exclude
45+
if (!isUrlMatch(href, rule)) {
46+
return true;
47+
}
4248
}
4349
}
4450
return false;
@@ -199,8 +205,8 @@ const ScriptMenuList = React.memo(
199205
window.close();
200206
}, []);
201207

202-
const handleExcludeUrl = useCallback((item: ScriptMenu, urlHost: string) => {
203-
scriptClient.excludeUrl(item.uuid, `*://${urlHost}/*`, isExclude(item, urlHost)).finally(() => {
208+
const handleExcludeUrl = useCallback((item: ScriptMenu, excludePattern: string, currentUrl: URL) => {
209+
scriptClient.excludeUrl(item.uuid, excludePattern, isExclude(item, currentUrl)).finally(() => {
204210
window.close();
205211
});
206212
}, []);
@@ -313,9 +319,9 @@ const ScriptMenuList = React.memo(
313319
status="warning"
314320
type="secondary"
315321
icon={<IconMinus />}
316-
onClick={() => handleExcludeUrl(item, url.host)}
322+
onClick={() => handleExcludeUrl(item, `*://${url.host}/*`, url)}
317323
>
318-
{(isExclude(item, url.host) ? t("exclude_on") : t("exclude_off")).replace("$0", `${url.host}`)}
324+
{(isExclude(item, url) ? t("exclude_on") : t("exclude_off")).replace("$0", `${url.host}`)}
319325
</Button>
320326
)}
321327
<Popconfirm

0 commit comments

Comments
 (0)