一文让你直接上手webComponents

251 阅读3分钟

前言

看完本文你会得到:

  • 什么是webComponents
  • webComponents slot 传递
  • webComponents 样式隔离
  • webComponents 属性监听
  • webComponents 事件冒泡传递

基本概念

  Web Components是一种用于构建可重用、独立且封装的前端组件的技术。它由一组Web标准组成,包括自定义元素、影子DOM和HTML模板等。通过使用Web Components,开发人员可以创建具有自定义功能和样式的HTML元素,并将其封装为独立的组件,以便在不同的项目中进行重复使用。

废话不多说!让我们直接走起~

基础目录

.
├── example
│   ├── bigbang.js
│   ├── index.html
│   ├── main.css
│   ├── main.js
│   └── package.json

初始化组件

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件初始化</title>
    <script src="./bigbang.js" type="module"></script>
  </head>
  <body>
    <big-bang></big-bang>
  </body>
</html>

bigbang.js


class BigBang extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "closed" });
    let div = document.createElement("div");
    div.textContent = "Hello, BigBang!";
    shadowRoot.append(div)
  }
}

customElements.define("big-bang", BigBang);

预览

slot 传递

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件初始化</title>
    <script src="./bigbang.js" type="module"></script>
  </head>
  <body>
    <big-bang>
      <h2 slot="title">我是标题</h2>
      <ul slot="list">
          <li>测试1</li>
          <li>测试2</li>
          <li>测试3</li>
      </ul>
    </big-bang>
  </body>
</html>

bigbang.js

const template = document.createElement("template");

template.innerHTML = `
  <div>
    <h1>hello 我是测试</h1>
    <slot name="title">我是默认的插槽【标题】</slot>
    <slot name="list">我是默认的插槽【列表】</slot>
  </div>
`;

class BigBang extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "closed" });
    let clone = template.content.cloneNode(true);
    shadowRoot.append(clone);
  }
}
customElements.define("big-bang", BigBang);

预览

详细代码参考

绑定样式

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件初始化</title>
    <script src="./bigbang.js" type="module"></script>
  </head>
  <body>
    <big-bang>
      <h2 slot="title">我是标题</h2>
      <ul slot="list">
          <li>测试1</li>
          <li>测试2</li>
          <li>测试3</li>
      </ul>
    </big-bang>
  </body>
</html>

main.css

* {
    box-sizing: border-box;
    padding: 0;
    margin: 0;
  }
  html {
    font-size: 20px;
    color-scheme: dark light;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
      Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  }
  header,
  main {
    padding: 1rem 4rem;
  }
  h2 {
    color: cornflowerblue;
  }
  h3 {
    border-bottom: 2px solid white;
  }
  
  div {
    border: 1px solid #000;
  }
  
  /* 对外控制组件中的样式 */
  ::part(demo) {
    color: red;
  }
  

Tips:不能指定复合选择器 = > ::part(demo) span

bigbang.css

.big-bang-box {
    border: 1px solid blue;
}

bigbang.js


const template = document.createElement("template");
template.innerHTML = `
  <style>
    @import url('./bigbang.css'); 

    .big-bang-box {
      border: 1px solid red; 
      padding:3rem;
      margin:3rem;
    }

    .big-bang-box h1 {
      color: #fff;
    }
    :host-context(main) {
      background-color: red;
    }
    
    :host(big-bang) {
      background-color: #d698dd;
      display: block;
    }
    /* shadow 专门的选择器 */
    :host {
      background-color: aliceblue;
      display: block;
    }

    /* ::slotted 选择器用于slot。 */
    ::slotted(h2)  {
      font-size: 4rem;
      background-color: #322533;
      color: #fff !important;
    }

    /* 不能这样用 */
    slot {

    }
  </style>
  <div class='big-bang-box'>
    <h1 part="demo">我是Web Components <span>111</span> </h1>
    <slot name="title">我是默认的插槽【标题】</slot>
  </div>
`;

class BigBang extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "closed" });
    let clone = template.content.cloneNode(true);
    shadowRoot.append(clone);
  }
}

customElements.define("big-bang", BigBang);

预览

  样式的控制可以在当前自定义的组件里面声明<style>标签控制当前的样式,类似于vue组件中的scoped,如果想从外面改变当前组件element元素的样式可以使用::part,但是不支持复合元素,ShadowDOM 提供了当前组件专有的选择器:host:host-context进行组件的使用;

:host选择器:

  它用于选择Shadow DOM树的根元素,即Shadow Root的宿主元素。可以将:host理解为表示当前Shadow DOM树的根节点。通过:host选择器,我们可以对整个Shadow DOM树的根节点应用样式。 示例:

:host {
  background-color: red;
}

:host-context选择器:

  它用于选择Shadow DOM树的宿主元素及其所有祖先元素,只要这些祖先元素满足指定的条件。:host-context可以看作是从Shadow DOM树中向外进行选择的操作。 示例:

:host-context(.container) {
  background-color: blue;
}

注意:

  • :host 选择器是 Shadow 专门的选择器,用于选择 Shadow DOM 的根元素。
  • host的优先级如下: :host < :host(big-bang) || :host-context(main)
  • :host(big-bang) || :host-context(main) 取决于谁写在后面,谁的优先级高

参考资料:

详细代码参考

绑定属性

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>属性监听</title>
    <link rel="stylesheet" href="./main.css" />
    <script src="./bigbang.js" type="module"></script>
    <script src="./main.js" type="module"></script>
  </head>
  <body>
    <big-bang character="Leonard" color="cornflowerblue">
      <h2 slot="title">加载标题</h2>
    </big-bang>
  </body>
</html>

bigbang.js

const template = document.createElement("template");
template.innerHTML = `
    <style>
      :host{
        /* the shadow root */
        background-color: #333; /* default */
        color: white;
        display: block; /* critical */
      }
      ::slotted(h2){
        /* represents an h2 element that has been placed into a slot */
        font-weight: 300;
      }
      .root{
        position: relative;
        padding: 2rem;
      }
      .character{
        position: absolute;
        z-index: 10;
        top: -10rem;
        left: 0;
        font-size: 10rem;
        line-height:1;
        color: hsla(60, 50%, 80%, 0.32);
      }
    </style>
    <div class="root">
      <h1>Big Bang Theory</h1>
      <slot name="title">我是默认的插槽【标题】</slot>
    </div>
`;

class BigBang extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({ mode: "closed" });
    let clone = template.content.cloneNode(true);
    this.root.append(clone);
  }

  static get observedAttributes() {
    return ["character", "color"];
  }

  get character() {
    return this.getAttribute("character");
  }
  set character(value) {
    // 可以进行数据过滤处理~
    this.setAttribute("character", value);
  }

  get color() {
    return this.getAttribute("color");
  }
  set color(value) {
    // 可以进行数据过滤处理~
    this.setAttribute("color", value);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name.toLowerCase() === "character") {
      const div = this.root.querySelector(".root");

      let p = div.querySelector("p")
        ? div.querySelector("p")
        : document.createElement("p");
      p.className = "character";
      p.textContent = newValue;
      div.append(p);
    }
    if (name.toLowerCase() === "color") {
      this.style.backgroundColor = newValue;
    }
  }
}

customElements.define("big-bang", BigBang);
// <big-bang>

预览

  根据上面的代码 我们定义了charactercolor 两个属性,然后通过attributeChangedCallback 去监听,当前传入的状态,紧接着在最外层找到当前的组件big-bang 点击进行属性值切换就得到了以上的效果

参考资料:

详细代码参考

事件通信

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>事件通信</title>
    <link rel="stylesheet" href="./main.css" />
    <script src="./main.js"></script>
    <script src="./bigbang.js" type="module"></script>
  </head>
  <body>
    <main>
      <h3>传递参数如下:</h3>
      <h4></h4>
      <br/>
      <big-bang color="lightcoral" action="goodbye">
        <h2 slot="title">It all started with...</h2>
        <span slot="done">Ok</span>
      </big-bang>
    </main>
  </body>
</html>

bigbang.js

const template = document.createElement("template");
template.innerHTML = `
    <style>
      :host{
        /* the shadow root */
        background-color: #333; /* default */
        color: white;
        display: block; /* critical */
      }
      ::slotted(h2){
        /* represents an h2 element that has been placed into a slot */
        font-weight: 300;
      }
      .root{
        position: relative;
        padding: 2rem;
      }
      button{
        font-size: 1.2rem;
        border: none;
        background-color: #222;
        color: #eee;
        padding: 0.25rem 2rem;
        cursor: pointer;
      }
      button:active{
        background-color: #eee;
        color: #222;
      }
      
    </style>
    <div class="root">
      <h1>Big Bang Theory</h1>
      <slot name="title">我是默认的插槽【标题】</slot>
      <p>
        <button><slot name="done"></slot></button>
      </p>
    </div>
`;

class BigBang extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({ mode: "closed" });

    let clone = template.content.cloneNode(true);
    this.root.append(clone);

    let btnSlot = this.root.querySelector("slot[name=done]");
    let htmlSlot = btnSlot.assignedNodes()[0]; // 找到分配给Slot的元素

    if (htmlSlot) {
      btnSlot.addEventListener("slotchange", (ev) => {
        console.log(htmlSlot, "===>slotchange");
      });

      btnSlot.parentElement.addEventListener("click", (ev) => {
        let action =
          this.action && typeof window[this.action] === "function"
            ? window[this.action]
            : this.defaultActionForBigBangButton;

        action('我是传递的参数');
      });
    } else {
      btnSlot.parentElement.remove();
    }
  }

  defaultActionForBigBangButton() {
    console.log("Missing a VALID action attribute value");
  }

  // 加载
  connectedCallback() {
    console.log("added to page");
    if (this.color) {
      this.color = "red";
    }
  }

  // 卸载
  disconnectedCallback() {
    console.log("removed from page");
  }

  // Attributes and Properties...
  static get observedAttributes() {
    return ["color", "action"];
  }

  get color() {
    return this.getAttribute("color");
  }
  set color(value) {
    this.setAttribute("color", value);
  }

  get action() {
    return this.getAttribute("action");
  }
  set action(value) {
    this.setAttribute("action", value);
  }

  get customAttribute() {
    return this.getAttribute("customAttribute");
  }
  set customAttribute(value) {
    this.setAttribute("customAttribute", value);
  }

  attributeChangedCallback(attributeName, oldVal, newVal) {

    console.log(attributeName,'===>attributeName')
    if (attributeName.toLowerCase() === "color") {
      this.style.backgroundColor = newVal;
    }
  }
}

customElements.define("big-bang", BigBang);
// <big-bang>
  1. main.js

document.addEventListener('DOMContentLoaded', () => {
});

function hello(ev) {
  console.log(ev);
}
function goodbye(value) {
  let bb = document.querySelector('big-bang');
  let h4 = document.querySelector('h4');
  h4.textContent = value

  bb.remove()
}

预览

  通过执行big-bang组件的action属性执行了全局方法:goodbye 进行了数据通信,并且执行了整个组件的删除操作,也可以使用slotchange方法来自定义监听插槽变化,实现一些逻辑处理

参考资料:

详细代码参考

结语

  以上就是整个webComponents的演示,代码逻辑比较简单,感兴趣的小伙伴可以自行运行或者访问源码进行clone实现,如果整篇能帮助你的话记得点赞收藏哦~ 再见了 老baby们!