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(open 或 closed) |
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 只第一个生效。