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
+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 { 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',
+77 -91
View File
@@ -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;
+76
View File
@@ -0,0 +1,76 @@
/**
* DOM工具类函数
*/
/**
* 为元素添加事件监听器
* @param {string} id - 元素ID
* @param {string} event - 事件类型
* @param {Function} handler - 事件处理函数
*/
export function addEventListenerById(id, event, handler) {
const ele = document.getElementById(id);
if (ele) {
window.eventManager.add(ele, event, handler);
}
}
/**
* 判断元素是否包含指定类名
* @param {*} elem
* @param {*} cls
* @returns
*/
export function hasClass(elem, cls) {
cls = cls || '';
// 检测类名是否为''
if (cls.replace(/\s/g, '').length === 0) return false;
// 检测elem类名是否包含cls指定类名
return new RegExp(' ' + cls + ' ').test(' ' + elem.className + ' ');
}
/**
* 为元素添加类名
* @param {*} elem
* @param {*} cls
*/
export function addClass(elem, cls) {
if (!elem || !cls) return;
// 统一空白字符(避免 \n \t 等导致匹配问题)
let current = elem.className.replace(/[\t\r\n]/g, ' ').trim();
// 已存在则忽略
const classes = current.split(/\s+/);
if (classes.includes(cls)) return;
// 添加并规整
classes.push(cls);
elem.className = classes.join(' ').trim();
}
/**
* 为元素删除类名
* @param {*} elem
* @param {*} cls
*/
export function removeClass(elem, cls) {
if (!elem || !cls) return;
// 使用 classList 优先(更安全)
if (elem.classList) {
elem.classList.remove(cls);
return;
}
// 传统写法的修复版本
let klass = ' ' + elem.className.replace(/[\t\r\n]/g, ' ') + ' ';
// 持续删除目标 class
while (klass.indexOf(' ' + cls + ' ') !== -1) {
klass = klass.replace(' ' + cls + ' ', ' ');
}
// 过滤多余空格
elem.className = klass.trim().replace(/\s+/g, ' ');
}
+32
View File
@@ -0,0 +1,32 @@
export class EventManager {
constructor() {
this.listeners = [];
}
add(ele, type, handler, options) {
ele.addEventListener(type, handler, options);
this.listeners.push({ ele, type, handler, options });
}
remove(ele, type, handler, options) {
ele.removeEventListener(type, handler, options);
this.listeners = this.listeners.filter(
(listener) =>
listener.ele !== ele ||
listener.type !== type ||
listener.handler !== handler ||
listener.options !== options
);
}
removeAll() {
this.listeners.forEach((listener) => {
listener.ele.removeEventListener(
listener.type,
listener.handler,
listener.options
);
});
this.listeners = [];
}
}
+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;
}
}
+54
View File
@@ -0,0 +1,54 @@
export class TimerManager {
constructor() {
// 定时器对象数组
this.timers = [];
}
/**
* 添加定时器到数组中
* @param {*} fn
* @param {*} delay
* @param {...any} args
* @returns
*/
setTimeout(fn, delay, ...args) {
const id = window.setTimeout(fn, delay, ...args);
this.timers.push(id);
return id;
}
/**
* 添加定时器到数组中
* @param {*} fn
* @param {*} delay
* @param {...any} args
* @returns
*/
setInterval(fn, delay, ...args) {
const id = window.setInterval(fn, delay, ...args);
this.timers.push(id);
return id;
}
/**
* 清除定时器
* @param {*} id
*/
clearTimeout(id) {
window.clearTimeout(id);
this.timers = this.timers.filter((timerId) => timerId !== id);
}
clearInterval(id) {
window.clearInterval(id);
this.timers = this.timers.filter((timerId) => timerId !== id);
}
cleanAll() {
this.timers.forEach((timerId) => {
window.clearTimeout(timerId);
window.clearInterval(timerId);
});
this.timers = [];
}
}
+113
View File
@@ -0,0 +1,113 @@
/**
* 时间戳工具类函数
*/
/**
* 更新时间戳显示
* @param {*} ele
* @param {*} showMilliseconds
*/
export function updateTimestamp(ele, showMilliseconds = true) {
const timestamp = Date.now();
ele.textContent = showMilliseconds ? timestamp : Math.floor(timestamp / 1000);
}
/**
* 转换时间戳为日期格式
* @param {string} timestamp - 输入的时间戳
* @param {boolean} isSeconds - 是否为秒格式
* @param {string} timeZone - 时区,默认为本地时区
* @returns {string} 格式化后的日期字符串
*/
export function convertTimestampToDate(
timestamp,
isSeconds,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
) {
// 转换为数字
let timestampNum = Number(timestamp);
// 如果是秒,转换为毫秒
if (isSeconds) {
timestampNum = timestampNum * 1000;
}
// 检查是否为有效数字
if (isNaN(timestampNum)) {
return '请输入有效的时间戳';
}
// 创建日期对象
const date = new Date(timestampNum);
// 检查日期是否有效
if (isNaN(date.getTime())) {
return '无效的日期';
}
// 使用Intl.DateTimeFormat来根据时区格式化日期
const formatter = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timeZone,
});
return formatter.format(date);
}
/**
* 转换日期格式为时间戳
* @param {string|Date} date - 输入的日期字符串或Date对象
* @param {string} timeZone - 时区,默认为本地时区
* @returns {number|string} 时间戳或错误信息
*/
export function convertDateToTimestamp(
date,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
) {
try {
// 创建日期对象
const dateObj = new Date(date);
// 检查日期是否有效
if (isNaN(dateObj.getTime())) {
return '无效的日期';
}
// 如果提供了时区,则需要特殊处理
if (timeZone) {
// 获取给定时区相对于UTC的时间差(毫秒)
const utc = dateObj.getTime() + dateObj.getTimezoneOffset() * 60000;
// 计算目标时区相对于UTC的偏移量
const targetOffset = getTimeZoneOffset(timeZone);
// 返回目标时区对应的时间戳
const targetTimestamp = utc + targetOffset;
return targetTimestamp;
} else {
// 没有时区参数,直接返回时间戳
const timestamp = dateObj.getTime();
console.log('timestamp: ', timestamp);
return timestamp;
}
} catch (error) {
return '日期转换错误: ' + error.message;
}
}
/**
* 获取指定时区相对于UTC的偏移量(毫秒)
* @param {string} timeZone - 时区名称
* @returns {number} 偏移量(毫秒)
*/
function getTimeZoneOffset(timeZone) {
const now = new Date();
const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
const target = new Date(now.toLocaleString('en-US', { timeZone: timeZone }));
return target.getTime() - utc.getTime();
}