diff --git a/sidepanel/js/index.js b/sidepanel/js/index.js index bb07739..1009ac5 100644 --- a/sidepanel/js/index.js +++ b/sidepanel/js/index.js @@ -1,10 +1,17 @@ -console.log('Before creating router'); +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', stackPages: false, routes: [ + { + path: '/', + name: 'redirect', + html: 'pages/timestamp.html', + }, { path: '/home', name: 'timestamp', @@ -15,23 +22,23 @@ const config = { path: '/todo', name: 'todo', html: 'pages/todolist.html', - script: '', }, ], }; +window.scriptManager = new ScriptManager(); +window.timerManager = new TimerManager(); +window.eventManager = new EventManager(); + const router = new Router(); -console.log('Router created:', router); router.init(config); -console.log('Router initialized'); -document.addEventListener('click', (event) => { - const btn = event.target.closest('[data-route]'); +// click delegation +document.addEventListener('click', (e) => { + const btn = e.target.closest('[data-route]'); if (!btn) return; - - const path = btn.getAttribute('data-route'); - if (!path) return; - - // 触发 hash 路由跳转 - window.location.hash = path; + const p = btn.getAttribute('data-route'); + location.hash = p; + // update active styling + document.querySelectorAll('[data-route]').forEach((b) => b.classList.toggle('active', b === btn)); }); diff --git a/sidepanel/js/timestamp.js b/sidepanel/js/timestamp.js index de167dd..70d6d5c 100644 --- a/sidepanel/js/timestamp.js +++ b/sidepanel/js/timestamp.js @@ -14,7 +14,6 @@ import { addEventListenerById } from '../utils/domUtils.js'; (function (global) { global.timestampInit = async function () { - console.log('timestamp init'); // 初始化时间戳显示 initTimestampDisplay(); diff --git a/sidepanel/modules/Router.js b/sidepanel/modules/Router.js index 33b2ad1..68fed2f 100644 --- a/sidepanel/modules/Router.js +++ b/sidepanel/modules/Router.js @@ -1,141 +1,111 @@ -import { getParamsUrl, closure, genKey } from '../utils/routeUtils.js'; -import { addClass, removeClass } from '../utils/domUtils.js'; -import { loadScript } from '../utils/scriptLoader.js'; +import { TimerManager } from '../utils/timerManager.js'; +import { EventManager } from '../utils/eventManager.js'; +import { ScriptManager } from '../utils/scriptManager.js'; class Router { constructor() { this.routes = {}; this.beforeFun = null; this.afterFun = null; - this.routerViewId = 'router-view'; + this.routerViewId = 'app'; this.redirectRoute = null; this.stackPages = true; this.routerMap = []; this.historyFlag = ''; this.history = []; + this.scriptManager = window.scriptManager; + this.eventManager = window.eventManager; + this.timerManager = window.timerManager; } /** * 初始化路由 */ 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); - }); + // 监听路由变化 + window.addEventListener('hashchange', () => this.urlChange()); + window.addEventListener('load', () => this.urlChange()); + window.lintTo = (path) => this.naviage(path); } - async loadPage(route) { - console.log('加载页面:', route); - const html = await fetch(route.html).then((r) => r.text()); - document.getElementById(this.routerViewId).innerHTML = html; + map() { + if (!this.routerMap.length) { + console.error('请配置路由'); + return; + } - const initName = route.name + 'Init'; - await loadScript(route.script, initName); - - if (window[initName] && route.script) { - await window[initName](); + for (const r of this.routerMap) { + if (r.name == 'redirect') this.redirectRoute = r.path; + this.routes[r.path] = r; } } /** - * 加载 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) { - const { path, query } = getParamsUrl(); - console.log('路由变化page{}, query{}', path, query); - this.urlChange(); + naviage(path) { + window.location.hash = path; } /** * 路由解析与渲染 */ urlChange() { - const currentHash = getParamsUrl(); - const { path, query } = currentHash; + const path = location.hash.replace('#', '') || this.redirectRoute; + console.log('当前路由:', path); + const route = this.routes[path]; - if (!this.routes[path]) { + if (!route) { location.hash = this.redirectRoute; return; } - const next = () => this.changeView(currentHash); + const doChange = async () => { + this.timerManager.cleanAll(); + this.eventManager.removeAll(); - 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 r of this.routerMap) { - if (r.name === 'redirect') { - this.redirectRoute = r.path; - } else if (!this.redirectRoute) { - this.redirectRoute = this.routerMap[0].path; + const mount = document.getElementById(this.routerViewId); + if (!mount) { + console.error('挂载点不存在:', this.routerViewId); + return; } - this.routes[r.path] = { - name: r.name, - html: r.html, - script: r.script, - }; + if (!this.stackPages) mount.innerHTML = ''; + + if (route.html) { + try { + const html = await fetch(route.html).then((r) => r.text()); + mount.innerHTML = html; + } catch (e) { + mount.innerHTML = `

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

`; + } + } + + if (route.script) { + await this.scriptManager.loadScript({ + path: route.script, + name: route.name, + isModule: route.isModule, + deps: route.deps, + }); + } + + await this.scriptManager.runInit(route.name); + + this.currentRoute = route; + if (this.afterFun) this.afterFun(route); + }; + + if (this.beforeFun) { + this.beforeFun({ to: route, next: doChange }); + } else { + doChange(); } } @@ -160,20 +130,6 @@ class Router { 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/eventManager.js b/sidepanel/utils/eventManager.js new file mode 100644 index 0000000..03ec2d8 --- /dev/null +++ b/sidepanel/utils/eventManager.js @@ -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 = []; + } +} \ No newline at end of file diff --git a/sidepanel/utils/routeUtils.js b/sidepanel/utils/routeUtils.js deleted file mode 100644 index 90a4524..0000000 --- a/sidepanel/utils/routeUtils.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 获取路由的路径和详细参数 - * @returns - */ -export function getParamsUrl() { - // 获取路由 - const hasDetail = location.hash.split('?'); - // 获取路由名称 - const hasName = hasDetail[0].split('#')[1]; - // 获取请求参数 - const params = hasDetail[1] ? hasDetail[1].split('&') : []; - // 解析请求参数 - let query = {}; - for (let i = 0; i < params.length; i++) { - let param = params[i].split('='); - query[param[0]] = param[1]; - } - - return { - path: hasName, - query: query, - params: params, - }; -} - -/** - * 闭包返回函数 - * @param {*} name - * @returns - */ -export function closure(name) { - return (currentHash) => { - window.name && window[name](currentHash); - }; -} - -/** - * 生成随机key - * @returns - */ -export function genKey() { - const KEY_TEMPLATE = 'xxxxxxxx'; - return KEY_TEMPLATE.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} diff --git a/sidepanel/utils/scriptLoader.js b/sidepanel/utils/scriptLoader.js deleted file mode 100644 index 8ac4a96..0000000 --- a/sidepanel/utils/scriptLoader.js +++ /dev/null @@ -1,30 +0,0 @@ -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); - }); -} diff --git a/sidepanel/utils/scriptManager.js b/sidepanel/utils/scriptManager.js new file mode 100644 index 0000000..2d7397d --- /dev/null +++ b/sidepanel/utils/scriptManager.js @@ -0,0 +1,113 @@ +export class ScriptManager { + constructor() { + this.currentScriptEl = null; + this.currentModule = null; + this.currentName = null; + } + + async loadScript({ path, name, isModule = false, 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() { + 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; + } +} diff --git a/sidepanel/utils/timerManager.js b/sidepanel/utils/timerManager.js new file mode 100644 index 0000000..c0af2cc --- /dev/null +++ b/sidepanel/utils/timerManager.js @@ -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 = []; + } +} \ No newline at end of file