feat(router): 重构路由模块并优化页面加载逻辑

- 引入 ScriptManager、TimerManager 和 EventManager 统一管理资源
- 路由配置支持默认重定向路径 '/'
- 移除旧版 scriptLoader 与 routeUtils,使用新的模块化加载机制
- 增加路由生命周期控制,自动清理定时器和事件监听器
- 改进点击代理逻辑,增强按钮激活状态切换
- 页面切换时卸载旧脚本及资源,防止内存泄漏
- 修复初始化日志冗余问题,提升代码可维护性
This commit is contained in:
雨霖铃
2025-12-09 22:22:31 +08:00
parent 2761dbe13f
commit 28fc3b409f
8 changed files with 282 additions and 199 deletions
+19 -12
View File
@@ -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'; import { Router } from '../modules/Router.js';
const config = { const config = {
routerViewId: 'app', routerViewId: 'app',
stackPages: false, stackPages: false,
routes: [ routes: [
{
path: '/',
name: 'redirect',
html: 'pages/timestamp.html',
},
{ {
path: '/home', path: '/home',
name: 'timestamp', name: 'timestamp',
@@ -15,23 +22,23 @@ const config = {
path: '/todo', path: '/todo',
name: 'todo', name: 'todo',
html: 'pages/todolist.html', html: 'pages/todolist.html',
script: '',
}, },
], ],
}; };
window.scriptManager = new ScriptManager();
window.timerManager = new TimerManager();
window.eventManager = new EventManager();
const router = new Router(); const router = new Router();
console.log('Router created:', router);
router.init(config); router.init(config);
console.log('Router initialized');
document.addEventListener('click', (event) => { // click delegation
const btn = event.target.closest('[data-route]'); document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-route]');
if (!btn) return; if (!btn) return;
const p = btn.getAttribute('data-route');
const path = btn.getAttribute('data-route'); location.hash = p;
if (!path) return; // update active styling
document.querySelectorAll('[data-route]').forEach((b) => b.classList.toggle('active', b === btn));
// 触发 hash 路由跳转
window.location.hash = path;
}); });
-1
View File
@@ -14,7 +14,6 @@ import { addEventListenerById } from '../utils/domUtils.js';
(function (global) { (function (global) {
global.timestampInit = async function () { global.timestampInit = async function () {
console.log('timestamp init');
// 初始化时间戳显示 // 初始化时间戳显示
initTimestampDisplay(); initTimestampDisplay();
+64 -108
View File
@@ -1,141 +1,111 @@
import { getParamsUrl, closure, genKey } from '../utils/routeUtils.js'; import { TimerManager } from '../utils/timerManager.js';
import { addClass, removeClass } from '../utils/domUtils.js'; import { EventManager } from '../utils/eventManager.js';
import { loadScript } from '../utils/scriptLoader.js'; import { ScriptManager } from '../utils/scriptManager.js';
class Router { class Router {
constructor() { constructor() {
this.routes = {}; this.routes = {};
this.beforeFun = null; this.beforeFun = null;
this.afterFun = null; this.afterFun = null;
this.routerViewId = 'router-view'; this.routerViewId = 'app';
this.redirectRoute = null; this.redirectRoute = null;
this.stackPages = true; this.stackPages = true;
this.routerMap = []; this.routerMap = [];
this.historyFlag = ''; this.historyFlag = '';
this.history = []; this.history = [];
this.scriptManager = window.scriptManager;
this.eventManager = window.eventManager;
this.timerManager = window.timerManager;
} }
/** /**
* 初始化路由 * 初始化路由
*/ */
init(config) { init(config) {
// 配置路由
this.routerMap = config?.routes || this.routerMap; this.routerMap = config?.routes || this.routerMap;
this.routerViewId = config?.routerViewId || this.routerViewId; this.routerViewId = config?.routerViewId || this.routerViewId;
this.stackPages = config?.stackPages ?? this.stackPages; this.stackPages = config?.stackPages ?? this.stackPages;
// 映射路由表
this.map(); this.map();
window.linkTo = (path) => { // 监听路由变化
if (path.includes('?')) { window.addEventListener('hashchange', () => this.urlChange());
location.hash = `${path}&key=${genKey()}`; window.addEventListener('load', () => this.urlChange());
} else { window.lintTo = (path) => this.naviage(path);
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) { map() {
console.log('加载页面:', route); if (!this.routerMap.length) {
const html = await fetch(route.html).then((r) => r.text()); console.error('请配置路由');
document.getElementById(this.routerViewId).innerHTML = html; return;
}
const initName = route.name + 'Init'; for (const r of this.routerMap) {
await loadScript(route.script, initName); if (r.name == 'redirect') this.redirectRoute = r.path;
this.routes[r.path] = r;
if (window[initName] && route.script) {
await window[initName]();
} }
} }
/** /**
* 加载 HTML 文件 * 导航
*/ */
async loadHTML(htmlPath) { naviage(path) {
try { window.location.hash = path;
const resp = await fetch(htmlPath);
if (!resp.ok) throw new Error(`HTML 加载失败: ${resp.status}`);
return await resp.text();
} catch (err) {
return `<h2>页面加载失败:${err.message}</h2>`;
}
}
/**
* 处理路由历史变化
*/
historyChange(event) {
const { path, query } = getParamsUrl();
console.log('路由变化page{}, query{}', path, query);
this.urlChange();
} }
/** /**
* 路由解析与渲染 * 路由解析与渲染
*/ */
urlChange() { urlChange() {
const currentHash = getParamsUrl(); const path = location.hash.replace('#', '') || this.redirectRoute;
const { path, query } = currentHash; console.log('当前路由:', path);
const route = this.routes[path];
if (!this.routes[path]) { if (!route) {
location.hash = this.redirectRoute; location.hash = this.redirectRoute;
return; return;
} }
const next = () => this.changeView(currentHash); const doChange = async () => {
this.timerManager.cleanAll();
this.eventManager.removeAll();
if (this.beforeFun) { const mount = document.getElementById(this.routerViewId);
this.beforeFun({ to: { path, query }, next }); if (!mount) {
} else { console.error('挂载点不存在:', this.routerViewId);
next(); return;
}
}
/**
* 渲染页面
*/
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;
} }
this.routes[r.path] = { if (!this.stackPages) mount.innerHTML = '';
name: r.name,
html: r.html, if (route.html) {
script: r.script, try {
}; const html = await fetch(route.html).then((r) => r.text());
mount.innerHTML = html;
} catch (e) {
mount.innerHTML = `<h2>页面加载失败:${e.message}</h2>`;
}
}
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 必须是函数'); 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 }; export { Router };
+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 = [];
}
}
-48
View File
@@ -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);
});
}
-30
View File
@@ -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);
});
}
+113
View File
@@ -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;
}
}
+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 = [];
}
}