feat(sidepanel): 实现基于组件的架构并优化路由管理

- 引入 BaseComponent 基类统一管理事件、定时器和脚本加载
- 重构 timestamp 模块为继承自 BaseComponent 的类组件
- 新增 ScriptManager 工具类用于动态加载和卸载脚本
- 修改路由配置支持按需加载页面对应的 JS 模块
- 路由切换时自动销毁旧组件实例以防止内存泄漏
- 更新文件引用路径适配新的目录结构
- 修复 rollup 配置中的语法问题及端口配置
- 移除无用的日志输出和冗余代码提升性能
This commit is contained in:
雨霖铃
2025-12-12 00:01:00 +08:00
parent 4a76561a85
commit aaefc9c3fc
12 changed files with 281 additions and 245 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { defineConfig } from 'rollup'; import {defineConfig} from 'rollup';
import resolve from '@rollup/plugin-node-resolve'; import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
@@ -14,7 +14,7 @@ const devPlugins = [
}), }),
serve({ serve({
open: true, open: true,
port: 8082, port: 3000,
contentBase: ['.', 'sidepanel'], contentBase: ['.', 'sidepanel'],
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
+70
View File
@@ -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;
+6 -5
View File
@@ -1,7 +1,7 @@
import { ScriptManager } from '../utils/scriptManager.js'; import {ScriptManager} from './utils/scriptManager.js';
import { TimerManager } from '../utils/timerManager.js'; import {TimerManager} from './utils/timerManager.js';
import { EventManager } from '../utils/eventManager.js'; import {EventManager} from './utils/eventManager.js';
import { Router } from '../modules/Router.js'; import {Router} from '../modules/Router.js';
const config = { const config = {
routerViewId: 'app', routerViewId: 'app',
@@ -9,8 +9,9 @@ const config = {
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'redirect', name: 'timestamp',
html: 'pages/timestamp.html', html: 'pages/timestamp.html',
script: '../dist/timestamp.js',
}, },
{ {
path: '/home', path: '/home',
+53 -67
View File
@@ -1,54 +1,40 @@
import { import {
handleToggleUnit,
handleStopTimer,
handleStartTimer,
handleConvertTimestamp,
handleConvertDateToTimestamp,
handleCopyTimestamp,
handleClosePanel, handleClosePanel,
handleTimezoneResult, handleConvertDateToTimestamp,
handleConvertTimestamp,
handleCopyTimestamp,
handleStartTimer,
handleStopTimer,
handleTimezoneInput, handleTimezoneInput,
handleTimezoneResult,
handleToggleUnit,
} from '../modules/eventHandlers.js'; } from '../modules/eventHandlers.js';
import { updateTimestamp } from '../utils/timestampUtils.js'; import {updateTimestamp} from './utils/timestampUtils.js';
import { addEventListenerById } from '../utils/domUtils.js'; import {timeZoneList} from './const/timezone.js';
import { timeZoneList } from './const/timezone.js';
const BaseComponent = window.BaseComponent;
// 定义一个全局变量来保存是否显示毫秒
let showMilliseconds = true;
// 定义一个全局变量来保存计时器
let timestampInterval;
// 获取本地时区 // 获取本地时区
const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone; const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
/** class Timestamp extends BaseComponent {
* 初始化时间戳显示 constructor() {
*/ super();
function initTimestampDisplay() { this.init();
// 定义一个全局变量来保存是否显示毫秒
this.showMilliseconds = true;
// 定义一个全局变量来保存计时器
this.timestampInterval = null;
}
async init() {
const currentTimestampValue = document.getElementById('current-timestamp-value'); const currentTimestampValue = document.getElementById('current-timestamp-value');
// 初始化时更新一次时间戳 // 初始化时更新一次时间戳
updateTimestamp(currentTimestampValue, showMilliseconds); updateTimestamp(currentTimestampValue, this.showMilliseconds);
// 添加时间戳定时器 this.setInterval(() => updateTimestamp(currentTimestampValue, this.showMilliseconds), 100);
if (timestampInterval) {
console.log('clearInterval');
clearInterval(timestampInterval);
window.timerManager.clearInterval(timestampInterval);
}
timestampInterval = setInterval(
() => updateTimestamp(currentTimestampValue, showMilliseconds),
100
);
window.timerManager.setInterval(timestampInterval);
initTimeZoneList();
initTimestampAndDate();
}
/**
* 初始化时区列表
*/
function initTimeZoneList() {
const timezoneResultSelect = document.getElementById('timezone-result'); const timezoneResultSelect = document.getElementById('timezone-result');
const timezoneInputSelect = document.getElementById('timezone-input'); const timezoneInputSelect = document.getElementById('timezone-input');
@@ -65,44 +51,44 @@ function initTimeZoneList() {
timezoneInputSelect.add(new Option(timeZoneList[i], i)); timezoneInputSelect.add(new Option(timeZoneList[i], i));
} }
} }
}
/**
* 初始化时间戳和日期输入框
*/
function initTimestampAndDate() {
const timestampInput = document.getElementById('timestamp-input'); const timestampInput = document.getElementById('timestamp-input');
const dateInput = document.getElementById('datetime-input'); const dateInput = document.getElementById('datetime-input');
timestampInput.value = Date.now(); timestampInput.value = Date.now();
dateInput.value = new Date().toLocaleString('sv-SE'); dateInput.value = new Date().toLocaleString('sv-SE');
}
// 初始化时间戳显示 this.bindBtnEvent();
initTimestampDisplay(); }
// 绑定时间戳相关按钮事件 bindBtnEvent() {
addEventListenerById( // 绑定时间戳相关按钮事件
this.bindEvent(
'toggle-unit-btn', 'toggle-unit-btn',
'click', 'click',
() => (showMilliseconds = handleToggleUnit(showMilliseconds)) () => (this.showMilliseconds = handleToggleUnit(this.showMilliseconds))
); );
// 绑定计时器相关按钮事件
addEventListenerById('stop-timer-btn', 'click', () => handleStopTimer(timestampInterval)); // 绑定计时器相关按钮事件
addEventListenerById( this.bindEvent('stop-timer-btn', 'click', () => handleStopTimer(this.timestampInterval));
this.bindEvent(
'start-timer-btn', 'start-timer-btn',
'click', 'click',
() => (timestampInterval = handleStartTimer(timestampInterval, showMilliseconds)) () => (this.timestampInterval = handleStartTimer(this.timestampInterval, this.showMilliseconds))
); );
// 绑定时间戳转换按钮事件 // 绑定时间戳转换按钮事件
addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp); this.bindEvent('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp);
// 绑定日期转换按钮事件 // 绑定日期转换按钮事件
addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp); this.bindEvent('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp);
// 绑定复制时间戳按钮事件 // 绑定复制时间戳按钮事件
addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp); this.bindEvent('copy-timestamp-btn', 'click', handleCopyTimestamp);
// 绑定关闭面板按钮事件 // 绑定关闭面板按钮事件
addEventListenerById('close-panel-btn', 'click', handleClosePanel); this.bindEvent('close-panel-btn', 'click', handleClosePanel);
// 绑定切换时区事件 // 绑定切换时区事件
addEventListenerById('timezone-result', 'change', handleTimezoneResult); this.bindEvent('timezone-result', 'change', handleTimezoneResult);
// 绑定输入时区事件 // 绑定输入时区事件
addEventListenerById('timezone-input', 'change', handleTimezoneInput); this.bindEvent('timezone-input', 'change', handleTimezoneInput);
}
}
window.timestamp = Timestamp;
+98
View File
@@ -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;
}
}
@@ -25,7 +25,6 @@ export class TimerManager {
* @returns * @returns
*/ */
setInterval(fn, delay, ...args) { setInterval(fn, delay, ...args) {
console.log('setInterval in TimerManager');
const id = window.setInterval(fn, delay, ...args); const id = window.setInterval(fn, delay, ...args);
this.timers.push(id); this.timers.push(id);
return id; return id;
+24 -23
View File
@@ -1,7 +1,3 @@
import { TimerManager } from '../utils/timerManager.js';
import { EventManager } from '../utils/eventManager.js';
import { ScriptManager } from '../utils/scriptManager.js';
class Router { class Router {
constructor() { constructor() {
this.routes = {}; this.routes = {};
@@ -11,11 +7,11 @@ class Router {
this.redirectRoute = null; this.redirectRoute = null;
this.stackPages = true; this.stackPages = true;
this.routerMap = []; this.routerMap = [];
this.historyFlag = '';
this.history = [];
this.scriptManager = window.scriptManager; 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('hashchange', () => this.urlChange());
window.addEventListener('load', () => this.urlChange()); window.addEventListener('load', () => this.urlChange());
window.lintTo = (path) => this.naviage(path); window.lintTo = (path) => this.navigate(path);
} }
map() { map() {
@@ -41,7 +37,7 @@ class Router {
} }
for (const r of this.routerMap) { 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; this.routes[r.path] = r;
} }
} }
@@ -49,7 +45,7 @@ class Router {
/** /**
* 导航 * 导航
*/ */
naviage(path) { navigate(path) {
window.location.hash = path; window.location.hash = path;
} }
@@ -67,27 +63,26 @@ class Router {
} }
const doChange = async () => { const doChange = async () => {
this.timerManager.cleanAll(); if (this.currentinstance && typeof this.currentinstance.destroy === 'function') {
this.eventManager.removeAll(); console.log(`销毁旧组件${this.currentRoute.name}`);
this.currentinstance.destroy();
this.currentinstance = null;
}
const mount = document.getElementById(this.routerViewId); const mount = document.getElementById(this.routerViewId);
if (!mount) { if (!mount) {
console.error('挂载点不存在:', this.routerViewId); console.error('挂载点不存在:', this.routerViewId);
return; return;
} }
if (!this.stackPages) mount.innerHTML = ''; if (!this.stackPages) mount.innerHTML = '';
if (route.html) { if (route.html) {
try { try {
const html = await fetch(route.html).then((r) => r.text()); mount.innerHTML = await fetch(route.html).then((r) => r.text());
mount.innerHTML = html;
} catch (e) { } catch (e) {
mount.innerHTML = `<h2>页面加载失败:${e.message}</h2>`; mount.innerHTML = `<h2>页面加载失败:${e.message}</h2>`;
} }
} }
console.log('当前组件:', route.script);
if (route.script) { if (route.script) {
await this.scriptManager.loadScript({ await this.scriptManager.loadScript({
path: route.script, path: route.script,
@@ -97,17 +92,23 @@ class Router {
}); });
} }
console.log('当前组件:', route.name); const newInstance = await this.scriptManager.getComponentInstance(route.name);
await this.scriptManager.runInit(route.name); console.log(`newInstance${newInstance}`);
if (newInstance) {
this.currentinstance = newInstance;
if (typeof newInstance.init === 'function') {
newInstance.init();
}
}
this.currentRoute = route; this.currentRoute = route;
if (this.afterFun) this.afterFun(route); if (this.afterFun) this.afterFun(route);
}; };
if (this.beforeFun) { if (this.beforeFun) {
this.beforeFun({ to: route, next: doChange }); this.beforeFun({to: route, next: doChange});
} else { } else {
doChange(); doChange().then(r => console.log(r));
} }
} }
@@ -134,4 +135,4 @@ class Router {
} }
} }
export { Router }; export {Router};
+2 -6
View File
@@ -1,10 +1,6 @@
import { import {convertDateToTimestamp, convertTimestampToDate, updateTimestamp,} from '../js/utils/timestampUtils.js';
updateTimestamp,
convertTimestampToDate,
convertDateToTimestamp,
} from '../utils/timestampUtils.js';
import { timeZoneList } from '../js/const/timezone.js'; import {timeZoneList} from '../js/const/timezone.js';
/** /**
* 切换单位按钮事件处理器 * 切换单位按钮事件处理器
-115
View File
@@ -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;
}
}