本文以 React + Vite 浏览器插件项目为例,展示如何实现 Shadow DOM 样式隔离。
技术栈要求:
其他框架适用性:
我们在开发浏览器插件的时候,浏览器插件分为 4 个部分的 UI:
前三个(popup、side panel、new tab)都是独立页面,所以不会有样式问题。但是第四个 content script 的内容是镶嵌到宿主网页的,在写 Content script 的时候,你的样式可能会被主网站的样式所影响。
这个时候我们就需要一个隔离样式的方式。
隔离样式分为两种:
我们知道了 Shadow DOM 可以隔离样式,但还有一个问题:我们现在的 CSS 基础使用的是 Tailwind CSS 这个框架来做样式输出。
这样就会遇到一个问题——我们都知道 Tailwind 使用 REM 这个相对于根元素 font-size 的单位来做 spacing 和 font-size 的处理。
Shadow DOM 并不会隔离 REM 这个相对于根元素 font-size 单位换算的特性。如果主网站的根元素的 font-size 不是 16px,你的插件样式就会受到影响。
基础换算公式:
1rem = 根元素的 font-size (通常是 16px)
Tailwind 单位换算示例:
p-4 = padding: 1rem = 16px
p-2 = padding: 0.5rem = 8px
text-base = font-size: 1rem = 16px
text-lg = font-size: 1.125rem = 18px
我们需要把 REM 换成 EM,在 Shadow DOM 的根上设置一个 font-size 为 16px,这样 EM 单位就会基于 Shadow DOM 内部的字体大小进行计算,而不会受到主网站根元素的影响。
通常以前 Tailwind 是基于 PostCSS 来做 CSS 语法的换算,但是现在 Tailwind V4 已经从 PostCSS 独立出来使用 Lightning CSS 了。
我们当然也可以切换到 PostCSS 上面,因为 Tailwind 依旧支持这种方式,因为有很多以前的 PostCSS 插件需要使用。
比如在 tailwind v3 版本,你就可以使用 postcss 插件的方式,直接搜索插件就好了。
现在我们可以使用 Vite 的一个特性,结合运行时转换来解决这个问题:
这个实现将 Shadow DOM 创建、样式注入和 React 应用渲染整合在一起,形成了完整的工作流程。Vite 的 ?inline
后缀会将 CSS 文件作为字符串导入,而不是注入到页面的 <head>
中。
为什么这样有效?
:root
)的 font-sizereactContainer.style.fontSize = "16px"
,我们确保了 EM 单位始终以 16px 为基准计算HTML 根元素与 Shadow DOM 根元素的区别:
现在我们解决了 font-size 换算的问题,但我们还有一个问题:有一些 CSS 变量是设置在 HTML 根元素(:root
)上面的。
我们就需要设置在 Shadow DOM 的根元素(:host
)上面,确保这些变量在 Shadow DOM 内部可用。
在 Shadow DOM 的 CSS 文件中,我们直接使用 :host
来定义 CSS 变量:
这样所有的 CSS 变量都会被正确地应用到 Shadow DOM 的根元素上,而不会受到主网站样式的影响。
现在还差最后一个问题:在 Shadow DOM 环境中,无障碍阅读功能的支持存在一些限制。
无障碍阅读 是指为视力障碍用户提供的屏幕阅读器支持,通过语音朗读网页内容帮助他们使用网页。常见的屏幕阅读器包括 NVDA、JAWS 等。
React ARIA 是提供无障碍访问功能的 React 库,它会根据 ARIA 属性来判断元素的交互状态和行为。但在 Shadow DOM 中可能失效,主要原因:
这可能会导致你的 hover、click、press 或者 active 这种依赖于 React ARIA 的交互状态不会很好地触发。
如果你使用的 UI 库支持 Shadow DOM 的无障碍访问,那就没问题。如果不支持,可能需要手动实现一些交互功能。
对于大多数浏览器插件来说,这个问题影响有限,因为插件功能相对简单,无障碍访问需求较少。但如果你的 UI 交互性不够流畅,可能需要注意这个问题。
基于以上所有技术点,这里是一个完整的 Shadow DOM 样式隔离实现示例: