基于Express+axios实现菜单切换时动态加载JS仅执行一次如何解决?
问题根本原因
浏览器对 type="module" 的ES模块有全局单例缓存规则,同一个URL的模块仅会被下载、解析、执行一次。你第一次插入脚本时模块正常执行,后续重复插入同URL的脚本节点时,浏览器会直接复用已缓存的模块实例,不会重新运行模块内的顶层代码,所以看起来脚本没有生效。
解决方案
方案1:加随机查询参数强制刷新模块(改动最小,快速生效)
直接给动态加载的脚本URL拼接时间戳或随机数作为查询参数,让每次请求的URL唯一,跳过模块缓存,仅需修改一行代码:
// 原代码 script.setAttribute('src', `/js/pages/ui-${event.value}-controller.mjs`); // 改为 script.setAttribute('src', `/js/pages/ui-${event.value}-controller.mjs?t=${Date.now()}`);
该方案改动量极小,适合快速验证修复效果,缺点是每次切换路由都会重新请求脚本文件,有少量额外网络开销。
方案2:缓存模块导出的初始化方法(性能最优,推荐生产环境使用)
将对应页面的控制器逻辑封装为可重复调用的初始化函数导出,第一次加载模块后缓存导出的方法,后续切换回对应页面时直接调用初始化方法即可,无需重复插入脚本。
修改后的控制器逻辑:
// 全局缓存已加载的控制器模块 const loadedControllers = new Map(); const mainSidebarUiMouseclick = async (event) => { const content = document.getElementById('content'); const pageKey = event.value; // 清空内容前可调用前一个页面的destroy方法清理资源 content.innerHTML = ''; try { // 加载页面内容 const res = await axios.get(`${window.location.href}/${pageKey}`); content.innerHTML = res.data; // 已加载过模块,直接调用初始化方法 if (loadedControllers.has(pageKey)) { loadedControllers.get(pageKey).init(); console.info(`@system, 页面控制器已复用`); return; } // 首次加载对应模块 switch(pageKey) { case 'diary': const module = await import(`/js/pages/ui-${pageKey}-controller.mjs`); // 缓存模块导出的方法 loadedControllers.set(pageKey, module); // 调用初始化方法执行页面逻辑 module.init(); console.info(`@system, 动态脚本加载完成`); break; // 其他页面逻辑同理 } } catch (error) { console.error(`@error, ${error}`); } };
同步修改对应的控制器脚本(比如ui-diary-controller.mjs),把原有顶层执行逻辑封装为导出方法:
// 页面初始化逻辑封装为init函数导出 export function init() { // 原有页面逻辑:绑定事件、渲染数据等 const diaryList = document.querySelector('#diary-list'); // ... } // 可选导出销毁方法,切换路由时清理资源避免内存泄漏 export function destroy() { // 移除绑定的事件、取消未完成的请求、清理定时器等 }
该方案避免了重复的网络请求,性能更好,也方便统一管理页面生命周期。
补充注意事项
- 如果采用方案1,且脚本内绑定了DOM事件,记得每次执行时先移除旧的事件监听,避免重复绑定导致异常。
- 切换路由清空内容前,建议先清理前一个页面的未完成异步请求、定时器等资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Minwoo Kim
相关产品推荐
相关产品推荐

