美文网首页
Web组件——原生WebComponents

Web组件——原生WebComponents

作者: 我叫Aliya但是被占用了 | 来源:发表于2019-05-08 18:37 被阅读0次

于 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

相关文章

网友评论

      本文标题:Web组件——原生WebComponents

      本文链接:https://www.haomeiwen.com/subject/thrlgqtx.html