Bootstrap

初步认识 Web Components 并实现一个按钮

目录

1.Web Components 基本概念

1.1 三个场景

1.2 是什么

2.使用 Custom Elements 实现一个按钮

2.1 概念介绍

2.1.1 Shadow DOM

2.1.2 Element.attachShadow()

2.1.3 在组件中 使用 Shadow DOM 基本步骤

2.1.4 attributeChangedCallback

2.1.5 get observedAttributes

2.1.6 dispatchEvent()

2.1.7 CustomEvent

2.2 实践出真知

2.2.1 初步实现按钮

2.2.2 给按钮加属性-传递

2.2.3 给按钮加属性-映射

2.2.4 给按钮加事件-普通事件

2.2.5 给按钮加事件-自定义事件


通过这篇文章,你可以学习到:

  • 如何使用原生 JavaScript 构建一个 Web Component
  • 如何在应用程序中使用 Web Component

1.Web Components 基本概念

1.1 三个场景

  • Vue2 升级到 Vue3 后,饿了么官方组件库也被迫从 ElementUI 升级到 ElementPlus,聪明的你不禁会发出疑问:难道每次框架升级都需要升级组件库吗?
  • xx项目,参考了个 React 开源项目,但开源项目组件用 React 写的,而我得用 Vue 开发
  • xx项目,作为总集公司,其他厂商需要使用总集提供的组件库,他们不得不投入时间成本学习 Vue3,老板因此很生气

针对上面的场景,预言家 Google 早在2011年就推出了解决方案 —— Web Component

1.2 是什么

Web Components 也被叫做 Custom Elements,它已经成为 浏览器标准 API

下面是各大浏览器针对 Web Components(Custom Elements) 的支持情况:

Custom Elements (V1) | Can I use... Support tables for HTML5, CSS3, etcicon-default.png?t=N7T8https://caniuse.com/custom-elementsv1

开发者可以在 Custom Elements 中,封装结构(HTML)、样式(CSS)和行为(JavaScript),最终生成自定义元素标签(使用方式类似于原生 html 标签),不会受框架限制

举个例子:

<!-- 原生 html 按钮 -->
<button name="button">按钮</button>
  
<!-- ElementPlus 按钮 -->
<el-button type="success">按钮</el-button>

<!-- Custom Elements -->
<my-button text="按钮"></my-button>

2.使用 Custom Elements 实现一个按钮

2.1 概念介绍

温馨提示:也可以食用完下面的实践出真知,再回头阅读这儿的概念介绍,这样更容易理解

2.1.1 Shadow DOM

一种将 DOM、样式、行为 封装在一个可重用的、封装的组件中的技术

Shadow DOM 可以帮助开发者避免样式和 DOM 冲突,提高代码的可维护性和可重用性(大家可以回忆一下修改 Ionic 组件样式时的那种感觉)

它允许开发者创建自定义元素,这些元素可以在页面上使用,就像普通的 HTML 元素一样

我们看下 Ionic 官网按钮示例,可以看出 ionic 组件就是用 Shadow DOM 写的自定义元素

2.1.2 Element.attachShadow()

该方法返回 ShadowRoot 对象,用于将一个 Shadow DOM 附加到 指定元素 上

  // 通过 Element.attachShadow() 获取 ShadowRoot 对象
  // open shadow root:元素可以从 js 外部访问根节点
  // closed 拒绝从 js 外部访问关闭的 shadow root 节点
  const shadowRoot = this.attachShadow({ mode: "open" });
  // 把 Shadow DOM 附加到 template 上
  shadowRoot.appendChild(templateDOM.content.cloneNode(true));

关于操作 shadow root 节点的必要性:这个当然很重要了

比如修改 Shadow Dom 中某个节点的 innerHTML

constructor() {
  // ...
  // 获取按钮文字 DOM
  this.btnTextDOM = shadowRoot.querySelector(".button-inner");
}
render() {
  this.btnTextDOM.innerHTML = this.text;
}

2.1.3 在组件中 使用 Shadow DOM 基本步骤

总体思路:创建组件模板,给组件加 Shadow DOM

具体步骤:

  1. 创建一个 <template> 元素,将组件的 HTML 结构放在其中
  2. 使用 Element.attachShadow() 方法将 Shadow DOM 附加到组件元素上
  3. 将 <template> 元素的内容复制到 Shadow DOM 中

2.1.4 attributeChangedCallback

当自定义元素的 属性 变化(增加、移除、更改)时调用

attributeChangedCallback(name, oldVal, newVal) {
  this[name] = newVal;
  this.render();
}

注意:此方法通常与 get observedAttributes() 结合使用

2.1.5 get observedAttributes

如果需要在属性变化后,在 attributeChangedCallback 回调函数中执行某些操作,则必须监听这个属性

通过定义 get observedAttributes() 来监听属性变化

static get observedAttributes() {
  return ["text"];
}

注意:

  • get observedAttributes() 方法只会在类内部进行调用,不会被实例化对象调用
  • 此方法通常与 attributeChangedCallback() 结合使用

2.1.6 dispatchEvent()

dispatchEvent() 方法是用来触发指定事件的

它接受一个 Event 对象作为参数,该对象描述了要触发的事件的类型、是否冒泡、是否可以取消等信息

调用 dispatchEvent() 方法后,会在当前元素上触发指定的事件,并且事件会沿着 DOM 树向上传播,直到到达根节点或者被取消

// 获取要触发事件的元素
const myElement = document.querySelector('#my-element');

// 创建一个自定义事件
const myEvent = new CustomEvent('my-event', {
  detail: {
    message: 'Hello world!'
  }
});

// 触发自定义事件
myElement.dispatchEvent(myEvent);

EventTarget.dispatchEvent() - Web API 接口参考 | MDNEventTarget 的 dispatchEvent() 方法会向一个指定的事件目标派发一个 Event,并以合适的顺序(同步地)调用所有受影响的 EventListener。标准事件处理规则(包括事件捕获和可选的冒泡过程)同样适用于通过手动使用 dispatchEvent() 方法派发的事件。icon-default.png?t=N7T8https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/dispatchEvent

2.1.7 CustomEvent

CustomEvent 是用来创建自定义事件的构造函数,它可以创建一个自定义事件对象,该对象可以包含任意的数据,用于在 DOM 中传递信息(PS:所有传递的数据都要放在 details 对象里)

与原生事件不同,自定义事件可以自定义事件类型、是否冒泡、是否可以取消等信息

这里不做详细说明

CustomEvent - Web API 接口参考 | MDNCustomEvent 接口表示由程序出于某个目的而创建的事件。icon-default.png?t=N7T8https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent

2.2 实践出真知

2.2.1 初步实现按钮

我们先来搭建一个最基本的按钮,基本步骤如下:

  • 创建 customElements template 模板 —— LC_BUTTON_CONTENT,该模板包含样式、结构
  • 新建一个继承自 HTMLElement 的类 —— LcButton,此类会被用于创建 customElements
  • 在 LcButton 中,动态创建 template 标签,并填充内容 LC_BUTTON_CONTENT
  • 通过 Element.attachShadow() 获取 ShadowRoot 对象,通过它把 Shadow DOM 追加到 template 上
  • 在页面中像使用 div 一样,使用 lc-button 标签
    <script>
      // 模板内容
      const LC_BUTTON_CONTENT = `
      <style>
        .button-container {
          /* background: yellow; */
        }

        .button-inner {
          display: inline-block;
          padding: 12px 20px;
          background-color: red;
          border-radius: 4px;
          border: none;
          font-size: 14px;
          color: #fff;
          cursor: pointer;
        }
      </style>

      <section class="button-container">
        <div class="button-inner">自定义元素做的按钮</div>
      </section>
    `;

      class LcButton extends HTMLElement {
        constructor() {
          super();
          // 创建 template
          const templateDOM = document.createElement("template");
          // 填充模板内容
          templateDOM.innerHTML = LC_BUTTON_CONTENT;

          // 通过 Element.attachShadow() 获取 ShadowRoot 对象
          // open shadow root:元素可以从 js 外部访问根节点
          // closed 拒绝从 js 外部访问关闭的 shadow root 节点
          const shadowRoot = this.attachShadow({ mode: "open" });
          // 把 Shadow DOM 附加到 template 上
          shadowRoot.appendChild(templateDOM.content.cloneNode(true));
        }
      }

      // 创建 Custom Elements(自定义元素)
      window.customElements.define("lc-button", LcButton);
    </script>

效果展示:

2.2.2 给按钮加属性-传递

该如何理解 属性 呢?

  • 类似于 vue 中的 props
  • 又或者原生 img 标签中的 src 属性
  • Custom Elements 内部也需要 监听 用户传入的属性

监听属性变化,基本步骤如下:

  • 定义 get observedAttributes() 监听属性变化 —— 比如 text
  • 定义 attributeChangedCallback,添加属性变化时进行的操作 —— 比如给 shadow dom 里的节点内容赋值
class LcButton extends HTMLElement {
  constructor() {
    super();
    // 创建 template
    const templateDOM = document.createElement("template");
    // 填充模板内容
    templateDOM.innerHTML = LC_BUTTON_CONTENT;

    // 通过 Element.attachShadow() 获取 ShadowRoot 对象
    // open shadow root:元素可以从 js 外部访问根节点
    // closed 拒绝从 js 外部访问关闭的 shadow root 节点
    const shadowRoot = this.attachShadow({ mode: "open" });
    // 把 Shadow DOM 附加到 template 上
    shadowRoot.appendChild(templateDOM.content.cloneNode(true));
    // 获取按钮文字 DOM
    this.btnTextDOM = shadowRoot.querySelector(".button-inner");
  }

  render() {
    // 修改文字内容
    this.btnTextDOM.innerHTML = this.text;
  }

  // 如果需要在元素属性变化后,触发 attributeChangedCallback 回调函数,我们必须监听这个属性
  // 通过定义 observedAttributes() 来实现监听属性变化
  static get observedAttributes() {
    return ["text"];
  }

  // 当自定义元素的 属性 变化(增加、移除、更改)时调用
  attributeChangedCallback(name, oldVal, newVal) {
    this[name] = newVal;
    this.render();
  }
}

效果展示:

2.2.3 给按钮加属性-映射

传递属性,使用的方式是

<lc-button text="给按钮加属性"></lc-button>

映射属性,使用的方式是

const element = document.querySelector("lc-button");
element.text = "给按钮加属性-映射";

所谓传递属性,就是通过 attributeChangedCallback 来监听用户传入的 text,监听到变化后,开发者需要手动给 this.text 赋值

        // 当自定义元素的 属性 变化(增加、移除、更改)时调用
        attributeChangedCallback(name, oldVal, newVal) {
          this[name] = newVal;
          this.render();
        }

所谓映射属性,就是用户每次修改 text,都会自动通过 get()/set() 函数来 获取/设置 this.text,开发者不需要手动给 this.text 赋值了

        get text() {
          return this.getAttribute("text");
        }

        set text(value) {
          this.setAttribute("text", value);
        }

        // 当自定义元素的 属性 变化(增加、移除、更改)时调用
        attributeChangedCallback(name, oldVal, newVal) {
          // this[name] = newVal;
          this.render();
        }

如果强行赋值,会导致栈溢出 因为:

  • 在 attributeChangedCallback 中的语句 this.text = newVal 会自动调用 get()/set() 函数
  • get()/set() 函数函数执行会导致 this.text 发生变化
  • this.text 发生变化又会让 attributeChangedCallback 被调用
  • 如此循环往复

2.2.4 给按钮加事件-普通事件

在自定义元素 内部 加事件监听,并把自定义元素 内部值 抛出去

      class LcButton extends HTMLElement {
        constructor() {
          // 获取按钮文字 DOM
          this.btnTextDOM = shadowRoot.querySelector(".button-inner");
          // 在自定义组件 内部 添加事件
          this.btnTextDOM.addEventListener("click", () => {
            console.log("在自定义组件 内部 添加事件");
            // 把内部值传出去
            this.onClick("把内部值传出去", 666);
          });
        }
      }

在自定义元素 外部 加事件监听,并接收自定义组件 内部传出来的值

      const element = document.querySelector("lc-button");

      // 在自定义组件 外部 添加事件
      element.addEventListener("click", () => {
        console.log("在自定义组件 外部 添加事件");
      });

      // 接收自定义组件 内部 传出来的值
      element.onClick = (value1, value2) => {
        console.log('接收内部传出来的值', value1, value2);
      };

2.2.5 给按钮加事件-自定义事件

所谓自定义事件,就是自定义组件内部开发者定义的、格式为 onXXX 格式的事件,在自定义组件外部可以通过 addEventListener 监听到

举个例子,下面的 onCustomClick 就是自定义事件

// 接收 自定义组件 内部的 自定义事件 传出来的值
element.addEventListener("onCustomClick", (val) => {
  console.log("接收 自定义组件 内部的 自定义事件 传出来的值\n", val);
});

在自定义组件内部,可以通过 dispatchEvent() 和 new CustomEvent() 来添加自定义事件

这里为了方便触发,我规定了在点击时,发出这个 onCustomClick 事件

// 在自定义组件 内部 添加事件
this.btnTextDOM.addEventListener("click", () => {
  // dispatchEvent() 方法会向一个指定的 事件目标 派发一个 Event
  this.dispatchEvent(
    // new CustomEvent() 方法创建一个新的 CustomEvent 对象
    new CustomEvent("onCustomClick", {
      // 需要把想要传递的参数包裹在一个包含detail属性的对象,否则传递的参数不会被挂载
      detail: {
        keysone: "onCustomClick 自定义事件抛出的值",
        keystwo: Number(new Date()),
      },
    })
  );
});

效果如下:

通过上面一系列精彩的操作,可以看出手写一个 Web Component 组件,还是比较费劲的

但没关系,后面会介绍更简单的方式,开发 Wep Components 组件,敬请期待!

;