Web Components技术初探

image.png

前言

不知不觉,2019年即将接近尾声,现有前端三大框架也各自建立着自己的生态、自己的使用群体。从angular1.0跨时代的开创了前端MVVM模型(在其他平台已经存在的模型,如WPF),到React组件化设计思路的诞生,到Vue借鉴两位前辈的思路,创造属于自己的技术体系。

随着各大框架版本的更迭,组件化的思路因为大大提高了开发效率依旧一直是各大框架的核心(angular从angular2开始),从未改变。其实,早在react诞生之前,组件化这个概念,已经在2011年前端开发者大会上被提出并完成纳入w3c标准。到现在,基本主流的浏览器都对他进行了兼容。本文便是对这一技术的初探,大家写腻了三大框架,不妨看看原生的组件要怎么玩

image.png

WebComponent中的三个概念

在WebComponents技术体系中,主要由以下三项技术所组成,通过组合这三项技术,可以创建属于自己功能的组件。

  1. Custom elements(自定义元素) 用于定义自定义标签。
  2. Shadow DOM(影子DOM) 类似于沙盒,将dom结构附加到元素上,保证功能或者样式的私有,而不用担心污染其他功能或者样式。
  3. HTML templates(HTML模板) 可以当做缺少了数据绑定的vuetemplate标签,主要承担了组件结点渲染,也提供了slot插入内容。

基于以上的内容简介,我们来看看这三项技术具体要怎么使用

Custom elements

Cumtom elements 这个概念对于写惯了三大框架的开发者而言非常的用于理解,自定义标签,我们在其他框架经常通过组件的形式,使用自己定义的标签,就拿vue来举例,我们在vue中会见到下面这样的代码。

<template>
  <x-toast>测试</x-toast>
</template>
<script>
  import Toast from 'Toast';
  export default {
    components:{
      'x-toast': Toast
    }
  // ...省略其他代码
  }
</script>

在这里,"x-toast"就是一个自定义标签,用于定义自己的功能,对于Web Component,我们可以使用CustomElementRegistry.define方法来自定义元素,该方法接受三个参数

  1. 表示所创建的元素名称的符合DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线
  2. 用于定义元素行为的
  3. 一个包含 extends属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。

基于以上的定义,我们可以这样定义一个这样的标签。

CustomElementRegistry.define('todo-list', TodoList);

针对第二个参数TodoList,我们参照参数描述,主要用于定义元素行为的,他拥有两种类型,通过继承来确定类型方式

  1. **Autonomous custom elements , 独立元素,即html中可以直接使定义的标签,需要继承 ** HTMLElement
class TodoList extend HTMLElement {
  construct(){
    super();
  }
}
CustomElementRegistry.define('todo-list', TodoList);

在html中我们就可以直接这么使用

  <todo-list><todo-list/>
  1. Customized built-in elements 继承自基本元素,并不像独立元素一样,他依赖于div,p等基本元素标签,通过继承对应的标签,来拓展其功能,具体使用的时候,通过is属性来区分原生标签。
class TodoList extend HTMLParagraphElement {
  construct(){
    super();
  }
}
CustomElementRegistry.define('todo-list', TodoList, {extends: 'p'});

在html中需要配合is属性使用

<p is="todo-list">

在这里我们提到了如何定义一个元素(组件),对应vue/react组件,WebComponents也有属于自己的生命周期钩子函数,当我们定义一个元素时,他会在元素的不同阶段触发他们。

  1. connectedCallback:当元素首次被插入文档DOM时,被调用。
  2. disconnectedCallback:当元素从文档DOM中删除时,被调用。
  3. adoptedCallback:当元素被移动到新的文档时,被调用。
  4. attributeChangedCallback: 当元素增加、删除、修改自身属性时,被调用。

在这4个钩子函数中 1、2、4非常好理解,我们都可以从其他框架找到对应,第3可能就比较难于理解,什么叫移动到新的文档时被调用,咱们通过一个例子来说明

function createWindow(srcdoc) {
  let p = new Promise(resolve => {
    let f = document.createElement('iframe');
    f.srcdoc = srcdoc || '';
    f.onload = e => {
      resolve(f.contentWindow);
    };
    document.body.appendChild(f);
  });
  return p;
}

// 1. 创建2个Iframe w1,和w2
Promise.all([createWindow(), createWindow()])
  .then(([w1, w2]) => {
    // 2. 在w1这个iframe中创建了一个自定义元素'x-adopt'
    w1.customElements.define('x-adopt', class extends w1.HTMLElement {
      adoptedCallback() {
        console.log('Adopted!');
      }
    });
    // 3. 实例化这个自定义元素
    let a = w1.document.createElement('x-adopt');

    // 4. 将这个自定义元素插入w2这个iframe中
    w2.document.body.appendChild(a);
  });

上面这个例子,便是移动到新的文档中,我在iframe1中创建了一个属于iframe1的新的元素,但是却将他插入iframe2,这样就是将的其插入其他文档,因此会触发adoptedCallback生命周期钩子。

注意在WebComponents的attributeChangedCallback,这个生命周期钩子之中,我们要通过定义observedAttributes这个静态方法,约定你要监听的属性,才会触发attributeChangedCallback回调,如下所示

  class CustomInput extends Base{

  // 定义监听属性
  static get observedAttributes() {
    return ['value'];
  }

  // 当自定义元素的一个属性被增加、移除或更改时被调用。:
  attributeChangedCallback(name, oldValue, newValue) {
  }
}

customElements.define('custom-input', CustomInput);

如上面代码所示,当我改变了custom-inputvalue属性时,才会触发attributeChangedCallback,回调,如果你改变了name或者其他非value属性的时候,便不会触发(第一次写Web Components时可能需要注意,第一次本人就怎么也没有办法触发这个回调)

Shadow DOM

Shadow DOM其实并不是一个新的概念,很早之前,Chrome就可以通过控制台Setting显示页面的Shadow DOM

image.png

把这一项勾选后,你在通过控制台Elements看看元素,你会发现有一些原生的标签,也有属于自己的Shadow DOM, 如截图中的input元素。
image.png

#shadow-root称为起始根节点 ,在图中可以看到,他是寄宿在input标签之上,当然这是一个最简单的Shadow DOM,其实 Shadow DOM也和普通元素一样,可以嵌套使用,可以在一个#shadow-root中嵌入别的#shadow-root。如原生标签video就是如此,大家可以自己打开控制台看看。

image.png

介绍了这么多Shadow DOM的知识,他的主要功能如上面介绍的,他主要保证功能或者样式的私有,而不用担心污染其他功能或者样式

那么我们来看看他是怎么和Web Components结合使用

对HTML元素而言,他的实例中有一个方法 attachShadow,他会返回shadowRoot并挂载到这个元素实例上,因此我们只需要调用他,便可以生成Shadow DOM;

class TodoList extend HTMLElement {
  construct(){
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
  }
}
CustomElementRegistry.define('todo-list', TodoList);

我们看见,在调用方法时传入了一个对象,对象中有这样的属性{ mode: 'open' },当mode传入open时,你可以通过元素实体this.shadowRoot获取shadowDOM节点,当传入close时,便没法这样获取,如video标签,你无法通过this.shadowRoot获取到,学过JAVA等面向对象的同学应该会发现,这是不是和将属性定义为private以及public很像呢?

template

template是这三个概念之中最简单的了,即使用<template>标签,来完成shadowRoot结点渲染,因为<template>标签并不会渲染到html元素上,因此我们可以利用这一特性来复用template。如以下的代码

<template id="my-paragraph">
  <style>
    :host{
    }
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>My paragraph</p>
</template>
customElements.define('my-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById('my-paragraph');
      let templateContent = template.content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateContent.cloneNode(true));
  }
})

我们通过getElementById获取到模板,然后拿到他的内容,通过拿到的shadowRoot元素appendChild到结点中去,是不是非常的简单?

我们注意到模板中有一段style写了一个伪类 :host,这个伪类主要是给其宿主元素添加样式。我们可以使用类似这样的选择器,控制不同class下,结点的样式。
如以下样式只在自定义元素存在test这个类才会生效,如<x-test class="test"></x-foo>

:host(.test:host) {
  ...
}

一点优化

相对于传统的template标签,接触webpack后我更喜欢通过模块化的方式引入,对webpack而言,一切皆是模块,我们只需要写html文件,通过对应loader进来后即可,如:

// template.html
<style>
    :host{
    }
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>My paragraph</p>
import template from 'template.html';
customElements.define('my-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById('my-paragraph');
      let templateContent = template.content;

      const shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.innerHTML = template; // 通过Import 是不是更方便了呢?
  }
})

总结

Web Components作为原生的组件化方案,没有数据绑定写起来还是挺麻烦的,不过,对于一些小工具而言,天然不需要任何依赖,项目纯净,写起来也是不错的,最近我也在使用Web Component写一个chrome插件,用于YAPI Mock拦截,也欢迎大家体验使用。
https://github.com/JackyTianer/yapi-mock-chrome-plugin

写文不易,如果觉得文章有用,动动手点个赞吧,您的赞是我创作的最大动力!谢谢

参考文章

维基百科_WebComponent
Web开发技术 WebComponents
自定义元素