可重用组件的圣杯:自定义元素、Shadow DOM 和 NPM
已发表: 2022-03-10即使是最简单的组件,人力成本也可能很高。 UX 团队进行可用性测试。 一系列利益相关者必须在设计上签字。
开发人员进行 AB 测试、可访问性审计、单元测试和跨浏览器检查。 一旦你解决了一个问题,你就不想重复那个努力了。 通过构建一个可重用的组件库(而不是从头开始构建所有东西),我们可以不断利用过去的努力,避免重新审视已经解决的设计和开发挑战。

建立一个组件库对于像谷歌这样拥有大量网站组合的公司特别有用,这些网站都共享一个共同的品牌。 通过将他们的 UI 编码为可组合的小部件,大公司既可以加快开发时间,又可以实现跨项目的视觉和用户交互设计的一致性。 在过去的几年里,人们对样式指南和模式库的兴趣有所增加。 鉴于多个开发人员和设计师分布在多个团队中,大公司寻求实现一致性。 我们可以做得比简单的色板更好。 我们需要的是易于分发的代码。
共享和重用代码
手动复制和粘贴代码毫不费力。 然而,让代码保持最新是一场维护噩梦。 因此,许多开发人员依赖包管理器来跨项目重用代码。 尽管它的名字,Node Package Manager 已经成为前端包管理的无与伦比的平台。 目前 NPM 注册表中有超过 700,000 个包,每月下载数十亿个包。 任何带有 package.json 文件的文件夹都可以作为可共享包上传到 NPM。 虽然 NPM 主要与 JavaScript 相关联,但一个包可以包含 CSS 和标记。 NPM 使重用变得容易,更重要的是,更新代码。 无需在无数地方修改代码,您只需更改包中的代码。
标记问题
Sass 和 Javascript 可以通过使用 import 语句轻松移植。 模板语言赋予 HTML 相同的能力——模板可以以部分的形式导入 HTML 的其他片段。 例如,您可以为您的页脚编写一次标记,然后将其包含在其他模板中。 说存在多种模板语言是轻描淡写的。 将自己与一个绑定严重限制了代码的潜在可重用性。 另一种方法是复制和粘贴标记并将 NPM 仅用于样式和 javascript。
这是《金融时报》在其Origami组件库中采用的方法。 在她的演讲“你不能让它更像 Bootstrap 吗?” Alice Bartlett 总结道:“没有让人们在他们的项目中包含模板的好方法”。 谈到他在 Lonely Planet 维护组件库的经验时,Ian Feather 重申了这种方法的问题:
“一旦他们复制了该代码,他们实际上就是在削减一个需要无限期维护的版本。 当他们复制一个工作组件的标记时,它有一个指向 CSS 快照的隐式链接。 如果您随后更新模板或重构 CSS,则需要更新散布在您网站上的所有模板版本。”
解决方案:Web 组件
Web 组件通过在 JavaScript 中定义标记来解决这个问题。 组件的作者可以自由更改标记、CSS 和 Javascript。 组件的使用者可以从这些升级中受益,而无需手动浏览项目更改代码。 可以通过终端通过简洁的npm update
与项目范围内的最新更改同步。 只有组件的名称及其 API 需要保持一致。
安装 Web 组件就像在终端中输入npm install component-name
一样简单。 Javascript 可以包含在 import 语句中:
<script type="module"> import './node_modules/component-name/index.js'; </script>
然后,您可以在标记中的任何位置使用该组件。 这是一个将文本复制到剪贴板的简单示例组件。
请参阅 CodePen 上 CSS GRID (@cssgrid) 的 Pen Simple Web 组件演示。
以组件为中心的前端开发方法已经无处不在,由 Facebook 的 React 框架引入。 不可避免地,鉴于现代前端工作流程中框架的普遍性,许多公司已经使用他们选择的框架构建了组件库。 这些组件只能在该特定框架内重用。

大型公司很少有统一的前端,从一个框架到另一个框架的replatorming 并不少见。 框架来来去去。 为了实现跨项目的最大潜在重用,我们需要与框架无关的组件。


“这些年来,我使用 Dojo、Mootools、Prototype、jQuery、Backbone、Thorax 和 React 构建了 Web 应用程序……我希望能够将我一直在使用的那个杀手级 Dojo 组件带到我的 React 中今天的应用程序。”
— Dion Almaer,谷歌工程总监
当我们谈论 web 组件时,我们谈论的是自定义元素与 shadow DOM 的组合。 自定义元素和影子 DOM 是 W3C DOM 规范和 WHATWG DOM 标准的一部分——这意味着 Web 组件是一种 Web 标准。 自定义元素和 shadow DOM终于在今年实现了跨浏览器支持。 通过使用原生 Web 平台的标准部分,我们确保我们的组件能够在快速变化的前端重组和架构重新思考周期中存活下来。 Web 组件可以与任何模板语言和任何前端框架一起使用——它们是真正的交叉兼容和互操作的。 它们可以在任何地方使用,从 Wordpress 博客到单页应用程序。

制作 Web 组件
定义自定义元素
总是可以组成标签名称并将其内容显示在页面上。
<made-up-tag>Hello World!</made-up-tag>
HTML 被设计为容错的。 上面将呈现,即使它不是有效的 HTML 元素。 这样做从来没有很好的理由——偏离标准化标签传统上是一种不好的做法。 然而,通过使用自定义元素 API 定义新标签,我们可以使用具有内置功能的可重用元素来扩充 HTML。 创建自定义元素很像在 React 中创建组件——但这里是扩展HTMLElement
。
class ExpandableBox extends HTMLElement { constructor() { super() } }
对super()
的无参数调用必须是构造函数中的第一条语句。 构造函数应该用于设置初始状态和默认值以及设置任何事件监听器。 需要定义一个新的自定义元素,为其 HTML 标记和元素对应的类命名:

customElements.define('expandable-box', ExpandableBox)
将类名大写是一种惯例。 然而,HTML 标记的语法不仅仅是一种约定。 如果浏览器想要实现一个新的 HTML 元素并且他们想要将其称为可扩展框怎么办? 为防止命名冲突,新的标准化 HTML 标记不会包含破折号。 相比之下,自定义元素的名称必须包含破折号。
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
自定义元素生命周期
API 提供了四种自定义元素反应——可以在类中定义的函数,这些函数将自动调用以响应自定义元素生命周期中的某些事件。
connectedCallback在自定义元素添加到 DOM 时运行。
connectedCallback() { console.log("custom element is on the page!") }
这包括使用 Javascript 添加元素:
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
以及简单地在页面中包含带有 HTML 标记的元素:
<expandable-box></expandable-box> // "custom element is on the page"
任何涉及获取资源或渲染的工作都应该在这里。
disconnectedCallback在从 DOM 中删除自定义元素时运行。
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
当自定义元素被采用到新文档中时, adoptedCallback
会运行。 您可能不需要经常担心这个问题。
在添加、更改或删除attributeChangedCallback
时运行 attributeChangedCallback。 它可用于侦听对标准化原生属性(如disabled或src )以及我们制作的任何自定义属性的更改。 这是自定义元素最强大的方面之一,因为它可以创建用户友好的 API。
自定义元素属性
有很多 HTML 属性。 为了让浏览器不会浪费时间在任何属性更改时调用我们的attributeChangedCallback
,我们需要提供一个我们想要监听的属性更改的列表。 对于这个例子,我们只对一个感兴趣。
static get observedAttributes() { return ['expanded'] }
所以现在我们的attributeChangedCallback
只会在我们更改自定义元素上扩展属性的值时被调用,因为它是我们列出的唯一属性。
HTML 属性可以有相应的值(想想href、src、alt、value等),而其他属性要么是真要么是假(例如disabled、selected、required )。 对于具有相应值的属性,我们将在自定义元素的类定义中包含以下内容。
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
对于我们的示例元素,属性将是 true 或 false,因此定义 getter 和 setter 有点不同。
get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }
现在已经处理了样板文件,我们可以使用attributeChangedCallback
。
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
传统上,配置 Javascript 组件需要将参数传递给init
函数。 通过利用attributeChangedCallback
,可以制作一个仅通过标记即可配置的自定义元素。
Shadow DOM 和自定义元素可以单独使用,您可能会发现自定义元素本身很有用。 与 shadow DOM 不同,它们可以被填充。 但是,这两个规范可以很好地结合使用。
使用 Shadow DOM 附加标记和样式
到目前为止,我们已经处理了自定义元素的行为。 然而,关于标记和样式,我们的自定义元素相当于一个空的无样式<span>
。 要将 HTML 和 CSS 封装为组件的一部分,我们需要附加一个影子 DOM。 最好在构造函数中执行此操作。
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
不要担心理解模式的含义——你必须包含它的样板,但你几乎总是想要open
。 这个简单的示例组件只会渲染文本“hello world”。 与大多数其他 HTML 元素一样,自定义元素可以有子元素——但默认情况下不会。 到目前为止,我们定义的上述自定义元素不会将任何子元素呈现到屏幕上。 要显示标签之间的任何内容,我们需要使用slot
元素。
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
我们可以使用样式标签将一些 CSS 应用于组件。
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
这些样式只适用于组件,因此我们可以自由地使用元素选择器,而这些样式不会影响页面的其他任何内容。 这简化了 CSS 的编写,不需要像 BEM 这样的命名约定。
在 NPM 上发布组件
NPM 包通过命令行发布。 打开一个终端窗口并移动到一个您想变成可重用包的目录。 然后在终端中输入以下命令:
- 如果您的项目还没有 package.json,
npm init
将引导您生成一个。 -
npm adduser
将您的机器链接到您的 NPM 帐户。 如果您没有预先存在的帐户,它将为您创建一个新帐户。 -
npm publish

如果一切顺利,您现在在 NPM 注册表中有一个组件,可以在您自己的项目中安装和使用——并与全世界共享。

Web 组件 API 并不完美。 自定义元素目前无法在表单提交中包含数据。 渐进增强的故事不是很好。 处理可访问性并不像应有的那么容易。
尽管最初于 2011 年宣布,但浏览器支持仍然不是普遍的。 Firefox 支持将于今年晚些时候到期。 尽管如此,一些知名网站(如 Youtube)已经在使用它们。 尽管它们目前存在缺陷,但对于普遍共享的组件来说,它们是唯一的选择,并且在未来,我们可以期待它们所提供的令人兴奋的补充。