本教程介绍了不使用JavaScript框架编写的标准Web组件。你将了解它们是什么,以及如何在你自己的网络项目中采用它们。对HTML5、CSS和JavaScript有一定了解是必要的。
什么是网络组件?
理想情况下,你的开发项目应该使用简单、独立的模块。每个模块都应该有明确的单一责任。代码被封装起来:只需要知道在一组输入参数的情况下将会输出什么。其他开发者不应该需要检查实现(当然,除非有一个错误)。
大多数语言都鼓励使用模块和可重复使用的代码,但浏览器开发需要混合使用HTML、CSS和JavaScript来呈现内容、样式和功能。相关的代码可能被分割到多个文件中,并可能以意想不到的方式冲突。
诸如React、Vue、Svelte和Angular等JavaScript框架和库通过引入它们自己的组件化方法缓解了一些令人头痛的问题。相关的HTML、CSS和JavaScript可以被合并到一个文件中。不幸的是。
- 这是另一件需要学习的事情
- 框架的发展和更新往往会引起代码的重构甚至重写
- 在一个框架中编写的组件通常不会在另一个框架中工作,而且
- 框架可能很重,而且受限于JavaScript中可实现的功能
十年前,jQuery引入的许多概念被添加到浏览器中(如querySelector、closest、classList等)。今天,供应商正在实现无需框架就能在浏览器中原生工作的网络组件。
这需要一些时间。Alex Russell在2011年提出了最初的建议。谷歌的(polyfill)Polymer框架于2013年问世,但三年后才在Chrome和Safari浏览器中出现本地实现。有一些充满争议的谈判,但Firefox在2018年增加了支持,随后Edge(Chromium版本)在2020年支持。
网络组件是如何工作的?
考虑到HTML5<video> 和<audio> 元素,它允许用户使用一系列的内部按钮和控件来播放、暂停、倒带和快进媒体。默认情况下,浏览器会处理布局、造型和功能。
网络组件允许你添加自己的HTML自定义元素--比如一个<word-count> 标签来计算页面中的字数。该元素的名称必须包含一个连字符(-),以保证它不会与正式的HTML元素发生冲突。
然后为你的自定义元素注册一个ES2015 JavaScript类。它可以附加DOM元素,如按钮、标题、段落等。为了确保这些元素不会与页面的其他部分发生冲突,你可以将它们附加到一个内部的Shadow DOM,该DOM有自己的范围样式。你可以把它想象成一个小型的<iframe> ,尽管诸如字体和颜色等CSS属性是通过级联继承的。
最后,你可以用可重复使用的HTML模板将内容附加到你的Shadow DOM上,这些模板通过<slot> 标签提供一些配置。
与框架相比,标准的网络组件是很简陋的。它们不包括数据绑定和状态管理等功能,但Web组件有一些引人注目的优势。
- 它们是轻量级和快速的
- 它们可以实现在JavaScript中不可能实现的功能(如Shadow DOM)。
- 它们可以在任何JavaScript框架内工作
- 它们会被支持很多年--如果不是几十年的话
你的第一个网络组件
要开始使用,请在任何网页上添加一个<hello-world></hello-world> 元素。(关闭标签是必不可少的:你不能定义一个自我关闭的<hello-world /> 标签)。
创建一个名为hello-world.js 的脚本文件,并从同一个 HTML 页面加载它(ES 模块是自动延迟的,所以它可以放在任何地方 - 但在页面中越早越好)。
<script type="module" src="./hello-world.js"></script>
<hello-world></hello-world>
在你的脚本文件中创建一个HelloWorld 类。
// <hello-world> Web Component
class HelloWorld extends HTMLElement {
connectedCallback() {
this.textContent = 'Hello, World!';
}
}
网络组件必须扩展HTMLElement接口,它实现了每个HTML元素的默认属性和方法。
注意:Firefox可以扩展特定的元素,如HTMLImageElement 和HTMLButtonElement 。但是,这些元素不支持Shadow DOM,而且这种做法在其他浏览器中也不支持。
每当自定义元素被添加到文档中,浏览器就会运行connectedCallback() 方法。在这种情况下,它会改变内部文本。(不使用阴影DOM)。
该类必须在CustomElementRegistry中与你的自定义元素一起注册。
// register <hello-world> with the HelloWorld class
customElements.define( 'hello-world', HelloWorld );
加载页面,出现 "Hello World"。新的元素可以在CSS中使用hello-world { ... } 选择器进行样式设计。
参见CodePen上SitePoint(@SitePoint)的Pen
组件
。
创建一个<word-count> 组件
一个<word-count> 组件是更复杂的。这个例子可以生成阅读文章的字数或分钟数。可以使用国际化API来输出正确格式的数字。
可以添加以下元素属性。
round="N":将字数四舍五入到最接近的N(默认10)。minutes:显示阅读分钟数而不是字数(默认为false)。wpm="M":一个人每分钟可以阅读的字数(默认为200)。locale="L": 地区,如en-US或fr-FR(默认来自<html>lang属性,或en-US,如果没有的话)
任何数量的<word-count> 元素都可以被添加到一个页面中。比如说。
<p>
This article has
<word-count round="100"></word-count> words,
and takes
<word-count minutes></word-count> minutes to read.
</p>
WordCount构造器
一个新的WordCount 类被创建在一个名为word-count.js 的JavaScript模块中。
class WordCount extends HTMLElement {
// cached word count
static words = 0;
constructor() {
super();
// defaults
this.locale = document.documentElement.getAttribute('lang') || 'en-US';
this.round = 10;
this.wpm = 200;
this.minutes = false;
// attach shadow DOM
this.shadow = this.attachShadow({ mode: 'open' });
}
静态的words 属性存储了一个页面中的字数的计数。这被计算了一次并被重复使用,所以其他<word-count> 元素不需要重复工作。
当每个对象被创建时,constructor() 函数被运行。它必须调用super() 方法来初始化父HTMLElement ,然后可以根据需要设置其他默认值。
附加一个阴影DOM
构造函数还定义了一个影子DOM,其中包括 attachShadow()并在shadow 属性中存储了一个引用,因此它可以在任何时候被使用。
mode 可以被设置为。
"open":外层页面的JavaScript可以使用Element.shadowRoot访问Shadow DOM,或"closed": 外层页面无法访问Shadow DOM。
这个组件附加了纯文本,外部的修改并不关键。使用open ,这样页面上的其他JavaScript就可以查询到内容了。比如说。
const wordCount = document.querySelector('word-count').shadowRoot.textContent;
观察WordCount属性
任何数量的属性都可以被添加到这个Web组件中,但它只关注上面列出的四个属性。一个静态的observedAttributes() 属性返回一个要观察的属性数组。
// component attributes
static get observedAttributes() {
return ['locale', 'round', 'minutes', 'wpm'];
}
当这些属性中的任何一个在HTML或JavaScript的.setAttribute() 方法中被设置时,一个attributeChangedCallback() 方法被调用。它被传递给属性名称、以前的值和新的值。
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
// update property
if (oldValue === newValue) return;
this[property] = newValue || 1;
// update existing
if (WordCount.words) this.updateCount();
}
this.updateCount(); 调用会渲染组件,因此如果一个属性在第一次显示后被改变,它可以重新运行。
WordCount的渲染
当Web组件被附加到一个文档对象模型上时,connectedCallback() 方法被调用。它应该运行任何需要的渲染。
// connect component
connectedCallback() {
this.updateCount();
}
在Web组件的生命周期中,还有两个函数可用,尽管它们在这里没有必要。
disconnectedCallback()调用:当Web组件从Document Object Model中移除时被调用。它可以清理状态,中止Ajax请求,等等。adoptedCallback():当Web组件从一个文档移到另一个文档时被调用。我从来没有发现它的用途!
updateCount() 方法计算字数,如果以前没有做过的话。它使用第一个<main> 标签的内容,如果没有,则使用页面<body> 。
// update count message
updateCount() {
if (!WordCount.words) {
// get root <main> or </body>
let element = document.getElementsByTagName('main');
element = element.length ? element[0] : document.body;
// do word count
WordCount.words = element.textContent.trim().replace(/\s+/g, ' ').split(' ').length;
}
然后它用字数或分钟数(如果设置了minutes 属性)更新Shadow DOM。
// locale
const localeNum = new Intl.NumberFormat( this.locale );
// output word or minute count
this.shadow.textContent = localeNum.format(
this.minutes ?
Math.ceil( WordCount.words / this.wpm ) :
Math.ceil( WordCount.words / this.round ) * this.round
);
}
} // end of class
然后,Web组件类被注册。
// register component
window.customElements.define( 'word-count', WordCount );
参见CodePen上SitePoint(@SitePoint)的Pen
<字数>组件
