[译] 2018 来谈谈 Web Component

对不少人来讲,组件已经成为他们开发工做中的核心概念。组件提供了一种健壮的模型,容许咱们用一个个更小的更简单的封装好的部件来搭建出复杂的应用程序。组件的概念在 Web 上已经存在一段时间了,好比在 JavaScript 生态的早期,Dojo Toolkit 已经在它的 Dijit 插件系统里面应用了组件这个概念。css

现代框架好比说 React、Angular、Vue 和 Dojo 进一步把组件放在开发的前列,并做为核心要素用在它们本身的框架结构上。然而,虽然说组件结构变得愈来愈广泛,可是各类各样的框架和库也衍生出一个纷繁复杂、四分五裂的组件生态。这种分裂经常将一些团队钉死在某个特定的框架上,哪怕时间、技术的更迭也不会轻易地改变。html

解决这种割裂的形势,让 Web 组件模型统一化,这项工做已经在努力推动中。最先的努力当数 “Web Component” 规范说明 circa 2011 的出现,并在同年的 Fronteers Conference 大会上由 Alex Russell 将之宣之于众。该 Web Component 规范的产生和发展,旨在提供一种权威的、浏览器能理解的方式来建立组件。在作出跨浏览器支持的组件方案这件事上咱们还有不少事情要作,但已经比以往任什么时候候更接近目标了。理论上讲,这些规范和实践铺平了组件间相互做用相互结合的道路,即便这些组件出自不一样的供应方(好比 React,好比 Vue)。下面咱们开始探索 Web Component 规范的组成。前端

组成部分

Web Component 并不是单一的技术,而是由一系列 W3C 定义的浏览器标准组成,使得开发者能够构建出浏览器原生支持的组件。这些标准包括:react

  • HTML Templates(译者注:模板)and Slots(译者注:插槽) — 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口
  • Shadow DOM(译者注:影子节点) — 对标签和样式的一层 DOM 包装
  • Custom Elements(译者注:自定义元素) — 带有特定行为且用户自命名的 HTML 元素

这里还有另外一个 Web Component 规范,HTML Imports,用于将 HTML 代码及 Web Component 导入到网页中。然而,在交叉参考 ES Module 规范后,Firefox 团队认为这不是一种最佳实践,该规范也就没多少人在推进了。android

Shadow DOM 和 Custom Element 规范经历了一些迭代,如今都已是第二个版本(v1)。在 2016 年 2 月,有人推进将 Shadow DOM 和 Custom Element 并入 DOM 标准规范里面,而再也不做为独立的规范存在。ios

template 标签和 slot 标签

HTML 模板是支持度最高的特性,能够说是 Web Component 规范最直观的体现。它容许开发者定义一个直到被复制使用时才会进行渲染的 HTML 标签块。你能够参考下面的简单示例来定义一个模板:git

<template id="custom-template> <h1>HTML Templates are rad</h1> </template> 复制代码

一旦 DOM 里面定义了这样的一个模板,就能够在 JavaScript 里面引用了:github

const template = document.getElementById("custom-template");
const templateContent = template.content;
const container = document.getElementById("container");
const templateInstance = templateContent.cloneNode(true);
container.appendChild(templateInstance);
复制代码

像上面那样写,就能够借助 cloneNode 函数来复用这个模板。提到 <template> 标签就不得不提 <slot> 标签。slot 标签容许开发者经过特定接入点来动态替换模板中的 HTML 内容。它用 name 属性来做为惟一识别标志(译者注,就相似普通 DOM 节点的 id 属性):web

<template id="custom-template">
    <p><slot name="custom-text">We can put whatever we want here!</slot></p>
</template>
复制代码

slot 标签在 Custom Element 的注入中很是有用。它容许开发者在写好的 Custom Element 里面设置标记。当 Custom Element 里面的节点用到了 slot 属性做为标记,那这个节点就会替换掉模板里面对应的 slot 标签。npm

Shadow DOM

在页面上定位具体的节点这是 web 开发的一个基本能力。CSS 选择器不只能够用来给节点加样式,还能够用来查询特定的 DOM 集合。这一般发生在根据一个标识符选择特定节点,比方说使用 document.querySelectorAll 就能够找到整个 DOM 树中匹配指定选择器的节点数组。然而,若是应用程序很是庞大,有不少节点有冲突的 class 属性,那又该怎么办?此时,程序就不知道哪一个节点是想被选中的,bug 也就随之产生。若是可能的话,将部分 DOM 节点抽象出来,隔离开来,让它们不会被 DOM 选择器选择到,那岂不是很好?Shadow DOM 就能作到,它容许开发者将一些节点放到独立的子树上来实现隔离。根本上说 Shadow DOM 提供了一种健壮的封装方式来作到页面节点的隔离,这也是 Web Component 的核心优点。

与此类似,CSS 的类和 ID 应用于全局样式时也会出现相似的问题。冲突的命名标示会致使样式的相互覆盖。那参考上面 DOM 树选择节点的思路,若是能将 CSS 样式限制在某个 DOM 的子树上,不就能够避免全局样式冲突,解决问题?比较有名的样式设置技术好比 CSS Modules 或者 Styled Components,它们的核心出发点之一就是为了解决这个问题。举个例子,CSS 模块技术经过对类名和模块名进行哈希处理,赋予每一个 CSS 样式惟一的标识符从而避免冲突。Shadow DOM 跟它们不一样之处在于它并不对类名作处理,而是直接就把这个做为原生特性来支持。它将部分 DOM 节点隔离开来使得咱们的网站和程序少了不可预知的变化,更加稳定。

那在代码层面上该怎么操做?能够这样将 Shadow DOM 附加到一个节点上:

element.attachShadow({mode: 'open'});
复制代码

这里 attachShadow 函数接受一个含 mode 属性的对象做为参数。Shadow DOM 能够打开关闭打开时使用 element.shadowRoot 就能够拿到 DOM 子树,反之若是关闭了则会拿到 null。接着建立一个 Shadow DOM 就会建立一个阴影的边界,在封装节点的同时封装样式。默认状况下该节点内部的全部样式会被限制仅在这个影子树里生效,因而样式选择器写起来就短得多了。Shadow DOM 一般能够和 HTML 模板结合使用:

const shadowRoot = element.attachShadow({mode: 'open'});
shadowRoot.appendChild(templateContent.cloneNode(true));
复制代码

如今这个 element 就有一个影子树,影子树的内容是模板的一个复制。Shadow DOM、 <template> 标签、<slot> 标签在这里和谐地应用在一块儿,构造出了可复用、封装良好的组件。

经过 Custom Element 进一步封装

HTML 的 template 和 slot 标签提供了复用性和灵活性,Shadow DOM 提供了封装方法。而 Custom Element 再进一步,将全部这些特性打包在一块儿成为有本身名字的可反复使用的节点,让它能够像常规 HTML 节点同样用起来。

定义一个 Custom Element

定义 Custom Element 要用到 JavaScript。Custom Element 依赖 ES2015+ 的 Class 特性,用 Class 做为其声明模式,一般是从 HTMLElement 或它的子类继承而来。这里有一个 Custom Element 的例子,使用 ES2015+ 语法建立,用于计数:

// 咱们定义一个 ES6 的类,拓展于 HTMLElement
class CounterElement extends HTMLElement {
    constructor() {
        super();
 
        // 初始化计数器的值
        this.counter = 0;
 
        // 咱们在当前 custom element 上附加上一个打开的影子根节点
        const shadowRoot= this.attachShadow({mode: 'open'});
 
        // 咱们使用模板字符串来定义一些内嵌样式
        const styles=`
            :host {
                position: relative;
                font-family: sans-serif;
            }
 
            #counter-increment, #counter-decrement {
                width: 60px;
                height: 30px;
                margin: 20px;
                background: none;
                border: 1px solid black;
            }
 
            #counter-value {
                font-weight: bold;
            }
        `;
 
        // 咱们给影子根节点提供一些 HTML
        shadowRoot.innerHTML = `
            <style>${styles}</style>
            <h3>Counter</h3>
            <slot name='counter-content'>Button</slot>
            <button id='counter-increment'> - </button>
            <span id='counter-value'>; 0 </span>;
            <button id='counter-decrement'> + </button>
        `;
 
        // 咱们能够经过影子根节点查询内部节点
        // 就好比这里的按钮
        this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
        this.decrementButton = this.shadowRoot.querySelector('#counter-decrement');
        this.counterValue = this.shadowRoot.querySelector('#counter-value');
 
        // 咱们能够绑定事件,用类方法来响应
        this.incrementButton.addEventListener("click", this.decrement.bind(this));
        this.decrementButton.addEventListener("click", this.increment.bind(this));
 
    }
 
    increment() {
        this.counter++
        this.invalidate();
    }
 
    decrement() {
        this.counter--
        this.invalidate();
    }
 
    // 当计数器的值发生变化时调用
    invalidate() {
        this.counterValue.innerHTML = this.counter;
    }
}
 
// 这里定义了能够在 DOM 树上直接使用的真实节点
customElements.define('counter-element', CounterElement);
复制代码

特别注意最后一行,那里注册了能够用在 DOM 里面的 Custom Element。

Custom Element 的种类

上面代码展现了如何从 HTMLElement 接口作拓展,然而咱们还能够从更具体的节点上拓展,好比 HTMLButtonElement。Web Component 规范提供了一个完整的可供继承的接口列表

Custom Element 可分为两种主要类型:独立自定义元素(Autonomous custom elements)内置自定义元素(Customized built-in elements)。独立自定义元素和那些早已定义且不继承自特定接口的节点相似(译者注:就是咱们日常使用的 DOM 节点)。一个独立自定义元素只要在页面必定义上,就能够像常规 HTML 节点那样使用。举个例子,上面定义的计数节点,既能够在 HTML 中经过 <counter-element></counter-element> 定义,也能够在 JavaScript 中用 document.createElement('counter-element') 来建立。

内置自定义元素在使用上略有不一样,当 HTML 定义节点时能够传一个 is 属性到标准节点上(好比 <button is='special-button'>),又或者使用 document.createElement 时传一个 is 属性做为参数(好比 document.createElement("button", { is: "special-button" })。

Custom Element 的生命周期

Custom Element 也有一系列的生命周期事件,用于管理组件链接和脱离 DOM :

  • connectedCallback:链接到 DOM
  • disconnectedCallback: 从 DOM 上脱离
  • adoptedCallback: 跨文档移动

一种常见错误是将 connectedCallback 用作一次性的初始化事件,然而实际上你每次将节点链接到 DOM 时都会被调用。取而代之的,在 constructor 这个 API 接口调用时作一次性初始化工做会更加合适。

此处还有一个 attributeChangedCallback 事件能够用来监听节点(译者注:使用 Custom Element 定义的节点)属性的变化,而后经过这个变化来更新内部状态。不过,要想用上这个能力,必须先在节点类里面定义一个名为 observedAttributes 的 getter:

constructor() {
    super();
    // ...
    this.observedAttributes();
}
 
get observedAttributes() {return ['someAttribute']; } 
// 其余方法
复制代码

从这里起就能够经过 attributeChangedCallback 来处理节点属性的变化:

attributeChangedCallback(attributeName, oldValue, newValue) {
    if (attributeName==="someAttribute") {
        console.log(oldValue, newValue)
        // 根据属性变化作一些事情
    }
}
复制代码

支持度如何?

截至 2018 年 6 月,Shadow DOM 第二版和 Custom Element 第二版在 Chrome、Safari、三星浏览器上已经支持,还被 Firefox 列为要支持的特性,但愿很大。而 Edge 依然在考虑是否支持。在这个时间点,Github 仓库 webcomponents 上已经有了一系列的 polyfill。这些 polyfill 使得包括 IE11 在内的全部当下活跃的浏览器上都能运转 Web Component。该 webcomponents 库包含多种形态,既提供了一个包含全部必要 polyfill 的脚本(webcomponents-bundle.js),也提供了一个经过特性检测来只加载必要 polyfill 的版本(webcomponents-loader.js)。若是使用第二种,你仍是必须将各个 polyfill 文件都放到服务器上来保证加载器能够加载到。

对于那些代码中只能用 ES5 的状况,还必须加载一个 custom-elements-es5-adapter.js 文件,并且它必须首先加载,不能跟组件代码打包在一块儿。之因此须要这个适配文件是由于 Custom Element 必须 继承自 HTMLElement 类,且构造函数中必须以 ES2015 的方式调用 super()(这在 ES5 代码里看起来会很困惑!)。在 IE11 中仍是会因为不支持 ES2015 的类特性而抛出错误,不过能够忽略之

Web Component 和框架

历史上,Web Component 最大的支持者之一是 Polymer 库。Polymer 针对 Web Component API 添加了一些语法糖使得定义和传递组件变得更加容易。在最新版本 Polymer3 中,它与时俱进用上了 ES2015 的模块特性而且使用 npm 做为标准的包管理工具,跟上了其余的现代框架。Web Component 编码工具的另外一种形态则更像是编译器而非框架。StencilSvelte 这两个框架就是这样。它们使用各自的工具 API 来书写组件,而后编译成原生的 Web Component。一些框架好比 Dojo 2, 则选择容许开发者编写特定框架的组件,不过也容许编译成原生 Web Component 就是了。在 Dojo2 中这是用 @dojo/cli tools 来实现的。

努力实现原生的 Web Component 的一个愿景,是但愿跨越不一样团队不一样项目来共用组件,即便它们用的是不一样的框架。当下不一样的框架和 Web Component 规范有不一样的关系,有些更贴近规范有些则否则。已经有一些指引告诉咱们怎么在诸如 ReactAngular 这样的框架中用上原生的 Web Component ,但它们的实现上仍是带着浓浓的框架特点。有一个很好的资源能够帮你理解这些关系,那就是 Rod Dodson 的 Custom Elements Everywhere,它经过测试用例测出不一样框架想和 Custom Element(Web组件规范的核心) 结合的难易程度。

最后的想法

围绕 Web Component 的使用和炒做不断持续此起彼伏。这意味着,随着 Web Component 获得愈来愈好的支持,polyfill 将逐渐淡出咱们的视野,组件书写将更加简洁和快速。Shadow DOM 容许开发者写一些简单的限定区域有效的 CSS,这无疑更加容易管理,一般性能也会更好。Custom Element 提供了一种统一的方法来定义组件,这些组件能够(理论上)跨代码库和团队来使用。目前有一些额外的规范建议,开发者能够根据基本规范加以利用:

这些补充规范能够为原生 web 平台增长更多功能,让开发者不用再去理解那么多抽象概念,释放更多的潜力。

该基本规范毫无疑问是一套强大的工具,但最终它是否能发挥最大的效用仍是要取决于用到它的框架、开发者和团队。目前如 React、Vue、Angular 这样的框架已经大大占据了开发者的大脑,它们会由于这些原生态的技术和工具而逐渐败下阵来吗?只能让时间来见证了。


下一步

你是否但愿在你的下一个项目或框架中用上 Web Component?联系咱们,探讨下咱们能够怎么帮到你!

SitePen On-Demand Development 能够获取帮助,它有咱们对 JavaScript 和 TypeScript 大大小小问题的快速有效解决方案。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索