1. 简介
Shadow DOM 允许作者将其页面划分为“组件”, 即一些标记子树,其细节只与组件本身相关, 而与外部页面无关。 这降低了用于页面某一部分的样式 意外过度应用并使页面另一部分显示错误的可能性。 然而,这种样式屏障也使页面在实际想要与组件交互时 更难与其组件交互。
本规范定义了 ::part() 伪元素, 它允许作者从外部页面的上下文中,对 影子 树中有意暴露的特定元素进行样式设置。 结合自定义属性, 外部页面可以将特定值 (如主题颜色) 传入组件,由组件自行处理, 这些伪元素允许组件与外部页面 以安全而强大的方式交互, 在不放弃所有控制的情况下 保持封装。
测试
shadow parts 的通用测试
- all-hosts.html (实时测试) (源代码)
- animation-part.html (实时测试) (源代码)
- chaining-invalid-selector.html (实时 测试) (源代码)
- complex-matching.html (实时测试) (源代码)
- complex-non-matching.html (实时 测试) (源代码)
- different-host.html (实时测试) (源代码)
- double-forward.html (实时测试) (源代码)
- grouping-with-checked.html (实时 测试) (源代码)
- grouping-with-disabled.html (实时 测试) (源代码)
- host-stylesheet.html (实时测试) (源代码)
- inner-host.html (实时测试) (源代码)
- interaction-with-nested-pseudo-class.html (实时 测试) (源代码)
- interaction-with-placeholder.html (实时 测试) (源代码)
- interaction-with-pseudo-elements.html (实时 测试) (源代码)
- invalidation-complex-selector-forward.html (实时 测试) (源代码)
- invalidation-complex-selector.html (实时 测试) (源代码)
- invalidation-part-pseudo.html (实时 测试) (源代码)
- multiple-parts.html (实时测试) (源代码)
- part-after-combinator-invalidation.html (实时 测试) (源代码)
- part-mutation-pseudo.html (实时 测试) (源代码)
- part-nested-pseudo.html (实时 测试) (源代码)
- precedence-part-vs-part.html (实时 测试) (源代码)
- pseudo-classes-after-part.html (实时 测试) (源代码)
- pseudo-elements-after-part.html (实时 测试) (源代码)
- serialization.html (实时测试) (源代码)
- simple-forward-shorthand.html (实时 测试) (源代码)
- simple-forward.html (实时测试) (源代码)
- simple-important.html (实时测试) (源代码)
- simple-important-important.html (实时 测试) (源代码)
- simple-important-inline.html (实时 测试) (源代码)
- simple.html (实时测试) (源代码)
- simple-important.html (实时测试) (源代码)
- simple-inline.html (实时测试) (源代码)
- style-sharing.html (实时测试) (源代码)
1.1. 动机
为了让自定义元素完全有用,并与内置元素一样有能力, 应该可以从外部对它们的部分进行样式设置。 究竟哪些内容可以从外部设置样式,应由元素作者控制。 此外,自定义元素应该能够为样式设置提供一个稳定的“API”。 也就是说,用于设置自定义元素某一部分样式的选择器 不应暴露或要求了解该元素的内部细节。 自定义元素作者应该能够更改元素的内部细节, 同时保持选择器不变。
先前提出的用于对影子树内部进行样式设置的方法, 即 >>> 组合器, 结果证明过于强大, 它将组件过多的内部结构暴露给审查, 破坏了使用 Shadow DOM 带来的一些封装好处。 因此, 以及其他与性能相关的原因, >>> 组合器最终被移除。
这使我们只能使用自定义属性作为对影子树内部设置样式的唯一方式: 组件会声明它使用某些自定义 属性来设置其内部样式, 然后外部页面可以根据需要在影子 宿主上设置这些属性, 让继承把这些值传递到需要它们的位置。 这对于许多简单的主题化用例非常有效。
然而,在某些情况下,这种方式会失效。 如果组件希望允许对其影子树中的某个内容进行任意样式设置, 唯一的方法就是定义数百个自定义 属性(每个希望允许控制的 CSS 属性一个), 这显然在可用性和性能方面 都很荒谬。 如果作者还希望根据 :hover 等伪类 以不同方式设置组件样式, 情况会更加复杂; 组件需要为每个伪类 (以及每种组合, 如 :hover:focus, 导致组合爆炸) 复制所使用的自定义属性。 这会进一步加剧可用性和性能问题。
我们引入 ::part(),以更优雅且性能更好的方式处理这种情况。 与其把所有内容都塞进自定义 属性名称中, 该功能存在于选择器和样式规则语法中, 就像它本该如此。 这对于组件作者 和组件用户都更可用, 应该具有更好的性能, 并允许更好的封装/API 表面。
需要注意的是,::part() 绝对没有提供新的理论能力。 它不是 >>> 组合器的翻版, 它只是对作者已经可以通过自定义 属性做到的事情,提供了一种更方便、更一致的语法。 通过将元素中显式“发布”的部分 (part element map) 与它恰好包含的子部分分离, 它也有助于封装, 因为作者可以使用 ::part(),而不必担心 意外的过度样式设置。
2. 暴露影子元素:
影子树中的元素可以使用 part 和 exportparts 属性, 导出给树外部的样式表进行样式设置。每个元素都有一个part 名称列表,它是一个 token 的有序集合。
每个元素都有一个转发 part 名称列表,它是一个列表,其中包含若干元组,每个元组包含一个字符串表示被转发的 内部 part, 以及一个字符串给出它将被暴露成的名称。
每个影子根都可以被认为具有一个part 元素映射,其键是字符串,值是元素的有序 集合。
part 元素映射仅作为本规范中计算样式算法的一部分进行描述。 它不会通过 DOM 暴露, 因为计算它可能开销很大, 并且暴露它可能允许访问闭合影子根内部的元素。
Part 元素映射会受到元素的添加和移除, 以及 DOM 中元素的part 名称列表和转发 part 名称列表变化的影响。
-
对于 outerRoot 内的每个后代 el:
-
对于 el 的part 名称 列表中的每个 name,将 el 追加到 outerRoot 的part 元素映射[name]。
-
如果 el 本身是一个影子宿主, 则令 innerRoot 为其影子根。
-
对于 el 的转发 part 名称列表中的每个 innerName/outerName:
-
如果 innerName 是一个 ident:
-
令 innerParts 为 innerRoot 的part 元素 映射[innerName]
-
将 innerParts 中的元素追加到 outerRoot 的part 元素 映射[outerName]
-
-
如果 innerName 是一个伪元素名称:
-
将 innerRoot 中具有该名称的伪元素追加 到 outerRoot 的part 元素 映射[outerName]。
-
-
-
2.1. 命名影子元素:
part
属性
影子树中的任何元素都可以具有一个 part
属性。
该属性用于将该元素暴露到影子
树外部。
测试
part 属性被解析为一个由空格分隔的 token 列表,表示此元素的 part 名称。
注: 可以给一个 part 多个名称。 “part name” 应被视为类似于 class, 而不是 id 或 tagname。
<style>
c-e::part(textspan) { color: red; }
</style>
<template id="c-e-template">
<span part="textspan">这段文本将是红色</span>
</template>
<c-e></c-e>
<script>
// 将模板添加为自定义元素 c-e
...
</script>
2.2. 转发影子元素:
exportparts
属性
影子树中的任何元素都可以具有一个 exportparts 属性。
如果该元素是影子宿主,
则该属性用于允许由此影子树外部的规则
对 影子
树内部宿主中的 part 进行样式设置(就像它们是与宿主在同一树中、
由 part 属性命名的元素一样)。
测试
exportparts 属性被解析为一个由逗号分隔的 part 映射列表。 每个 part 映射是以下之一:
innerIdent : outerIdent-
将
innerIdent/outerIdent添加到 el 的转发 part 名称列表。 ident-
将
ident/ident添加到 el 的转发 part 名称列表。注: 这是
ident : ident的简写。 ::ident : outerIdent-
如果
::ident是完全 可样式化伪元素的名称, 则将::ident/outerIdent添加到 el 的转发 part 名称列表。 否则,不执行任何操作。 - 其他任何内容
-
为错误恢复/未来兼容性而忽略。
注: 可以将一个子 part 映射到多个名称。
<style>
c-e::part(textspan) { color: red; }
</style>
<template id="c-e-outer-template">
<c-e-inner exportparts="innerspan: textspan"></c-e-inner>
</template>
<template id="c-e-inner-template">
<span part="innerspan">
这段文本将是红色,因为包含它的 shadow
host 将 innerspan 转发到文档中,名称为 "textspan",
并且文档样式匹配它。
</span>
<span part="textspan">
这段文本不会是红色,因为文档样式中的 textspan
无法匹配内部自定义元素里的 part,
如果它没有被转发的话。
</span>
</template>
<c-e></c-e>
<script>
// 将模板添加为自定义元素 c-e-inner、c-e-outer
...
</script>
exportparts
属性中使用,
以伪装成其所在组件的 ::part():
<template id=custom-element-template>
<p exportparts="::before : preceding-text, ::after : following-text">
Main text.
</template>
使用该模板的元素 可以使用类似 x-component::part(preceding-text) 的选择器来定位其影子中的 p::before 伪元素, 因此组件用户不需要知道 前置文本是作为伪元素实现的。
3. 选择影子元素:::part() 伪元素
::part()
伪元素
允许你选择通过
part
属性暴露出来的元素。
其语法为:
::part() = ::part( <ident>+ )
测试
::part() 伪元素只有在原始元素是影子 宿主时,才会匹配任何内容。
一个选项卡条控件可能有多个
带有 part="tab" 的元素,
它们都会被 ::part(tab) 选中。
如果一次只有一个选项卡处于活动状态,
可以用 part="tab active" 对其进行特殊标记,然后由 ::part(tab active)(或 ::part(active tab),因为顺序
无关紧要)选中。
::part() 伪元素是一个完全可样式化 伪元素。 如果原始元素的影子 根的part 元素映射包含指定的 <ident>,::part() 表示以该 ident 为键的元素; 如果提供了多个 idents,并且 part 元素 映射包含它们全部, 它表示以每个 ident 为键的元素的交集。 否则,它不匹配任何内容。
::part() 伪元素根据其在原始元素影子树中的位置 进行继承。
如果 <x-panel> 的内部确认按钮使用了类似
part="label => confirm-label" 的内容,将按钮的内部 part 向上转发到面板自己的
part 元素映射中,
那么类似 x-panel::part(confirm-label) 的选择器就只会选择该一个
按钮的 label,
而忽略任何其他 label。
4. 对 Element
接口的扩展
partial interface Element { [SameObject ,PutForwards =value ]readonly attribute DOMTokenList ; };part
测试
part 属性的 getter 必须返回一个 DOMTokenList 对象, 其关联元素是上下文对象, 且其关联属性的本地名称是 part。 这个特定 DOMTokenList 对象的 token 集也称为元素的 parts。
在 DOM 规范中将其定义为 superglobal。 [w3c/csswg-drafts Issue #3424]
5. 用于解析的微语法
5.1. 解析 part 映射的规则
有效 part 映射 是一个由 token 组成的元组, 这些 token 由 U+003A COLON 字符分隔, 且 U+003A COLON 前后可以有任意数量的空格字符。 token 不得包含 U+003A COLON 或 U+002C COMMA 字符。
解析 part 映射的规则如下:
-
令 input 为正在解析的字符串。
-
令 position 为指向 input 的指针,最初指向该 字符串的开头。
-
收集一串码点,这些码点是 空格字符
-
收集一串码点,这些码点不是 空格字符或 U+003A COLON 字符, 并令 first token 为结果。
-
如果 first token 为空,则返回 error。
-
收集一串码点,这些码点是 空格字符。
-
如果已经到达 input 的末尾,则返回元组(first token, first token)
-
如果 position 处的字符不是 U+003A COLON 字符,则返回 error。
-
消耗 U+003A COLON 字符。
-
收集一串码点,这些码点是 空格字符。
-
收集一串码点,这些码点不是 空格字符或 U+003A COLON 字符。 并令 second token 为结果。
-
如果 second token 为空,则返回 error。
-
收集一串码点,这些码点是 空格字符。
-
如果 position 没有越过 input 的末尾,则返回 error。
-
返回元组(first token, second token)。
5.2. 解析 part 映射列表的规则
有效 part 映射列表是若干有效 part 映射, 由 U+002C COMMA 字符分隔, 且 U+002C COMMA 前后可以有任意数量的空格字符
解析 part 映射列表的规则如下:
-
令 input 为正在解析的字符串。
-
按逗号拆分字符串 input。 令 unparsed mappings 为由此产生的字符串列表。
-
对于 unparsed mappings 中的每个字符串 unparsed mapping, 运行以下子步骤:
-
如果 unparsed mapping 为空或仅包含空格字符, 则继续循环的下一次迭代。
-
令 mapping 为使用 解析 part 映射的规则解析 unparsed mapping 的结果。
-
如果 mapping 是 error,则继续循环的下一次迭代。 这允许客户端跳过无法理解的新语法。
-
将 mapping 追加到 mappings。
-
6. 隐私注意事项
本规范定义了定位页面上元素样式的新方式, 这些元素已经可以通过其他方式完全设置样式。 因此,它不会引入新的隐私注意事项。
7. 安全注意事项
由于影子树有意不是安全边界, 而只是为页面作者提供便利, 以这种方式将其暴露给选择器不会引入新的安全注意事项。
8. 变更
自 2018 年 11 月 15 日的首次公开工作 草案以来的变更
-
为 ::part() 添加对多个名称的支持
-
将 'part name map' 重命名为转发 part 名称列表
-
重构了 'part element list' 算法
-
将各种 ::part() 细节移至完全 可样式化伪元素
-
增加了 Web Platform Tests 覆盖
-
小幅编辑性改进