编程技术文章分享与教程

网站首页 > 技术文章 正文

除了Vue、React,你知道如何原生实现WebComponent吗?

hmc789 2024-11-15 19:37:21 技术文章 2 ℃

此文由掘金@hpstream_授权发布

https://juejin.cn/post/7034796986889043999

谈到WebComponent 很多人很容易想到Vue,React中的组件。但其实H5原生也已经支持了组件的编写。
关于Web Components,MDN是如此定义其概念和使用的:

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

作为开发者,我们都知道尽可能多的重用代码是一个好主意。这对于自定义标记结构来说通常不是那么容易 — 想想复杂的HTML(以及相关的样式和脚本),有时您不得不写代码来呈现自定义UI控件,并且如果您不小心的话,多次使用它们会使您的页面变得一团糟。

Web Components旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

?

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>

「2、Custom elements(自定义元素)」

class HpButton extends HTMLElement {
      constructor() {
        super();
        //...
        
      }
    }
// 定义了一个自定义标签 组件
window.customElements.define('hp-button', HpButton)

「3、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://github.com/mdn/web-components-examples/tree/master/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);

事件

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

折叠面板的案例

  • 完成模版部分的定义:
 <!-- 没有实际意义, 不会渲染到页面上 -->
  <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>

  • 创建组件:
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: {
          name: this.getAttribute('name'),
          isShow: this.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

  • 页面使用:
<!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>

更多:

案例Demo(https://github.com/mdn/web-components-examples)

Tags:

标签列表
最新留言