feat(router): 重构路由模块并优化页面加载逻辑
- 引入 ScriptManager、TimerManager 和 EventManager 统一管理资源 - 路由配置支持默认重定向路径 '/' - 移除旧版 scriptLoader 与 routeUtils,使用新的模块化加载机制 - 增加路由生命周期控制,自动清理定时器和事件监听器 - 改进点击代理逻辑,增强按钮激活状态切换 - 页面切换时卸载旧脚本及资源,防止内存泄漏 - 修复初始化日志冗余问题,提升代码可维护性
This commit is contained in:
+19
-12
@@ -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;
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
+56
-100
@@ -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 {
|
|
||||||
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);
|
console.error('挂载点不存在:', this.routerViewId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('路由:', route);
|
if (!this.stackPages) mount.innerHTML = '';
|
||||||
this.loadPage(route);
|
|
||||||
|
|
||||||
// 执行 after 钩子
|
if (route.html) {
|
||||||
if (this.afterFun) this.afterFun(currentHash);
|
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,
|
||||||
map() {
|
name: route.name,
|
||||||
for (let r of this.routerMap) {
|
isModule: route.isModule,
|
||||||
if (r.name === 'redirect') {
|
deps: route.deps,
|
||||||
this.redirectRoute = r.path;
|
});
|
||||||
} else if (!this.redirectRoute) {
|
|
||||||
this.redirectRoute = this.routerMap[0].path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.routes[r.path] = {
|
await this.scriptManager.runInit(route.name);
|
||||||
name: r.name,
|
|
||||||
html: r.html,
|
this.currentRoute = route;
|
||||||
script: r.script,
|
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 };
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user