在 html 中直接使用 ES6 模块中定义的参数化函数

Use parameterised functions defined in ES6 module directly in html

嵌入在 HTML 脚本中的 ES6 模块内定义的函数对该脚本不可用。因此,如果您有如下语句:

<button onclick="doSomething();">Do something</button>

在你的 HTML 中,你的 doSomething() 函数位于嵌入在 HTML 脚本中的 ES6 模块中,当你 运行 脚本。

提出了解决当前问题的好方法,建议您通过修改 HTML 将您的功能“绑定”到按钮:

<button id="dosomethingbutton">Do something</button>

并使用模块本身创建链接:

document.getElementById('dosomethingbutton').addEventListener('click', doSomething);

这工作正常,但如果您的原始按钮更复杂一点并且已参数化怎么办?例如:

<button onclick="doSomething('withThisString');">Do Something with String</button>

“绑定”所能提供的最多似乎仅限于与事件相关的情况——我找不到将其与数据相关联的方法。我完全无法找到解决此问题的方法,我们将不胜感激。

我想补充一点,虽然这个问题可能看起来有点晦涩,但我认为迁移到 Firebase 9 的任何人都会感兴趣。除其他更改外,迁移还需要您移动 javascript 代码到 ES6 模块中(其中命名空间不能直接用于 HTML DOM),因此最简单的 HTML 很可能会立即遇到这些问题。欢迎提供建议。

This works fine, but what if your original button was a bit more sophisticated and was parameterised?

有几个解决方案:

  1. 一个data-*属性:

    <button id="the-button" data-string="withThisString">Do Something with String</button>
    
    document.getElementById("the-button").addEventListener("click", function() {
        doSomething(this.getAttribute("data-string"));
    });
    

    (更多内容见下文。)

  2. 绑定事件时绑定字符串

    <button id="the-button">Do Something with String</button>
    
    document.getElementById("the-button").addEventListener("click", () => {
        doSomething("withThisString");
    });
    

上面有很多变体,如果您将 doSomething 与具有不同字符串的多个按钮一起使用,您可以使用 class 和循环而不是 id,但这是一般的想法。


关于 data-* 属性的事情:如果你愿意,你可以通过 data-* 属性和一个连接事物的函数使这个过程完全 HTML 驱动。例如,假设您有这些按钮:

<button data-click="doThisx@module1">Do This</button>
<button data-click="doThat@module2">Do That</button>
<button data-click="doTheOther@module3">Do The Other</button>

您可以使用一个可重复使用的函数来连接它们:

class EventSetupError extends Error {
    constructor(element, msg) {
        if (typeof element === "string") {
            [element, msg] = [msg, element];
        }
        super(msg);
        this.element = element;
    }
}
export async function setupModuleEventHandlers(eventName) {
    try {
        const attrName = `data-${eventName}`;
        const elements = [...document.querySelectorAll(`[${attrName}]`)];
        await Promise.all(elements.map(async element => {
            const attrValue = element.getAttribute(`data-${eventName}`);
            const [fname, modname] = attrValue ? attrValue.split("@", 2) : [];
            if (!fname || !modname) {
                throw new EventSetupError(
                    element,
                    `Invalid '${attrName}' attribute "${attrValue}"`
                );
            }
            // It's fine if we do import() more than once for the same module,
            // the module loader will return the same module
            const module = await import(`./${modname}.js`);
            const fn = module[fname];
            if (typeof fn !== "function") {
                throw new EventSetupError(
                    element,
                    `Invalid '${attrName}': no '${fname}' on module '${modname}' or it isn't a function`
                );
            }
            element.addEventListener(eventName, fn);
        }));
    } catch (error) {
        console.error(error.message, error.element);
    }
}

使用它来查找和连接点击处理程序:

import { setupModuleEventHandlers } from "./data-attr-event-setup.js";
setupModuleEventHandlers("click")
.catch(error => {
    console.error(error.message, error.element);
});

它是一次性管道,但在 HTML 中为您提供相同的基于属性的体验(事件处理程序仍然可以从另一个 data-* 属性获取参数信息,或者您可以将其烘焙到你的设置功能)。 (该示例依赖于 dynamic import, but that's supported by recent versions of all major browsers,并且在不同程度上依赖于捆绑器。

有几十种方法可以实现它,我不是在推广它,只是举一个例子,说明如果你愿意,你可以很容易地做到这一点。

但实际上,这就是 React、Vue、Ember、Angular、Lit 等库发挥作用的地方。

虽然T.J克劳德已经回答了这个问题,但我想我可能会补充一些难以挤入的观点作为评论。

一旦我深入了解我的 Firebase V9 转换,我开始发现“模块命名空间”问题的一些后果是相当深远的。如上所述,我最初的问题中引用的示例很容易处理,但我发现我还需要弄清楚如何处理“动态”HTML 以响应来自数据库的可变情况。在这种情况下,我的 javascript 最初会创建一个包含 HTML 块的字符串,例如:

realDiv = `
<div>
    <button onclick = "function fn1 (param1a, param1b,...);">button1</button>
    <button onclick = "function fn2 (param2a, param2b,...);">button2</button>
etc
</div>
`

然后将其放入应用程序 HTML 骨架中定义的“真正的” realdiv 中,并带有

document.getElementById("realdiv") = realDiv;

现在,由于上述原因,一旦 javascript 在模块中,这种安排就不再有效了。

我学会采用的模式(再次感谢 T.J Crowder)遵循以下原则:

realDiv = `
<div>
    <button id = "button1" data-param1a="param1a" data-param1b="param1b";">button1</button>
    <button id = "button2" data-param2a="param2a" data-param2b="param2b";">button2</button>
etc
</div>
`

然后我会像以前一样将生成的代码放入我的 HTML 框架中

document.getElementById("realdiv") = readlDiv;

现在您已经将代码嵌入到 DOM 中(假设我已经统计了您生成的按钮数量),我会为它们创建绑定javascript 的最后一点像这样:

for (let i = 0; i>buttonCount; i++) {
    document.getElementById('button' + i).onclick = function() { fn1 (this.getAttribute('data-param1a'), this.getAttribute('data-param1b') };
etc
}

我发现当我需要让 onclick 启动许多不同的功能时,使用这种模式创建 onclicks 对于保持清晰度特别有帮助。