美文网首页
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