Web组件——原生WebComponents

于 2011 年面世的 Web Components 是一套功能组件,让开发者可以使用 HTML、CSS 和 JavaScript 创建可复用的组件。这意味着你无需 React 或 Angular 等框架也能创建组件。不仅如此,这些组件还都可以无缝集成到这些框架中。
                        —— 本文参考地址

各浏览器支持情况,更多浏览器去caniuse.com

Talk is cheap, Show me the code

class MyElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // here the element has been inserted into the DOM
  }
}
window.customElements.define('my-element', MyElement);

如上,就完成了一个原生 WebComponents 的定义
HTMLElement为浏览器原生对象,无需引用
connectedCallback为钩子函数,相当于vue中的mounted

const myElement2 = document.createElement('my-element');
document.body.appendChild(myElement2);

// MyElement是一个ES6类, 所以还可以这样
// const el = customElements.get('my-element');
// const myElement2 = new el();
// document.body.appendChild(myElement2);
运行代码,结果如图

钩子(生命周期)函数

  1. constructor:构造函数,元素创建时(new、createElement)触发
  2. connectedCallback => mounted(vue)
  3. disconnectedCallback => destroyed
  4. adoptCallback:document.adoptNode(element)时触发
  5. attributeChangedCallback:元素属性变化时触发
    原文:每当属性更改已添加到 observedAttributes 数组时都会调用此方法
      class MyElement extends HTMLElement {
        constructor() {
          super();
          console.info('constructor')
        }

        static get observedAttributes() {
          return ['foo', 'bar'];
        }

        connectedCallback() {
          console.info('connectedCallback')
          // here the element has been inserted into the DOM
        }

        attributeChangedCallback(attr, oldVal, newVal) {
          console.info('attributeChangedCallback')
          switch(attr) {
            case 'foo':
              // do something with 'foo' attribute

            case 'bar':
              // do something with 'bar' attribute

          }
        }
      }
      window.customElements.define('my-element', MyElement);

      const myElement2 = document.createElement('my-element');
      myElement2.setAttribute('foo', '996')
      document.body.appendChild(myElement2);

运行结果

constructor
attributeChangedCallback
connectedCallback

以上即为生命周期方法的执行顺序

  1. whenDefined: 当html不是由js追加到dom上,而是直接编写在html里时,当代码执行到customElements.define()时触发

重写RadioBox

<style>
  my-element { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; }
</style>
<my-element></my-element>
<my-element checked="yes"></my-element>
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['checked', 'disabled'];
  }

  constructor() {
    super();
  }

  attributeChangedCallback(attr, oldVal, newVal) {
    switch(attr) {
      case 'checked':
        newVal == 'yes' 
          ? this.style.border = '7px solid #4ac153'
          : this.style.border = '1px solid #eaeaea'
        break;
      case 'disabled':
        break;
    }
  }

  connectedCallback () {
    this.onclick = e => {
      if (this.getAttribute('checked') == 'yes') this.setAttribute('checked', 'no')
      else this.setAttribute('checked', 'yes')
    }
  }
}
window.customElements.define('my-element', MyElement);
运行效果

setter 和 自定义方法

setter:提供外部可访问的、可修改的属性
自定义方法: 自定义组件中定义自定义方法,可在外部方法

    <my-element value=2 title="玩手机"></my-element>
    <my-element value=3 title="睡觉" checked="yes" id="my3"></my-element>
class MyElement extends HTMLElement {
  ...

  // 自定义组件外部方法
  getValue() {
    return this.getAttribute('checked') == 'yes' && this.getAttribute('disabled') != 'yes' 
      ? this.getAttribute('value')
      : null
  }

  // 使用 setter 就能将一个 property 映射到一个 attribute 属性上
  set disabled(isDisabled) {
    this.setAttribute('disabled', isDisabled);
  }
  get disabled() {
    return this.getAttribute('disabled') == 'yes';
  }
}
window.customElements.define('my-element', MyElement);

console.info(document.querySelector('#my3').getValue())
document.querySelector('#my3').disabled = 'yes'
console.info(document.querySelector('#my3').getValue())

> 3
> null

Shadow DOM

使用 Shadow DOM 时,自定义元素的 HTML 和 CSS 会完全封装在组件内部。
其实 Shadow DOM 也用在几个原生 HTML 元素上,如<video><svg>
Shadow DOM 还提供真正的作用域 CSS,可以(设置为)不从周围的 CSS 继承任何值

class RadioBox extends HTMLElement { 

  // 构造函数
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `<p>Hello world</p>`;
  }
  
}
window.customElements.define('my-popup', MyPopup);
Shadow DOM在Chrome中的显示

特性

一、HTML:自带懒加载

  • 在实际插入 DOM 树之前,它将不会被显示或解析,包括js、css

二、JS

  1. this.attachShadow({mode: 'open'})
    open:可在开发工具中检查,并通过查询、配置任何公开的 CSS 属性或监听它抛出的事件来交互
    close: 不允许组件的使用者以任何方式与它交互,也无法监听到它抛出的事件
  2. this.shadowRoot
    可以用它来调用document上所有操作DOM的方法,如querySelector等

三、 CSS

  1. 组件的所有 CSS 都在<style>标签内定义
  2. 可以使用<link rel =“stylesheet”>调用外部样式
  3. :host { } 表示组件自身
  4. :host { all: initial; } 禁止组件使用外围css属性。默认情况下,自定义元素会从周围的 CSS 继承一些属性,例如 color 和 font 等
  5. #title { color: val(--mycolor) } 无法从外部设置自定义元素内的任何节点的样式,除非主动暴露css变量:--xxx
// 把样式封闭进组件
class RadioBox extends HTMLElement {
  // 构造函数
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>
        :host { cursor: pointer; --pop-color: #4ac153;  /*css变量*/ }
        :host::after { content: ' '; clear:both; display: block; }
        :host i { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; float: left; }
        :host([disabled=yes]) i { background-color: #eaeaea; border-color: #ccc; }
        :host([checked=yes]) i { border: 7px solid var(--pop-color); }
      </style>

      <i></i>
    `;
  }

  connectedCallback () {
    this.onclick = e => {
      if (this.getAttribute('disabled') == 'yes') return
      this.setAttribute('checked', this.getAttribute('checked') == 'yes'
        ? 'no'
        : 'yes')
    }
  }
}
window.customElements.define('radio-box', RadioBox);

Slot

为单选按钮添加文本

<radio-box value="phone">玩手机</radio-box>
<radio-box checked="yes" value="sleep">睡觉</radio-box>
class RadioBox extends HTMLElement {
  // 构造函数
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>
        :host { cursor: pointer; --pop-color: #4ac153;  /*css变量*/ }
        :host::after { content: ' '; clear:both; display: block; }
        :host i { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; float: left; }
        :host([disabled=yes]) i { background-color: #eaeaea; border-color: #ccc; }
        :host([checked=yes]) i { border: 7px solid var(--pop-color); }
        /* slot 样式 */
        ::slotted(*) { color: #333; }
        slot { line-height: 22px; padding-left: 10px; display: inline; }
      </style>

      <i></i><slot>选项</slot>
    `;
  }

  // mounted
  connectedCallback () {
    this.onclick = e => {
      if (this.getAttribute('disabled') == 'yes') return
      this.setAttribute('checked', this.getAttribute('checked') == 'yes'
        ? 'no'
        : 'yes')
    }
  }
  
  getValue() {
    return this.getAttribute('checked') == 'yes' && this.getAttribute('disabled') != 'yes' 
      ? this.getAttribute('value')
      : null
  }

  // 使用 setter 就能将一个 property 映射到一个 attribute 属性上
  set disabled(isDisabled) {
    this.setAttribute('disabled', isDisabled);
  }
  get disabled() {
    return this.getAttribute('disabled') == 'yes';
  }
  set checked(val) {
    this.setAttribute('checked', val);
  }
  get checked() {
    return this.getAttribute('checked') == 'yes';
  }
}
window.customElements.define('radio-box', RadioBox);
效果

添加 radioGroup

<radio-group>
    <radio-box value="phone">玩手机</radio-box>
    <radio-box checked="yes" value="sleep">睡觉</radio-box>
</radio-group>
<script>
    console.info(document.querySelector('radio-group').values)  // -> ["sleep"]
</script>
class radioGroup extends HTMLElement {
  constructor () {
    super()
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `<slot></slot>`;
  }

  connectedCallback () {

  }

  get values () {
    let v = []
    this.shadowRoot.querySelector('slot').assignedNodes().forEach(item => {
      if (item.nodeName == "RADIO-BOX")
      item.getValue() && v.push(item.getValue())
    })
    return v
  }
}
window.customElements.define('radio-group', radioGroup)

模板元素

当 Web 组件需要根据不同情况呈现完全不同的标记时,可以使用不同的模板来完成此任务

<template>内的html不渲染、js不执行、不引入外部资源

class ResultTemplate extends HTMLElement {
  // 构造函数
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <template id="good">
        <style>.good{color:green}</style>
        <p class="good">是个好选择</p>
        <script>console.info('good good')</script>
      </template>

      <template id="bad">
        <style>.bad{color:red}</style>
        <p class="bad">对身体不好</p>
        <script>console.info('bad bad')</script>
      </template>

      <div id="container">
        这样做:
      </div>
    `;
  }

  connectedCallback() {
    setTimeout(() => {
    // 不用cloneNode,<template>内容将会被移动,而不是复制
      const content = this.shadowRoot.querySelector('#good').content.cloneNode(true);

      this.container = this.shadowRoot.querySelector('#container');
      this.container.appendChild(content);   // 只有在此时才会渲染

    }, 2000)
  }
}
window.customElements.define('result-template', ResultTemplate);

执行后发现,css被加载了,但js并没有被执行。尝试引入js文件,也并没有被引用。

扩展原生元素 与旧浏览器

扩展原生元素,原文说浏览器支持情况更差,具体不再研究,感兴趣的可以看本文

旧浏览器兼容

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

推荐阅读更多精彩内容