开始构建 Web Components!第一部分:标准
如今,基于组件的UI风靡一时。事实上,它已经非常成熟,人们甚至开始将老式的 jQuery 小部件重新命名为“jQuery 组件” ;)
我们所说的“组件”通常指的是独立、可复用的 UI 组件,一旦编写完成,我们就可以将其插入到应用中的任何地方。精美的交互式按钮、精心设计的引言,以及经久不衰的卡片小部件,都是非常适合组件化的设计类型的例子。
你知道吗?Web 有自己的原生组件模块,无需使用任何库。真的!你可以编写、发布和复用单文件组件,它们可以在任何优秀的浏览器和框架(如果你喜欢的话)上运行。继续阅读,了解详情!
概述
Web Components
是一个总称,指的是一组四种浏览器标准,它们共同构成了 Web 的原生组件模型。
<template>
元素让你可以快速重用DOM的部分- 自定义元素将JS类连接到自定义HTML标签
- Shadow DOM将您的内容隐藏在页面其余部分
- JavaScript 模块用于打包和发布组件
这些标准各自构成了这道难题的一部分。在这篇入门文章中,我们将简要介绍每个标准,并解释它们如何在实际的 Web 开发中帮助我们。
<template>
元素
组件的基本理念是可复用的 UI。为了实现这一点,我们需要一种为组件定义模板的方法。如果你熟悉 React,那么你可能之前使用过JSX。如果你更喜欢 Angular,那么你可能已经在 JavaScript 模板字面量中定义过模板。
该<template>
元素允许我们定义 HTML 片段,这些片段只有在 JavaScript 克隆后才会添加到文档中。浏览器只需解析一次该 HTML 片段(例如,在文档加载时),然后就可以在需要时以低成本克隆它。
下面是模板元素实际作用的一个(真正人为设计的)示例:
<template id="dialog-template">
<dialog>
<p></p>
<button>⚓️ All Ashore!</button>
</dialog>
</template>
<label>
Type a <abbr title="message"> 💌</abbr>
<input id="input"/>
</label>
<button id="clone-it"><abbr title="Go!">🦑 Ahoy!</abbr></button>
<script>
document.getElementById('clone-it').onclick = () => superAlert(input.value);
function superAlert(message) {
// get a reference to the template
const template = document.getElementById('dialog-template');
// clone or "stamp" the template's contents
const clone = template.content.cloneNode(true);
// Make any changes to the stamped content
const diag = clone.firstElementChild;
// <dialog> element polyfill
dialogPolyfill.registerDialog(diag);
diag.firstElementChild.textContent = message;
diag.lastElementChild.onclick = function closeModal() {
diag.close();
diag.remove();
}
document.body.appendChild(diag)
diag.showModal();
}
</script>
使用<template>
元素既简单又高效。我做了一个小小的基准测试,用三种方式构建一个简单的表格:克隆模板元素、直接使用 DOM API以及设置innerHTML
。克隆模板元素是最快的,DOM API 稍慢一些,而克隆模板元素innerHTML
是迄今为止最慢的。
因此,该<template>
元素允许我们解析一次 HTML,然后根据需要多次重用它。这正是我们可重用组件所需要的!
在MDN上阅读有关<template>
元素及其DOM API 的更多信息。
自定义元素
我们要了解的第二个标准是自定义元素。它的作用正如其名称所示:它允许您定义自己的自定义 HTML 标签。现在,您不必再满足于简单的<div>
和,还可以使用和来<span>
标记您的页面。<super-div>
<wicked-span>
自定义元素的工作方式与内置元素类似;将它们添加到您的文档中,赋予它们子元素,并在其上使用常规 DOM API 等。您可以在使用常规元素的任何地方使用自定义元素,包括在流行的 Web 框架中
所有自定义元素标签名称都必须包含短划线,以区别于内置元素。这还能避免在同一个应用中使用自定义元素时发生名称冲突<bobs-input>
。<sallys-input>
此外,自定义元素可以拥有自己的自定义属性、DOM 属性、方法和行为。
如何使用自定义元素的示例:
<section>
<p>Twinkle, twinkle, little <super-span animation="shine">star</super-span>.</p>
<awesome-button exuberant>Shine it!</awesome-button>
</section>
自定义元素被定义为JavaScript 类window.customElements
,并通过其方法在对象上注册define
,该方法有两个参数:一个用于定义元素名称的字符串,以及一个用于定义其行为的 JavaScript 类。
这个例子让一个无聊的老<span>
表情拥有了超能力!快来试试吧。
customElements.define('super-span', class SuperSpan extends HTMLElement {
/**
* `connectedCallback` is a custom-element lifecycle callback
* which fires whenever the element is added to the document
*/
connectedCallback() {
this.addEventListener('click', this.beAwesome.bind(this))
this.style.display = 'inline-block';
this.setAttribute('aria-label', this.innerText);
switch (this.innerText) {
case 'star': this.innerText = '⭐️';
}
}
/**
* You can define your own methods on your elements.
* @param {Event} event
* @return {Animation}
*/
beAwesome(event) {
let keyframes = [];
let options = {duration: 300, iterations: 5, easing: 'ease-in-out'}
switch (this.getAttribute('animation')) {
case 'shine': keyframes = [
{opacity: 1.0, blur: '0px', transform: 'rotate(0deg)'},
{opacity: 0.7, blur: '2px', transform: 'rotate(360deg)'},
{opacity: 1.0, blur: '0px', transform: 'rotate(0deg)'},
];
}
return this.animate(keyframes, options)
}
});
自定义元素拥有诸如生命周期回调和可观察属性等内置功能。我们将在后续文章中介绍这些功能。剧透预警:您可以在 MDN 上阅读有关自定义元素的所有内容。
影子 DOM
文档树中隐藏着什么,隐藏在阴影中,隐藏在无辜节点不敢涉足的黑暗地方?
哒哒哒哒哒哒哒!影子 DOM!
我是黑暗。我是黑夜。我是暗影之主!
虽然“Shadow DOM”听起来有点奇怪,但事实证明你已经用它很多年了。每次你使用<video>
带有控件的元素,或者<input>
带有数据列表的元素,或者其他类似日期选择器元素的元素时,你都在使用 Shadow DOM。
Shadow DOM 只是一个 HTML 文档片段,它对用户可见,但同时与文档的其余部分隔离。类似于 iframe 将一个文档与另一个嵌入文档隔离开来的方式,Shadow root 将文档的一部分与主文档隔离开来。
例如,视频元素中的控件实际上是一个独立的 DOM 树,像蝙蝠侠一样,隐藏在页面的阴影中。全局样式不会影响视频控件,反之亦然。
为什么隔离 DOM 是一件好事?在开发任何规模较大的 Web 应用时,CSS规则和选择器都可能很快失控。你可能为页面的某个部分编写了完美的 CSS,但你的同事却在后续的级联中否决了你的样式。更糟糕的是,你添加到应用中的新功能可能会破坏现有内容,而没有人会注意到!
随着时间的推移,针对这个问题已经开发出了许多解决方案,从严格的命名约定到“CSS-in-JS”,但没有一个特别令人满意。有了影子 DOM,我们就可以在浏览器中内置一个全面的解决方案。
Shadow DOM 隔离了 DOM 节点,让您可以自由地设置组件的样式,而不必担心应用的其他部分会破坏它们。style
您无需费力地使用晦涩难懂的类名或将所有内容塞进属性中,而是可以用一种简单直接的方式设置组件的样式:
<template id="component-template">
<style>
:host {
display: block;
}
/* These styles apply only to button Elements
* within the shadow root of this component */
button {
background: rebeccapurple;
color: inherit;
font-size: inherit;
padding: 10px;
border-radius: 4px;
/* CSS Custom Properties can pierce the shadow boundary,
* allowing users to style specific parts of components */
border: 1px solid var(--component-border-color, ivory);
width: 100%;
}
</style>
<!-- This ID is local to the shadow-root. -->
<!-- No need to worry that another #button exists. -->
<button id="button">I'm an awesome button!</button>
</template>
<style>
/* These styles affect the entire document, but not any shadow-roots inside of it */
button {
background: cornflowerblue;
color: white;
padding: 10px;
border: none;
margin-top: 20px;
}
/* Custom Elements can be styled just like normal elements.
* These styles will be applied to the element's :host */
button,
awesome-button {
width: 280px;
font-size: inherit;
}
</style>
<awesome-button></awesome-button>
<button id="button">I'm an OK button!</button>
<section id="display">
<abbr title="click">🖱</abbr> a <abbr title="button">🔲</abbr>
</section>
Shadow DOM 是 Web 组件的秘密武器。它让组件变得独立,让我们可以放心地将它们放入页面,而不必担心破坏应用的其他部分。
从 Firefox 63 开始,它可以在所有优秀的浏览器上原生使用。
在 MDN 上了解有关 Shadow DOM 的更多信息
有了这三个标准:模板、自定义元素和 Shadow DOM,我们就拥有了编写丰富的 UI 组件所需的一切,这些 UI 组件可以直接在浏览器中运行,而无需任何特殊的工具或构建步骤。第四个标准 JavaScript 模块使我们能够构建由自定义元素组成的复杂应用,并将我们的组件发布给其他人使用。
JavaScript 模块
当我们使用“模块”这个词时,我们指的是一个独立的软件,它拥有自己的作用域。换句话说,如果我foo
在某个模块中定义了一个变量,那么我只能在该模块内部使用该变量。如果我想在其他模块中访问它foo
,则需要先显式地导出它。
开发人员一直在寻找编写模块化 JavaScript 的方法,但直到最近(规范中自 2015 年以来,实践中大约在过去一年),JavaScript 才拥有自己的模块系统。
import { foo } from './foo.js'
const bar = 'bar'
export const baz = foo(bar)
关于模块有很多话要说,但就我们 的 目的 而言,使用它们来编写和发布 Web 组件就足够了。
这是一个简单的例子来激发您的兴趣。
// super-span.js
const options = {duration: 300, iterations: 5, easing: 'ease-in-out'}
const keyframes = [
{opacity: 1.0, blur: '0px', transform: 'rotate(0deg)'},
{opacity: 0.7, blur: '2px', transform: 'rotate(360deg)'},
{opacity: 1.0, blur: '0px', transform: 'rotate(0deg)'},
]
const template = document.createElement('template')
template.innerHTML = `
<style>
span {
display: inline-block;
font-weight: var(--super-font-weight, bolder);
}
</style>
<span><slot></slot></span>
<abbr title="click or mouse over">🖱</abbr>
`;
customElements.define('super-span', class SuperSpan extends HTMLElement {
$(selector) {
return this.shadowRoot && this.shadowRoot.querySelector(selector)
}
constructor() {
super()
this.shine = this.shine.bind(this)
const root = this.attachShadow({mode: 'open'})
root.appendChild(template.content.cloneNode(true))
this.addEventListener('click', this.shine)
this.addEventListener('mouseover', this.shine)
}
connectedCallback() {
const slot = this.$('slot')
const [node] = slot.assignedNodes()
this.setAttribute('aria-label', node.textContent)
node.textContent = '⭐️'
}
shine(event) {
this.$('span').animate(keyframes, options)
}
});
然后在我们的应用程序的 HTML 中:
<script type="module" src="./super-span.js"></script>
<super-span>star</super-span>
我的朋友们,这就是让您意识到 Web 组件有多么棒的瞬间。
现在,您可以轻松地将具有出色行为和语义的预制自定义元素直接导入到您的文档中,而无需任何构建步骤。
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Be Excellent to Each Other</title>
<script type="module" src="//unpkg.com/@power-elements/lazy-image/lazy-image.js?module"></script>
<script type="module" src="//unpkg.com/@granite-elements/granite-alert/granite-alert.js?module"></script>
<script type="module" src="//unpkg.com/@material/mwc-button/mwc-button.js?module"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Cross-platform, Framework-Agnostic, Reusable Components</h1>
</header>
<main>
<granite-alert id="alert" level="warning" hide>
<lazy-image role="presentation"
src="//placekitten.com/1080/720"
placeholder="//web-components-resources.appspot.com/static/logo.svg"
fade
></lazy-image>
</granite-alert>
<mwc-button id="button" raised>🚀 Launch</mwc-button>
<script>
const alert = document.getElementById('alert')
const button = document.getElementById('button')
const message = document.getElementById('message')
button.onclick = () => {
alert.hide = !alert.hide;
button.textContent = alert.hide ? '🚀 Launch' : '☠️ Close'
}
</script>
</main>
</body>
</html>
结论
Web 组件标准使我们能够构建独立、可重复使用的 UI,这些 UI 可直接在浏览器中运行,无需繁琐的构建步骤。这些组件可以在任何使用常规元素的地方使用:无论是在纯 HTML 中,还是在应用的框架驱动模板中。
在我们的下一篇文章中,如果上帝愿意的话,我们将了解webcomponentsjs polyfill如何让我们为那些本身不支持它们的浏览器设计组件和编写应用程序。
😀 感谢阅读!😁
查看本系列的下一篇文章
勘误表
- 本文的先前版本展示了访问 中的轻量级 DOM 属性和子项的示例
constructor
。此类工作应推迟到connectedCallback
。 - 自本文最初发布以来,微软已开始在 Edge 中开发 Web 组件标准。狂欢时间到了!