vlambda博客
学习文章列表

每天 React, Vue, 你知道如何原生实现 WebComponent吗?


谈到WebComponent 很多人很容易想到Vue,React中的组件。但其实H5原生也已经支持了组件的编写。

查看 Web Components MDN 文档,里面原话如下:

Web Components

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。Web Components旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。

Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。

  • Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

  • HTML templates(HTML模板):template 和 slot 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

上面的概念难以理解,我们通过一个例子看下如何编写一个组件;

案例一

  1. 什么是 HTML templates(HTML模板)?
 <template id="btn">
    <button class="hp-button">
      <slot></slot>
    </button>
  </template>
  1. Custom elements(自定义元素)
class HpButton extends HTMLElement {
      constructor() {
        super();
        //...
        
      }
    }
    // 定义了一个自定义标签 组件
window.customElements.define('hp-button', HpButton)
  1. Shadow DOM(影子DOM)
 let shadow = this.attachShadow({
          mode'open'
        });
        let btnTmpl = document.getElementById('btn');
        let cloneTemplate = btnTmpl.content.cloneNode(true)
        const style = document.createElement('style');
        let type = this.getAttribute('type') || 'default';
        const btnList = {
          'primary': {
            background'#ff0000',
            color'#fff'
          },
          'default': {
            background'#909399',
            color'#fff'
          }
        }
        style.textContent = `
                    .hp-button{
                        outline:none;
                        border:none;
                        border-radius:4px;
                        padding:5px 20px;
                        display:inline-flex;
                        background:${btnList[type].background};
                        color:${btnList[type].color};
                        cursor:pointer
                    }
                `

// dom操作具备移动型
  shadow.appendChild(style)
 shadow.appendChild(cloneTemplate)

一个简单完整的例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <style>
    :root {
      --background-color: black;
      --text-color: yellow
    }
  
</style>
  <hp-button type="primary">
    <input type="text">
    按钮
  </hp-button>
  <hp-button>珠峰按钮</hp-button>
  <!-- 内容是不会被渲染到视图上,不会影响页面展示,可以使用模板 -->
  <template id="btn">
    <button class="hp-button">
      <slot></slot>
    </button>
  </template>

  <script>
    class HpButton extends HTMLElement {
      constructor() {
        super();
       
        let shadow = this.attachShadow({
          mode'open'
        });
        let btnTmpl = document.getElementById('btn');
        let cloneTemplate = btnTmpl.content.cloneNode(true)
        const style = document.createElement('style');
        let type = this.getAttribute('type') || 'default';
        const btnList = {
          'primary': {
            background'#ff0000',
            color'#fff'
          },
          'default': {
            background'#909399',
            color'#fff'
          }
        }
        style.textContent = `
                    .hp-button{
                        outline:none;
                        border:none;
                        border-radius:4px;
                        padding:5px 20px;
                        display:inline-flex;
                        background:${btnList[type].background};
                        color:${btnList[type].color};
                        cursor:pointer
                    }
                `

        // dom操作具备移动型
        shadow.appendChild(style)
        shadow.appendChild(cloneTemplate)
      }
    }
    // 定义了一个自定义标签 组件
    window.customElements.define('hp-button', HpButton)
  
</script>
</body>
</html>

结论:原生组件与Vue,React的组件的概念是相似的,但是从写法上来看有区别。

深入学习

组件中还有重点的两部分:生命周期和事件。

生命周期

custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:

  • connectedCallback:当 custom element首次被插入文档DOM时,被调用。
  • disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
  • adoptedCallback:当 custom element被移动到新的文档时,被调用。
  • attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。

我们来看一下它们的一下用法示例。下面的代码出自life-cycle-callbacks示例(查看在线示例:https://mdn.github.io/web-components-examples/life-cycle-callbacks/)。这个简单示例只是生成特定大小、颜色的方块。custom element看起来像下面这样

生命周期的代码的具体示例:

class Square extends HTMLElement {
  // Specify observed attributes so that
  // attributeChangedCallback will work
  static get observedAttributes() {
    return ['c''l'];
  }

  constructor() {
    // Always call super first in constructor
    super();

    const shadow = this.attachShadow({mode'open'});

    const div = document.createElement('div');
    const style = document.createElement('style');
    shadow.appendChild(style);
    shadow.appendChild(div);
  }

  connectedCallback() {
    console.log('Custom square element added to page.');
    updateStyle(this);
  }

  disconnectedCallback() {
    console.log('Custom square element removed from page.');
  }

  adoptedCallback() {
    console.log('Custom square element moved to new page.');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log('Custom square element attributes changed.');
    updateStyle(this);
  }
}

customElements.define('custom-square', Square);

事件

可以采用 disatchEventCustomEvent 来实现:

 document.querySelector('???').dispatchEvent(new CustomEvent('changeName', {
        detail: {
          name1111,
        }
      }))

折叠面板的案例

  1. 完成模版部分的定义:
 <!-- 没有实际意义, 不会渲染到页面上 -->
 <template id="collapse_tmpl">
   <div class="zf-collapse">
     <slot></slot>
   </div>
 </template>
 <template id="collapse_item_tmpl">
   <div class="zf-collapse-item">
     <div class="title"></div>
     <div class="content">
       <slot></slot>
     </div>
   </div>
 </template>
  1. 创建组件
class Collapse extends HTMLElement {
 constructor() {
   super();
   const shadow = this.attachShadow({
     mode'open'
   });
   const tmpl = document.getElementById('collapse_tmpl');
   let cloneTemplate = tmpl.content.cloneNode(true);
   let style = document.createElement('style');
   // :host 代表的是影子的根元素
   style.textContent = `
           :host{
               display:flex;
               border:3px solid #ebebeb;
               border-radius:5px;
               width:100%;
           }
           .zf-collapse{
               width:100%;
           }
       `

   shadow.appendChild(style);
   shadow.appendChild(cloneTemplate);

   let slot = shadow.querySelector('slot'); // 监控slot变化
   slot.addEventListener('slotchange', (e) => {
     this.slotList = e.target.assignedElements();
     this.render();
   })
 }
 static get observedAttributes() { // 监控属性的变化
   return ['active']
 }
 // update
 attributeChangedCallback(key, oldVal, newVal) {
   if (key == 'active') {
     this.activeList = JSON.parse(newVal);
     this.render();
   }
 }
 render() {
   if (this.slotList && this.activeList) {
     [...this.slotList].forEach(child => {
       child.setAttribute('active'JSON.stringify(this.activeList))
     });
   }
 }

}
export default Collapse
class CollapseItem extends HTMLElement {
 constructor() {
   super();
   let shadow = this.attachShadow({
     mode'open'
   });
   let tmpl = document.getElementById('collapse_item_tmpl');
   let cloneTemplate = tmpl.content.cloneNode(true);
   let style = document.createElement('style');
   this.isShow = true// 标识自己是否需要显示

   style.textContent = `
           :host{
               width:100%;
           }
           .title{
               background:#f1f1f1;
               line-height:35px;
               height:35px;
           }
           .content{
               font-size:14px;
           }
       `


   shadow.appendChild(style)
   shadow.appendChild(cloneTemplate);
   this.titleEle = shadow.querySelector('.title');

   this.titleEle.addEventListener('click', () => {
     // 如果将结果传递给父亲  组件通信?
     document.querySelector('zf-collapse').dispatchEvent(new CustomEvent('changeName', {
       detail: {
         namethis.getAttribute('name'),
         isShowthis.isShow
       }
     }))
   })
 }

 static get observedAttributes() { // 监控属性的变化
   return ['active''title''name']
 }
 // update
 attributeChangedCallback(key, oldVal, newVal) {
   switch (key) {
     case 'active':
       this.activeList = JSON.parse(newVal); // 子组件接受父组件的数据
       break;
     case 'title':
       this.titleEle.innerHTML = newVal; // 接受到title属性 作为dom的title
       break;
     case 'name':
       this.name = newVal
       break;
   }
   let name = this.name;
   if (this.activeList && name) {
     this.isShow = this.activeList.includes(name);
     this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none'
   }
 }
}
export default CollapseItem
  1. 页面使用:
<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>

<body>
 <zf-collapse>
   <zf-collapse-item title="Node" name="1">
     <div>nodejs welcome</div>
   </zf-collapse-item>
   <zf-collapse-item title="react" name="2">
     <div>react welcome</div>
   </zf-collapse-item>
   <zf-collapse-item title="vue" name="3">
     <div>vue welcome</div>
   </zf-collapse-item>
 </zf-collapse>

 <!-- 没有实际意义, 不会渲染到页面上 -->
 <template id="collapse_tmpl">
   <div class="zf-collapse">
     <slot></slot>
   </div>
 </template>
 <template id="collapse_item_tmpl">
   <div class="zf-collapse-item">
     <div class="title"></div>
     <div class="content">
       <slot></slot>
     </div>
   </div>
 </template>
 <!-- vite 实现原理 就依赖于 type="module" -->
 <script src="./index1.js" type="module"></script>
</body>
</html>

参考资料:

  • web Components MDN
  • 案例学习:https://github.com/mdn/web-components-examples

  • ❤️ 谢阅读

  • 如果觉得这篇文章还不错,来个分享点赞在看三连吧,让更多的人也看到~

  • 3.特殊阶段,带好口罩,做好个人防护。