技术分享
ShadowDOM css样式处理详解
00 分钟
2022-8-16
2023-6-23
type
status
date
slug
summary
tags
category
icon
password
Property
Jun 23, 2023 11:48 AM

ShadowDOM css样式处理详解

ShadowDOM是web components方案中非常重要的一个新增对象,它通过在custom element中使用attachShadow来开启,开启之后,一个HTMLElement将不再显示其原本内部的元素,而是显示其shadowRoot内的元素,shadowRoot是一个document fragment,是脱离原始文档流的一种存在,因此它具有css样式隔离性,通过这种隔离,我们可以很好的在应用中实现一些局部样式的重置和定义(当然,还有组件化效果)。本文将详细介绍你在处理shadowDOM时可能需要用到的一些样式处理方法。

样式隔离

你可以利用shadowDOM的特性来实现样式隔离,举个例子:
在默认情况下,snake中的文本样式继承了来自父元素的文本样式。但是你希望不要继承,除了通过css写重置的样式规则外,你还可以用shadowDOM来实现。
通过这种方案,可以帮助我们在一些情况下少写css,或者处理比较难的样式重置问题。

:host

:host让你可以选择shadowDOM的寄生元素,也就是开启shadowDOM的那个元素。shadowRoot是一个document fragment,这个fragment被安排在一个隐性的文档流中,寄生在host元素下,并替代了host元素的innerHTML。所以,在元素文档中host元素(也就是被开启shadowDOM的元素)还是一个正常可以被css描述的元素,但是它的内部元素的展示被拖到shadowRoot中展示,甚至不被展示。举个例子:
代码来源。这里,button元素被开启了shadowDOM,在shadowRoot的innerHTML中使用:host选择了button元素,从而可以从shadowDOM内部对被寄生的button元素进行样式修改。
同时,:host可以是一个选择器函数,例如:
这样,我们可以在shadowDOM内选择不同状态下的宿主元素进行样式调整,这种能力有的时候很有用。(为了方便记忆,你可以把它理解为 :host:is() 的综合效果。之所以要提这个,是因为你可能把一段:host css代码用在多处,通过:is的理解,你就可以更好的对一些特殊宿主元素做进一步控制。)但是,这一选择器能力只能针对宿主元素本身,有没有可能宿主元素不会发生任何状态的变化,而宿主元素的父元素、祖先元素发生变化,从而影响宿主元素的样式呢?通过:host-context()可以从宿主元素的祖先元素特征中挑选宿主元素。举个例子:
此时,dark-mode是被动态加上去的,所以,在shadowDOM内通过 :host-context() 就可以通过祖先元素的特性来选择宿主元素。
也就是说只有宿主元素在.dark-mode元素内部时,这段css才生效。
注意,你无法通过 :host div 这种语法从shadowDOM内部去控制shadowDOM外部宿主元素内部的元素的样式。听起来比较绕,但是在下文::slotted, :part()等部分,你会接触到这部分内容。

::shadow

::shadow伪类让你可以从shadowDOM外部(也就是正常的文档流中)选择shadowDOM的shadowRoot。举个例子:
这里从shadowDOM外部通过 #host::shadow 选中了shadowRoot,对上面标红的一行代码的样式进行了控制,这样,就实现从外部控制shadowDOM内部的元素的样式。甚至,如果一个shadowDOM内部还有其他shadowDOM,你还可以通过连写来找到对应的shadowRoot,例如:
不过这里需要注意,这一句里面以为着<x-pannel>是被直接写在x-tabs的shadowRoot的,用伪代码表示就是 xTabs.shadowRoot > xPannel.shadowRoot 不能跨shadowDOM选择。

/deep/

/deep/ 是一个连接符,它的作用和::shadow有点像,但是穿透性更强。如上面举例的x-tabs和x-pannel的连选问题,通过/deep/就可以直接跨shadowRoot选中。举个例子:
意味着<x-pannel>可能是直接写在x-tabs的shadowRoot中,也可能是在更深的shadowRoot中,总之只要x-pannel被放在x-tabs的shadowDOM中,就生效这一段css。
这一句代码威力极强,它让body中的所有.m1都生效,无论这个.m1是在正常文档流中,还是在一个shadowDOM中。
/deep/在一些场景下面非常有用,但是如果你不懂它们的用法,你就在一些需求下无能为力。举个例子,你怎么去控制<video>视频播放器里面的进度条呢?实际上<video>是一个浏览器自己实现的custom element,通过展开它的shadowRoot你就可以慢慢找到进度条,再通过/deep/来实现,例如:

::part()

ShadowDOM parts是一块比较复杂的内容,我尽可能讲到所有点,但一定会有遗漏。首先,::part()是一个伪选择器函数,为方便理解,你可以把它理解为 ::shadow :is([part=xxx]) 的组合效果,也就是在shadowRoot中挑选part属性为传入值的元素。举个例子:
首先,它和::shadow, /deep/一样,是在shadowDOM外部对内部的某个元素进行选择;其次,它需要在shadowRoot内的元素上用part/exportparts进行标记,在选择时传入标记的名称;最后,它是终结点,不能再找子元素,例如 ::part(xx) span ::part(a)::part(b) 是不允许的,也就是说选择时 ::part() 是结尾,不能再往下面找(这一点非常重要,对理解下面 ::slotted 也有帮助)。
这里面比较关键的点就是你必须使用 part/exportparts 对元素进行标记。例如:
这两个属性其实比较容易理解,你不要想太多就行,只是标记了名字。标记了名字之后,就可以在外部使用::part()选择它。

::theme()

就像/deep/是::shadow的增强一样,::theme()是对::part()的增强。::part()只能选择一层shadowDOM的标记元素,而::theme()可以跨越层级,进行深度的parts选择。

::soltted()

Web components中一个非常重要的标准涉及slot这个部分,它也是非常复杂和难懂的一个部分,对于浏览器厂商而言,也是比较难实现的部分。在使用中,我们是这样的:
在shadowRoot中使用slot作为占位符:
那么在显示的时候,会把<x-foo>的内容先放到<slot>的位置后,再渲染出来。但是非常坑的地方在于,被传到<slot>位置的两个div的样式,只使用外部文档定义的样式。这是一个非常奇葩的设计,也让slot部分的样式处理极为复杂。
简单总结一下一些规则:1. shadowDOM内部无法定义外部除宿主元素本身以外的其他元素的样式;2. <slot>是shadowRoot内的元素,但是作为占位符被替换后,替换内容的仍然是在shadowDOM外部,相当于slot只是一个用于显示外太空的显示器,显示器上显示的内容不符合地球上的物理规律;3. <slot>本身不会从shadowDOM中移除,它把外部传入的内容作为自己的子元素,所以,你可以在shadowRoot内调整slot的样式,但是由于slot是display:contents,所以也不占用文档占位;4. 在shadowRoot内通过给:host设定一些通用样式,这些样式又会作用到slot被替换后的外部元素上,虽然最终还是以外部设定的样式为准(外部元素仍然属于外部文档,因此,外部文档作用在它身上的样式优先级更高),这又使得样式处理更复杂了。你可以试试下面这份代码:
通过这些规则,你可以发现,slot部分的样式处理太复杂了,但是,你可以得到一个通用性比较强的原则:shadowDOM内部无法对外部样式做强处理。为了能够对在slot处传入的元素进行一定的样式处理,web components里面提供了目前还不够完善的::slotted()伪选择器函数。
::slotted()可以选中传入slot处的外部元素的顶级元素进行样式处理。在上面的代码中我们在shadowRoot中加入如下代码:
就可以覆盖外部对p的样式描述。注意,这里加入了!impotatnt,因为遵循上面的原则,内部无法对外部样式做强处理,所以外部的样式优先级更高,会覆盖内部::slotted(p)设计的效果,因此,加上!important又可以提交其权限,但是加入外部也加一个!important,那::slotted也毫无办法。
另外,一个重要注意点是,::slotted()只能选择传入的顶级元素,加入传入的元素是有更深嵌套的,是无法直接选择的,例如下面:
你是不能用 ::slotted(.sub) 选择到.sub的,但你可以通过 ::slotted(.top .sub) 选择到它。
另外,和::part()一样,::slotted()只能作为选择的尾节点,你不能做 ::slotted(.top) .sub 这种选择,没有用。另外,由于::slotted()只能直接选中一个节点,所以无法通过 + 连接符选择兄弟节点,比如 ::slotted(div + p) 也是无效的。这些都是坑,你需要特别注意。
早期的shadowDOM提案中有<content>和::content,现在关于content的提案已经从标准中移除了,千万不要再使用。

Css变量

Css变量在shadowDOM中是什么规则呢?不拐弯抹角了,shadowDOM内只应用:host上的css变量。也就是说,在正常的文档流中,使用:root,body之类的设定的css变量,是无法在shadowDOM内使用的。然而,:host上的css变量,无论是在shadowRoot内还是外部文档中设定的,都可以在shadowDOM内使用。举个例子:
在上面的代码中,在最外面的style里面、x-foo的style属性里、shadowRoot里面的style里面,三个地方都对x-foo都配置了--color,都可以在shadow-root中的元素上使用。当然,优先级还是不一样,这里要怎么去思考呢?css变量的优先级和css样式表的优先级一致;css变量是宿主元素的性质,因此,优先级遵循宿主元素css样式表的优先级。说人话,上面的例子中,优先级顺序如下:green < red < blue。为什么:host的优先级是最低的呢?这个我还真不知道,如果你有兴趣,了解后请在文末留言告知我。
Vue有一个提案,可以实现动态样式表,它的实现原理就是通过修改行内style属性的值,把css变量加到属性值中进行修改,从而做到动态样式的效果。我写的一个 web components 框架 sfcjs 也支持类似的动态样式功能,你可以通过npm找到这个包,但是原理和vue不同,是通过修改:host来实现。
总之,搞清楚css变量的优先级顺序之后,能为我们将来解决一些样式问题打好基础。

@import/link[rel=stylesheet]

在shadowRoot中使用@import或<link rel="stylesheet" />是生效的,你可以像在普通的html文档中使用它们一样使用。但是大部分情况下不推荐,因为你需要解决引入文件的路径问题,因为我们很多时候,创建shadowDOM的代码会放在js文件中,而js文件究竟会被哪个页面使用,你根本不知道,所以,这个路径问题就很复杂。

样式复用

除了通过import/link方案复用样式,实际上,由于我们大部分都是用js来写custom elements的,所以这些shadowDOM中的css代码常常也是被打包在js代码中,因此,要复用css,就需要在整个项目中,把css文本字符串独立出来,让后在需要用的地方使用这个字符串。最终的效果是,源码只有一份,但是在实际运行时,会在每一个shadowRoot中都出现一次这些css代码。

结语

本文详细介绍了shadowDOM中有关css样式处理的内容,基本上涵盖了你所需要知道的所有知识点,当然,可能还有一些更细节的东西没有讲到。掌握shadowDOM是现代web编程非常重要的一环,shadowDOM也是web编程区别于其他平台编程的重要对象,当然,掌握它你在小程序开发中也会如鱼得水。如果你对shadowDOM的css处理有自己的想法和疑问或补充,都可以在本文下方留言,一起探讨。
 

评论
  • Twikoo