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