阅读 183

案例实战,一文吃透 Web Components

image.png

Web Components 是 W3C 推动的一项标准,旨在丰富 HTML 的 DOM 特性,让 HTML 有更强大的复用能力。

一、没有对比,没有伤害

放眼前端技术发展历程,代码复用和模块化是永远绕不开的话题,我们看看前端三大件(CSS,HTML,JavaScript)表现如何。

image.png

CSS 远有 @import url 语法,近有 sass,less,stylus,css modules 等一大堆方案实现,原生的 CSS 体系也支持定义变量和使用了:

:root {
  --first-color: #488cff;
  --second-color: #ffff8c;
}
#firstParagraph {
  background-color: var(--first-color);
  color: var(--second-color);
}
复制代码

JavaScript 不遑多让, IIFE 模式和原型模式是其逻辑复用的基本能力。近几年 CMD,AMD,CommonJS 等各种规范对 JS 模块化持续加成,ES6 的定版更是官方爸爸的大力加持。相比之下,HTML 就略显惨淡。

早年间 HTML 似乎有一个 imports 语法支持导入 html 片段 <link rel="import" href="myfile.html">,但笔者没使用过,不做过多讨论。但到 MDN 查了一下发现已经不再支持这个语法了。。。HTML Imports

image.png

当然,在复用和组件化的潮流中 HTML 也不是那么的格格不入,毕竟时下最流行的 Vue,React,Ng 都是实现自定义 HTML 组件强有力框架。然而框架各自为阵,语法和使用方式不同,学习成本不同,总不是长久之计。

正所谓,天下大势,合久必分,分久必合。

Web Components 能否带来新的大一统局势呢,让我们细细看来。

二、Web Components 基础

1.三板斧

首先要明确的是 Web Components 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

image.png

这三项技术分别为(摘自 mdn):

  • Custom elements(自定义元素): 一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML 模板): <template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

闲话少叙,让我们看看该如何使用它们。

2.先看个栗子 🌰

假设我们要实现一个博客列表页,大概长这个样子:

image.png

按照组件化的思想,我们希望每个博文都是单个组件,可单独使用。首先我们使用 template 构造博客组件内容结构:

<template id="tpl">
  <style>
    article {width: 20%; margin: 20px auto; border: solid 1px gray; padding: 8px;}
    header {
      background: lightblue; color: #fff;
      font-size: 24px; border: solid 1px lightblue;
    }
  </style>
  <article>
    <header>博客标题</header>
    <section>博客内容博客内容博客内容博客内容博客内容博客内容...</section>
  </article>
</template>
复制代码

然后使用 customElement 创建自定义组件:

class SelfDiv extends HTMLElement {
  constructor() {
    super()
    document.body.appendChild(tpl.content.cloneNode(true))
  }
}
customElements.define('my-blog', SelfDiv)
复制代码

最后,在页面上使用 <my-blog /> 标签就能出现我们想要的内容。详细代码请猛戳这里。至此,HTML 原生自带的自定义组件功能完全满足需求,市面上各种令人头大的前端框架都可以抛之脑后啦,完结撒花✿✿ヽ(°▽°)ノ✿!!!

============================= 一条讲道理的分割线 ======================

然而事情并没这么简单,仔细查看 SelfDiv 组件会发现我们只是单纯的把 template 内容挂在 body 上,一点也不优雅;而且说好的 shadow DOM 呢,咋也没见着。不急,接下来让我们慢慢丰富这个例子。

首先,思考以下两个问题:

1.怎么把组件脱离真实 DOM,做到内部独立?

2.组件如何做到内容自定义?

3.shadow DOM 改造

针对第一个问题,其核心诉求是希望自定义组件做到内部独立(样式,事件等),借用 shadow 可以实现这一目标。定义 shadow DOM 只需要一个方法即可:Element.attachShadow(shadowRootInit),其中 shadowRootInit 是一个字典对象,包含两个值:mode 和 delegatesFocus;具体内容详见 attachShadow

我们将刚才的组件进行改造:

class SelfDiv extends HTMLElement {
  constructor() {
    super()
    let shadow = this.attachShadow({ mode: 'open' })
    shadow.appendChild(tpl.content.cloneNode(true))
  }
}
复制代码

改造关键点是借助 attachShadow 创建了一个 shadowRoot 对象,然后把模板内容挂在 shadowRoot 上。为了验证 shadow 的隔离性,我们在页面填写一些其他内容:

<my-blog />
<article>shadow 外面的 article 元素</article>
复制代码

可以看到页面效果如下:

image.png

细心的朋友会发现,自定义元素外的 article 并没有应用上边框和背景样式效果。是因为整个 shadowRoot 对象和文档对象完全隔离的,结构大致如下:

image.png

至此,我们的组件脱离了原生 DOM 的魔爪,可自由灵活地配置在页面任何地方。

4.template 和 slot 的妙用

再看第二个问题:组件如何做到内容自定义?。说白了咱现在的组件不能自定义博客标题和内容,完全不可用。这个时候就要请出 slot 插槽。插槽具有一个 name 属性作为标识,通常在 template 中定义,在使用时可以根据标识对其填充任何 HTML 元素。定义略显晦涩,直接看代码:

<template id="tpl">
  <style>
    article {width: 20%; margin: 20px auto; border: solid 1px gray; padding: 8px;}
    header {
      background: lightblue; color: #fff;
      font-size: 24px; border: solid 1px lightblue;
    }
  </style>
  <article>
    <header><slot name='title'>博客标题</slot></header>
    <section><slot name='cont'>博客内容博客内容博客内容博客内容博客内容博客内容...</slot></section>
  </article>
</template>
复制代码

还是刚才的 template 结构,只不过这里我们把标题用 name='title' 的具名 slot 包裹起来,而博客内容用的是 name='cont' 的 slot。我们在页面这样使用自定义组件:

<my-blog>
  <span slot='title'>第一篇博文</span>
  <p slot='cont'>这是第一篇博文内容这是第一篇博文内容这是第一篇博文内容这是第一篇博文内容...</p>
</my-blog>

<my-blog>
  <span slot='title'>第二篇博文</span>
  <p slot='cont'>这是第二篇博文内容...</p>
</my-blog>
复制代码

页面展示的效果如下,可以看到组件已经做到完全自定义。完整例子可以猛戳这里:codepen.io/xutaogit/pe…

image.png

三、Web Components 进阶

相信看完上文整个例子,我们已经对 Web Compoents 的基本功能大致掌握了。但要把它们真正投入生产使用,总觉得还差强人意。你可能会吐槽:

  • 现在的模板都太简单了,我想随意操作模板的内容,给它添加事件啥的。。。
  • 自定义组件确实有了,但我想在组件不同的阶段做不同的事情,怎么搞呢?
  • slot 看着挺有用,但它使用规则定义的太死了,不太灵活的亚子
  • 。。。

问题看似很多,但归根到底还是没把”三板斧“吃透,接下来我们将它抬上熔炉,一起来”炼炼“看。

Custom elements

前文我们只讲到如何创建一个自定义元素,然后将其挂载到真实 DOM 或者 shadow DOM 上。接下来进一步看看它还有哪些其他特别的功能。

1.定义和使用

你可以通过 CustomElementRegistry.define() 返回的绑定在 window 对象上的 customElements 实例进行元素自定义。其语法为 customElements.define(name, constructor, options);

  • name : 元素名。只能用短线连接的英文字符,不能是单个单词。例:name='my-comp'
  • constructor:元素构造器。必须继承自 HTMLElement 或其子类(例如:HTMLParagraphElement 等)
  • options:控制元素如何定义,只支持一个选项 extends,用于指明定义的元素继承自何种类型元素。(例如:{ extends: 'p' })

前两个参数应该比较好理解,第三个参数特别要注意:extends 指明的元素和 constructor 继承的元素保持一致。代码演示如下:

class ExtendP extends HTMLParagraphElement {
  constructor (){
    super()
    let iRoot = this.attachShadow({mode:'open'})
    let span = document.createElement('span')
    span.innerHTML = "扩展自 HTMLParagraphElement"
    iRoot.appendChild(span)
  }
}

customElements.define('extend-p', ExtendP, {extends: 'p'})
复制代码

这里 extends 指明的元素是 HTMLParagraphElement,在调用 define 方法的时候 extends 必须指明为 p,同时组件也只能用在 p 元素上,方式上有别于一般的自定义元素:

<p is="extend-p"></p>
复制代码

以上,总结出两条自定义组件的使用方法:

  • 扩展自 HTMLElement 的自定义元素,直接使用 <元素名></元素名>
  • 扩展自特定子类 Element 的元素,使用 is 属性访问 <扩展Element is="元素名"></扩展Element>

实例相关代码:codepen.io/xutaogit/pe…

2.生命周期

在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:

  • connectedCallback:当 custom element 首次被插入文档DOM时,被调用。
  • disconnectedCallback:当 custom element 从文档DOM中删除时,被调用。
  • adoptedCallback:当 custom element 被移动到新的文档时,被调用。
  • attributeChangedCallback: 当 custom element 增加、删除、修改自身属性时,被调用。

image.png

具体使用方法可以参考官网文档,实例代码搬运至此,可点击体验:生命周期。 特别要说明的一点是 attributeChangedCallback 方法的触发依赖于 get 函数 observedAttributes 的实现。

attributeChangedCallback(name, oldValue, newValue) {
  console.log('name 为属性名,需要通过 observedAttributes 监听才能触发获得值');
  // do something
}
static get observedAttributes() {return ['attr1', 'attr2']; } // return 返回一个数组,可用于监听多个属性
复制代码

Shadow DOM

这一“板斧”算是 WebComponents 最重要的特性了,有了它才真正实现了 HTML 的复用 —— 将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离。查看之前博文例子可以看到 Shadow DOM 接口返回一个 shadow-root 节点,在此节点内部的样式和行为都不会影响外面元素,可作为微前端子应用间样式隔离的选择方案之一,这属于另一话题暂且按下不表。

image.png

1. shadow 模式

可以使用 Element.attachShadow() 方法将一个 shadow root 附加到任何一个元素上,它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed

let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
复制代码

open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,可以使用 DOM APIs 对它进行操作。

class SelfDiv extends HTMLElement {
  constructor() {
    super()
    let shadow = this.attachShadow({mode: 'open'}) 
    shadow.appendChild(tpl.content.cloneNode(true))
    
    console.log('===shadow root===', this.shadowRoot)
  }
}
复制代码

2.可绑定的类型

还有一点要特别注意的事,不是任何类型的元素都可以附加到 shadow root 下面。出于安全考虑,目前只有这些类型元素可以附加到 shadow DOM 上(有效的自定义元素也可):可附加的元素

image.png

template 和 slot

前文提到 slot 的使用规则过于刻板不太灵活,其实 slot 分为具名非具名两种,可书写更灵活的使用方式。这里有几个要点需特别说明:

  • 如果你调用 template 时未显式使用具名 slot,具名 slot 会显示默认内容。
  • 具名 slot 被使用时,非具名 slot 默认不展示
  • 如果在 template 中定义了非具名 slot,使用模板时标签间的内容会直接替换非具名 slot 部分。

假设 template 有两个 slot 如下:

<template id="tpl">
  <header>
    <slot name='title'>具名slot:博客标题</slot>
  </header>
  <slot>非具名slot</slot>
  <section>博客内容博客内容博客内容</section>
</template>
复制代码

该模板有一个具名 title 的 slot 和一个非具名的 slot,若使用该模板进行组件自定义:

class MyBlog extends HTMLElement {
  constructor(){
    super()
    let shadow = this.attachShadow({mode:'open'})
    shadow.appendChild(tpl.content.cloneNode(true))
  }
}
customElements.define('my-blog', MyBlog)
复制代码

场景一:在页面上使用时,直接访问 <my-blog></my-blog>,具名 slot 和非具名 slot 都直接展示:

<!-- 直接调用,未使用slot,具名和非具名都直接展示-->
<my-blog></my-blog>
复制代码

image.png

场景二:如果访问具名 slot,且模板内不传入其他内容,只会展示具名 slot 部分:

<!-- 使用具名slot,非具名slot不展示 -->
<my-blog>
  <h2 slot='title'>自定义标题</h2>
</my-blog>
复制代码

image.png

场景三:同时为具名和非具名 slot 赋值:

<!-- 模板非slot 内容会替换非具名内容 -->
<my-blog>
  <h2 slot='title'>自定义标题</h2>
  <span>自定义非具名部分</span>
</my-blog>
复制代码

image.png

具体代码可以猛戳自定义组件slot

四、生态和周边

目前市面上有一些基于 WebComponent 标准开发的组件库:Omiuxy-ui。另外还发现一个比较有意思的 CSS 库 css-doodle,用 WebComponent 实现酷炫的 CSS 效果,可以为个人网站增色不少。

以上,谢谢阅读。

image.png

文章分类
前端