使用示例介绍Web组件

246 阅读5分钟

我将通过实现选项卡面板来演示编写一个 Web 组件。最终的选项卡将如下所示。您可以在这个代码仓库中找到源代码。

图片

Web 组件是内置在浏览器中的一项标准。在撰写本文时,几乎所有主流浏览器都支持这一功能。然而,Web 组件往往被像 React 和 Angular 这样的流行单页应用框架所掩盖,导致其地位被低估。我认为这一功能被低估是因为 Web 组件(WC)早于 React 出现,并且不需要导入任何外部库。足够了解历史,现在让我们看看如何编写一个组件。

创建一个 Web 组件需要两个步骤:

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

就是这样!一个 Web 组件已经可以使用了。在注册 Web 组件时,名称必须包含一个连字符,这就是为什么它是 wc-tab 而不是 wctab。这个名称是使用该 Web 组件所必需的。我们只需创建一个与名称相同的标签来使用它,如下所示:

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

在浏览器中打开 HTML 文件时,什么也不会显示。目前来说,它与空的 <div> 元素并无区别。让我们在开标签和闭标签之间添加一些内容。

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

这实际上在浏览器中显示了 "Hello, World!"!

Shadow Root

在 Web 组件中,您几乎总是应该启用 Shadow Root。Shadow Root 提供了一个具有 Web 组件作为其根元素的作用域 DOM 树。这使我们能够导入 CSS 样式而不会污染全局范围。也就是说,我们可以使用 CSS 样式表,并且这些样式只会在该自定义元素内部生效。任何与自定义组件外部的匹配 CSS 选择器的标签都不受影响。可以在我们的构造函数中启用此功能,如下所示:

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

通过在构造函数中调用 attachShadow({ mode: 'open' }),我们创建了一个开放式的 Shadow Root。现在,我们可以在 Shadow Root 中添加元素和样式,而不会对外部文档产生影响

一旦进行了这个更改,浏览器中打印的 "Hello, World!" 就消失了。当附加了 Shadow DOM 后,它会替换我们现有的子元素。Web 组件有一些生命周期回调函数,其中之一是 connectedCallback。它在 Web 组件附加到 DOM 时立即被调用。让我们添加它!

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

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

Tab - Example

让我们定义一下选项卡组件的设计。我们的 Web 组件将把每个选项卡作为一个 <div> 元素来定义。该 Web 组件应该按照以下方式定义选项卡及其内容:

 <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 方法来生成界面。

 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
 }

在上述代码中,我们添加了 connectedCallback 回调函数,并在其中调用了 render 方法。render 方法用于在 Shadow Root 中渲染组件的内容。

现在我们将渲染逻辑从生命周期方法中分离出来,让我们编写界面的代码

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);
 }

我们将组件的内容放在了 render 方法中,并将其设置为 Shadow Root 的 innerHTML。这样,当组件附加到 DOM 时,connectedCallback 会被调用并触发内容的渲染。

注意,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");
 }

这个方法通过为选项卡添加一个 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())

非常好,选项卡组件已经准备就绪。还有一些在此示例中未使用到的概念值得一提,包括自定义属性、模板和插槽。根据组件的需求使用适当的功能即可。

完整代码:github.com/rasvi24/wc-…

代码演示:codepen.io/f2er/pen/Ex…