From aaefc9c3fcbf8a1f4a47ca6209fa52f5f00f9e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 12 Dec 2025 00:01:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(sidepanel):=20=E5=AE=9E=E7=8E=B0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E7=BB=84=E4=BB=B6=E7=9A=84=E6=9E=B6=E6=9E=84=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B7=AF=E7=94=B1=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 BaseComponent 基类统一管理事件、定时器和脚本加载 - 重构 timestamp 模块为继承自 BaseComponent 的类组件 - 新增 ScriptManager 工具类用于动态加载和卸载脚本 - 修改路由配置支持按需加载页面对应的 JS 模块 - 路由切换时自动销毁旧组件实例以防止内存泄漏 - 更新文件引用路径适配新的目录结构 - 修复 rollup 配置中的语法问题及端口配置 - 移除无用的日志输出和冗余代码提升性能 --- rollup.config.js | 6 +- sidepanel/js/components/BaseComponet.js | 70 +++++++++ sidepanel/js/index.js | 11 +- sidepanel/js/timestamp.js | 168 ++++++++++----------- sidepanel/{ => js}/utils/domUtils.js | 0 sidepanel/{ => js}/utils/eventManager.js | 0 sidepanel/js/utils/scriptManager.js | 98 ++++++++++++ sidepanel/{ => js}/utils/timerManager.js | 3 +- sidepanel/{ => js}/utils/timestampUtils.js | 0 sidepanel/modules/Router.js | 47 +++--- sidepanel/modules/eventHandlers.js | 8 +- sidepanel/utils/scriptManager.js | 115 -------------- 12 files changed, 281 insertions(+), 245 deletions(-) create mode 100644 sidepanel/js/components/BaseComponet.js rename sidepanel/{ => js}/utils/domUtils.js (100%) rename sidepanel/{ => js}/utils/eventManager.js (100%) create mode 100644 sidepanel/js/utils/scriptManager.js rename sidepanel/{ => js}/utils/timerManager.js (95%) rename sidepanel/{ => js}/utils/timestampUtils.js (100%) delete mode 100644 sidepanel/utils/scriptManager.js diff --git a/rollup.config.js b/rollup.config.js index 5c924d4..25204e5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,4 @@ -import { defineConfig } from 'rollup'; +import {defineConfig} from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; @@ -14,7 +14,7 @@ const devPlugins = [ }), serve({ open: true, - port: 8082, + port: 3000, contentBase: ['.', 'sidepanel'], headers: { 'Access-Control-Allow-Origin': '*', @@ -42,4 +42,4 @@ export default defineConfig([ }, plugins: [...basePlugins, ...devPlugins], }, -]); +]); \ No newline at end of file diff --git a/sidepanel/js/components/BaseComponet.js b/sidepanel/js/components/BaseComponet.js new file mode 100644 index 0000000..dede0ee --- /dev/null +++ b/sidepanel/js/components/BaseComponet.js @@ -0,0 +1,70 @@ +export class BaseComponent { + constructor() { + this._timerManager = window.timerManager; + this._scriptManager = window.scriptManager; + this._eventManager = window.eventManager; + } + + /** + * 安全绑定事件,并自动记录以记录销毁 + * @param {string | HTMLElement} target 目标元素 + * @param {string} type 事件类型 + * @param {Function} handler 回调函数 + */ + bindEvent(target, type, handler) { + + console.log('Bind 1111;') + // 获取目标元素 + const ele = typeof target === 'string' ? document.getElementById(target) : target; + + if (!ele) { + console.error(`Element with ID ${target} not found.`); + return; + } + + ele.addEventListener(type, handler); + + this._eventManager.add(ele, type, handler); + } + + /** + * 创建定时器 + * @param {Function} fn 定时器回调函数 + * @param {number} ms 定时器间隔时间(毫秒) + */ + setInterval(fn, ms) { + this._timerManager.setInterval(fn, ms); + } + + /** + * 创建定时器 + * @param {Function} fn 定时器回调函数 + * @param {number} ms 定时器间隔时间(毫秒) + */ + setTimeout(fn, ms) { + this._timerManager.setTimeout(fn, ms); + } + + /** + * 加载脚本 + * @param {string} path 脚本路径 + * @param {string} name 脚本名称 + * @param {boolean} isModule 是否为模块 + * @param {string[]} deps 依赖的脚本 + */ + loadScript(path, name, isModule = false, deps = []) { + this._scriptManager.loadScript({path, name, isModule, deps}); + } + + /** + * 销毁组件 + */ + destroy() { + this._eventManager.removeAll(); + this._timerManager.cleanAll(); + this._scriptManager.unloadScript().then(r => console.log(r)); + console.log('destroy success'); + } +} + +window.BaseComponent = BaseComponent; diff --git a/sidepanel/js/index.js b/sidepanel/js/index.js index dea687d..e8c5298 100644 --- a/sidepanel/js/index.js +++ b/sidepanel/js/index.js @@ -1,7 +1,7 @@ -import { ScriptManager } from '../utils/scriptManager.js'; -import { TimerManager } from '../utils/timerManager.js'; -import { EventManager } from '../utils/eventManager.js'; -import { Router } from '../modules/Router.js'; +import {ScriptManager} from './utils/scriptManager.js'; +import {TimerManager} from './utils/timerManager.js'; +import {EventManager} from './utils/eventManager.js'; +import {Router} from '../modules/Router.js'; const config = { routerViewId: 'app', @@ -9,8 +9,9 @@ const config = { routes: [ { path: '/', - name: 'redirect', + name: 'timestamp', html: 'pages/timestamp.html', + script: '../dist/timestamp.js', }, { path: '/home', diff --git a/sidepanel/js/timestamp.js b/sidepanel/js/timestamp.js index 434a667..5efff57 100644 --- a/sidepanel/js/timestamp.js +++ b/sidepanel/js/timestamp.js @@ -1,108 +1,94 @@ import { - handleToggleUnit, - handleStopTimer, - handleStartTimer, - handleConvertTimestamp, - handleConvertDateToTimestamp, - handleCopyTimestamp, handleClosePanel, - handleTimezoneResult, + handleConvertDateToTimestamp, + handleConvertTimestamp, + handleCopyTimestamp, + handleStartTimer, + handleStopTimer, handleTimezoneInput, + handleTimezoneResult, + handleToggleUnit, } from '../modules/eventHandlers.js'; -import { updateTimestamp } from '../utils/timestampUtils.js'; -import { addEventListenerById } from '../utils/domUtils.js'; -import { timeZoneList } from './const/timezone.js'; +import {updateTimestamp} from './utils/timestampUtils.js'; +import {timeZoneList} from './const/timezone.js'; + +const BaseComponent = window.BaseComponent; -// 定义一个全局变量来保存是否显示毫秒 -let showMilliseconds = true; -// 定义一个全局变量来保存计时器 -let timestampInterval; // 获取本地时区 const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone; -/** - * 初始化时间戳显示 - */ -function initTimestampDisplay() { - const currentTimestampValue = document.getElementById('current-timestamp-value'); - - // 初始化时更新一次时间戳 - updateTimestamp(currentTimestampValue, showMilliseconds); - - // 添加时间戳定时器 - if (timestampInterval) { - console.log('clearInterval'); - clearInterval(timestampInterval); - window.timerManager.clearInterval(timestampInterval); +class Timestamp extends BaseComponent { + constructor() { + super(); + this.init(); + // 定义一个全局变量来保存是否显示毫秒 + this.showMilliseconds = true; + // 定义一个全局变量来保存计时器 + this.timestampInterval = null; } - timestampInterval = setInterval( - () => updateTimestamp(currentTimestampValue, showMilliseconds), - 100 - ); - window.timerManager.setInterval(timestampInterval); - initTimeZoneList(); - initTimestampAndDate(); -} + async init() { + const currentTimestampValue = document.getElementById('current-timestamp-value'); -/** - * 初始化时区列表 - */ -function initTimeZoneList() { - const timezoneResultSelect = document.getElementById('timezone-result'); - const timezoneInputSelect = document.getElementById('timezone-input'); + // 初始化时更新一次时间戳 + updateTimestamp(currentTimestampValue, this.showMilliseconds); - // 获取本地时区在列表中的索引 - const localTimeZoneIndex = timeZoneList.indexOf(localTimeZone); + this.setInterval(() => updateTimestamp(currentTimestampValue, this.showMilliseconds), 100); - for (let i = 0; i < timeZoneList.length; i++) { - if (i === localTimeZoneIndex) { - // 默认选中本地时区 - timezoneResultSelect.add(new Option(timeZoneList[i], i, true, true)); - timezoneInputSelect.add(new Option(timeZoneList[i], i, true, true)); - } else { - timezoneResultSelect.add(new Option(timeZoneList[i], i)); - timezoneInputSelect.add(new Option(timeZoneList[i], i)); + const timezoneResultSelect = document.getElementById('timezone-result'); + const timezoneInputSelect = document.getElementById('timezone-input'); + + // 获取本地时区在列表中的索引 + const localTimeZoneIndex = timeZoneList.indexOf(localTimeZone); + + for (let i = 0; i < timeZoneList.length; i++) { + if (i === localTimeZoneIndex) { + // 默认选中本地时区 + timezoneResultSelect.add(new Option(timeZoneList[i], i, true, true)); + timezoneInputSelect.add(new Option(timeZoneList[i], i, true, true)); + } else { + timezoneResultSelect.add(new Option(timeZoneList[i], i)); + timezoneInputSelect.add(new Option(timeZoneList[i], i)); + } } + + const timestampInput = document.getElementById('timestamp-input'); + const dateInput = document.getElementById('datetime-input'); + + timestampInput.value = Date.now(); + dateInput.value = new Date().toLocaleString('sv-SE'); + + this.bindBtnEvent(); + } + + bindBtnEvent() { + // 绑定时间戳相关按钮事件 + this.bindEvent( + 'toggle-unit-btn', + 'click', + () => (this.showMilliseconds = handleToggleUnit(this.showMilliseconds)) + ); + + // 绑定计时器相关按钮事件 + this.bindEvent('stop-timer-btn', 'click', () => handleStopTimer(this.timestampInterval)); + this.bindEvent( + 'start-timer-btn', + 'click', + () => (this.timestampInterval = handleStartTimer(this.timestampInterval, this.showMilliseconds)) + ); + // 绑定时间戳转换按钮事件 + this.bindEvent('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp); + // 绑定日期转换按钮事件 + this.bindEvent('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp); + // 绑定复制时间戳按钮事件 + this.bindEvent('copy-timestamp-btn', 'click', handleCopyTimestamp); + // 绑定关闭面板按钮事件 + this.bindEvent('close-panel-btn', 'click', handleClosePanel); + // 绑定切换时区事件 + this.bindEvent('timezone-result', 'change', handleTimezoneResult); + // 绑定输入时区事件 + this.bindEvent('timezone-input', 'change', handleTimezoneInput); } } -/** - * 初始化时间戳和日期输入框 - */ -function initTimestampAndDate() { - const timestampInput = document.getElementById('timestamp-input'); - const dateInput = document.getElementById('datetime-input'); - - timestampInput.value = Date.now(); - dateInput.value = new Date().toLocaleString('sv-SE'); -} - -// 初始化时间戳显示 -initTimestampDisplay(); - -// 绑定时间戳相关按钮事件 -addEventListenerById( - 'toggle-unit-btn', - 'click', - () => (showMilliseconds = handleToggleUnit(showMilliseconds)) -); -// 绑定计时器相关按钮事件 -addEventListenerById('stop-timer-btn', 'click', () => handleStopTimer(timestampInterval)); -addEventListenerById( - 'start-timer-btn', - 'click', - () => (timestampInterval = handleStartTimer(timestampInterval, showMilliseconds)) -); -// 绑定时间戳转换按钮事件 -addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp); -// 绑定日期转换按钮事件 -addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp); -// 绑定复制时间戳按钮事件 -addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp); -// 绑定关闭面板按钮事件 -addEventListenerById('close-panel-btn', 'click', handleClosePanel); -// 绑定切换时区事件 -addEventListenerById('timezone-result', 'change', handleTimezoneResult); -// 绑定输入时区事件 -addEventListenerById('timezone-input', 'change', handleTimezoneInput); +window.timestamp = Timestamp; \ No newline at end of file diff --git a/sidepanel/utils/domUtils.js b/sidepanel/js/utils/domUtils.js similarity index 100% rename from sidepanel/utils/domUtils.js rename to sidepanel/js/utils/domUtils.js diff --git a/sidepanel/utils/eventManager.js b/sidepanel/js/utils/eventManager.js similarity index 100% rename from sidepanel/utils/eventManager.js rename to sidepanel/js/utils/eventManager.js diff --git a/sidepanel/js/utils/scriptManager.js b/sidepanel/js/utils/scriptManager.js new file mode 100644 index 0000000..1574026 --- /dev/null +++ b/sidepanel/js/utils/scriptManager.js @@ -0,0 +1,98 @@ +export class ScriptManager { + constructor() { + this._loadedClasses = new Map(); + this.currentScriptEl = null; + this.currentModule = null; + this.currentName = null; + } + + async loadScript({path, name, isModule = false, deps = []}) { + console.log('loadScript', name); + if (this._loadedClasses.has(name)) { + console.log(`Component ${name} loaded from cache`); + return this._loadedClasses.get(name); + } + + for (const dep of deps) { + await this._appendScript({path: dep, isModule: false}); + } + + let componentReference = null; + + console.log(isModule); + if (isModule) { + const module = await import(path); + this.currentModule = module; + componentReference = module; + } else { + await this._appendScript({path, isModule: false}); + componentReference = window[name] || null; + } + + if (componentReference) { + this._loadedClasses.set(name, componentReference); + } + + this.currentName = name; + return componentReference; + } + + async _appendScript({path, isModule}) { + const s = document.createElement('script'); + s.src = path; + s.type = isModule ? 'module' : 'text/javascript'; + s.async = false; + + return new Promise((resolve, reject) => { + s.onload = () => { + this.currentScriptEl = s; + console.log(s); + resolve(); + }; + s.onerror = () => { + reject(new Error(`Failed to load script: ${path}`)); + } + document.body.appendChild(s); + }); + } + + async unloadScript() { + console.log('unloadScript'); + if (this.currentScriptEl) { + try { + this.currentScriptEl.remove(); + } catch (e) { + console.error(e); + } + } + + this.currentModule = null; + this.currentName = null; + this.currentScriptEl = null; + } + + async getComponentInstance(name) { + const ComponentClassReference = this._loadedClasses.get(name); + const BaseClassForCheck = window.BaseComponent; + + if (!ComponentClassReference) { + console.error('ComponentClassReference not found'); + return null + } + + const ComponentClass = ComponentClassReference.default + || ComponentClassReference; + + if (typeof ComponentClass === 'function') { + if (!(ComponentClass.prototype instanceof BaseClassForCheck)) { + console.error(`Component ${name} does not inherit BaseComponent.`); + return null; + } + + return new ComponentClass(); + } + + console.error(`Component ${name} does not exist`); + return null; + } +} diff --git a/sidepanel/utils/timerManager.js b/sidepanel/js/utils/timerManager.js similarity index 95% rename from sidepanel/utils/timerManager.js rename to sidepanel/js/utils/timerManager.js index 802eb3d..4ff5c9b 100644 --- a/sidepanel/utils/timerManager.js +++ b/sidepanel/js/utils/timerManager.js @@ -25,7 +25,6 @@ export class TimerManager { * @returns */ setInterval(fn, delay, ...args) { - console.log('setInterval in TimerManager'); const id = window.setInterval(fn, delay, ...args); this.timers.push(id); return id; @@ -52,4 +51,4 @@ export class TimerManager { }); this.timers = []; } -} \ No newline at end of file +} diff --git a/sidepanel/utils/timestampUtils.js b/sidepanel/js/utils/timestampUtils.js similarity index 100% rename from sidepanel/utils/timestampUtils.js rename to sidepanel/js/utils/timestampUtils.js diff --git a/sidepanel/modules/Router.js b/sidepanel/modules/Router.js index d2a86c1..f250e42 100644 --- a/sidepanel/modules/Router.js +++ b/sidepanel/modules/Router.js @@ -1,7 +1,3 @@ -import { TimerManager } from '../utils/timerManager.js'; -import { EventManager } from '../utils/eventManager.js'; -import { ScriptManager } from '../utils/scriptManager.js'; - class Router { constructor() { this.routes = {}; @@ -11,11 +7,11 @@ class Router { this.redirectRoute = null; this.stackPages = true; this.routerMap = []; - this.historyFlag = ''; - this.history = []; + this.scriptManager = window.scriptManager; - this.eventManager = window.eventManager; - this.timerManager = window.timerManager; + + // 页面实例 + this.currentinstance = null; } /** @@ -31,7 +27,7 @@ class Router { // 监听路由变化 window.addEventListener('hashchange', () => this.urlChange()); window.addEventListener('load', () => this.urlChange()); - window.lintTo = (path) => this.naviage(path); + window.lintTo = (path) => this.navigate(path); } map() { @@ -41,7 +37,7 @@ class Router { } for (const r of this.routerMap) { - if (r.name == 'redirect') this.redirectRoute = r.path; + if (r.name === 'redirect') this.redirectRoute = r.path; this.routes[r.path] = r; } } @@ -49,7 +45,7 @@ class Router { /** * 导航 */ - naviage(path) { + navigate(path) { window.location.hash = path; } @@ -67,27 +63,26 @@ class Router { } const doChange = async () => { - this.timerManager.cleanAll(); - this.eventManager.removeAll(); + if (this.currentinstance && typeof this.currentinstance.destroy === 'function') { + console.log(`销毁旧组件${this.currentRoute.name}`); + this.currentinstance.destroy(); + this.currentinstance = null; + } const mount = document.getElementById(this.routerViewId); if (!mount) { console.error('挂载点不存在:', this.routerViewId); return; } - if (!this.stackPages) mount.innerHTML = ''; - if (route.html) { try { - const html = await fetch(route.html).then((r) => r.text()); - mount.innerHTML = html; + mount.innerHTML = await fetch(route.html).then((r) => r.text()); } catch (e) { mount.innerHTML = `

页面加载失败:${e.message}

`; } } - console.log('当前组件:', route.script); if (route.script) { await this.scriptManager.loadScript({ path: route.script, @@ -97,17 +92,23 @@ class Router { }); } - console.log('当前组件:', route.name); - await this.scriptManager.runInit(route.name); + const newInstance = await this.scriptManager.getComponentInstance(route.name); + console.log(`newInstance${newInstance}`); + if (newInstance) { + this.currentinstance = newInstance; + if (typeof newInstance.init === 'function') { + newInstance.init(); + } + } this.currentRoute = route; if (this.afterFun) this.afterFun(route); }; if (this.beforeFun) { - this.beforeFun({ to: route, next: doChange }); + this.beforeFun({to: route, next: doChange}); } else { - doChange(); + doChange().then(r => console.log(r)); } } @@ -134,4 +135,4 @@ class Router { } } -export { Router }; +export {Router}; diff --git a/sidepanel/modules/eventHandlers.js b/sidepanel/modules/eventHandlers.js index b18b6d0..ca56de9 100644 --- a/sidepanel/modules/eventHandlers.js +++ b/sidepanel/modules/eventHandlers.js @@ -1,10 +1,6 @@ -import { - updateTimestamp, - convertTimestampToDate, - convertDateToTimestamp, -} from '../utils/timestampUtils.js'; +import {convertDateToTimestamp, convertTimestampToDate, updateTimestamp,} from '../js/utils/timestampUtils.js'; -import { timeZoneList } from '../js/const/timezone.js'; +import {timeZoneList} from '../js/const/timezone.js'; /** * 切换单位按钮事件处理器 diff --git a/sidepanel/utils/scriptManager.js b/sidepanel/utils/scriptManager.js deleted file mode 100644 index a944465..0000000 --- a/sidepanel/utils/scriptManager.js +++ /dev/null @@ -1,115 +0,0 @@ -export class ScriptManager { - constructor() { - this.currentScriptEl = null; - this.currentModule = null; - this.currentName = null; - } - - async loadScript({ path, name, isModule = false, deps = [] }) { - console.log('loadScript'); - console.log({ path, name, isModule, deps }) - await this.unloadScript(); - - for (const dep of deps) { - await this._appendScript({ path: dep, isModule: false }); - } - - if (isModule) { - const absPath = (path = '?t' + Date.now()); - this.currentModule = await import(absPath); - this.currentScriptEl = null; - this.currentName = name; - return this.currentModule; - } else { - await this._appendScript({ path, isModule: false }); - this.currentName = name; - return window[name] || null; - } - } - - async _appendScript({ path, isModule }) { - const s = document.createElement('script'); - try { - s.src = path + '?t=' + Date.now(); - s.type = isModule ? 'module' : 'text/javascript'; - s.async = false; - s.onload = () => { - this.currentScriptEl = s; - }; - } catch (e) { - throw new Error(`Failed to load script: ${path}`); - } finally { - document.body.appendChild(s); - } - } - - async unloadScript() { - console.log('unloadScript'); - try { - if (this.currentName && window[this.currentName]) { - const mod = window[this.currentName]; - const unName = this._findUnmountName(mod); - if (unName && typeof mod[unName] === 'function') { - await mod[unName](); - } - } - - if (this.currentModule) { - const mod = this.currentModule; - const unName = this._findUnmountName(mod); - if (unName && typeof mod[unName] === 'function') { - await mod[unName](); - } - } - } catch (e) { - console.error(e); - } - - if (this.currentScriptEl) { - try { - this.currentScriptEl.remove(); - } catch (e) { - console.error(e); - } - } - this.currentModule = null; - this.currentName = null; - } - - _findInitName(obj) { - if (!obj) return null; - const candidates = ['init', 'start', 'main']; - for (const c of candidates) { - if (typeof obj[c] === 'function') { - return c; - } - } - return null; - } - - _findUnmountName(obj) { - if (!obj) return null; - const candidates = ['unmount', 'stop', 'destroy']; - for (const c of candidates) { - if (typeof obj[c] === 'function') { - return c; - } - } - return null; - } - - async runInit(name) { - if (this.currentModule) { - const mod = this.currentModule; - const init = this._findInitName(mod); - if (init) return mod[init](); - return null; - } else if (name && window[name]) { - const mod = window[name]; - const init = this._findInitName(mod); - if (init) return mod[init](); - } - - return null; - } -}