Web组件最佳实践
编写 Web 组件很难。编写优秀的 Web 组件更是难上加难。过去一年我一直在构建AstroUXDS Web 组件,从中我深刻体会到,一个优秀的 React/Vue/Angular/FoobarJS 组件并不一定就是优秀的 Web 组件。对于那些刚刚接触 Web 组件的人来说,这份指南正是我一年前梦寐以求的。
注意:这其中很多内容主要涉及设计系统的实施。
你为什么要这样做?
Web Components 的前景和魅力确实令人难以抗拒。能够编写和维护一套可在任何框架中使用的代码库,几乎立刻就能吸引所有人。然而,Web Components 并非万能灵药。它需要一套全新的方法和思维模式。很多人会告诉你 Web Components 很棒:“看看发布一个按钮组件是多么容易,它完全封装了你设计系统的样式!” 但他们没有告诉你的是,现在你必须考虑如何让按钮与表单正确交互,或者如何处理辅助功能。
当你选择编写 Web 组件时,你便承担了全部责任,必须仔细考虑每一种可能的用例和场景,同时兼顾开发者体验、用户体验和可维护性。务必做好万全准备,考虑每一个细节。否则,用户会非常不满,因为 Shadow DOM 容错率极低。很多时候,开发者自己也无能为力。
记住,我们编写的是自定义(HTML)元素。这些原子需要足够灵活,才能创造宇宙。
Web组件的剖析
要编写一个优秀的 Web 组件,你需要对所有可用的 API 有深入的了解。你需要在可扩展性和易用性之间不断权衡。对于任何特定的功能,你都应该仔细考虑使用哪个 API。
如果你熟悉框架,可能已经了解插槽、属性和事件。Web Components 为我们提供了两个独特的 API——CSS 自定义属性和 CSS 阴影部分。现在,组件的样式拥有了自己的 API。好好利用这一点吧。
老虎机
- ✅ 非常灵活
- ❌ 增加组件代码的复杂性
- ❌ 要求开发人员编写更多样板代码
插槽可能是扩展性最强的 API,因为它们位于 Shadow DOM 之外,可以包含任何自定义 HTML。
属性/特性
- ✅ 易于使用
- ✅ 用户熟悉
- ❌ 灵活性不高
属性和特性是最为人熟知的概念,通常用于控制状态之类的东西。然而,在允许自定义内容方面,它们的灵活性最差。
例如:
<my-component content="This is my content!"></my-component>
如果只需要显示一个简单的字符串,这个组件就很好用。但如果我想传入自己的 HTML 代码呢?比如,我想添加一个 SVG 图标或者一个完整的表单。我不可能把所有这些都塞进一个字符串里。这样一来,这个组件对我来说就没什么用了。
方法
- ✅ 提供独特的功能
- ❌ 需要 JavaScript
如果你的组件需要执行某种操作,那么公共方法就非常有用。一个典型的模态框组件就是一个很好的例子,它可能包含 `on`show()和 ` hide()on` 方法。在这种情况下,仅仅使用一个prop 可能不足以满足开发者在模态框打开后open执行某些操作的需求,因为此时模态框可能尚未完全打开。开发者需要使用模态框的 `on`方法,该方法可以返回一个 Promise,并在模态框打开完成后解析。show()
CSS 自定义属性
- ✅ 灵活
- ❌ 如果使用不当,DX效果不佳
CSS 自定义属性是允许开发者穿透 Shadow DOM 的两种方法之一。请记住,my-button { background: red; }由于 Shadow DOM 的封装性,直接使用 `{{ background-color }}` 是无效的。但是,如果您使用 CSS 自定义属性来控制背景颜色,开发者就可以执行类似 `{{ background-color }}` 的操作--button-bg-color: red;。
早期,CSS 自定义属性是开发者自定义 Web 组件样式的唯一途径。这导致许多早期用户添加了数量惊人的 CSS 自定义属性,例如--button-border-radius`<style>`、--button-text-color`<style> --button-font-family`、`<style>` 等等,几乎每个你能想到的 CSS 属性都有对应的自定义属性。简直一团糟。幸运的是,我们后来有了更好的方法——CSS 阴影部件。
但CSS自定义属性仍然有其用武之地:
CSS 变量的作用域限定于宿主元素,并且可以在整个组件中重复使用。一个很好的 CSS 变量示例是 `
--border-widthborder-width`,它可以在组件中重复使用,以确保所有内部元素的边框宽度相同。- Shoelace - 何时使用 CSS 自定义属性
CSS阴影部分
- ✅ 非常灵活
- ❌ 如果使用不当,维护性可能会受到影响
- ❌ 要求开发人员编写更多样板代码
CSS Shadow Parts 解决了“如何为 XYZ 设置样式”的问题。它们允许你定义自定义元素的组成“部分”。发挥你内心的 Zeldman 精神吧!Shadow Parts 应该具有一定的语义含义,代表组件的抽象部分。由于它们是 API 的一部分,因此你需要谨慎对待公开的内容。
有时候,“如何设置 XYZ 的样式”的答案是“不需要设置”。也许你不想让背景颜色可以随意设置。相反,你可以设置一个属性,只接受几个预先筛选过的选项。
- 零件名称应尽可能在所有组件中保持一致。
- 阴影部分不能嵌套。
- 阴影部分只能是单个元素。
my-componet::part(base) > svg { display: none; }这样行不通。
尽量避免将所有元素都设为阴影部分。一旦某个元素成为阴影部分,之后想要修改其标记就需要进行重大更改。更多详情请参阅“何时创建 CSS 部分” 。
如果你的组件足够小(原子级别),那么每个元素最终都可能有自己的阴影部分,这完全没问题。
合适的工具
现在我们来看一个非常简单的功能——我们需要编写一个按钮组件,它可以显示两种不同的变体:主要变体和次要变体。我们该如何实现呢?
道具
<my-button type="primary"></my-button>
<my-button type="secondary"></my-button>
用一种方法
const el = document.querySelector('my-button')
el.setType('primary')
el.setType('secondary')
使用 CSS 自定义属性
my-button {
--button-background-color: var(--color-primary);
--button-border-color: var(--color-primary);
--button-text-color: var(--color-text);
// + all hover, active, focus states sheesh
}
使用 CSS 阴影部件
my-button::part(container) {
background-color: var(--color-primary);
border-color: var(--color-primary);
// etc etc
}
这里列出了四种展示特定功能的不同方法。就易用性而言,属性(prop)显然是最佳选择。但现在想象一下,如果我们想允许两种以上的颜色呢?如果我们想允许任何颜色,只要它在设计系统中定义呢?我们就需要添加 30 多个属性选项。
关键在于,何时使用哪个API并没有唯一的最佳答案。这取决于你想允许什么以及怎样才能提供最佳的用户体验。
有主见的最佳实践
1. 采用声明式编程——避免使用数组和对象属性
记住,我们编写的是自定义 HTML 元素。我们的组件必须在浏览器中可用,无需框架,也无需 JavaScript。把这个用例看作是最低要求。我个人的检验标准是:“一个青少年能在他们的 MySpace 页面上使用这个元素吗?”
那么,我们来看一个基本的 List 组件。你的第一次尝试可能如下所示:
<my-list
data="
[
{
id: 1,
text: "Item 1"
},
{
id: 2,
text: "Item 2"
}
...
]
"
>
</my-list>
如果你使用 JavaScript 框架来处理繁重的数据绑定工作,这种方法效果很好。但如果你使用的是纯 HTML,那么现在你就不得不编写一些 JavaScript 代码了:
const data = [...]
const el = document.querySelector('my-list')
el.data = data
如果想让列表项变成链接呢?或者添加图标呢?如果想让每三个列表项打开一个模态框,每十个列表项导航到一个页面呢?
一切推倒重来。
<my-list>
<my-list-item>Item 1</my-list-item>
<my-list-item>
<my-icon/> Item 2
</my-list-item>
</my-list>
通过创建一个新my-list-item组件,我们突然变得更加灵活,可以避免无休止的“如果……会怎样”的问题。
如果必须使用数组或对象,请确保仅将它们作为属性接受,不要将它们作为特性反映出来,这是出于性能考虑。
用肯特·C·多兹的话来说,要避免令人心碎的因素。
2. 不要设置属性样式
<my-component open></my-component>
my-component {
display: none;
}
my-component[open] {
display: block;
}
要让这个例子正常运行,你需要格外小心地确保正确地反映open属性值。如果有人更改了open属性值,而你忘记将其反映到相应的属性中,你的组件就会出错,而且这会非常难以调试。
相反,请使用内部类并为其设置样式。
3. :主机样式神圣不可侵犯
谨慎设置 :host 的样式。您在此处放置的任何内容都不会被 Shadow DOM 封装,因此,使用您的组件的开发人员可以对其进行更改。:host 样式通常最适合用于默认属性,例如display。
4. (尝试)默默失败
<select>如果尝试将 `<div>` 作为子元素传递,会抛出错误吗<h2>?不会。HTML 会静默失败。我们也应该尊重控制台,尽力避免用不必要的警告和错误信息污染它。
只有在绝对无法继续执行的情况下才抛出错误。如果要抛出错误,请稍作停顿,思考原因,确保有充分的理由。当然,有时抛出错误是不可避免的。
在 AstroUXDS 中,我们通常只对弃用功能发出警告。但这只是我们个人风格上的一个决定。
5. 数据流 - 属性向下传递,事件向上传递
关于数据流的传统理念依然不变:向下传递 props,向上传递事件。向上提升状态。随便你怎么称呼它。如果两个同级组件需要相互通信,它们很可能需要一个父级中介组件。
6. 窃取代码。(我不是律师)
说真的,如今的互联网是这一代人右键点击“查看源代码”并“吸收”他人成果的结果。我们就是这样走到今天的。正因如此,互联网才成为最具民主性的平台。分享和开放的理念早已融入你的浏览器之中。如果你自己没有在中学时尝试通过复制粘贴从别处找到的零散HTML代码来为乐队搭建网站的经历,我敢保证你至少认识一个有这种经历的人。
所以,要站在巨人的肩膀上,不要重复发明轮子之类的老生常谈。当你遇到问题时,去看看其他人是如何解决的。选择你最喜欢的方案。(例如,表格就是一个很有趣的例子。)
我发现的一些最好的资源包括:
- Shoelace——堪称 Web 组件库的黄金标准。本文中的许多最佳实践都源自 Shoelace 自身的最佳实践文档。我强烈建议您完整阅读多遍。我对优秀 Web 组件的理解完全建立在研读 Shoelace 源代码的基础上。
- Ionic——为数不多的早期采用并大力推广 Web 组件的开发者之一。他们的组件经受住了实战考验。其组件的关注度令人惊叹。卓越的用户体验,以及 Web 组件如何服务于所有框架开发者的完美案例。
- Spectrum Web Components是 Adobe 设计系统的 Web 组件版本。
- OpenUI虽然不是一个库,但却是设计全新组件时最有价值的资源之一。我经常从中汲取灵感,解决诸如组件命名、预期属性等琐碎问题。
- MDN - 想要寻找灵感,不妨回归经典。如果你要构建一个已存在的自定义元素,通常最好默认使用原生元素的行为。构建 Web 组件让我对 HTML 有了新的认识。> 小贴士:在 Chrome 开发者工具中,你可以启用“显示用户代理的 Shadow DOM”来查看所有你喜爱的经典元素的 Shadow DOM。
- Web.dev 的自定义元素最佳实践- 另一份优秀的通用最佳实践列表。
