Web 组件和 Shadow DOM

如果你曾经使用过 React,你会很熟悉 Web 组件。它们是自定义的、可复制的 HTML 片段,可以在代码的其他地方引用。Web 组件是它们自己的 HTML 规范,因此您可能会惊讶于它们可以与纯 Javascript 和 HTML 一起独立使用。让我们来看看如何做到这一点。

想象一下,我们有一个状态栏项目,我们可以在多个地方重复使用它。它的结构可能如下所示:

<div class="status-bar">
    <div class="size">
        <div class="small">-</div>
        <div class="big">+</div>
    </div>
    <div class="status-bar-progress">
        <div class="progress">
            <div class="progress-percentage"></div>
        </div>
    </div>
</div>

想象一下,必须将它粘贴到多个位置并对其进行维护,以使其看起来始终相同——如果有一天我们更改状态栏会怎样?我们需要返回所有出现状态栏的地方,并更新它们!Web 组件允许你在多个地方引用这段 HTML,并给它一个唯一的标签,即:

<status-bar></status-bar>

我们如何做到这一点?让我们看看如何制作自己的 Web 组件,以及它是多么容易。

步骤 1. Javascript#

是的,你猜对了,如果你想制作 web 组件,你需要使用 Javascript。让我们快速看一下如何创建一个非常简单的 DOM 元素,我们称之为“段落paragraph”。

class Paragraph extends HTMLElement {
  constructor() {
    super()
    this.innerHTML = '<p>Hello</p>'
  }
}

// define adds a custom element "alpha-paragraph" using the rules defined in the "Paragraph" class
customElements.define('alpha-paragraph', Paragraph);
<alpha-paragraph></alpha-paragraph>

其输出将是一个段落,其中包含“Hello”一词。Web 组件依赖于类结构。我们constructor()在这里使用来指示标签加载时会发生什么。

其他功能

除了构造函数,我们还可以将其他函数添加到一个类connectedCallback()中,例如当 DOM 元素附加到页面attributeChangedCallback()时触发,当元素的属性发生变化时触发,给我们一些灵活性:

class Paragraph extends HTMLElement {
  constructor() {
    super()
    this.innerHTML = '<p>Hello</p>'
  }
  connectedCallback() {
      // ...
  }
  attributeChangedCallback() {
      // ...
  }
}
customElements.define('alpha-paragraph', Paragraph);

步骤 2. 扩展功能#

现在我们有一种方法可以创建一个简单的元素,将回调事件附加到它,并将事件附加到属性更改,让我们考虑 CSS。在制作独立且可克隆的 Web 组件时,我们希望它在多个地方的物理外观相同。为此,它需要自己的自定义 CSS。我们如何将 CSS 添加到 Web 组件中?

为此,我们需要使用一种叫做shadow DOM的东西。这些本质上是仅附加到一个特定项目的项目。我们可以通过启用影子 DOM 并将我们的 CSS 附加到它来将其用于 Web 组件。

具有特定 CSS 的自定义元素

class Paragraph extends HTMLElement {
  constructor() {
    super()
    // Attach shadow DOM
    let shadow = this.attachShadow({mode: 'open'});
	
  	// Append our Paragraph
    shadow.innerHTML = '<p>Hello</p>'
    
    // Add in our CSS
    let style = document.createElement('style');
    let elCss = style.textContent = `
    	p {
      	color: red;
        font-size: 1.25rem;
      }
    `;
    // Append our CSS
    shadow.appendChild(style);
  }
}

customElements.define('alpha-paragraph', Paragraph);

在上面的示例中,任何alpha-paragraph元素都会有一个段落,其red字体大小为1.25rem.

外面的所有段落alpha-paragraph都不会是红色的,因此您可以独立设置其他 CSS 样式。

Step 3. 结合模板标签#

我们可以使用 HTML 标签代替将我们的 Web 组件硬编码到 Javascript 中。HTML 有两个我们可以在这里使用的标签,templateslot.

模板标签是指整个 Web 组件,而插槽是该模板的一小部分,可以更改。模板标签的示例如下所示:

<template id="alphaParagraph">
    <slot name="paragraph-text"><p>Default Text</p></slot>
    <p>Another, unchangeable paragraph</p>
</template>

第 2 行的插槽是指我们可以更改的内容。在我看来,我们真的不需要使用模板标签,但插槽很有用。我们可以alpha-paragraph通过更改 innerHTML 来更新我们的插槽:

class Paragraph extends HTMLElement {
  constructor() {
    super()
    // Attach shadow DOM
    let shadow = this.attachShadow({mode: 'open'});
	
  	// Append our Paragraph    
    shadow.innerHTML = `
        <slot name="paragraph-text"><p>Default Text</p></slot>
        <p>Another, unchangeable paragraph</p>
    `;

    // Add in our CSS
    let style = document.createElement('style');
    let elCss = style.textContent = `
    	p {
      	color: red;
        font-size: 1.25rem;
      }
    `;
    // Append our CSS
    shadow.appendChild(style);
  }
}

customElements.define('alpha-paragraph', Paragraph);

然后我们可以使用 HTML 中的 ‘slot’ 属性将插槽替换为我们自己的自定义元素。在下面的示例中,整个槽被替换为包含文本“自定义文本”的段落元素。

<alpha-paragraph color="blue">
  <p slot="paragraph-text">
    Custom Text
  </p>
</alpha-paragraph>

步骤 4. 属性#

由于所有 HTML 元素都可以具有属性,因此我们可以使用该getAttribute函数访问它们。因此,我们可以轻松地重写我们的代码以获得自定义颜色:

class Paragraph extends HTMLElement {
  constructor() {
    super()
    // Attach shadow DOM
    let shadow = this.attachShadow({mode: 'open'});
	
  	// Append our Paragraph    
    shadow.innerHTML = `
        <slot name="paragraph-text"><p>Default Text</p></slot>
        <p>Another, unchangeable paragraph</p>
    `;

    // Our custom color
    let color = 'red';
    if(this.getAttribute('color') !== null) {
      color = this.getAttribute('color');
    }

    // Add in our CSS
    let style = document.createElement('style');
    let elCss = style.textContent = `
    	p {
      	color: ${color};
        font-size: 1.25rem;
      }
    `;
    // Append our CSS
    shadow.appendChild(style);
  }
}

customElements.define('alpha-paragraph', Paragraph);
<alpha-paragraph color="blue">
  <p slot="paragraph-text">
    Custom Text
  </p>
</alpha-paragraph>

通过将其全部放入我们的 Javascript 中,我们确保我们可以轻松地将这个 Web 组件导入其他地方。下面是我们最终 Web 组件的演示:

<alpha-paragraph>
  <p slot="paragraph-text">
    Custom Text
  </p>
</alpha-paragraph>
p {
  color: blue;
}
class Paragraph extends HTMLElement {
  constructor() {
    super()
    // Attach shadow DOM
    let shadow = this.attachShadow({
      mode: 'open'
    });

    // Append our Paragraph    
    shadow.innerHTML = `
        <slot name="paragraph-text"><p>Default Text</p></slot>
        <p>Another, unchangeable paragraph</p>
    `;

    // Add in our CSS
    let style = document.createElement('style');
    let elCss = style.textContent = `
    	p {
      	color: red;
        font-size: 1.25rem;
      }
    `;
    // Append our CSS
    shadow.appendChild(style);
  }
}

customElements.define('alpha-paragraph', Paragraph);