摘要:本文着眼于使用具有内置功能和样式的组件来扩充HTML。 我们还将学习如何通过 NPM 使这些自定义元素在项目中得到重用。


即便是最简单的组件,人力成本也可能很高。 UX 团队进行要可用性测试。 涉及到的利益相关者必须对设计签字确认。

之后是开发人员进行 AB 测试,可访问性审计,单元测试和跨浏览器检查。 一旦解决了这个问题,你就不想再次重复这项工作了。 通过构建可重用的组件库(而不是从头开始构建所有内容),我们就可以不断复用过去的工作,避免重新审视已经解决的设计和开发过程。

构建组件库对于像Google这样的公司尤为重要,他们拥有很多具有相同品牌的网站。 通过把 UI 编码为可组合小部件,这些大公司既可以减少开发时间,又可以实现跨项目的可视化和用户交互设计的一致性。在过去几年中,人们对样式指南和模式库的兴趣不断增加。由于开发人员和设计师一般都分布在多个团队中,所以大公司需要寻求实现一致性的方法,比如提供简单的颜色样本。不过对于我们来说,可以比他们做得更好。 我们需要的是易于分发的代码

共享和重用代码

手动复制和粘贴代码很容易。但是把代码保持在最新版是维护上的噩梦。所以许多开发者依赖包管理器来跨项目重用代码。 尽管名字是 Node Package Manager, 但是它已成为前端包管理的独一无二的平台。 目前在 NPM 上注册的包超过700,000个,每月下载数十亿次。 含有 package.json 文件的任何文件夹都可以作为可共享包上传到NPM。 虽然NPM主要与JavaScript相关联,但包中也可以包含 CSS 和标记。 NPM使重用变得很容易,这对更新代码尤为重要:你无需在各种地方修改代码,所做的是只需在包中更新代码即可。

标记存在的问题

使用 import 语句可以对Sass和Javascript 进行轻松移植。 模板语言赋予了 HTML 相同的能力 —— 模板能以局部形式导入到 HTML 的其他片段。 比如你可以只需为页脚编写一次标记,然后将其包含在其他模板中即可。 另一种方法是复制并粘贴标记,并只对样式和 javascript 使用NPM。

这是英国“金融时报”在 Origami 组件库中用到的方法。Alice Bartlett在她的演讲中总结道:“你不能让它更像是 Bootstrap 吗?”,“并没有什么好办法能让人们在他们的项目中包含模板”。 Ian Feather谈到他在 Lonely Planet 维护组件库的经历,重申了这种方法存在的问题:

“一旦复制了这些代码,他们基本上就会削减一个需要无限期维护的版本。当复制工作组件的标记时,它具有到该点的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>

See the Pen Simple web component demo by CSS GRID (@cssgrid) on CodePen.

在CodePen上的代码演示:https://codepen.io/cssgrid/pen/KemvbM

在前端开发中,以组件为中心的方法已经变得无处不在,Facebook 的 React 框架就使用了这种方法。考虑到在现代前端开发工作中框架的普遍性,许多公司已经在用他们选择的框架构建了组件库。这些组件只能在该特定框架内重用。

A component from IBM’s Carbon Design System

IBM Carbon Design System的一个组件。 仅能用于 React 应用。其他在 React 中构建的组件库的主要案例包括Atlassian 的 Atlaskit 和 Shopify 的 Polaris

对规模较大的公司来说,很少有统一的前端,从一个框架转到另一个框架的重新布局并不罕见。各种框架你方唱罢我登场。 为了在项目中实现最大程度的潜在重用,我们需要与*框架无关*的组件。

通过在npmjs.com对组件的搜索结果揭示了一个支离破碎的Javascript生态系统

通过在npmjs.com对组件的搜索结果揭示了一个支离破碎的Javascript生态系统。

随着时间的推移,框架也在不断变化。

随着时间的推移,框架也在不断变化。

“多年来我使用 Dojo、Mootools、Prototype、jQuery、Backbone、Thorax 和 React 构建了 Web 应用……我希望能把我开发的 Dojo 组件用到现在的 React 应用中。”

—— 谷歌工程总监Dion Almaer

当我们谈论Web组件时,我们讨论的是自定义元素与 shadow DOM 的组合。 自定义元素和 shadow DOM 是W3C DOM 规范和 WHATWG DOM 标准的一部分 —— 这意味着 Web 组件是 Web 标准的一部分 。自定义元素和 shadow DOM 最终 会实现跨浏览器支持。 通过使用原生 Web 平台的标准部分,我们确保自己的组件能够在前端重组和不断重构的快速变化周期中生存下来。 Web组件可以与任何一种模板语言和前端框架一起使用 —— 它们是真正交叉兼容和可互操作的。 从 Wordpress 博客到单页应用程序,可以在任何场合下使用。

Rob Dodson的Custom Elements Everywhere项目记录了 Web 组件与各种客户端 Javascript 框架的互操作性。 这里面 React 出现的异常值,希望能在 React 17 中解决

Rob Dodson的 Custom Elements Everywhere 项目记录了 Web 组件与各种客户端 Javascript 框架的互操作性。 这里面 React 出现的异常值,希望能在 React 17 中解决。

制作Web组件

定义一个自定义元素

生成 made-up-tag 标记并使其内容显示在页面上。

<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) // 无效
customElements.define('what-ever', Whatever) // 有效

定制元素生命周期

API 提供了四种自定义元素响应 —— 可以在类中定义函数,这些函数会自动调用来响应自定义元素生命周期中的某些事件。

当自定义元素添加到 DOM 时会执行 connectedCallback

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"

所有涉及到获取资源或渲染的工作都应该在这里。

从 DOM 中删除自定义元素时,将运行 disconnectedCallback

disconnectedCallback() {
    console.log("element has been removed")
}
document.querySelector("expandable-box").remove() //"element has been removed"

当自定义元素被采用到新文档中时,将会运行 adoptedCallback 。 你可能不需要太关心这个问题。

在添加、更改或删除属性时运行 attributeChangedCallback 。 它可以用于监听标准化本机属性(如 disabledsrc )的更改,以及我们定义的任何自定义属性。 这是自定义元素最强大的功能之一,因为它可以创建用户友好的 API。

定制元素属性

因为有很多 HTML 属性,所以当任何属性发生变化时,浏览器都不会浪费时间去调用我们的 attributeChangedCallback ,因此需要提供一个我们想要监听的属性更改列表。对于这个例子,我们只对一个感兴趣。

static get observedAttributes() {
            return ['expanded']
        }

所以现在 attributeChangedCallback 只会在我们更改自定义元素上expanded属性的值时被调用,因为它是我们列出的唯一属性。

HTML 属性可以有相应的值(例如 href,src,alt,value 等 ),而其他值可以是true或false (例如 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')
}
        
// setAttribute的第二个参数是必需的,所以我们将用一个空字符串填充
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}!!`);
  // 每次属性被更改时执行某些操作
}

配置 Javascript 组件会涉及将参数传递给 init 函数。 通过使用 attributeChangedCallback ,可以创建一个可以使用标记配置的自定义元素。

Shadow DOM 和自定义元素可以单独使用,你可以找到对自己有用的自定义元素。 与 shadow DOM 不同,它们可以是 Polyfill 。 不过它们配合得很好。

使用SHADOW DOM附加标记和样式

到目前为止,我们已经处理了自定义元素的行为。 但是关于标记和样式,我们的自定义元素相当于空的无样式 <span> 。 要将HTML和CSS封装为组件的一部分,还需要附加一个shadow 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 包通过命令行进行发布。 打开一个终端窗口并切换到你想要变成可重用包的目录中,然后在终端中键入以下命令:

  1. 如果你的项目还没有 package.json, npm init 将会引导你创建一个。
  2. npm adduser 把你的机器链接到你的 NPM 帐户。 如果你还没有注册,它将为你创建一个新的账号。
  3. npm publish

如果一切顺利的话,在 NPM 列表中会出现你的组件,可以在你自己的项目中安装和使用 —— 并与全世界共享。

Web组件API并不完美。自定义元素目前还无法在表单提交中包含数据。 渐进式增强并不是很好,对可访问性的处理并不怎么容易

尽管最初在 2011 年宣布,但浏览器还没有普遍支持。 Firefox 将在今年(2018)晚些时候提供支持。 尽管如此,一些备受瞩目的网站(如 Youtube )已经在使用它们。 尽管目前还存在缺点,但对于普遍可共享的组件而言,它们是唯一的选择,并且在未来它们提供的令人兴奋的新功能非常值得期待