Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/pages/batchupdate/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {
} from "@App/app/service/service_worker/types";
import { dayFormat } from "@App/pkg/utils/day_format";
import { IconSync } from "@arco-design/web-react/icon";
import { useAppContext } from "../store/AppContext";
import { SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts";
import { subscribeMessage } from "@App/pages/store/global";

const CollapseItem = Collapse.Item;
const { GridItem } = Grid;
Expand All @@ -29,8 +29,6 @@ const { Text } = Typography;
const pageExecute: Record<string, (data: any) => void> = {};

function App() {
const { subscribeMessage } = useAppContext();

const AUTO_CLOSE_PAGE = 8; // after 8s, auto close
const getUrlParam = (key: string): string => {
return (location.search?.includes(`${key}=`) ? new URLSearchParams(location.search).get(`${key}`) : "") || "";
Expand Down
18 changes: 8 additions & 10 deletions src/pages/options/routes/ScriptList/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { loadScriptFavicons } from "@App/pages/store/favicons";
import { parseTags } from "@App/app/repo/metadata";
import { getCombinedMeta } from "@App/app/service/service_worker/utils";
import { cacheInstance } from "@App/app/cache";
import { useAppContext } from "@App/pages/store/AppContext";

// 组件与工具
import { type SearchFilterRequest } from "./SearchFilter";
Expand All @@ -39,6 +38,8 @@ import type {
TSortedScript,
} from "@App/app/service/queue";
import { type useTranslation } from "react-i18next";
import { subscribeMessage } from "@App/pages/store/global";
import { HookManager } from "@App/pkg/utils/hookManger";

export type TFilterKey = null | string | number;

Expand All @@ -64,7 +65,6 @@ export type TSelectFilterKeys = keyof TSelectFilter;
export function useScriptDataManagement() {
const [scriptList, setScriptList] = useState<ScriptLoading[]>([]);
const [loadingList, setLoadingList] = useState<boolean>(true);
const { subscribeMessage } = useAppContext();

// 初始化列表与 Favicon 加载
useEffect(() => {
Expand Down Expand Up @@ -188,18 +188,16 @@ export function useScriptDataManagement() {
},
} as const;

const unhooks = [
const hookMgr = new HookManager();
hookMgr.append(
subscribeMessage<TScriptRunStatus>("scriptRunStatus", pageApi.scriptRunStatus),
subscribeMessage<TInstallScript>("installScript", pageApi.installScript),
subscribeMessage<TDeleteScript[]>("deleteScripts", pageApi.deleteScripts),
subscribeMessage<TEnableScript[]>("enableScripts", pageApi.enableScripts),
subscribeMessage<TSortedScript[]>("sortedScripts", pageApi.sortedScripts),
];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
};
}, [subscribeMessage]);
subscribeMessage<TSortedScript[]>("sortedScripts", pageApi.sortedScripts)
);
return hookMgr.unhook;
}, []);

return { scriptList, setScriptList, loadingList };
}
Expand Down
17 changes: 7 additions & 10 deletions src/pages/options/routes/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ import { blackListSelfCheck } from "@App/pkg/utils/match";
import { obtainBlackList } from "@App/pkg/utils/utils";
import CustomTrans from "@App/pages/components/CustomTrans";
import { useSystemConfig } from "./utils";
import { useAppContext } from "@App/pages/store/AppContext";
import { subscribeMessage } from "@App/pages/store/global";
import { SystemConfigChange, type SystemConfigKey } from "@App/pkg/config/config";
import { type TKeyValue } from "@Packages/message/message_queue";
import { useEffect, useMemo } from "react";
import { systemConfig } from "@App/pages/store/global";
import { initRegularUpdateCheck } from "@App/app/service/service_worker/regular_updatecheck";
import { HookManager } from "@App/pkg/utils/hookManger";

function Setting() {
const { subscribeMessage } = useAppContext();

const [editorConfig, setEditorConfig, submitEditorConfig] = useSystemConfig("editor_config");
const [cloudSync, setCloudSync, submitCloudSync] = useSystemConfig("cloud_sync");
const [language, setLanguage, submitLanguage] = useSystemConfig("language");
Expand Down Expand Up @@ -84,7 +83,8 @@ function Setting() {
script_menu_display_type: setScriptMenuDisplayType,
editor_type_definition: setEditorTypeDefinition,
} as const;
const unhooks = [
const hookMgr = new HookManager();
hookMgr.append(
subscribeMessage<TKeyValue<SystemConfigKey>>(
SystemConfigChange,
({ key, value: _value }: TKeyValue<SystemConfigKey>) => {
Expand All @@ -102,12 +102,9 @@ function Setting() {
});
}
}
),
];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
};
)
);
return hookMgr.unhook;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
32 changes: 14 additions & 18 deletions src/pages/popup/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import type { ScriptMenu, TPopupScript } from "@App/app/service/service_worker/t
import { systemConfig } from "@App/pages/store/global";
import { isChineseUser, localePath } from "@App/locales/locales";
import { getCurrentTab } from "@App/pkg/utils/utils";
import { useAppContext } from "../store/AppContext";
import { subscribeMessage } from "@App/pages/store/global";
import type { TDeleteScript, TEnableScript, TScriptRunStatus } from "@App/app/service/queue";
import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts";
import { HookManager } from "@App/pkg/utils/hookManger";

const CollapseItem = Collapse.Item;

Expand Down Expand Up @@ -138,17 +139,16 @@ function App() {
return url?.hostname ?? "";
}, [currentUrl]);

const { subscribeMessage } = useAppContext();
useEffect(() => {
let isMounted = true;
const hookMgr = new HookManager();

const updateScriptList = (update: TUpdateEntryFn, options?: TUpdateListOption) => {
// 当 启用/禁用/菜单改变 时,如有必要则更新 list 参考
setScriptList((prev) => updateList(prev, update, options));
setBackScriptList((prev) => updateList(prev, update, options));
};

const unhooks = [
hookMgr.append(
// 订阅脚本啟用状态变更(enableScripts),即时更新对应项目的 enable。
subscribeMessage<TEnableScript[]>("enableScripts", (data) => {
updateScriptList((item) => {
Expand Down Expand Up @@ -196,7 +196,7 @@ function App() {
});
if (!url) return;
popupClient.getPopupData({ url, tabId }).then((resp) => {
if (!isMounted) return;
if (!hookMgr.isMounted) return;

// 响应健全性检查:必须包含 scriptList,否则忽略此次更新
if (!resp || !resp.scriptList) {
Expand Down Expand Up @@ -225,16 +225,16 @@ function App() {
);
});
}
}),
];
})
);

const onCurrentUrlUpdated = (url: string, tabId: number) => {
pageTabIdRef.current = tabId;
checkScriptEnableAndUpdate();
popupClient
.getPopupData({ url, tabId })
.then((resp) => {
if (!isMounted) return;
if (!hookMgr.isMounted) return;

// 确保响应有效
if (!resp || !resp.scriptList) {
Expand All @@ -255,14 +255,14 @@ function App() {
})
.catch((error) => {
console.error("Failed to get popup data:", error);
if (!isMounted) return;
if (!hookMgr.isMounted) return;
// 设为安全预设,避免 UI 因错误状态而崩溃
setScriptList([]);
setBackScriptList([]);
setIsBlacklist(false);
})
.finally(() => {
if (!isMounted) return;
if (!hookMgr.isMounted) return;
setLoading(false);
});
};
Expand All @@ -272,15 +272,15 @@ function App() {
systemConfig.getEnableScript(),
systemConfig.getCheckUpdate(),
]);
if (!isMounted) return;
if (!hookMgr.isMounted) return;
setIsEnableScript(isEnableScript);
setCheckUpdate(checkUpdate);
};
const queryTabInfo = async () => {
// 仅在挂载时读取一次页签信息;不绑定 currentUrl 以避免重复查询
try {
const tab = await getCurrentTab();
if (!isMounted || !tab) return;
if (!hookMgr.isMounted || !tab) return;
const newUrl = tab.url || "";
setCurrentUrl((prev) => {
if (newUrl !== prev) {
Expand All @@ -296,12 +296,8 @@ function App() {

checkScriptEnableAndUpdate();
queryTabInfo();
return () => {
isMounted = false;
for (const unhook of unhooks) unhook();
unhooks.length = 0;
};
}, [subscribeMessage]);
return hookMgr.unhook;
}, []);

const { handleEnableScriptChange, handleSettingsClick, handleNotificationClick } = {
handleEnableScriptChange: (val: boolean) => {
Expand Down
22 changes: 6 additions & 16 deletions src/pages/store/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useState, createContext, type ReactNode, useEffect, useContext } from "react";
import { messageQueue } from "./global";
import { HookManager } from "@App/pkg/utils/hookManger";
import { subscribeMessage } from "@App/pages/store/global";

export const fnPlaceHolder = {
setEditorTheme: null,
Expand All @@ -9,7 +11,6 @@ export type ThemeParam = { theme: "auto" | "light" | "dark" };
export interface AppContextType {
colorThemeState: "auto" | "light" | "dark";
updateColorTheme: (theme: "auto" | "light" | "dark") => void;
subscribeMessage: <T>(topic: string, handler: (msg: T) => void) => () => void;
// 指引模式
setGuideMode: (mode: boolean) => void;
guideMode: boolean;
Expand Down Expand Up @@ -66,15 +67,6 @@ export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
});
const [guideMode, setGuideMode] = useState(false);

const subscribeMessage = <T,>(topic: string, handler: (msg: T) => void) => {
return messageQueue.subscribe<T & { myMessage?: T }>(topic, (data) => {
const message = data?.myMessage || data;
if (typeof message === "object") {
handler(message as T);
}
});
};

useEffect(() => {
const pageApi = {
onColorThemeUpdated({ theme }: ThemeParam) {
Expand All @@ -83,11 +75,10 @@ export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
},
} as const;

const unhooks = [subscribeMessage<ThemeParam>("onColorThemeUpdated", pageApi.onColorThemeUpdated)];
return () => {
for (const unhook of unhooks) unhook();
unhooks.length = 0;
};
const hookMgr = new HookManager();
hookMgr.append(subscribeMessage<ThemeParam>("onColorThemeUpdated", pageApi.onColorThemeUpdated));

return hookMgr.unhook;
Comment on lines +78 to +81
Copy link
Member

@CodFrm CodFrm Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

就一个subscribe,为了统一的话,问题不大

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 统一写法
  2. 主要是用来避开 closure GC 问题

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 主要是用来避开 closure GC 问题

实例化一个HookManager可比closure GC开销大多了

}, []);

const updateColorTheme = (theme: "auto" | "light" | "dark") => {
Expand All @@ -100,7 +91,6 @@ export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
value={{
colorThemeState,
updateColorTheme,
subscribeMessage,
setGuideMode,
guideMode,
}}
Expand Down
9 changes: 9 additions & 0 deletions src/pages/store/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ export const globalCache = new Map<string, any>();
export const message = new ExtensionMessage();
export const systemClient = new SystemClient(message);

export const subscribeMessage = <T>(topic: string, handler: (msg: T) => void) => {
return messageQueue.subscribe<T & { myMessage?: T }>(topic, (data) => {
const message = data?.myMessage || data;
if (typeof message === "object") {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodFrm 这个改 if (message && typeof message === "object") { 是避免 message 是 null 的问题。不过没所谓。不改也行。

Copy link
Member

@CodFrm CodFrm Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个myMessage是做啥的来着,感觉也没什么地方用,也没必要用?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onScriptUpdateCheck 有用到
当初怕跟其他 message 消息传递冲突,做了一个新名字
先这样保留名字吧。日后可以再另外PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

挺奇怪的,那就日后再说吧

handler(message as T);
}
});
};

initLocales(systemConfig);
13 changes: 13 additions & 0 deletions src/pkg/utils/hookManger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class HookManager {
public isMounted: boolean = true;
private unhooks: ((...args: any) => any)[] | null = [];
public append(...fns: ((...args: any) => any)[]) {
this.unhooks!.push(...fns);
}
public readonly unhook = () => {
this.isMounted = false;
for (const unhook of this.unhooks!) unhook();
this.unhooks!.length = 0;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HookManager 目前通过非空断言使用 this.unhooks(append / unhook 内的 this.unhooks!)。如果 unhook 被重复调用,或在 unhook 之后误调用 append,会直接抛运行时异常。建议让 unhook/append 在已 unhook 状态下幂等(例如 unhooks 为 null 时直接 return),并把 unhooks 的函数类型收敛为 () => void,减少 any 带来的误用。

Suggested change
private unhooks: ((...args: any) => any)[] | null = [];
public append(...fns: ((...args: any) => any)[]) {
this.unhooks!.push(...fns);
}
public readonly unhook = () => {
this.isMounted = false;
for (const unhook of this.unhooks!) unhook();
this.unhooks!.length = 0;
// 存储卸载时调用的钩子函数;unhook 后置为 null
private unhooks: (() => void)[] | null = [];
public append(...fns: (() => void)[]) {
// 已经 unhook 的情况下保持幂等,直接忽略追加
if (!this.unhooks) return;
this.unhooks.push(...fns);
}
public readonly unhook = () => {
// 已经 unhook 过则保持幂等
if (!this.unhooks) return;
this.isMounted = false;
for (const unhook of this.unhooks) {
unhook();
}
this.unhooks.length = 0;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

雖然好像沒必要。但小心為上吧
80b1378

this.unhooks = null;
};
}
1 change: 1 addition & 0 deletions tests/pages/options/MainLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ vi.mock("@App/pages/store/global", () => ({
error: vi.fn(),
warning: vi.fn(),
},
subscribeMessage: () => vi.fn(),
}));

vi.mock("@App/pkg/utils/utils", () => ({
Expand Down
1 change: 1 addition & 0 deletions tests/pages/popup/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ vi.mock("@App/pages/store/global", () => ({
error: vi.fn(),
warning: vi.fn(),
},
subscribeMessage: () => vi.fn(),
}));

vi.mock("@App/pkg/utils/utils", () => ({
Expand Down
Loading