Template 与 Shadow DOM

深入理解 HTML template 元素与 Shadow DOM:声明式 Shadow DOM、shadowroot 属性、Web Components 基础。

Template 与 Shadow DOM

一、template 元素

1.1 基本概念

<template> 元素是一种机制,用于保存客户端模板。这些模板不会立即渲染,但可以通过 JavaScript 动态实例化。与直接操作 DOM 相比,template 提供了一种更清晰、更安全的方式来创建可复用的结构。

<template id="card-template">
  <div class="card">
    <h2></h2>
    <p></p>
  </div>
</template>
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);

// 填充数据
clone.querySelector('h2').textContent = 'Card Title';
clone.querySelector('p').textContent = 'Card content';

document.body.appendChild(clone);

1.2 template 的特性

  • 惰性:内容不会渲染,不会执行脚本
  • 可克隆template.content 是 DocumentFragment,便于复制
  • 任意位置template 可以放在 <head><body><tbody>
<!-- 在 head 中定义模板 -->
<head>
  <template id="t">
    <style> p { color: blue; } </style>
    <p>Template in head</p>
  </template>
</head>

二、Shadow DOM

2.1 什么是 Shadow DOM

Shadow DOM 是一种封装机制,允许在 DOM 树中创建独立的子树,具有独立的样式和结构。这与 iframe 的隔离类似,但更轻量。

const host = document.querySelector('#host');
const shadow = host.attachShadow({ mode: 'open' });

shadow.innerHTML = `
  <style>
    p { background: lightblue; }
  </style>
  <p>Inside Shadow DOM</p>
`;

2.2 声明式 Shadow DOM

HTML 支持直接通过 <template> 创建 Shadow Root,无需 JavaScript:

<article>
  <template shadowrootmode="open">
    <style>
      p { padding: 8px; background-color: plum; }
    </style>
    <p>I'm in the shadow DOM.</p>
  </template>
</article>

2.3 shadowroot 属性详解

属性 作用
shadowrootmode 创建 shadow root(openclosed
shadowrootclonable 克隆时包含 shadow root
shadowrootdelegatesfocus 委托焦点到第一个可聚焦元素
shadowrootserializable 允许序列化 shadow root
shadowrootcustomelementregistry 附加自定义元素注册表
<!-- 带焦点委托的声明式 Shadow DOM -->
<template shadowrootmode="open" shadowrootdelegatesfocus>
  <button>Focusable Button</button>
</template>

三、样式封装

3.1 Shadow DOM 的样式隔离

Shadow DOM 内的样式默认不会泄漏到外部,外部样式也默认不会影响内部:

const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
  <style>
    /* 这个 p 样式只影响 shadow 内部 */
    p { color: red; }
  </style>
  <p>Red text inside shadow</p>
`;

// 全局样式不会渗透进来
document.body.innerHTML = '<p>Black text</p>';

3.2 ::slotted 伪元素

外部提供的元素可以通过 ::slotted 伪元素选择器匹配:

<!-- Shadow Host -->
<my-component>
  <div slot="content">Hello</div>
</my-component>
// Shadow DOM 内部
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
  <style>
    /* 匹配 slot 传入的元素 */
    ::slotted(div) {
      color: blue;
      font-weight: bold;
    }
  </style>
  <slot name="content"></slot>
`;

3.3 :host 伪类

:host 允许从 Shadow DOM 内部设置宿主元素的样式:

<template id="card-template">
  <style>
    :host {
      display: block;
      border: 1px solid #ccc;
      padding: 16px;
    }
    :host([highlighted]) {
      border-color: gold;
      background: #fffef0;
    }
  </style>
  <slot></slot>
</template>

四、实际应用

4.1 可复用卡片组件

<template id="article-card">
  <style>
    :host {
      display: flex;
      flex-direction: column;
      border: 1px solid #ddd;
      border-radius: 8px;
      overflow: hidden;
    }
    .thumbnail {
      width: 100%;
      height: 160px;
      object-fit: cover;
    }
    .content { padding: 16px; }
    .title { margin: 0 0 8px; font-size: 1.25rem; }
    .desc { color: #666; margin: 0; }
  </style>
  <img class="thumbnail" />
  <div class="content">
    <h3 class="title"></h3>
    <p class="desc"></p>
  </div>
</template>

<script>
class ArticleCard extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('article-card');
    this.attachShadow({ mode: 'open' })
        .appendChild(template.content.cloneNode(true));
  }

  static get observedAttributes() {
    return ['title', 'description', 'image'];
  }

  attributeChangedCallback(name, old, val) {
    const el = this.shadowRoot;
    if (name === 'title') el.querySelector('.title').textContent = val;
    if (name === 'description') el.querySelector('.desc').textContent = val;
    if (name === 'image') el.querySelector('.thumbnail').src = val;
  }
}
customElements.define('article-card', ArticleCard);
</script>

<!-- 使用 -->
<article-card
  title="Understanding Shadow DOM"
  description="A deep dive into Shadow DOM"
  image="/thumbnails/shadow-dom.jpg">
</article-card>

4.2 列表渲染

<template id="list-view">
  <style>
    :host { display: block; }
    ul { list-style: none; padding: 0; margin: 0; }
    ::slotted(li) { padding: 8px; border-bottom: 1px solid #eee; }
  </style>
  <ul>
    <slot></slot>
  </ul>
</template>

<list-view>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</list-view>

五、面试高频问题

Q: template 和普通 HTML 片段有什么区别?

回答要点:普通 HTML 片段会被解析并渲染,<template> 内的内容不会立即解析也不会渲染,直到被 JavaScript 获取和克隆。template 是一种惰性的、用于脚本的结构化片段。

Q: Shadow DOM 和 iframe 的区别是什么?

回答要点:Shadow DOM 是轻量级的 DOM 级别隔离,样式和结构独立但不创建新文档;iframe 创建独立的文档和浏览上下文,隔离更彻底但更重量。Shadow DOM 适合组件级封装,iframe 适合完整应用隔离。

Q: 声明式 Shadow DOM 有什么优势?

回答要点:声明式 Shadow DOM 允许在 HTML 中直接定义 Shadow Tree,无需 JavaScript。这对于 SEO、首次渲染性能和渐进增强都有帮助。但需要注意浏览器解析规则——多个声明式 shadow root 只第一个生效。


参考资料

延展阅读