part 与 exportparts
part 全局属性用于将 Shadow DOM 内部的元素暴露给外部 CSS,使宿主页面可以使用 ::part() 伪元素选择器来定制 Web Component 内部元素的样式。exportparts 属性则允许嵌套的 Shadow DOM 将内部组件的部分(parts)转发到外层。
前置知识
阅读本节前,建议先了解:translate 翻译控制
基础概念
什么是 CSS Parts
Web Components 的 Shadow DOM 提供了强大的样式封装(Style Encapsulation),外部 CSS 无法直接选择 Shadow DOM 内部的元素。但有时我们需要允许外部定制组件的某些部分样式,part 属性就是为此设计的。
html
<!-- 组件内部:标记可被外部样式化的部分 -->
<template id="my-button">
<style>
/* 内部默认样式 */
.btn { padding: 8px 16px; border: none; border-radius: 4px; }
.label { font-size: 14px; }
</style>
<button class="btn">
<span class="label"><slot></slot></span>
</button>
</template>part 和 exportparts 的作用
| 属性 | 使用位置 | 作用 |
|---|---|---|
part | Shadow DOM 内部元素 | 将元素标记为可被外部样式化的"部分" |
::part() | 宿主页面 CSS | 选择 Shadow DOM 内部被标记的部分 |
exportparts | Shadow DOM host 元素 | 将嵌套 Shadow DOM 的 parts 转发到外层 |
语法
定义 part
html
<!-- 在 Shadow DOM 模板中 -->
<template id="my-card">
<div class="card" part="card">
<div class="card-header" part="header">
<h3 class="card-title" part="title">标题</h3>
</div>
<div class="card-body" part="body">
<slot></slot>
</div>
<div class="card-footer" part="footer">
<slot name="actions"></slot>
</div>
</div>
</template>使用 ::part() 定制样式
css
/* 宿主页面中,使用 ::part() 选择组件内部部分 */
my-card::part(card) {
border: 2px solid #e2e8f0;
border-radius: 12px;
}
my-card::part(header) {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
my-card::part(title) {
font-size: 20px;
font-weight: 700;
}
my-card::part(footer) {
padding: 12px;
border-top: 1px solid #e2e8f0;
}多个 part 名称
一个元素可以有多个 part 名称:
html
<!-- 元素可以有多个 part 名称 -->
<div part="container wrapper">
<button part="btn btn-primary">提交</button>
</div>
<!-- 外部可以使用任一名称选择 -->
my-widget::part(container) { /* ... */ }
my-widget::part(wrapper) { /* ... */ }
my-widget::part(btn) { /* ... */ }
my-widget::part(btn-primary) { /* ... */ }exportparts 转发
当组件内嵌套了另一个带有 Shadow DOM 的组件时,使用 exportparts 将内层组件的 parts 暴露到外层:
html
<!-- 外层组件模板 -->
<template id="outer-component">
<!-- 内层组件,转发其 parts -->
<inner-component exportparts="header, body, footer"></inner-component>
</template>html
<!-- 外层组件的使用者可以直接定制内层组件的部分 -->
<outer-component></outer-component>
<style>
/* 直接定制嵌套在内层组件中的元素 */
outer-component::part(header) {
background: #1e40af;
}
</style>详细说明
::part() 选择器的限制
::part() 选择器有以下限制:
css
/* 允许:直接选择宿主元素 + part */
my-component::part(label) {
color: red;
}
/* 允许:伪类和伪元素 */
my-component::part(input):focus {
outline: 2px solid blue;
}
my-component::part(label)::before {
content: '* ';
color: red;
}
/* 不允许:后代选择器(不能穿透 Shadow DOM) */
/* my-component::part(card) .title { ... } */
/* 不允许:复合选择器 */
/* my-component::part(card) > .body { ... } */限制总结:
| 选择器类型 | 是否支持 | 示例 |
|---|---|---|
host::part(name) | 支持 | my-card::part(title) |
host::part(name):pseudo | 支持 | my-card::part(btn):hover |
host::part(name)::pseudo | 支持 | my-card::part(label)::after |
host::part(name) descendant | 不支持 | my-card::part(body) p |
host::part(name) > child | 不支持 | my-card::part(header) > h3 |
与 CSS 变量配合
part 与 CSS 自定义属性配合使用可以实现更灵活的主题定制:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Part + CSS Variables 示例</title>
</head>
<body>
<script>
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
/* 使用 CSS 变量作为默认值,允许外部覆盖 */
padding: var(--btn-padding, 8px 16px);
background: var(--btn-bg, #3b82f6);
color: var(--btn-color, white);
border: var(--btn-border, none);
border-radius: var(--btn-radius, 6px);
font-size: var(--btn-font-size, 14px);
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: var(--btn-bg-hover, #2563eb);
}
</style>
<button part="button">
<span part="label"><slot></slot></span>
</button>
`;
}
}
customElements.define('my-button', MyButton);
</script>
<!-- 使用方式 1:通过 ::part() 定制样式 -->
<my-button class="large-btn">大按钮</my-button>
<style>
.large-btn::part(button) {
padding: 12px 32px;
font-size: 18px;
border-radius: 8px;
}
</style>
<!-- 使用方式 2:通过 CSS 变量定制 -->
<my-button class="danger-btn">危险操作</my-button>
<style>
.danger-btn {
--btn-bg: #dc2626;
--btn-bg-hover: #b91c1c;
--btn-radius: 20px;
}
</style>
</body>
</html>实战示例
完整的自定义卡片组件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Part 属性实战:自定义卡片组件</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 40px;
background: #f8fafc;
}
/* 通过 ::part() 定制组件内部样式 */
fancy-card {
--card-bg: white;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
fancy-card::part(card) {
background: var(--card-bg);
box-shadow: var(--card-shadow);
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
fancy-card:hover::part(card) {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
/* 暗色主题卡片 */
fancy-card.dark {
--card-bg: #1e293b;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
fancy-card.dark::part(header) {
background: linear-gradient(135deg, #0ea5e9, #6366f1);
}
fancy-card.dark::part(title) {
color: white;
}
fancy-card.dark::part(body) {
color: #94a3b8;
}
</style>
</head>
<body>
<h1>产品展示</h1>
<fancy-card class="dark">
<span slot="title">高级套餐</span>
<span slot="price">¥99/月</span>
<p>包含无限存储空间、优先技术支持和高级分析功能。</p>
<button slot="actions">立即订阅</button>
</fancy-card>
<script>
class FancyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card {
border: 1px solid #e2e8f0;
background: white;
}
.header {
padding: 16px 20px;
background: #f1f5f9;
}
.title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.price {
font-size: 24px;
font-weight: 700;
color: #3b82f6;
margin: 4px 0 0;
}
.body {
padding: 20px;
line-height: 1.6;
color: #475569;
}
.footer {
padding: 16px 20px;
border-top: 1px solid #e2e8f0;
text-align: right;
}
.footer ::slotted(button) {
padding: 8px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
</style>
<div class="card" part="card">
<div class="header" part="header">
<h3 class="title" part="title"><slot name="title">标题</slot></h3>
<div class="price" part="price"><slot name="price"></slot></div>
</div>
<div class="body" part="body">
<slot></slot>
</div>
<div class="footer" part="footer">
<slot name="actions"></slot>
</div>
</div>
`;
}
}
customElements.define('fancy-card', FancyCard);
</script>
</body>
</html>exportparts 嵌套转发
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>exportparts 嵌套转发示例</title>
</head>
<body>
<script>
// 内层组件:列表项
class ListItem extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #e2e8f0;
}
.icon { margin-right: 8px; }
.text { flex: 1; }
.action { margin-left: 8px; }
</style>
<div class="item" part="item">
<span class="icon" part="icon"><slot name="icon"></slot></span>
<span class="text" part="text"><slot></slot></span>
<span class="action" part="action"><slot name="action"></slot></span>
</div>
`;
}
}
customElements.define('list-item', ListItem);
// 外层组件:列表容器,转发内层 parts
class FancyList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.list {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
</style>
<div class="list" part="list">
<slot></slot>
</div>
`;
}
}
customElements.define('fancy-list', FancyList);
</script>
<!-- 外层组件的模板中转发内层 parts -->
<!--
在 fancy-list 的模板中,list-item 应使用:
<list-item exportparts="item, icon, text, action">
-->
<fancy-list>
<list-item exportparts="item, icon, text, action">
文件 A
<span slot="action">
<button>删除</button>
</span>
</list-item>
</fancy-list>
<!-- 外部可以直接使用转发过来的 parts -->
<style>
fancy-list::part(list) {
max-height: 300px;
}
fancy-list::part(item) {
/* 通过 exportparts 转发后可以直接选择 */
}
fancy-list::part(action) {
/* 定制操作按钮区域 */
}
</style>
</body>
</html>注意事项
安全性考虑
part 属性会暴露 Shadow DOM 的内部结构,在设计公开 API 时应谨慎:
html
<!-- 推荐:暴露语义化的 part 名称 -->
<div part="header">标题区域</div>
<div part="content">内容区域</div>
<!-- 避免:暴露内部实现细节 -->
<div part="flex-container-row">标题区域</div>
<div part="padding-20">内容区域</div>命名规范
html
<!-- 推荐:简洁、语义化的名称 -->
<button part="button">点击</button>
<span part="label">标签文字</span>
<div part="header">头部</div>
<div part="icon">图标</div>
<!-- 避免:过长的或特定框架的名称 -->
<button part="ant-design-primary-button-outlined">点击</button>
<span part="tw-text-sm-font-medium">标签</span>最佳实践
1. part 和 CSS 变量结合
part 适合控制结构性样式(布局、间距),CSS 变量适合控制主题性样式(颜色、字体):
css
/* ::part() 控制结构 */
my-card::part(body) {
padding: 24px;
display: grid;
grid-template-columns: 1fr 1fr;
}
/* CSS 变量控制主题 */
my-card {
--card-primary: #3b82f6;
--card-radius: 8px;
--card-font: system-ui;
}2. 文档化所有 parts
javascript
// 在组件类中通过 static 属性声明可用的 parts(文档用途)
class MyButton extends HTMLElement {
static get observedAttributes() {
return ['variant', 'size'];
}
// 用于文档生成的 parts 声明
static parts = {
'button': '按钮容器元素',
'label': '按钮文本标签',
'icon': '图标区域'
};
}下一节
继续学习:slot 插槽