Web Component介绍和示例

3,304 阅读4分钟

通过实现选项卡式面板来编写 Web 组件。完成的选项卡如下所示。
Web Component 是浏览器内置的标准,每个主要浏览器都支持此功能。这是一个被低估的功能。

完成一个Web Component需要两个步骤:

  1. 扩展 HTMLElement 的类。
  2. 将组件注册为自定义元素。
<!DOCTYPE html>
<html>
<head>
  <script>
    class WCTab extends HTMLElement { } //Step 1
    customElements.define("wc-tab", WCTab) //Step 2
  </script>
</head>
</html>

就是这样。Web组件已可供使用。在注册WC时,名称必须始终包含一个连字符,这就是wc-tab而不是wctab的原因。该名称是使用此WC所需的名称。我们可以使用它来创建一个具有相同名称的标签,如下所示。

<body>
  <wc-tab></wc-tab>
</body>

在浏览器中打开 html 不会显示任何内容。此时它并不比一个空的 div 好多少。让我们在开始和结束标记之间写一些东西。

<wc-tab>
  <p>Hello world!</p>
</wc-tab>

这实际上打印了 Hello world! 在浏览器中!

Shadow Root

您几乎总是应该在 WC 中启用Shadow Root。Shadow Root提供作用域 DOM 树,并将 Web 组件作为其根元素。这使我们能够导入 css 样式,而不会污染全局范围。这意味着我们可以使用 css 样式表,并且这些样式将仅应用于此自定义元素。自定义组件外部具有匹配 css 选择器的任何标记都不受影响。这可以在我们的构造函数中启用,如下所示。

class WCTab extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
  }
}

一旦进行此更改,浏览器中打印的 hello world 就消失了。当附加 Shadow DOM 时,它会替换我们现有的子元素。WC 的生命周期回调很少,其中之一是connectedCallback. 一旦 WC 附加到 dom,就会调用它。让我们添加它吧!

class WCTab extends HTMLElement {
  constructor() {
      super();
      this.shadow = this.attachShadow({ mode: "open" });
  }
  connectedCallback(){
    console.log("connected!");
  }
}

connected!当页面刷新时,这会在控制台中打印。

Tab - Example

让我们定义我们的选项卡组件将如何设计。我们的 WC 将每个选项卡作为 div。WC 应定义选项卡及其内容,如下所示。

<wc-tab>
  <div name="Tab 1">Tab 1 content</div>
  <div name="Tab 2">Tab 2 content</div>
  <div name="Tab 3">Tab 3 content</div>
</wc-tab>

我们将读取提供的子项作为输入,并生成一个 UI 将它们显示为选项卡。可以将每个选项卡作为其自己的自定义元素而不是 div 标签。在这个例子中我们将继续使用 div。让我们看看如何访问组件中的子组件。我们将在生命周期方法中执行此操作connectedCallback

connectedCallback(){
  let tabs = this.querySelectorAll("div");
  console.log(tabs);
}

不幸的是这不起作用。connectedCallback在子级附加到 DOM 之前调用。一旦它们被附加,就没有简单的方法来阅读它们。我们一起去MutationObserver。这会观察子级的变化并调用给定的回调。

connectedCallback() {
  let thisNode = this;
  let observer = new MutationObserver(function () {
    let tabs = thisNode.querySelectorAll("div");
    console.log(tabs);
  });
  
  // We are only interested in the children of
  // this component
  observer.observe(this, { childList: true });
}

现在打印出来了NodeList(3) [div, div, div]。这三个 div 是我们需要工作的三个选项卡。让我们添加一个 render 方法来生成 UI。

connectedCallback() {
  let thisNode = this;
  let observer = new MutationObserver(function () {
    thisNode.render();
  });

  // We are only interested in the children of
  // this component
  observer.observe(this, { childList: true });
}
render() {
  let tabs = this.querySelectorAll("div");
  // Generate UI
}

现在我们将渲染逻辑与生命周期方法分离,让我们来编写 UI。

render() {
  // Fetch the children as input
  let tabs = this.querySelectorAll("div");

  // Define basic structure
  this.shadowRoot.innerHTML = `
  <div class='tab-btn-container'></div>
  <div class='tab-panel-container'></div>
  `;
  let btnContainer = this.shadowRoot.querySelector(".tab-btn-container");
  let panelContainer = this.shadowRoot.querySelector(".tab-panel-container");

  for (let index = 0; index < tabs.length; index++) {
    let currentTab = tabs[index];
    this.addTab(currentTab, btnContainer, panelContainer)
  }
}

/**
* @param {HTMLElement} tab
* @param {HTMLElement} btnContainer
* @param {HTMLElement} panelContainer
*/
addTab(tab, btnContainer, panelContainer) {
  let tabBtn = document.createElement("button");
  let clonedTab = tab.cloneNode(true);
  let thisNode = this;
  let tabName = tab.getAttribute("name");
  tabBtn.textContent = tabName;
  tabBtn.setAttribute("name", tabName);
  btnContainer.appendChild(tabBtn);
  panelContainer.appendChild(clonedTab);
}

注意this.shadowRoot用于访问shadow DOM。它适用于所有自定义组件。

接下来,我们实现选择状态。任何时候只有一个选项卡处于活动状态。让我们添加一个方法来将选项卡标记为活动状态。

/**
* @param {String} tabName
*/
activate(tabName) {
  // Deactivate previously active tab if any
  let activeBtn = this.shadowRoot.querySelector(".tab-btn-container > button.active");
  if (activeBtn !== null) {
    activeBtn.classList.remove("active");
  }
  let activeTab = this.shadowRoot.querySelector(".tab-panel-container > div.active");
  if (activeTab !== null) {
    activeTab.classList.remove("active");
  }

  // Mark provided tab as active
  this.shadowRoot
    .querySelector(`.tab-btn-container > button[name='${tabName}']`)
    .classList.add("active");

  this.shadowRoot
    .querySelector(`.tab-panel-container > div[name='${tabName}']`)
    .classList.add("active");
}

此方法通过向其中添加活动类来激活选项卡。单击选项卡按钮时必须触发此操作。这是按如下方式完成的。

tabBtn.addEventListener("click", function () {
  thisNode.activate(tabName);
})

现在我们与组件进行了交互,让我们设计它的样式。Shadow DOM 没有 head 标签,因此我们可以直接在 ShadowRoot 中附加样式标签或带有样式表的链接标签。

generateStyle() {
  let style = document.createElement("style");
  style.textContent =
    `
  *{
    background-color: #13005A;
    color: white;
    font-size: 2rem;
    font-family: sans-serif;
  }
  .tab-panel-container{
    padding: 8px;
  }
  .tab-btn-container{
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
  }
  .tab-panel-container > div {
    display: none;
  }
  .tab-panel-container > div.active{
    display: block;
  }
  .tab-btn-container{
    display: flex;
    gap: 8px;
  }
  .tab-btn-container > button{
    background-color: #4e6183;
    border: none;
    outline: none;
    color: white;
    padding: 4px 8px;
    border-radius: 8px;
    cursor: pointer;
  }
  .tab-btn-container > button.active{
    background-color: #03C988;
  }
  `;
  return style;
}

样式的附加方式与任何其他元素相同。

this.shadowRoot.appendChild(this.generateStyle())

就是这样。选项卡组件已准备就绪。这里没有使用一些值得一提的概念,包括自定义属性、模板和插槽。仅使用组件所需的任何内容。