我将通过实现选项卡面板来演示编写一个 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())
非常好,选项卡组件已经准备就绪。还有一些在此示例中未使用到的概念值得一提,包括自定义属性、模板和插槽。根据组件的需求使用适当的功能即可。