HTML行为属于HTML

当您单击下面的按钮时,它会向您弹出一条消息。

当用户点击按钮时弹出一个窗口,这不是按钮本身所能支持的,您必须对其进行编码。有两种方法可以将自定义功能附加到 HTML 元素上:内联或使用事件监听器。

这是使用内联处理程序的方式:

<button onclick="alert('I was clicked!')">Click me</button>

这就是使用事件侦听器的方式:

<button>Click me</button>

<script>
const btn = document.querySelector("button")

btn.addEventListener("click", () => {
  alert('I was clicked!')
})
</script>

如果您以前从未考虑过这一点,您可能会做出反应:第一个示例(内联)似乎更好。它占用的空间要小得多,并将所有相关信息放在按钮上。可专家称,事实并非如此。MDN Web Docs 有这样的说法使用内联事件处理程序

你可以找到许多事件处理器属性的 HTML 属性的等价表示;但是,你不应该使用这些属性——它们被认为是不好的做法。如果你正在做一些非常快速的事情,使用事件处理器属性可能看起来很容易,但它们很快就会变得无法管理和低效。

或者,如果这还不够清楚:

你永远不应该使用 HTML 事件处理器属性——那些已经过时了,使用它们是不好的做法。

在我看来,这是完全错误的。在这一点上,新手们是对的。MDN 是一个巨大的资源,我理解他们推荐第二种形式的原因,但要恢复 HTML 的全部功能并用它构建持久的应用程序,与这种特殊的意识形态作斗争至关重要。我来解释一下。

<form>和function

MDN 不希望你使用内联事件处理程序,因为它们不希望你混合表单 (HTML)和函数 (JS)。这是一个编程原理,称为 分离关注点 和HTML/CSS/JS split 就是一个教科书式的例子。来自维基百科(在撰写本文时):

HTML主要用于组织网页内容,CSS用于定义内容呈现风格,JS 定义内容如何与用户交互和行为。

分离关注点是一个伟大的原则,但我认为这个原则划错了地方。在这种网页概念中,HTML 本质上是一个脚手架,你可以用 CSS(样式)和 JS(交互性)来装饰它。但 HTML 本身也具有交互性。它的交互性足以支持价值数十亿美元的业务,而不需要一行 JavaScript。

比方说,您想设置一个文本框和一个搜索按钮,以便人们可以搜索网页。首先,您需要为文本框设置一个 <input type=text>,为搜索设置一个 <button>。

<div>
  <input type=text name=q>
  <button>Search</button>
</div>

在页面上,它看起来像这样:


这个按钮没有任何作用,文本也不知道到哪里去了。但如果将 <div> 替换为 <form>,就像这样:

<form method=/search action=GET>
  <input type=text name=q>
  <button>Search</button>
</form>

然后点击按钮将输入内容作为查询提交,并导航到结果。因此,如果您在 https://example.com 上,而文本框里有猫,点击提交就会导航到 https://example.com/search?q=cats。在很长一段时间里,谷歌主页都是这样工作的。

HTML 完全定义了这一功能。当你点击按钮时,页面会进行一些交互操作–利用你的输入进行网络请求,而不涉及 JavaScript。它易于阅读,符合语义,并能在所有网络浏览器中永久运行。最重要的是,它证明了 “HTML 定义布局,JS 定义功能 “的整个概念在定义上是错误的。

增强语义

这样做的问题是,HTML 的功能非常有限,要增强这些功能,我们需要 JavaScript。表单验证就是一个很好的例子。此表格与上一个表格非常相似,只是现在它要求至少 8 个字符电子邮件地址:

<form>
  <input type="email" id="mail" name="mail" required minlength="8" />
  <button>Submit</button>
</form>

这也算是完成了任务。它可以防止用户提交太短或看起来不像电子邮件的内容,但不能让你自定义如何告知用户电子邮件地址的要求。这个表单是从 MDN 的表单验证页面改编而来的,演示了如何做到这一点:

<form novalidate>
  <input type="email" id="mail" name="mail" required minlength="8" />
  <span class="error" aria-live="polite"></span>
  <button>Submit</button>
</form>

现在有一个 <span>,一开始是空的,但如果电子邮件无效,就会填充一条错误信息。表单上还有一个 novalidate 属性,它告诉浏览器不要进行 HTML 内置的验证,因为我们将在 JavaScript 中完成所有验证。下面的 JavaScript 决定了信息的内容,并将其添加到 span 中。

const form = document.querySelector("form");
const email = document.getElementById("mail");
const emailError = document.querySelector("#mail + span.error");

email.addEventListener("input", (event) => {

  if (email.validity.valid) {
    emailError.textContent = "";
    emailError.className = "error";
  } else {
    showError();
  }
});

form.addEventListener("submit", (event) => {
  if (!email.validity.valid) {
    showError();
    event.preventDefault();
  }
});

function showError() {
  if (email.validity.valueMissing) {
    emailError.textContent = "You need to enter an email address.";
  } else if (email.validity.typeMismatch) {
    emailError.textContent = "Entered value needs to be an email address.";
  } else if (email.validity.tooShort) {
    emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`;
  }

  emailError.className = "error active";
}

该代码增强了 HTML,使其执行以下操作:

  • 如果用户提交了无效的电子邮件,请向他们显示错误消息
  • 如果电子邮件为空,请显示“您需要输入电子邮件地址”。
  • 如果电子邮件太短,请显示“电子邮件应至少为 8 个字符;你输入了 X“(其中 X
    是他们输入的字符数)
  • 如果电子邮件不是电子邮件,则显示“输入的值必须是电子邮件地址”。
  • 当用户开始键入时,删除错误消息

所有这些内容都没有内置在 HTML 中,因此必须用 JavaScript 来编写。大量的 JavaScript。但如果是这样呢?假设一下,你可以在 HTML 中设计以下界面:

<form>
  <input type="email"
         name="mail"
         required
         minlength="8"
         message-target="#email-error"
         value-missing-message="You need to enter an email address."
         type-mismatch-message="Entered value needs to be an email address."
         too-short-message="Email should be at least ${email.minLength} characters; you entered ${email.value.length}."
  >
  <span id=email-error></span>
  <button>Submit</button>
</form>

您不是在 JavaScript 中添加新消息,而是将它们写在输入本身上。那更好,因为
有几个原因:

  • 代码清晰易读。“输入太短”消息在哪里定义?在 too-short-messsage中。
  • 这种行为是局部的。我们不可能不知道有人修改了信息,也不可能不知道是在哪里修改的。
  • 该逻辑可以在不同的输入上轻松重用
  • 该接口会自动实现适合验证消息的aria-live设计模式

从某种意义上说,这些优点都是一样的:它们赋予 HTML 更丰富的语义。

希望你正在为此对着电脑屏幕嚎啕大哭。”你什么都没解决!进行验证是件复杂的事,而你只是通过设计一个完美的界面,用魔法将它化解了”。没错。没错。这就是界面应该做的。有了更好的语义,程序员就可以描述元素的作用,其他人就可以替他们处理细节问题。

但如果我们开始编写 JavaScript 库来丰富 HTML 的语义,而不是取而代之,那么我们可能会从两者中获得更多好处。

请记住,在现阶段,我所使用的自定义语义仍然纯粹是理论性的。我们稍后将讨论前向兼容性、数据属性和所有细节问题。我们的首要任务是承认,作为一种超文本标记语言,HTML 本身就具有功能性:”超 “表示我们添加到文本中的所有额外功能,如链接和表单。要为 HTML 构建良好的界面,就必须认真对待 HTML。

一旦我们做到了这一点,接下来的任务就是想出最好的办法,用我们自己的语义来增强其有限的语义。这一部分很难。

回到现实

好的,那么如果我们想丰富 HTML 的语义,正确的方法是什么?

这里的主要问题是,HTML 既是一种有生命力的标准,也是一种无情的向后兼容标准(有史以来第一个网站至今仍在运行,并能在现代网络浏览器上完美显示,这是一项了不起的成就)。因此,如果我在输入元素中添加了 too-short-messsage 属性,而几年后 WHATWG 又添加了一个新的 too-short-messsage 属性,那么网页就会以意想不到的方式出现问题。

Microformats是一个非常古老的标准,如今仍在使用,其中最著名的可能是作为 Webmentions 规范的一部分。微格式让你可以在类声明中添加属性,就像下面这样:

<a class="p-name u-url" href="https://alexpetros.com">Alex Petros</a>

解析网页的东西会知道,这个链接不是一个随机链接,而是一个以我的名字为文本(p-name)、以那个人的主页为 URL(u-url)的链接。这很有趣,但非常有限。你无法使用这样的类名实现自定义信息。

HTML 通过为自定义属性保留 data- 前缀来解决这个问题。这样做效果很好,一些自定义属性库(如 Turbo)也接受了这一做法。就拿他们文档中的这个例子来说,它使用 data-turbo-method 属性将链接的方法从 GET 改为 DELETE(我并不保证你是否应该这么做):

<a href="/articles/54" data-turbo-method="delete">Delete the article</a>

好用!它永远不会被 HTML 标准的未来更新所覆盖。如果你想用这种方式编写你的整个属性库,你可以这样做。

如果我听起来有点矛盾,那是因为我认为 data-* 属性的一切,从它们的名称到人们使用的例子,都表明它们是用来存储数据而不是行为的。当然,你也可以直接用它来扩展 HTML,但它的名字和冗长的语法确实让人不愿意用它来构建语义。如果你说数据属性是用于 “应与特定元素相关联的数据,但不必具有任何定义的含义”,那么人们就会这样使用它们。

我们之所以知道这是真的,是因为一些非常流行的 JavaScript 库完全摒弃了 data- 属性,而只是添加了带有前缀的自定义属性,而这些属性不太可能被添加到 HTML 中。经典的 AngularJS 使用 ng-,这种前缀如今仍在互联网上随处可见;Alpine.js 使用 x- 作为其 15 个自定义属性的前缀;htmx 也使用 hx-(不过 AngularJS 和 htmx 都支持在前缀前加上 data-,这只是为了方便迂腐的人使用)。

浏览器早已非正式地支持这一功能,而且效果也很好。下面是一个使用 Alpine.js 来切换任意属性的按钮:

<button x-on:click="open = ! open">Toggle</button>

在我看来,这是一个正确的总体思路,尽管我(主观上)不喜欢它的几乎所有内容。我觉得 open = ! open 有点奇怪(我猜它是一个全局变量吧?),用 x- 命名空间仍然是一个小麻烦,总的来说,它偏离了 HTML 的语义,我不喜欢这种方式。可以肯定的是,WHATWG 不会添加 x-on:click,但在撰写本文时也不能保证。

自定义属性(仍然)是一种方式

2009 年,在 HTML5 规范制定过程中,John Allsop 在他的博客 “HTML 5 中的语义 “中提倡认真对待自定义属性的可能性。

HTML 5 应采用大量新属性,而不是新元素。每个属性都与语义的类别或类型有关。例如,正如我在另一篇文章中详述的,HTML 包括结构语义、修辞语义、角色语义(从 XHTML 中采用)以及其他类别或类别的语义。

这些新属性的使用方法与类属性的使用方法类似:将描述元素性质的语义附加到元素上,或添加有关元素的元数据。

他举了几个例子,比如在一个段落中标注 “具有讽刺意味”(我原以为这是个可笑的例子,直到我想起人们其实一直在非正式地用”/s “之类的东西这样做):

<p rhetoric="irony">He’s a fantastic person.</p>

或者这个可以让你以机器可理解的格式指定时间的元素(后来通过引入

<span equivalent=“2009-05-01”>May Day next year</span>

这是一条正确的道路。这件事说明了它是什么,并以最适合人类阅读的方式指定了机器可解析的语义(虽然 “等价 “在这种情况下是一个糟糕的名称选择)。

要使其正常运行,还有很多问题需要解答,Allsop 当时也承认了这一点:

我将本节标题定为 “关于解决方案的一些想法”,因为要真正制定出可行的解决方案,还需要做大量的工作。悬而未决的问题包括以下几点。

  • 应该有多少种不同的语义属性?这些类别是否应该可以扩展?
  • 如何确定词汇表?
  • 我们是像开发人员使用类值那样简单地发明我们想要的术语,还是所有可能的值都应由标准化规范来确定?还是应该有一种机制,利用某种配置文件来发明(并希望共享)词汇表?
  • 如果两个词汇表之间存在冲突,比如两个不同的词汇表定义了两个相同的术语,该如何解决?
  • 我们是否需要一种名称间隔形式,或者是否存在其他机制?

这些问题中的许多仍然没有很好的答案,因为网络开发领域在其 “去他妈的,JavaScript 什么都能做 “的阶段,大多让这个问题变了味。如果每次都重写表单,就不需要扩展表单的行为。当我们开始走出那个时代时,我建议我们从 Allsop 离开的地方重新开始,将 HTML 打造成一个安全的可扩展超文本系统。

我们可以做的一件事就是立即正式认可 Kebab-case 属性,这与本建议大致相同(感谢 Deniz Akşimşek 向我展示了这一点)。这不仅能让许多最流行的 HTML 增强框架,以及互联网上的大量现有代码都能使用有效的 HTML,还能让使用用户或库定义的语义来扩展 HTML 的项目合法化。

好吧,亚历克斯,你会如何扩展这个按钮?

还记得从一开始就的按钮吗?

如果你想制作大量显示点击信息的按钮,最好的方法不是使用 onclick 或事件监听器,而是增强按钮,这样你就可以将任何按钮变成信息按钮。

<button alert message="I was clicked">Click me</button>

<script>
// Get all the buttons with the 'alert' attribute
const buttons = querySelectorAll('button[alert]')
buttons.forEach(btn => {
  // Get the message property of the button
  const message = btn.getAttribute('alert')
  // Set the button to alert that message when clicked
  btn.addEventListener('click', () => { alert(message) })
})
<script>

这样做的好处是可以在文档中的任何按钮上重复使用。对于不那么琐碎的应用,您可以将该行为捆绑到一个界面非常漂亮的库中(目前可能带有前缀)。

不过,如果您只需要为一个或两个按钮执行此操作,只需使用 onclick 即可。这样代码量更少,不需要指定 id,也不会让你跑到代码库的另一部分去看它做了什么。在我看来,这些都是 “最佳实践”。

备注

  • 至于如何实现 tooShortMessage 和相关属性,就留给读者去实践吧。
  • 对于按钮示例来说,type=alert 可能会更好,因为它扩展了现有的语义,但我真的不想讨论如何命名以实现兼容性。
  • 具有讽刺意味的是,与 document.getElementById 添加功能的方式相比,属性接口的内联定义更有优势,因为启用接口的代码实际上可以在元素间通用重用。
  • 有些人认为 “自定义语义 “是一个矛盾的说法,因为如果它不在 HTML 标准中,就不是 “语义”。其实语义并非如此。语义描述的是事物的表达能力。把它想象成一种语言:某种东西是不是一种语言,与有多少人讲这种语言无关;这只会影响学习这种语言的实用性。用户定义的语义可能是非标准的(至少在正式采用之前),但它们仍然是语义。
  • 很多人都非常讨厌在属性声明中看到哪怕是极少量的 JS 语法。我认为这有点愚蠢,但我也能理解。我将来会写关于这个问题的文章。

本文文字及图片出自 Behavior Belongs in the HTML

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注