本文最后更新于:2024年8月21日 早上
背景
最近使用 WXT 创建 Chrome 扩展 Google Search Console - Bulk Index Cleaner,实现时使用了 shadui/cn 作为 UI 组件库,同时在 Content Script 中使用了 Shadow DOM 以隔离 CSS,因此发现了一些弹窗相关组件的问题。
为什么是 Shadow DOM
正常情况下,使用 Chrome 扩展向网页中注入 UI 时,会受到网页原有 CSS 的影响,这会导致 TailwindCSS 这种 UI 框架用起来也很糟心,可能与原本网页的 CSS 产生冲突。而 Shadow DOM 可以解决这个问题,它可以将组件的 CSS 与网页的 CSS 隔离开。
例如原本的网页结构如下:
1 2 3 4 5 6 7 8 9 10 11 12
| <html> <head> <style> </style> </head> <body> <div class="content"> </div> </body> </html>
|
不使用 Shadow DOM 时注入一个 Dialog 组件的示例,Dialog 组件的 CSS 可能与网页原有的 CSS 产生冲突:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <html> <head> <style> </style> <style> </style> </head> <body> <div class="content"> </div> <div class="dialog-container"> </div> </body> </html>
|
使用 Shadow DOM 时注入一个 Dialog 组件的示例,可以看到 HTML/CSS 都被隔离在了 custom-element 之中,而且也不会受到网页原有的 CSS 影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <html> <head> <style> </style> </head> <body> <div class="content"> </div> <custom-element> #shadow-root <style> </style> <body> <div class="dialog-container"> </div> </body> </custom-element> </body> </html>
|
衍生问题
说完了它解决的问题,再说说它带来的问题。主要还是 shadui/cn 这个组件库中的弹窗组件都使用 Portal 渲染,而 Portal 渲染的位置默认是在 document.body 中,并非是在 custom-element 组件之中,因而 custom-element 组件中的 CSS 无法应用到外层的弹窗上。
例如下面是默认情况下 Dialog 在网页中渲染的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <custom-element> #shadow-root <style> .dialog-container { } </style> <body> </body> </custom-element>
<div class="dialog-container"> </div>
|
可以看到 Dialog 组件渲染在了 document.body 中,而不是在 custom-element 组件中,导致 Dialog 组件无法应用到 custom-element 组件中的注入的 CSS 样式。
解决
目前在使用 shadui/cn 时,已经发现了两个组件存在这个问题:Dialog 和 Toast,下面将给出解决方法。
Dialog
Dialog 组件比较容易解决,找到 components/ui/dialog 中的 DialogPortal 组件,设置 container
字段指定渲染的容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const DialogContent = React.forwardRef< React.ElementRef<typeof DialogPrimitive.Content>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> >(({ className, children, ...props }, ref) => ( <DialogPortal /* 这里是 custom element */ container={document .querySelector('custom-element') ?.shadowRoot?.querySelector('body')} > <DialogOverlay /> {/* 其他代码... */} </DialogPortal> ))
|
之后便可正常使用 Dialog 组件了,它现在会正确渲染到 custom-element 组件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <custom-element> #shadow-root <style> .dialog-container { } </style> <body> <div class="dialog-container"> </div> </body> </custom-element>
|
Toast
Toast 组件没有提供类似 Dialog 的 Portal 组件,需要在根组件中添加 Toaster 组件。
1 2 3 4 5 6 7 8 9 10
| <> <App /> {createPortal( <Toaster />, document .querySelector('custom-element') ?.shadowRoot?.querySelector('body'), )} </>
|
总结
以上就是使用 shadui/cn 时在指定 DOM 中渲染弹窗的方法,在编写 Chrome 扩展时虽然已经有了 WXT 这种非常优秀的开发者工具,但仍然存在与普通网页不同的问题。