【Chrome官方文章翻译】可重用的Web Components
可重用的Web Components
Web Components可以让开发者创造标签
,开发者可以增强现有HTML标签,扩展其他开发者编写的标签。
它提供了基于web标准的方式,可以让你用少量的、模块化的代码去编写可重用的组件。
定义一个新元素
使用window.customElements.define可以定义一个新元素。
它的第一个参数是标签名,第二个参数是一个继承HTMLElement的类。
1 | class AppDrawer extends HTMLElement {} |
那么如何使用它呢?你只要像使用正常的html标签一样使用它就可以了!
1 |
|
自定义的元素和普通的HTML元素没有区别。它的实例可以在页面中声明,然后使用JS定义它。它也可以使用事件监听等HTML元素具有的特性。这在后面将会详细介绍。
自定义元素的JS API
自定义元素的功能使用ES2015 class实现,由于它继承了HTMLElement,所以拥有完整的DOM API。这也意味着JS里的属性、方法成了DOM接口的一部分,就像是我们在用JS为标签创建API。
例子:
1 | class AppDrawer extends HTMLElement { |
在这个例子中,我们创建了拥有open属性和toggleDrawer()方法的app-drawer元素。
在定义元素的class语法中,this指向元素本身。在这个例子中它可以获取属性、监听事件。其实其他DOM API它都可以使用,比如获取children(this.children)、选择元素(this.querySelectorAll(‘.items’))等等。
自定义元素命名规则
必须包含破折号(-),这是为了区分自定义元素和HTML元素,并且确保兼容性(即使HTML增加新标签也不会导致冲突)
不能重复注册相同标签,否则会抛DOMException,因为这完全没必要。
不支持自闭合标签的写法,因为在HTML规范里只有一些标签是允许自闭合的。所以只有
<app-drawer></app-drawer>
这样写才是正确的。
自定义元素的生命周期(custom element reactions)
自定义元素有它的生命周期hooks。罗列如下:
名称 | 触发时机 |
---|---|
constructor | 实例被创建或升级时执行,通常用来初始化一些状态、设置事件监听或者创建shadow dom |
connectedCallback | 当元素被添加到DOM中触发,通常在这个时候进行数据请求等 |
disconnectedCallback | 元素在DOM中被移除时触发,通常做一些清理操作 |
attributeChangedCallback(attrName, oldVal, newVal) | 当监听的属性(在observedAttributes属性列表中)被增加、删除、更新、替换时触发。在元素被解析器创建或升级相应属性值初始化时也会触发 |
adoptedCallback | 元素被移动到一个新文档中触发(如:调用document.adoptNode(el))) |
例子:
1 | class AppDrawer extends HTMLElement { |
回调函数是同步的。比如当调用el.setAttribute(),attributeChangedCallback()会立刻执行。
这些回调函数不是在所有情况下都是可靠的,比如用户关闭标签页时disconnectedCallback不会执行。
JS和HTML属性
使用JS设置HTML属性
使用JS设置HTML属性是很常见的,比如在JS中执行
1 | div.id = 'my-id'; |
html属性就会变为
1 | <div id="my-id" hidden> |
这是很有用的功能。比如你想实现样式根据JS状态改变。
在下面这个例子中我们可以通过点击切换app-drawer元素的透明度,并为它设置一些属性。
1 |
|
元素升级
我们前面学习了使用customElements.define定义元素,它的定义和注册(使用)是一起的,但其实你可以在定义之前就注册(使用)这个元素。也就说你先注册<app-drawer>
,但不执行customElements.define('app-drawer', ...)
也是可以的。
因为浏览器对未知标签会区别对待。
如果你先元素标签,后面再调用define()方法定义元素,这种方式就叫做元素升级。
你可以使用window.customElements.whenDefined()
方法来监听元素在什么时候被定义。
1 | customElements.whenDefined('app-drawer').then(() => { |
下面这个例子,在所有子元素被升级后再做了一些操作。
1 | <share-buttons> |
元素定义
自定义元素可以在它的内部代码中使用DOM API管理自己的内容。而元素生命周期也使得这种管理更加方便。
例子 - 使用默认的HTML创建一个元素
1 | customElements.define('x-foo-with-markup', class extends HTMLElement { |
网页上会呈现为
1 | <x-foo-with-markup> |
使用Shadow DOM创建元素
Shadow DOM可以在页面中提供一块区域让元素有隔离的渲染和样式。你甚至可以将整个应用隐藏到一个标签中。
在constructor中调用this.attachShadow方法就可以使用Shadow DOM了。
1 |
|
网页呈现结果
1 | <x-foo-shadowdom> |
使用<template>
创建元素
template可以让你很方便地声明元素的结构。
例子 - 使用template注册一个Shadow DOM元素
1 | <template id="x-foo-from-template"> |
这个例子中有几个关键的知识点:
定义了一个新标签
<x-foo-from-template>
使用template创建Shadow DOM
由于Shadow DOM的存在,元素DOM是内置的
由于Shadow DOM的存在,元素的CSS也是内置的,并且样式作用限定在了元素的内部
给自定义元素加样式
即使你的元素使用Shadow DOM限定了样式,元素的样式也会受页面样式的影响。
页面的样式也被称为用户定义样式(user-defined styles)。如果它和Shadow DOM有相同的样式,则用户自定义样式生效。
给未定义的元素设置样式
元素未定义(升级)之前,你可以使用:not(:defined)伪类为它设置样式。
这种预设样式也是有用的,比如你可以让元素占有一定布局空间,即使它还没被定义。
下面这个例子,元素在被定义之前也占据一定空间。当然了,当元素被定义后,app-drawer:not(:defined)选择器也就失效了。
1 | app-drawer:not(:defined) { |
扩展元素
自定义元素API不仅对创建新的HTML元素有用的,对于扩展其他自定义元素甚至是浏览器内置元素也是有用的。
扩展自定义元素
拓展自定义元素使用继承它的class定义来完成。
例子 - 创建<fancy-app-drawer>
继承自<app-drawer>
1 | class FancyDrawer extends AppDrawer { |
继承原生HTML元素
假如你想创造一个功能更强大的<button>
,用来替代原生<button>
的功能和行为。最佳方案就是使用自定义元素去扩展已有HTML元素的功能。
这种继承自HTML元素的自定义元素也被称作定制内置元素(customized built-in element)。它不仅能获得原生元素的特性(属性、方法、可访问性),还能增强元素的功能。没有比使用定制内置元素去编写一个逐步增强的web应用更好的方式了。
注:定制内置元素不是所有浏览器都支持。
例子 - <FancyButton>
1 | // See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces |
我们注意到这里define()需要指定继承了哪个浏览器标签。这是必要的,因为即使是不同的标签也可能继承相同的DOM接口。比如<q>
、<blockquote>
就都继承HTMLQuoteElement。
定制内置元素可以为原生标签添加is=””属性。
1 | <!-- This <button> is a fancy button. --> |
或者使用new操作符
1 | let button = new FancyButton(); |
例子 - 扩展<img>
1 | <!-- This <img> is a bigger img. --> |
或者创建一个image实例
1 | const BiggerImage = customElements.get('bigger-img'); |
一些细节
未知元素vs未定义元素
HTML是开放、灵活的。比如你声明一个<randomtagthatdoesntexist>
标签是不会抛错的。这是因为html规范允许这么做,规范中这个标签会被解析为HTMLUnknownElement。
所以对于自定义元素,不合法的自定义元素命名可能会被解析为HTMLElement(或HTMLUnknownElement)。
API参考
全局customElements上的defines可以用来定义元素。
define(tagName, constructor, options)
例子:
1 | customElements.define('my-app', class extends HTMLElement { ... }); |
get(tagName)
传入一个有效的自定义元素名称,它会返回这个元素的构造函数。如果元素没被注册则返回undefined。
例子
1 | let Drawer = customElements.get('app-drawer'); |
whenDefined(tagName)
返回一个Promise,当元素被定义时会执行resolve。如果元素已经被定义,则立即执行resolve。当传入tagName是无效命名时执行reject。
例子:
1 | customElements.whenDefined('app-drawer').then(() => { |
历史和浏览器支持
历史
Chrome 36+实现了v0版的自定义元素API,使用的是document.registerElement(不是customElements.define)来定义元素。v0这个版本已经弃用了。
目前浏览器供应商们使用的都是现在的v1版,使用customElements.define()来定义元素。
浏览器支持
Chrome 54, Safari 10.1, Firefox 63都实现了v1版,Edg也在开发中。
你可以使用下面代码来判断浏览器是否支持v1版。
1 | const supportsCustomElementsV1 = 'customElements' in window; |
Polyfill
到浏览器广泛支持之前,v1版可以使用Polyfill。
我们建议使用webcomponents.js loader去实现web components polyfill的最优加载。它使用了特征检测,只有在需要的时候才会异步加载polyfill。
polyfill安装
1 | npm install --save @webcomponents/webcomponentsjs |
polyfill使用
1 | <!-- Use the custom element on the page. --> |
注:defined css伪类不能被polyfill。
总结
自定义元素让我们可以定义新的HTML标签,创建可复用的组件。结合其他的特性比如Shadow DOM、<template>
等,我们开始逐渐意识到Web Components这个蓝图。
它可以:
跨浏览器(web标准)创建和扩展可复用组件
不需要库或框架的开发(强大的JS/HTML)
熟悉的编程模型(DOM/CSS/HTML)
和其他新的web平台特性完美融合( (Shadow DOM,
<template>
, CSS自定义属性等)和浏览器DevTools的紧密集成
使用已有的可访问特性。
本文翻译自
- Reusable Web Components By Eric Bidelman
Engineer @ Google working on web tooling: Headless Chrome, Puppeteer, Lighthouse