You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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字符串中剥离,统一在视图初始化阶段执行:

  1. 更新AbstractView.js
export default class {
    constructor(params) {
        this.params = params;
    }

    setTitle(title) {
        document.title = title;
    }

    async getHtml() {
        return "";
    }

    // 新增初始化方法,子类可重写
    init() {}

    // 新增销毁方法,用于清理定时器、事件监听等资源
    destroy() {}
}
  1. 修改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);
    }
}
  1. 更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 18:07:23