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
自定义元素

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269