Express.js+Node.js开发SPA:getHtml()导入脚本无法执行求助
问题描述
我正在用Node.js和Express.js开发第一个单页应用(SPA),现在遇到个问题:通过getHtml()方法导入新视图时,里面的<script>元素会被当成字符串传输,没法作为可执行脚本运行。有人能告诉我怎么解决吗?谢谢!
2023年3月20日补充
没问题@pzaenger:
项目结构概览(标记绿色部分)
index页面代码
<body id="loginpage"> <div id="app"></div> <script type="module" src="/src/static/js/index.js"></script> <!--<script src="/src/static/js/functions/gettime.js"></script>--> <!--<script src="/src/static/js/functions/pushinsert.js"></script>--> </body>
index.js代码
import landingpage from "./views/landingpage.js"; import loginpage from "./views/loginpage.js"; import manualinsert from "./views/manualinsert.js"; import timerecording from "./views/timerecording.js"; const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$"); const getParams = match => { const values = match.result.slice(1); const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]); return Object.fromEntries(keys.map((key, i) => { return [key, values[i]]; })); }; const navigateTo = url => { history.pushState(null, null, url); router(); }; const router = async () => { const routes = [ { path: "/", view: loginpage }, { path: "/landingpage", view: landingpage }, { path: "/timerecording", view: timerecording }, { path: "/manualinsert", view: manualinsert} ]; // 测试每个路由是否匹配 const potentialMatches = routes.map(route => { return { route: route, result: location.pathname.match(pathToRegex(route.path)) }; }); let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null); if (!match) { match = { route: routes[0], result: [location.pathname] }; } const view = new match.route.view(getParams(match)); document.querySelector("#app").innerHTML = await view.getHtml(); }; window.addEventListener("popstate", router); document.addEventListener("DOMContentLoaded", () => { document.body.addEventListener("click", e => { if (e.target.matches("[data-link]")) { e.preventDefault(); navigateTo(e.target.href); } }); router(); });
AbstractView.js代码
export default class { constructor(params) { this.params = params; } setTitle(title) { document.title = title; } async getHtml() { return ""; } }
timerecording.js代码
import AbstractView from "./AbstractView.js"; export default class extends AbstractView { constructor(params) { super(params); this.setTitle("timerecording"); } async getHtml() { return ` <main id="timerecording"> <div id="timerecordingtest" class="content text-center"> <h1 id="timerecordingTime"></h1> <div class"justify-content-center"> <button type="submit" id="submitButton" class="btn btn-primary mb-2">Kommen</button> <button class="btn btn-secondary mb-2">Gehen</button> </div> <script> function getTime() { fetch('https://myurl') .then((response) => response.text()) .then((text) => { var utcDate = new Date(text); var formatter = new Intl.DateTimeFormat(undefined, { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, hour: 'numeric', minute: 'numeric', second: 'numeric' }); document.getElementById("timerecordingTime").innerHTML = formatter.format(utcDate); }); }; setInterval(getTime, 500); </script> </div> </main> `; } }
解决方法
直接把<script>写在HTML字符串里赋值给innerHTML时,浏览器出于安全机制不会自动执行这些脚本。推荐两种靠谱的处理方式:
方式一:分离视图的HTML与脚本逻辑(优先推荐)
修改AbstractView类新增初始化方法,将脚本逻辑从HTML字符串中剥离,统一在视图初始化阶段执行:
- 更新
AbstractView.js
export default class { constructor(params) { this.params = params; } setTitle(title) { document.title = title; } async getHtml() { return ""; } // 新增初始化方法,子类可重写 init() {} // 新增销毁方法,用于清理定时器、事件监听等资源 destroy() {} }
- 修改
timerecording.js,迁移脚本逻辑到init()
import AbstractView from "./AbstractView.js"; export default class extends AbstractView { constructor(params) { super(params); this.setTitle("timerecording"); this.intervalId = null; // 存储定时器ID,方便后续销毁 } async getHtml() { return ` <main id="timerecording"> <div id="timerecordingtest" class="content text-center"> <h1 id="timerecordingTime"></h1> <div class="justify-content-center"> <button type="submit" id="submitButton" class="btn btn-primary mb-2">Kommen</button> <button class="btn btn-secondary mb-2">Gehen</button> </div> </div> </main> `; } init() { const getTime = () => { fetch('https://myurl') .then((response) => response.text()) .then((text) => { const utcDate = new Date(text); const formatter = new Intl.DateTimeFormat(undefined, { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, hour: 'numeric', minute: 'numeric', second: 'numeric' }); document.getElementById("timerecordingTime").innerHTML = formatter.format(utcDate); }); }; // 启动定时器 this.intervalId = setInterval(getTime, 500); // 立即执行一次避免延迟 getTime(); } destroy() { // 切换路由时清理定时器,防止内存泄漏 if (this.intervalId) clearInterval(this.intervalId); } }
- 更新
router()函数,调用视图的初始化与销毁方法
// 全局存储当前视图实例,用于切换时销毁资源 let currentView = null; const router = async () => { // 原有路由匹配逻辑保持不变 const routes = [ { path: "/", view: loginpage }, { path: "/landingpage", view: landingpage }, { path: "/timerecording", view: timerecording }, { path: "/manualinsert", view: manualinsert} ]; const potentialMatches = routes.map(route => { return { route: route, result: location.pathname.match(pathToRegex(route.path)) }; }); let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null); if (!match) { match = { route: routes[0], result: [location.pathname] }; } const view = new match.route.view(getParams(match)); // 销毁上一个视图的资源 if (currentView && typeof currentView.destroy === 'function') { currentView.destroy(); } document.querySelector("#app").innerHTML = await view.getHtml(); // 初始化当前视图 view.init(); currentView = view; };
方式二:手动提取并执行脚本(不推荐,易引发问题)
如果必须保留HTML中的脚本标签,可以在渲染后手动提取脚本内容,创建新的script元素插入页面执行:
修改router()函数:
const router = async () => { // 原有路由匹配逻辑不变 const view = new match.route.view(getParams(match)); const html = await view.getHtml(); const appEl = document.querySelector("#app"); appEl.innerHTML = html; // 提取并执行脚本 const scriptEls = appEl.querySelectorAll("script"); scriptEls.forEach(scriptEl => { const newScript = document.createElement("script"); newScript.textContent = scriptEl.textContent; document.body.appendChild(newScript); scriptEl.remove(); }); };
这种方式需注意脚本中依赖的DOM元素是否已渲染完成,且容易出现重复执行、变量污染等问题,仅作为临时方案使用。
内容的提问来源于stack exchange,提问作者user21420967
相关产品推荐
相关产品推荐

