Skip to content

Commit 9c81f6c

Browse files
cyfung1031Copilot
andauthored
⚡️ 把 metadata 从 chrome.storage.session 抽走 (#1027)
* 把 metadata 从 chrome.storage.session 抽取 * Update src/pages/components/ScriptMenuList/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/pages/components/ScriptMenuList/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * added i18nLang mock --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b1a738d commit 9c81f6c

File tree

6 files changed

+98
-31
lines changed

6 files changed

+98
-31
lines changed

src/app/service/service_worker/popup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type { SystemConfig } from "@App/pkg/config/config";
2020
import { CACHE_KEY_TAB_SCRIPT } from "@App/app/cache_key";
2121
import { timeoutExecution } from "@App/pkg/utils/timer";
2222
import { v5 as uuidv5 } from "uuid";
23-
import { getCombinedMeta } from "./utils";
2423

2524
const enum ScriptMenuRegisterType {
2625
REGISTER = 1,
@@ -370,9 +369,10 @@ export class PopupService {
370369
run.isEffective = o.effective!;
371370
run.hasUserConfig = !!script.config;
372371
} else {
373-
if (script.selfMetadata) {
374-
script.metadata = getCombinedMeta(script.metadata, script.selfMetadata);
375-
}
372+
// 由于目前没有在 Popup 显示 @match @include @exclude, 所以以下代码暂不需要
373+
// if (script.selfMetadata) {
374+
// script.metadata = getCombinedMeta(script.metadata, script.selfMetadata);
375+
// }
376376
run = scriptToMenu(script);
377377
run.isEffective = o.effective!;
378378
}

src/app/service/service_worker/popup_scriptmenu.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export const scriptToMenu = (script: Script): ScriptMenu => {
1414
enable: script.status === SCRIPT_STATUS_ENABLE,
1515
updatetime: script.updatetime || 0,
1616
hasUserConfig: !!script.config,
17-
metadata: script.metadata,
17+
// 不需要完整 metadata。目前在 Popup 未使用 metadata。
18+
// 有需要时请把 metadata 里需要的部份抽出 (例如 @match @include @exclude),避免 chrome.storage.session 储存量过大
19+
// metadata: script.metadata,
1820
runStatus: script.runStatus,
1921
runNum: script.type === SCRIPT_TYPE_NORMAL ? 0 : script.runStatus === SCRIPT_RUN_STATUS_RUNNING ? 1 : 0,
2022
runNumByIframe: 0,

src/app/service/service_worker/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Script, SCRIPT_RUN_STATUS, SCMetadata, ScriptLoadInfo } from "@App/app/repo/scripts";
1+
import type { Script, SCRIPT_RUN_STATUS, ScriptLoadInfo } from "@App/app/repo/scripts";
22
import { type URLRuleEntry } from "@App/pkg/utils/url_matcher";
33
import { type IGetSender } from "@Packages/message/server";
44

@@ -192,7 +192,9 @@ export type ScriptMenu = {
192192
enable: boolean; // 脚本是否启用
193193
updatetime: number; // 脚本更新时间
194194
hasUserConfig: boolean; // 是否有用户配置
195-
metadata: SCMetadata; // 脚本元数据
195+
// 不需要完整 metadata。目前在 Popup 未使用 metadata。
196+
// 有需要时请把 metadata 里需要的部份抽出 (例如 @match @include @exclude),避免 chrome.storage.session 储存量过大
197+
// metadata: SCMetadata; // 脚本元数据
196198
runStatus?: SCRIPT_RUN_STATUS; // 脚本运行状态
197199
runNum: number; // 脚本运行次数
198200
runNumByIframe: number; // iframe运行次数

src/locales/locales.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,21 @@ export function watchLanguageChange(callback: (lng: string) => void) {
9898
};
9999
}
100100

101+
export const i18nLang = (): string => `${i18n?.language?.toLowerCase()}`;
102+
101103
export function i18nName(script: { name: string; metadata: SCMetadata }) {
102-
const m = script.metadata[`name:${i18n?.language?.toLowerCase()}`];
104+
const m = script.metadata[`name:${i18nLang()}`];
103105
return m ? m[0] : script.name;
104106
}
105107

106108
export function i18nDescription(script: { metadata: SCMetadata }) {
107-
const m = script.metadata[`description:${i18n?.language?.toLowerCase()}`];
109+
const m = script.metadata[`description:${i18nLang()}`];
108110
return m ? m[0] : script.metadata.description;
109111
}
110112

111113
// 判断是否是中文用户
112114
export function isChineseUser() {
113-
const language = i18n?.language?.toLowerCase();
115+
const language = i18nLang();
114116
return language.startsWith("zh-");
115117
}
116118

src/pages/components/ScriptMenuList/index.tsx

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
IconPlus,
2222
IconSettings,
2323
} from "@arco-design/web-react/icon";
24-
import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts";
24+
import type { SCMetadata } from "@App/app/repo/scripts";
25+
import { SCRIPT_RUN_STATUS_RUNNING, ScriptDAO } from "@App/app/repo/scripts";
2526
import { RiPlayFill, RiStopFill } from "react-icons/ri";
2627
import { useTranslation } from "react-i18next";
2728
import { ScriptIcons } from "@App/pages/options/routes/utils";
@@ -32,7 +33,10 @@ import type {
3233
ScriptMenuItemOption,
3334
} from "@App/app/service/service_worker/types";
3435
import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script";
35-
import { i18nName } from "@App/locales/locales";
36+
import { i18nLang, i18nName } from "@App/locales/locales";
37+
38+
// 用于读取 metadata
39+
const scriptDAO = new ScriptDAO();
3640

3741
const CollapseItem = Collapse.Item;
3842

@@ -114,8 +118,8 @@ const MenuItem = React.memo(({ menuItems, uuid }: MenuItemProps) => {
114118
MenuItem.displayName = "MenuItem";
115119

116120
interface CollapseHeaderProps {
117-
item: ScriptMenu;
118-
onEnableChange: (item: ScriptMenu, checked: boolean) => void;
121+
item: ScriptMenuEntry;
122+
onEnableChange: (item: ScriptMenuEntry, checked: boolean) => void;
119123
}
120124

121125
const CollapseHeader = React.memo(
@@ -164,12 +168,12 @@ const CollapseHeader = React.memo(
164168
CollapseHeader.displayName = "CollapseHeader";
165169

166170
interface ListMenuItemProps {
167-
item: ScriptMenu;
171+
item: ScriptMenuEntry;
168172
scriptMenus: GroupScriptMenuItemsProp;
169173
menuExpandNum: number;
170174
isBackscript: boolean;
171175
url: URL | null;
172-
onEnableChange: (item: ScriptMenu, checked: boolean) => void;
176+
onEnableChange: (item: ScriptMenuEntry, checked: boolean) => void;
173177
handleDeleteScript: (uuid: string) => void;
174178
}
175179

@@ -320,6 +324,13 @@ ListMenuItem.displayName = "ListMenuItem";
320324

321325
type TGrouppedMenus = Record<string, GroupScriptMenuItemsProp> & { __length__?: number };
322326

327+
type ScriptMenuEntry = ScriptMenu & {
328+
menuUpdated?: number;
329+
metadata: SCMetadata;
330+
};
331+
332+
let scriptDataAsyncCounter = 0;
333+
323334
// Popup 页面使用的脚本/选单清单元件:只负责渲染与互动,状态与持久化交由外部 client 处理。
324335
const ScriptMenuList = React.memo(
325336
({
@@ -335,23 +346,30 @@ const ScriptMenuList = React.memo(
335346
currentUrl: string;
336347
menuExpandNum: number;
337348
}) => {
338-
const [list, setList] = useState(
339-
[] as (ScriptMenu & {
340-
menuUpdated?: number;
341-
})[]
342-
);
349+
// extraData 为 undefined 时先等待异步加载完成,避免重复渲染
350+
const [extraData, setExtraData] = useState<
351+
| {
352+
uuids: string;
353+
lang: string;
354+
metadata: Record<string, SCMetadata>;
355+
}
356+
| undefined
357+
>(undefined);
358+
const [scriptMenuList, setScriptMenuList] = useState<ScriptMenuEntry[]>([]);
343359
const { t } = useTranslation();
344360

345361
const [grouppedMenus, setGrouppedMenus] = useState<TGrouppedMenus>({});
346362

347-
// 依 groupKey 进行聚合:将同语义(mainframe/subframe)命令合并为单一分组以供 UI 呈现。
348-
useEffect(() => {
349-
const list_ = list;
363+
const updateScriptMenuList = (scriptMenuList: ScriptMenuEntry[]) => {
364+
setScriptMenuList(scriptMenuList);
365+
// 因为 scriptMenuList 的修改只在这处。
366+
// 直接在这里呼叫 setGrouppedMenus, 不需要 useEffect
350367
setGrouppedMenus((prev) => {
368+
// 依 groupKey 进行聚合:将同语义(mainframe/subframe)命令合并为单一分组以供 UI 呈现。
351369
const ret = {} as TGrouppedMenus;
352370
let changed = false;
353371
let retLen = 0;
354-
for (const { uuid, menus, menuUpdated: m } of list_) {
372+
for (const { uuid, menus, menuUpdated: m } of scriptMenuList) {
355373
retLen++;
356374
const menuUpdated = m || 0;
357375
if (prev[uuid]?.menuUpdated === menuUpdated) {
@@ -383,7 +401,7 @@ const ScriptMenuList = React.memo(
383401
// 若无引用变更则维持原物件以降低重渲染
384402
return changed ? ret : prev;
385403
});
386-
}, [list]);
404+
};
387405

388406
const url = useMemo(() => {
389407
let url: URL;
@@ -399,8 +417,50 @@ const ScriptMenuList = React.memo(
399417
return url;
400418
}, [currentUrl]);
401419

420+
// string memo 避免 uuids 以外的改变影响
421+
const uuids = useMemo(() => script.map((item) => item.uuid).join("\n"), [script]);
422+
// eslint-disable-next-line react-hooks/exhaustive-deps
423+
const lang = useMemo(() => i18nLang(), [t]); // 当 t 改变时,重新检查当前页面语言
424+
425+
// 以 异步方式 取得 metadata 放入 extraData
426+
// script 或 extraData 的更新时都会再次执行
427+
useEffect(() => {
428+
if (extraData && extraData.uuids === uuids && extraData.lang === lang) {
429+
// extraData 已取得
430+
// 把 getPopupData() 的 scriptMenuList 和 异步结果 的 metadata 合并至 scriptMenuList
431+
const metadata = extraData.metadata;
432+
const newScriptMenuList = script.map((item) => ({ ...item, metadata: metadata[item.uuid] || {} }));
433+
updateScriptMenuList(newScriptMenuList);
434+
} else {
435+
// 取得 extraData
436+
scriptDataAsyncCounter = (scriptDataAsyncCounter % 255) + 1; // 轮出 1 ~ 255
437+
const lastCounter = scriptDataAsyncCounter;
438+
scriptDAO.gets(uuids.split("\n")).then((res) => {
439+
if (lastCounter !== scriptDataAsyncCounter) {
440+
// 由于 state 改变,在结果取得前 useEffect 再次执行,因此需要忽略上次结果
441+
return;
442+
}
443+
const metadataRecord = {} as Record<string, SCMetadata>;
444+
const nameKey = `name:${lang}`;
445+
for (const entry of res) {
446+
if (entry) {
447+
const m = entry.metadata;
448+
const [icon] = m.icon || m.iconurl || m.icon64 || m.icon64url || [];
449+
// metadataRecord 的储存量不影响 storage.session 但影响页面的记忆体
450+
// 按需要可以增加其他 metadata, 例如 @match @include @exclude
451+
metadataRecord[entry.uuid] = {
452+
icon: [icon], // 只储存单个 icon
453+
[nameKey]: [i18nName(entry)], // 只储存 i18n 的 name
454+
} satisfies SCMetadata;
455+
}
456+
}
457+
setExtraData({ uuids, lang, metadata: metadataRecord });
458+
// 再次触发 useEffect
459+
});
460+
}
461+
}, [script, uuids, lang, extraData]);
462+
402463
useEffect(() => {
403-
setList(script);
404464
// 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。
405465
const checkItems = new Map();
406466
for (const [_uuid, menus] of Object.entries(grouppedMenus)) {
@@ -429,7 +489,7 @@ const ScriptMenuList = React.memo(
429489
checkItems.clear();
430490
document.removeEventListener("keypress", sharedKeyPressListner);
431491
};
432-
}, [script, grouppedMenus]);
492+
}, [grouppedMenus]);
433493

434494
const handleDeleteScript = (uuid: string) => {
435495
// 本地先行移除列表项(乐观更新);若删除失败会显示错误讯息。
@@ -438,18 +498,18 @@ const ScriptMenuList = React.memo(
438498
});
439499
};
440500

441-
const onEnableChange = (item: ScriptMenu, checked: boolean) => {
501+
const onEnableChange = (item: ScriptMenuEntry, checked: boolean) => {
442502
scriptClient.enable(item.uuid, checked).catch((err) => {
443503
Message.error(err);
444504
});
445505
};
446506

447507
return (
448508
<>
449-
{list.length === 0 ? (
509+
{scriptMenuList.length === 0 ? (
450510
<Empty description={t("no_data")} />
451511
) : (
452-
list.map((item, _index) => (
512+
scriptMenuList.map((item, _index) => (
453513
<ListMenuItem
454514
key={`${item.uuid}`}
455515
url={url}

tests/pages/popup/App.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ vi.mock("@App/locales/locales", () => ({
8181
localePath: "",
8282
initLocales: vi.fn(),
8383
changeLanguage: vi.fn(),
84+
i18nLang: vi.fn((): string => "en"),
8485
i18nName: vi.fn((script: any) => script.name),
8586
i18nDescription: vi.fn((script: any) => script.metadata?.description || ""),
8687
matchLanguage: () => Promise.resolve(undefined),

0 commit comments

Comments
 (0)