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




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:提供外部可访问的、可修改的属性
自定义方法: 自定义组件中定义自定义方法,可在外部方法

    
    
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 元素上,如

class RadioBox extends HTMLElement { 

  // 构造函数
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `

Hello world

`; } } 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 都在 `; } 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

    为单选按钮添加文本

    玩手机
    睡觉
    
    class RadioBox extends HTMLElement {
      // 构造函数
      constructor() {
        super();
        const shadowRoot = this.attachShadow({mode: 'open'});
        shadowRoot.innerHTML = `
          
    
          选项
        `;
      }
    
      // 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

    
        玩手机
        睡觉
    
    
    
    class radioGroup extends HTMLElement {
      constructor () {
        super()
        const shadowRoot = this.attachShadow({mode: 'open'});
        shadowRoot.innerHTML = ``;
      }
    
      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 组件需要根据不同情况呈现完全不同的标记时,可以使用不同的模板来完成此任务