template 和 slot 配合 Web Components
讲解 template 与 slot 在 Web Components 中的分工与配合:template 定义内部结构,slot 接收外部内容,并通过最小 user-card 示例说明插槽分配、样式隔离及 ::slotted() 的适用条件
读完这篇,你能从零写出一个用 <template> 定义内部结构、用 <slot> 接收外部内容的 Web Component,并且搞清楚插槽内容为什么不受组件内部样式影响、什么时候该用 ::slotted()。
第一步:先分清 template 和 slot 各自负责什么
这一步要建立的心智模型是:<template> 负责"组件长什么样",<slot> 负责"外部内容往哪儿塞"。
<template> 元素是一块惰性 HTML。浏览器会解析它,但内容不会渲染、图片不会加载、脚本不会执行。它的内容挂在 template.content 属性上,是一个 DocumentFragment。
<slot> 元素只在 Shadow DOM 里有效,它是个占位符,作用是告诉浏览器:这个位置显示宿主元素(light DOM)里的哪个子节点。
两者配合起来的流程是:<template> 里写好结构和 <slot> 位置 → 挂到 shadow root → light DOM 里的子元素被"分配"到对应插槽显示。
注意:
<slot>写在普通文档里是没用的,它不会显示任何东西,也不会把兄弟节点吸进去。只有在 Shadow DOM 树里才具备分发能力。
第二步:搭一个最小可运行的自定义元素
这一步要做出一个能跑起来的骨架,做完你能在页面里看到一个带边框的卡片。
新建 index.html,完整内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>user-card demo</title>
</head>
<body>
<template id="user-card-tpl">
<style>
:host {
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px 16px;
font-family: system-ui, sans-serif;
}
</style>
<div class="name"><slot name="name">匿名用户</slot></div>
<div class="body"><slot></slot></div>
</template>
<user-card>
<span slot="name">张三</span>
<p>前端工程师,主要写后台管理系统。</p>
<p>业余时间折腾浏览器 API。</p>
</user-card>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById('user-card-tpl');
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define('user-card', UserCard);
</script>
</body>
</html>
几个硬性规则:
- 自定义元素名必须包含连字符(
user-card、my-button),不能是usercard这种单词。 - 同一个名字只能
customElements.define一次,重复注册会抛NotSupportedError。 attachShadow({ mode: 'open' })让element.shadowRoot可访问;用'closed'则外部拿不到,调试会变麻烦,新手建议先用'open'。
注意:这里必须写
tpl.content.cloneNode(true),那个true是深拷贝。如果直接shadow.appendChild(tpl.content),节点会被从 template 里搬走,页面上第二个<user-card>就会变成空白。
第三步:理解插槽的分配规则
这一步要弄明白"哪些元素会进哪个插槽",做完你能准确预测页面上每个节点的归属。
规则只有两条:
- 带
slot="xxx"属性的子元素,进入name="xxx"的具名插槽。 - 没有
slot属性的子元素,进入没有name的默认插槽,按文档顺序排列。
在刚才的例子中:<span slot="name">张三</span> 进 <slot name="name">,两个 <p> 进 <slot>。所以第一行显示"张三",下面两行显示两段介绍。
注意:只有宿主元素的直接子元素才会被分配。写成
<user-card><div><span slot="name">张三</span></div></user-card>,那个 span 不会被分配,因为它的父节点是 div 而不是宿主。
注意:同一个
name可以对应多个元素,它们会按顺序全部进入那个插槽,不会报错也不会覆盖。
第四步:给插槽准备默认内容
这一步要解决"使用者什么都不传时页面不能开天窗"的问题,做完你能给每个插槽兜底。
写法就是在 <slot> 标签中间放内容:<slot name="name">匿名用户</slot>。
行为要点:只要有任何元素被分配进这个插槽,fallback 内容就完全消失,不是两个叠加显示。默认插槽同理,<slot>暂无内容</slot> 在有人塞东西进去时就只显示塞进去的东西。
第五步:控制组件内部样式和插槽样式
这一步要搞清楚样式的作用边界,做完你就知道为什么改了 CSS 却不好使。
三件事分清楚:
1. :host 选择器指向宿主元素本身,可以写 :host { display: block; },也可以用属性选择器写主题:
:host([theme="dark"]) {
background: #1f1f1f;
color: #eee;
}
2. ::slotted() 只能选中被分配的顶层元素:
::slotted(span[slot="name"]) {
font-weight: 700;
color: #1a73e8;
}
::slotted(p) {
margin: 4px 0;
}
3. 外部文档的样式依然会作用于插槽里的节点。因为这些节点物理上还在 light DOM 树里,只是被"投影"到 shadow DOM 中渲染。所以使用方写 user-card p { color: red } 是生效的。
注意:
::slotted(div p)这种带后代的选择器是无效的,规范只允许::slotted()里写复合选择器匹配顶层元素。想影响插槽内容的子孙节点,只能靠可继承属性(color、font-family、line-height等)穿透,或者让使用方自己写样式。
第六步:用 slotchange 监听插槽内容变化
这一步要拿到"插槽里现在到底有什么",做完你能做数据统计、空状态切换之类的逻辑。
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(
document.getElementById('user-card-tpl').content.cloneNode(true)
);
this._nameSlot = shadow.querySelector('slot[name="name"]');
}
connectedCallback() {
this._onSlotChange = () => {
const nodes = this._nameSlot.assignedNodes();
const els = this._nameSlot.assignedElements();
console.log('原始节点', nodes, '仅元素', els);
};
this._nameSlot.addEventListener('slotchange', this._onSlotChange);
}
disconnectedCallback() {
this._nameSlot.removeEventListener('slotchange', this._onSlotChange);
}
}
assignedNodes() 返回分配到该插槽的节点;传 { flatten: true } 会把插槽套插槽的情况展平。assignedElements() 是它的元素版本,过滤掉了文本节点。
注意:页面首次解析时,如果插槽里已经有内容,
slotchange也会触发一次。但在 constructor 里监听可能赶不上这次触发,所以监听写在connectedCallback里更稳。
注意:不要在 constructor 里读
this.getAttribute('xxx')来决定渲染结果。元素被解析器创建时是同步升级的,那一刻属性可能还没解析完。要读属性请放到connectedCallback,或者用static get observedAttributes()+attributeChangedCallback。
第七步:用属性驱动内容,配合多个 template
这一步把组件从"静态壳子"变成"能跟数据联动",做完你可以用属性控制组件行为。
原文链接:https://www.gj0.com/thread-1815.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。