From 2761dbe13f4b46a8685ffe1e0541ff5a5401cf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Sun, 30 Nov 2025 23:34:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(sidepanel):=20=E9=87=8D=E6=9E=84=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=B3=BB=E7=BB=9F=E5=B9=B6=E4=BC=98=E5=8C=96=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除冗余的开发预览页面 `index.html` - 路由配置改为声明式结构,支持自动加载 HTML 和脚本 - 新增 `data-route` 属性用于导航按钮绑定路由路径 - 实现基于事件委托的路由跳转机制 - 重写 `Router.js` 核心逻辑,支持异步加载页面模块 - 改进 `domUtils.js` 中 `addClass` 和 `removeClass` 方法兼容性与健壮性 - 新增 `scriptLoader.js` 工具模块用于动态加载和清理页面脚本 - 时间戳页面初始化方式调整为 IIFE 并挂载至全局作用域 --- index.html | 32 --- sidepanel/index.html | 4 +- sidepanel/js/index.js | 53 ++--- sidepanel/js/timestamp.js | 48 ++--- sidepanel/modules/Router.js | 349 +++++++++++--------------------- sidepanel/utils/domUtils.js | 42 ++-- sidepanel/utils/scriptLoader.js | 30 +++ 7 files changed, 225 insertions(+), 333 deletions(-) delete mode 100644 index.html create mode 100644 sidepanel/utils/scriptLoader.js diff --git a/index.html b/index.html deleted file mode 100644 index a22fda8..0000000 --- a/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - Testing Tool - Development - - - -
-

Testing Tool - Development Preview

-

这是一个用于开发预览的页面,下方是sidepanel的实时预览。

- -
- - \ No newline at end of file diff --git a/sidepanel/index.html b/sidepanel/index.html index 9dfabcf..57812ef 100644 --- a/sidepanel/index.html +++ b/sidepanel/index.html @@ -10,8 +10,8 @@
diff --git a/sidepanel/js/index.js b/sidepanel/js/index.js index d522987..bb07739 100644 --- a/sidepanel/js/index.js +++ b/sidepanel/js/index.js @@ -1,48 +1,21 @@ console.log('Before creating router'); import { Router } from '../modules/Router.js'; -import * as PageModules from '../../dist/timestamp.js'; - const config = { - routerViewId: 'app', // 路由切换的挂载点 id - stackPages: false, // 多级页面缓存 - animationName: 'fade', // 切换页面时的动画 + routerViewId: 'app', + stackPages: false, routes: [ { path: '/home', - name: 'home', - callback: async () => { - // 1. 加载 HTML - const html = await fetch('pages/timestamp.html').then((r) => r.text()); - document.getElementById('home').innerHTML = html; - - // 2. 动态加载对应 JS - const script = document.createElement('script'); - script.src = '/dist/timestamp.js'; // IIFE 打包后的文件 - script.onload = () => { - // 3. 执行 IIFE 暴露的全局对象方法 - TimestampPage.init(); // 假设 timestamp.js 输出 name: 'TimestampPage' - }; - document.body.appendChild(script); - }, + name: 'timestamp', + html: 'pages/timestamp.html', + script: '../dist/timestamp.js', }, { path: '/todo', name: 'todo', - callback: async () => { - // 1. 加载 HTML - const html = await fetch('pages/todolist.html').then((r) => r.text()); - document.getElementById('todo').innerHTML = html; - - // 2. 动态加载对应 JS - const script = document.createElement('script'); - script.src = '/dist/timestamp.js'; // IIFE 打包后的文件 - script.onload = () => { - // 3. 执行 IIFE 暴露的全局对象方法 - TimestampPage.init(); // 假设 timestamp.js 输出 name: 'TimestampPage' - }; - document.body.appendChild(script); - }, + html: 'pages/todolist.html', + script: '', }, ], }; @@ -52,7 +25,13 @@ console.log('Router created:', router); router.init(config); console.log('Router initialized'); -document.getElementById('todo-jump').addEventListener('click', () => { - console.log('Jumping to todo page'); - window.linkTo('#/todo'); +document.addEventListener('click', (event) => { + const btn = event.target.closest('[data-route]'); + if (!btn) return; + + const path = btn.getAttribute('data-route'); + if (!path) return; + + // 触发 hash 路由跳转 + window.location.hash = path; }); diff --git a/sidepanel/js/timestamp.js b/sidepanel/js/timestamp.js index 8291fe4..de167dd 100644 --- a/sidepanel/js/timestamp.js +++ b/sidepanel/js/timestamp.js @@ -12,27 +12,29 @@ import { } from '../modules/eventHandlers.js'; import { addEventListenerById } from '../utils/domUtils.js'; -export function init() { - console.log('timestamp init') - // 初始化时间戳显示 - initTimestampDisplay(); +(function (global) { + global.timestampInit = async function () { + console.log('timestamp init'); + // 初始化时间戳显示 + initTimestampDisplay(); - // 绑定事件处理器 - // 绑定时间戳相关按钮事件 - addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit); - // 绑定计时器相关按钮事件 - addEventListenerById('stop-timer-btn', 'click', handleStopTimer); - addEventListenerById('start-timer-btn', 'click', handleStartTimer); - // 绑定时间戳转换按钮事件 - 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); -} + // 绑定事件处理器 + // 绑定时间戳相关按钮事件 + addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit); + // 绑定计时器相关按钮事件 + addEventListenerById('stop-timer-btn', 'click', handleStopTimer); + addEventListenerById('start-timer-btn', 'click', handleStartTimer); + // 绑定时间戳转换按钮事件 + 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); diff --git a/sidepanel/modules/Router.js b/sidepanel/modules/Router.js index f45b096..33b2ad1 100644 --- a/sidepanel/modules/Router.js +++ b/sidepanel/modules/Router.js @@ -1,284 +1,179 @@ import { getParamsUrl, closure, genKey } from '../utils/routeUtils.js'; +import { addClass, removeClass } from '../utils/domUtils.js'; +import { loadScript } from '../utils/scriptLoader.js'; -import { addClass, removeClass, hasClass } from '../utils/domUtils.js'; -// var config = { -// routerViewId: '#routerView', // 路由切换的挂载点 id -// stackPages: true, // 多级页面缓存 -// animationName: "slide", // 多级页面缓存 -// routes: [ -// // { -// // path: "/home", -// // name: "home", -// // callback: function(transition) { -// // home() -// // } -// // } -// ] -// } class Router { constructor() { - // 路由表 this.routes = {}; - // 路由跳转前执行 this.beforeFun = null; - // 路由跳转后执行 this.afterFun = null; - // 路由挂载点 this.routerViewId = 'router-view'; - // 路由重定向hash this.redirectRoute = null; - // 多级页面缓存 this.stackPages = true; - // 路由遍历 this.routerMap = []; - // 路由状态 this.historyFlag = ''; - // 路由历史 this.history = []; - // 动画名称 - this.animationName = 'fade'; - } - - init(config) { - this.routerMap = config ? config.routes : this.routerMap; - this.routerViewId = config ? config.routerViewId : this.routerViewId; - this.stackPages = config ? config.stackPages : this.stackPages; - - const name = document.querySelector('#app').getAttribute('data-animationName'); - if (name) { - this.animationName = name; - } - this.animationName = config ? config.animationName : this.animationName; - - this.map(); - - if (!this.routerMap.length) { - // 找到routerViewId 节点下的所有page 节点 - const pages = document.querySelectorAll(this.routerViewId + ' .page'); - for (let i = 0; i < pages.length; i++) { - // 遍历所有的page节点 - let page = pages[i]; - let hash = page.getAttribute('hash'); - let name = hash.substring(1); - this.routerMap.push({ - name: name, - path: hash, - callback: closure(name), - }); - } - } - - window.linkTo = (path) => { - console.log('path', path); - if (path.indexOf('?') !== -1) { - window.location.hash = path + '&key=' + genKey(); - } else { - window.location.hash = path + '?key=' + genKey(); - } - }; - - window.addEventListener('load', (event) => { - this.historyChange(event); - }); - - window.addEventListener('hashchange', (event) => { - this.historyChange(event); - }); } /** - * 路由历史纪录变化 - * @param {*} event + * 初始化路由 + */ + init(config) { + this.routerMap = config?.routes || this.routerMap; + this.routerViewId = config?.routerViewId || this.routerViewId; + this.stackPages = config?.stackPages ?? this.stackPages; + + // 映射路由表 + this.map(); + + window.linkTo = (path) => { + if (path.includes('?')) { + location.hash = `${path}&key=${genKey()}`; + } else { + location.hash = `${path}?key=${genKey()}`; + } + }; + + window.addEventListener('load', (e) => { + this.historyChange(e); + this.updateActiveButton(e); + }); + window.addEventListener('hashchange', (e) => { + this.historyChange(e); + this.updateActiveButton(e); + }); + } + + async loadPage(route) { + console.log('加载页面:', route); + const html = await fetch(route.html).then((r) => r.text()); + document.getElementById(this.routerViewId).innerHTML = html; + + const initName = route.name + 'Init'; + await loadScript(route.script, initName); + + if (window[initName] && route.script) { + await window[initName](); + } + } + + /** + * 加载 HTML 文件 + */ + async loadHTML(htmlPath) { + try { + const resp = await fetch(htmlPath); + if (!resp.ok) throw new Error(`HTML 加载失败: ${resp.status}`); + return await resp.text(); + } catch (err) { + return `

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

`; + } + } + + /** + * 处理路由历史变化 */ historyChange(event) { - // {path, query, params} - const currentHash = getParamsUrl(); - // router-#app-history - const nameString = 'router-' + this.routerViewId + '-history'; - - this.history = window.sessionStorage[nameString] - ? JSON.parse(window.sessionStorage[nameString]) - : []; - - // 返回上一级 - let back = false; - // 刷新页面 - let refresh = false; - // 前进上一级 - let forward = false; - // 获取当前路由的索引 - let index = 0; - - for (let i = 0; i < this.history.length; i++) { - let h = this.history[i]; - - // 判断是否是当前路由 - if (h.hash === currentHash.path && h.key === currentHash.query.key) { - // 获取当前路由的索引 - index = i; - // 判断是否是刷新页面 - if (i === this.history.length - 1) { - refresh = true; - } else { - // 判断是否是返回上一级 - back = true; - } - break; - } else { - // 判断是否是前进 - forward = true; - } - } - - if (back) { - this.historyFlag = 'back'; - this.history.length = index + 1; - } else if (refresh) { - this.historyFlag = 'refresh'; - } else { - this.historyFlag = 'forward'; - this.history.push({ - key: currentHash.query.key, - hash: currentHash.path, - query: currentHash.query, - }); - } - - console.log('historyFlag', this.historyFlag); - - if (!this.stackPages) { - this.historyFlag = 'forward'; - } - window.sessionStorage[nameString] = JSON.stringify(this.history); + const { path, query } = getParamsUrl(); + console.log('路由变化page{}, query{}', path, query); this.urlChange(); } /** - * 更改页面 - * @param {*} currentHash - */ - changeView(currentHash) { - const pages = document.querySelectorAll(' .page'); - const previousPage = document.querySelector('.' + this.routerViewId + ' .page.active'); - let currentPage = null; - let currHash = null; - - for (let i = 0; i < pages.length; i++) { - let page = pages[i]; - let hash = page.getAttribute('hash'); - page.setAttribute('class', 'page'); - if (hash === currentHash.path) { - currHash = hash; - currentPage = page; - } - } - - const enterName = 'enter-' + this.animationName; - const leaveName = 'leave-' + this.animationName; - - if (this.historyFlag === 'back') { - addClass(currentPage, 'current'); - if (previousPage) { - addClass(previousPage, leaveName); - } - - setTimeout(() => { - if (previousPage) { - removeClass(previousPage, leaveName); - } - }, 300); - } else if (this.historyFlag === 'forward' || this.historyFlag === 'refresh') { - if (previousPage) { - addClass(previousPage, leaveName); - } - addClass(currentPage, enterName); - setTimeout(() => { - if (previousPage) { - removeClass(previousPage, leaveName); - } - addClass(currentPage, enterName); - removeClass(currentPage, 'current'); - }, 300); - currentPage.scrollTop = 0; - this.routes[currHash].callback ? this.routes[currHash].callback() : null; - } - - this.afterFun ? this.afterFun(currentHash) : null; - } - - /** - * url改变 + * 路由解析与渲染 */ urlChange() { const currentHash = getParamsUrl(); - if (this.routes[currentHash.path]) { - if (this.beforeFun) { - this.beforeFun({ - to: { - path: currentHash.path, - query: currentHash.query, - }, - next: () => { - this.changeView(currentHash); - }, - }); - } else { - this.changeView(currentHash); - } - } else { + const { path, query } = currentHash; + + if (!this.routes[path]) { location.hash = this.redirectRoute; + return; + } + + const next = () => this.changeView(currentHash); + + if (this.beforeFun) { + this.beforeFun({ to: { path, query }, next }); + } else { + next(); } } /** - * 映射路由 + * 渲染页面 + */ + async changeView(currentHash) { + const { path } = currentHash; + const route = await this.routes[path]; + const mountEl = document.getElementById(this.routerViewId); + + if (!mountEl) { + console.error('挂载点不存在:', this.routerViewId); + return; + } + + console.log('路由:', route); + this.loadPage(route); + + // 执行 after 钩子 + if (this.afterFun) this.afterFun(currentHash); + } + + /** + * 构建路由映射 */ map() { - for (let i = 0; i < this.routerMap.length; i++) { - let route = this.routerMap[i]; - if (route.name === 'redirect') { - this.redirectRoute = route.path; - } else { + for (let r of this.routerMap) { + if (r.name === 'redirect') { + this.redirectRoute = r.path; + } else if (!this.redirectRoute) { this.redirectRoute = this.routerMap[0].path; } - let newPath = route.path; - let path = newPath.replace(/\s+/g, ''); - this.routes[path] = { - callback: route.callback, + this.routes[r.path] = { + name: r.name, + html: r.html, + script: r.script, }; } } /** - * 切换页面前的hook - * @param {*} callback hook执行函数 + * before 钩子 */ - beforeEach(callback) { - if (Object.prototype.toString.call(callback) === '[object Function]') { - // 判断callback是否为函数 + if (typeof callback === 'function') { this.beforeFun = callback; } else { - // 抛出错误 - console.trace('beforeEach callback must be a function'); + console.trace('beforeEach 必须是函数'); } } /** - * 切换页面后的hook - * @param {*} callback hook执行函数 + * after 钩子 */ - afterEach(callback) { - if (Object.prototype.toString.call(callback) === '[object Function]') { - // 判断callback是否为函数 + if (typeof callback === 'function') { this.afterFun = callback; } else { - // 抛出错误 - console.trace('afterEach callback must be a function'); + console.trace('afterEach 必须是函数'); } } + + updateActiveButton(event) { + const currentRoute = location.hash.replace('#', '') || '/home'; + + document.querySelectorAll('[data-route]').forEach((btn) => { + const route = btn.getAttribute('data-route'); + if (route === currentRoute) { + console.log('当前路由:', route); + addClass(btn, 'active'); + } else { + removeClass(btn, 'active'); + } + }); + } } export { Router }; diff --git a/sidepanel/utils/domUtils.js b/sidepanel/utils/domUtils.js index 0f56feb..5e9894c 100644 --- a/sidepanel/utils/domUtils.js +++ b/sidepanel/utils/domUtils.js @@ -98,9 +98,18 @@ export function hasClass(elem, cls) { * @param {*} cls */ export function addClass(elem, cls) { - if (!hasClass(elem, cls)) { - elem.className = elem.className === '' ? cls : elem.className + ' ' + 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(); } /** @@ -109,13 +118,22 @@ export function addClass(elem, cls) { * @param {*} cls */ export function removeClass(elem, cls) { - if (hasClass(elem, cls)) { - let newClass = ' ' + elem.className.replace(/[\t\r\n]/g, '') + ' '; - // 删除指定类名 - while (newClass.indexOf(' ' + cls + ' ') >= 0) { - newClass = newClass.replace(' ' + cls + ' ', ' '); - } - // 重新赋值 - elem.className = newClass.replace(/^\s+|\s+$/); + 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, ' '); +} \ No newline at end of file diff --git a/sidepanel/utils/scriptLoader.js b/sidepanel/utils/scriptLoader.js new file mode 100644 index 0000000..8ac4a96 --- /dev/null +++ b/sidepanel/utils/scriptLoader.js @@ -0,0 +1,30 @@ +let currentScript = null; +export function loadScript(path, globalInitName) { + console.log('load script', currentScript); + + if (currentScript) { + console.log('remove script') + currentScript.remove(); + currentScript = null; + } + + if (globalInitName && window[globalInitName]) { + delete window[globalInitName]; + } + + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = path; + script.type = 'module'; + + script.onload = () => { + currentScript = document.querySelector(`script[src='${path}']`); + console.log('script loaded', currentScript); + resolve(); + }; + + script.onerror = reject; + + document.body.appendChild(script); + }); +}