原文地址:developers.google.com/web/fundame…
原文作者:
发布时间:
自定义元素允许你扩展HTML并定义你自己的标签。它们是一个令人难以置信的强大功能,但它们也是低级的,这意味着并不总是清楚如何最好地实现你自己的元素。
为了帮助你创造最好的体验,我们整理了这份检查表。它分解了我们认为成为一个表现良好的自定义元素所需要的所有东西。
检查表
影子DOM
| / | 创建一个影子根来封装样式。 |
|---|---|
| 为什么呢? | 将样式封装在元素的影子根中,可以确保它在任何地方都能使用。如果开发者希望将你的元素放在另一个元素的影子根中,这一点尤其重要。这甚至适用于像复选框或单选按钮这样的简单元素。有可能你的影子根里面的内容只有样式本身。 |
| 例子 | <howto-checkbox>元素。 |
| / | 在构造函数中创建你的影子根。 |
|---|---|
| 为什么要这样做? | 构造函数是你对你的元素拥有专属知识的时候。这是一个设置实现细节的好时机,你不希望其他元素乱来。在稍后的回调中做这些工作,比如connectedCallback,意味着你需要防止你的元素被分离,然后重新连接到文档的情况。 |
| 示例 | <howto-checkbox>元素。 |
| / | 将元素创建的所有子元素放入它的影子根。 |
|---|---|
| 为什么? | 元素创建的子元素是其实现的一部分,应该是私有的。如果没有影子根的保护,外部的JavaScript可能会无意中干扰这些子元素。 |
| 示例 | <howto-tabs>元素。 |
| / | 使用<slot>将light DOM子代投射到你的影子DOM中。 |
|---|---|
| 为什么要这样做? | 允许你的组件的用户在你的组件中指定内容作为HTML子元素,使你的组件更可组合。当浏览器不支持自定义元素时,嵌套的内容仍然是可用的、可见的和可访问的。 |
| 示例 | <howto-tabs>元素。 |
设置一个:host显示风格(例如block、inline-block、flex),除非你喜欢默认的inline。
--- | ---
为什么? 自定义元素在默认情况下是显示:内联的,所以设置它们的宽度或高度不会有任何影响。这往往会让开发人员感到惊讶,并可能导致与布局页面相关的问题。除非你喜欢内联显示,否则你应该总是设置一个默认的显示值。
示例 <howto-checkbox>元素。
添加一个:host显示样式,尊重隐藏属性。
--- | ---
为什么要这样做? | 具有默认显示样式的自定义元素,例如 :host { display: block },将覆盖较低特异性的内置隐藏属性。如果你期望在你的元素上设置隐藏属性会使其显示:none,这可能会让你感到惊讶。除了默认的显示样式外,还可以使用 :host([hidden]) { display: none } 添加对 hidden 的支持。
示例 | <howto-checkbox>元素。
属性和特性
不要覆盖作者设置的全局属性。
--- | ---
为什么呢? | 全局属性是指那些存在于所有HTML元素上的属性。一些例子包括tabindex和role。一个自定义的元素可能希望将它的初始tabindex设置为0,这样它就可以被键盘聚焦。但是你应该先检查使用你的元素的开发者是否已经将其设置为其他值。例如,如果他们将tabindex设置为-1,这就是一个信号,表明他们不希望该元素是交互式的。
示例 | <howto-checkbox>元素。这将在不要覆盖页面作者中进一步解释。
始终接受基元数据(字符串、数字、布尔值)作为属性或特性。
--- | ---
为什么呢? | 自定义元素,就像它们的内置元素一样,应该是可配置的。配置可以通过声明的方式、通过属性或通过JavaScript属性来传递。理想情况下,每个属性都应该链接到相应的属性。
示例 | <howto-checkbox>元素。
目的是让原始数据属性和属性保持同步,从属性反映到属性,反之亦然。
--- | ---
为什么要这样做? | 你永远不知道用户将如何与你的元素交互。他们可能会在JavaScript中设置一个属性,然后期望使用像getAttribute()这样的API来读取这个值。如果每一个属性都有一个对应的属性,并且两者都能反映出来,那么将使用户更容易使用你的元素。换句话说,调用setAttribute('foo', value)也应该设置一个对应的foo属性,反之亦然。当然,这个规则也有例外。你不应该反映高频属性,例如视频播放器中的currentTime。使用你的最佳判断。如果用户似乎会与某个属性或特性进行交互,而且反映它并不麻烦,那么就这样做。
示例 | <howto-checkbox>元素。这将在避免重入问题中进一步解释。
旨在只接受富数据(对象、数组)作为属性。 --- | --- 为什么这么说呢? | 一般来说,没有内置的HTML元素通过其属性接受富数据(纯JavaScript对象和数组)的例子。相反,富数据是通过方法调用或属性来接受的。接受富数据作为属性有几个明显的缺点:将一个大对象序列化为字符串的成本很高,而且任何对象引用都会在这个字符串化过程中丢失。例如,如果你串联一个对象,而这个对象有对另一个对象的引用,或者可能是一个DOM节点,那么这些引用就会丢失。
不要将富数据属性反映到属性中。 --- | --- 为什么要这样做? | 将富数据属性反映到属性是不必要的昂贵,需要序列化和反序列化相同的JavaScript对象。除非你有一个只能用这个功能来解决的用例,否则可能最好避免使用这个功能。
考虑检查那些可能在元素升级前已经设置的属性。
--- | ---
为什么这样做? | 使用您的元素的开发人员可能会在元素的定义被加载之前尝试在元素上设置一个属性。如果开发者使用的是一个框架,该框架处理加载组件,将它们标记到页面上,并将它们的属性绑定到模型上,那么这种情况尤其如此。
示例 | <howto-checkbox>元素。更多解释请见 "让属性变懒"。
不要自应用类。 --- | --- 为什么? | 需要表达自己状态的元素应该使用属性来表达。类属性通常被认为是由使用你的元素的开发者所拥有的,而你自己写到它可能会无意中踩到开发者的类。
事件
针对内部组件活动派遣事件。 --- | --- 为什么? | 您的组件可能会因为只有您的组件才知道的活动而改变属性,例如,如果一个计时器或动画完成,或者一个资源完成加载。响应这些变化来通知宿主组件的状态发生了变化,这很有帮助。
不要响应宿主设置一个属性(向下数据流)来调度事件。
--- | ---
为什么要这样做呢? | 响应宿主设置一个属性而调度事件是多余的(宿主知道当前的状态,因为它刚刚设置了它)。响应主机设置属性而调度事件可能会导致数据绑定系统的无限循环。
示例 | <howto-checkbox>元素。
解释者
不要覆盖页面作者
使用你的元素的开发者有可能想要覆盖它的一些初始状态。例如,改变它的ARIArole或用tabindex改变焦点性。在应用你自己的值之前,请检查这些属性和其他全局属性是否已经被设置。
connectedCallback() {
if (!this.hasAttribute('role'))
this.setAttribute('role', 'checkbox');
if (!this.hasAttribute('tabindex'))
this.setAttribute('tabindex', 0);
让属性变得懒惰
开发者可能会在你的元素的定义被加载之前就试图设置它的属性。如果开发者使用的是一个框架,该框架处理加载组件,将它们插入到页面,并将它们的属性绑定到模型上,那么这种情况尤其如此。
在下面的例子中,Angular声明性地将模型的isChecked属性绑定到checkbox的checked属性上。如果howto-checkbox的定义是懒加载的,那么Angular有可能会在元素升级之前尝试设置checked属性。
<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>
自定义元素应该通过检查其实例上是否已经设置了任何属性来处理这种情况。<howto-checkbox>使用一个名为_upgradeProperty()的方法演示了这种模式。
connectedCallback() {
...
this._upgradeProperty('checked');
}
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
_upgradeProperty()从未升级的实例中捕获值,并删除该属性,因此它不会对自定义元素自己的属性设置器产生阴影。这样,当元素的定义最终被加载时,它可以立即反映出正确的状态。
避免重入问题
例如,使用attributeChangedCallback()将状态反映到底层属性是很有诱惑力的。
// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'checked')
this.checked = newValue;
}
但如果属性设置者也反映到属性上,这可能会造成一个无限循环。
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
// OOPS! This will cause an infinite loop because it triggers the
// attributeChangedCallback() which then sets this property again.
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
另一种方法是允许属性设置者反映到属性,并让获取者根据属性确定其值。
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
get checked() {
return this.hasAttribute('checked');
}
在这个例子中,添加或删除属性也会设置属性。
最后,attributeeChangedCallback()可以用来处理应用ARIA状态等副作用。
attributeChangedCallback(name, oldValue, newValue) {
const hasValue = newValue !== null;
switch (name) {
case 'checked':
// Note the attributeChangedCallback is only handling the *side effects*
// of setting the attribute.
this.setAttribute('aria-checked', hasValue);
break;
...
}
}