我们已经讨论过如何创建 web 组件比你想象的更容易,但规范的另一个方面我们还没有讨论,它是一种自定义(或者说增强)内置元素的方法。 它类似于创建完全自定义或“自治”的元素(比如之前文章中的<zombie-profile>
元素),但需要一些区别。
文章系列
- Web Components 比你想象的更容易
- 交互式 Web Components 比你想象的更容易
- 在 WordPress 中使用 Web Components 比你想象的更容易
- 用 Web Components 增强内置元素比你想象的更容易 (你就在这里)
- 上下文感知 Web Components 比你想象的更容易
- Web 组件伪类和伪元素比你想象的更容易
自定义的内置元素使用is
属性告诉浏览器,这个内置元素不是来自堪萨斯州的温顺、戴着眼镜的元素,而是来自 Web 组件星球的比子弹还快的、准备拯救世界的元素。(对堪萨斯人无意冒犯。你们也超级棒。)
增强温顺的元素不仅让我们可以享受元素的格式、语法和内置功能的优势,而且我们还得到了一个搜索引擎和屏幕阅读器已经知道如何解释的元素。 屏幕阅读器必须猜测<my-func>
元素中发生了什么,但对<nav is="my-func">
元素中发生的事情有一定的了解。(如果你有 func,请为了所有美好的事物,不要把它放在元素中。想想孩子们。)
这里需要指出的是,Safari(以及一些更小众的浏览器)只支持自治元素,不支持这些自定义的内置元素。 我们将在后面讨论解决此问题的 polyfill。
在我们掌握这个技巧之前,让我们从重写我们在 第一篇文章 中创建的<apocalyptic-warning>
元素开始,将其作为自定义的内置元素重写。(代码也在 CodePen 演示中提供。)
实际上,更改非常简单。 我们不会扩展通用的HTMLElement
,而是扩展一个特定的元素,在本例中是<div>
元素,它有类HTMLDivElement
。 我们还将在customElements.defines
函数中添加第三个参数:{extends: 'div'}
。
customElements.define(
"apocalyptic-warning",
class ApocalypseWarning extends HTMLDivElement {
constructor() {
super();
let warning = document.getElementById("warningtemplate");
let mywarning = warning.content;
const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
mywarning.cloneNode(true)
);
}
},
{ extends: "div" }
);
最后,我们将 HTML 从<apocalyptic-warning>
标签更新为<div>
标签,这些标签包含一个is
属性,该属性设置为“apocalyptic-warning”,如下所示
<div is="apocalyptic-warning">
<span slot="whats-coming">Undead</span>
</div>
提醒:如果你在 Safari 中查看以下内容,你将看不到任何漂亮的 Web 组件效果 *挥拳怒视 Safari*
只有某些元素可以附加阴影根。 其中一部分原因是,将阴影根附加到比如<a>
元素或<form>
元素可能会存在安全隐患。 可用元素列表主要是布局元素,如<article>
、<section>
、<aside>
、<main>
、<header>
、<div>
、<nav>
和<footer>
,以及文本相关的元素,如<p>
、<span>
、<blockquote>
和<h1>
-<h6>
。 最后,我们还有 body 元素以及任何有效的自治自定义元素。
添加阴影根不是我们创建 Web 组件的唯一方法。 在其基础上,Web 组件是将功能烘焙到元素中的一种方式,我们不需要在阴影中添加额外的标记来做到这一点。 让我们创建一个带有内置灯箱功能的图像来说明这一点。
我们将采用一个普通的<img>
元素,并添加两个属性:第一个是is
属性,它表示此<img>
是一个自定义的内置元素;第二个是一个数据属性,它包含将显示在灯箱中的较大图像的路径。(由于我使用的是 SVG,我只是使用了相同的 URL,但你可以在站点中嵌入一个较小的光栅图像,并在灯箱中嵌入一个较大版本。)
<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />
由于我们不能为这个<img>
做阴影 DOM,因此不需要<template>
元素、<slot>
元素或任何其他这些东西。 我们也不会有任何封装的样式。
因此,让我们直接跳到 JavaScript
customElements.define(
"light-box",
class LightBox extends HTMLImageElement {
constructor() {
super();
// We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.
let lb = document.createElement("div");
// Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here
lb.style.display = "none";
lb.style.position = "absolute";
lb.style.height = "100vh";
lb.style.width = "100vw";
lb.style.top = 0;
lb.style.left = 0;
lb.style.background =
"rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
lb.style.backgroundSize = "contain";
lb.addEventListener("click", function (evt) {
// We’ll close our light box by clicking on it
this.style.display = "none";
});
this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image
this.addEventListener("click", function (evt) {
// Opens the light box when the image is clicked.
lb.style.display = "block";
});
}
},
{ extends: "img" }
);
现在我们知道了自定义的内置元素是如何工作的,我们需要努力确保它们在任何地方都能工作。 是的,Safari,这个白眼是给你的。
WebComponents.org 有一个通用的 polyfill,它可以处理自定义的内置元素和自治元素,但因为它可以处理很多内容,所以它可能比你需要的多得多,特别是如果你只是想在 Safari 中支持自定义的内置元素。
由于 Safari 支持自治自定义元素,我们可以用自治自定义元素(例如<lightbox-polyfill>
)替换<img>
。“这将只有两行代码!”作者天真地对自己说。 37 个小时盯着代码编辑器、两次精神崩溃、以及对职业道路的严重重新评估之后,他意识到如果他想编写这两行代码,就需要开始打字。 最后,它变成了大约 60 行代码(但你可能足够厉害,可以用 10 行代码做到这一点)。
灯箱的原始代码大部分可以保持原样(虽然我们很快就会添加一个新的自治自定义元素),但需要做一些小的调整。 在自定义元素的定义之外,我们需要设置一个布尔值。
let customBuiltInElementsSupported = false;
然后在LightBox
构造函数中,我们将布尔值设置为true
。 如果不支持自定义的内置元素,构造函数将不会运行,布尔值也不会被设置为true
;因此,我们直接测试了是否支持自定义的内置元素。
在我们使用这个测试来替换我们的自定义内置元素之前,我们需要创建一个作为 polyfill 使用的自治自定义元素,即<lightbox-polyfill>
。
customElements.define(
"lightbox-polyfill", // We extend the general HTMLElement instead of a specific one
class LightBoxPoly extends HTMLElement {
constructor() {
super();
// This part is the same as the customized built-in element’s constructor
let lb = document.createElement("div");
lb.style.display = "none";
lb.style.position = "absolute";
lb.style.height = "100vh";
lb.style.width = "100vw";
lb.style.top = 0;
lb.style.left = 0;
lb.style.background =
"rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
lb.style.backgroundSize = "contain";
// Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.
const shadowRoot = this.attachShadow({ mode: "open" });
// We create an image element to display the image on the page
let lbpimg = document.createElement("img");
// Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image
lbpimg.setAttribute("src", this.getAttribute("src"));
lbpimg.setAttribute("alt", this.getAttribute("alt"));
// Add the div and the image to the `shadowRoot`
shadowRoot.appendChild(lb);
shadowRoot.appendChild(lbpimg);
// Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.
lb.addEventListener("click", function (evt) {
this.style.display = "none";
});
lbpimg.addEventListener("click", function (evt) {
lb.style.display = "block";
});
}
}
);
现在我们已经准备好了自治元素,我们需要一些代码来在浏览器不支持自定义的<img>
元素时替换它。
if (!customBuiltInElementsSupported) {
// Select any image with the `is` attribute set to `light-box`
let lbimgs = document.querySelectorAll('img[is="light-box"]');
for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images
let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element
// Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.
replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));
replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));
replacement.shadowRoot.querySelector("div").style.background =
"rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";
// Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing
lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);
// Remove the customized built-in image
lbimgs[i].remove();
}
}
就是这样! 我们不仅构建了自治自定义元素,而且还构建了自定义内置元素——包括如何在 Safari 中使它们工作。 而且我们还获得了结构化、语义化的 HTML 元素的所有好处,包括让屏幕阅读器和搜索引擎了解这些自定义元素是什么。
放手去自定义那些内置元素吧,不要有任何顾虑!
感觉我们又回到了起点…
或者,这是否过于简单化?!
您好,很棒的文章! 我无法让 Safari polyfill 工作… 您究竟在哪里将
customBuiltInElementsSupported
设置为true
? ;)嘿,Rasso,抱歉,如果它不清楚。 我在
LightBox
类中的super();
调用之后将其设置为 true,即class LightBox extends HTMLImageElement {
constructor() {
super();
customBuiltInElementsSupported = true;
但它应该在构造函数中的几乎任何地方都能工作。 文章中的最后一个示例包含完整的上下文,如果你需要更多参考。
嘿,感谢您写这篇文章!
到目前为止,我一直避免使用这种方法,因为 Safari 团队声明他们不喜欢它,也不会实现它。 因此,在我看来,它就像一匹死马,即使其他浏览器供应商正在支持它。
我对此有复杂的心情,尤其是我不喜欢无限期地添加 polyfill,更像是临时解决方案。 您如何看待这个问题?
嗨,Andreas,你说的很有道理。 Safari 不愿支持自定义内置元素是许多开发人员的痛点,也应该如此。
我对永久性的 polyfill 也持类似看法。 它们通常应该是一种临时解决方案。
也就是说,我认为每项技术都是我工具箱中的工具。 对于许多应用程序来说,Safari 的不愿支持会胜过 SEO 和屏幕阅读器的优势,但我可以预见,在某些应用程序中,这些优势会超过为 Safari 添加 polyfill 的弊端。 或者,在渐进增强的情况下,可能会忽略 Safari 的 polyfill。 例如,我使用的灯箱示例实际上并没有添加新信息,也没有改变对网站的理解。 我可以预见它在 Safari 上会被忽略,因为 img 元素本身在 Safari 中仍然有效。
相关 评论和讨论。