避免样式污染:Shadow DOM 在浏览器油猴脚本中的实战应用
- 前端开发
- 1天前
- 9热度
- 0评论
0 前言
Violentmonkey(暴力猴)和 Tampermonkey(油猴)是两款流行的浏览器脚本管理器,它们的主要功能是帮助用户管理和运行用户脚本。其中 Tampermonkey 是闭源的,可能不适合对代码透明度有严格要求的用户。Violentmonkey 是 Tampermonkey 的开源实现,兼容 Tampermonkey 所有能力,遵循 MIT 协议,代码透明,适合对隐私和安全性有较高要求的用户。
在开发浏览器油猴脚本(UserScript)时,我们经常需要引入第三方 CSS 框架(如 Tailwind CSS)和 UI 库(如 SweetAlert2)来快速构建美观的界面。然而,这些样式库的全局特性往往会"污染"目标网站的样式,导致页面布局被意外修改的面目全非。
本文将分享如何通过 Shadow DOM 技术实现完美的样式隔离,让油猴脚本的 UI 组件既能享受现代 CSS 框架的便利,又不会影响目标网站的原有样式。
1 问题背景
1.1 样式污染的痛点
在开发一个媒体链接抓取工具的油猴脚本时,我遇到了这样的问题:
- Tailwind CSS 的全局重置:Tailwind 的
preflight样式会重置全局样式,影响目标网站的布局 - SweetAlert2 的样式冲突:弹窗库的样式可能与网站的自定义样式产生冲突
- CSS 选择器污染:即使使用命名空间,也难以完全避免样式泄露
2 Shadow DOM:完美的环境隔离方案
2.1 什么是 Shadow DOM?
Shadow DOM 是 Web Components 标准的一部分,它提供了一个封装的 DOM 树,具有以下特性:
- 样式隔离:Shadow DOM 内的样式不会泄露到外部,外部样式也不会影响 Shadow DOM
- DOM 封装:Shadow DOM 内的元素对外部不可见(除非使用
mode: 'open') - 作用域隔离:JavaScript 作用域也被隔离
2.2 为什么选择 Shadow DOM?
相比其他方案,Shadow DOM 具有以下优势:
- 原生支持:现代浏览器都支持,无需 polyfill
- 完美隔离:真正的样式隔离,不是"伪隔离"
- 性能优秀:浏览器原生实现,性能开销小
- 维护简单:不需要复杂的命名空间管理
3 技术实现
3.1 创建 Shadow DOM 容器
首先,我们需要创建一个 Shadow DOM 容器来承载所有 UI 组件:
function createShadowHost() {
if (shadowHost) return shadowRoot;
// 创建宿主元素
shadowHost = document.createElement('div');
shadowHost.id = 'm3u8-capture-shadow-host';
// 覆盖整个视口,但不阻止页面交互
shadowHost.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;';
document.body.appendChild(shadowHost);
// 创建 Shadow DOM(open 模式允许外部访问)
shadowRoot = shadowHost.attachShadow({ mode: 'open' });
return shadowRoot;
}
关键点:
- 使用 pointer-events: none 让宿主元素不阻止页面交互
- 子元素通过 pointer-events: auto 恢复交互能力
- mode: 'open' 允许外部 JavaScript 访问(调试方便)
3.2 在 Shadow DOM 内加载 CSS
接下来,我们需要将 Tailwind CSS 和 SweetAlert2 的样式加载到 Shadow DOM 内:
// ==UserScript==
// @author lzw
// @updateURL https://raw.githubusercontent.com/lzwme/m3u8-dl/refs/heads/main/client/m3u8-capture.user.js
// @downloadURL https://raw.githubusercontent.com/lzwme/m3u8-dl/refs/heads/main/client/m3u8-capture.user.js
// @match *://*/*
// @grant GM_addElement
// @resource SwalJS https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.js
// @resource SwalCSS https://s4.zstatic.net/ajax/libs/sweetalert2/11.16.1/sweetalert2.min.css
// @resource TailwindCSS https://s4.zstatic.net/ajax/libs/tailwindcss/2.2.19/tailwind.min.css
// ==/UserScript==
// 在 Shadow DOM 内加载 Tailwind CSS
addCssOrScript('TailwindCSS', shadowRoot, 'css');
// 在 Shadow DOM 内加载 SweetAlert2 CSS,并修改部分内容以适应 Shadow DOM
addCssOrScript(GM_getResourceText("SwalCSS").replace(':root{', `#m3u8-capture-container{`).replace(/body/g, ""), shadowRoot, 'css');
// 添加基础样式重置
const styleTextContent = `
:host {
all: initial;
font-family: system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
`;
addCssOrScript(styleTextContent, shadowRoot, 'css');
/** 使用 GM_addElement 创建 style 或 script 元素,避免 CSP 策略拦截问题 */
function addCssOrScript(key, parentEl = document.head, type = 'css') {
// 如果 key 长度小于 50,则认为是资源文本,否则认为是字符串
const textContent = key.length < 50 ? GM_getResourceText(key) : key;
return GM_addElement(parentEl, type === 'css' ? 'style' : 'script', {
type: type === 'css' ? 'text/css' : 'text/javascript',
textContent: textContent
});
}
关键点:
- :host 选择器用于设置 Shadow DOM 宿主元素的样式
- all: initial 重置所有样式,确保不影响外部
- 通过 GM_getResourceText 和 GM_addElement 创建 style 或 script 元素并添加至 Shadow DOM 内,可以避免 CSP 拦截
- 对于 SweetAlert2,需要修改css样式内容以适应 Shadow DOM 环境
3.3 创建 UI 组件
所有 UI 组件都创建在 Shadow DOM 内:
function createUI() {
const root = createShadowHost();
const panel = document.createElement('div');
panel.className = 'fixed w-[420px] bg-white border-2 border-blue-500 rounded-xl shadow-2xl';
panel.style.cssText = 'position: fixed; pointer-events: auto; z-index: 1059;';
// 将面板添加到 Shadow DOM
root.appendChild(panel);
}
3.4 处理第三方库:SweetAlert2 的特殊处理
SweetAlert2 默认会在 document.body 上创建弹窗,我们需要让它支持 Shadow DOM。这里有两个步骤。
步骤一:修改并加载 JS 源码
通过 GM_addElement 和字符串替换,修改 SweetAlert2 的挂载目标:
function loadSwal() {
let swalTarget = document.body;
// 定义挂载容器函数
globalThis.SetSwalTarget = (newTarget) => (swalTarget = newTarget);
globalThis.GetSwalTarget = () => swalTarget;
// 读取并修改 SweetAlert2 源码
const SwalJS = GM_getResourceText("SwalJS")
// 替换挂载目标
.replace(/document\.body/g, "GetSwalTarget()");
// 使用 GM_addElement 创建 script,避免 CSP 拦截
addCssOrScript(SwalJS, document.head || document.documentElement, 'script').then(() => {
// 创建 SweetAlert2 容器
const swalContainer = document.createElement('div');
shadowRoot.appendChild(swalContainer);
// 设置 SweetAlert2 的目标容器
SetSwalTarget(swalContainer);
Swal = Swal.mixin({ target: swalContainer });
})
}
关键点:
- 使用 GM_addElement 而不是 eval,避免 CSP 安全策略拦截
- 通过字符串替换修改源码中的 document.body 为自定义函数
- 使用 Swal.mixin() 配置默认目标容器
步骤二:CSS 作用域处理
加载 Swal CSS 样式,并修改内容以兼容 CSS 作用域:
const SwalCSS = GM_getResourceText("SwalCSS").replace(/body/g, "").replace(':root{', `#m3u8-capture-container{`);
const swalStyle = document.createElement('style');
swalStyle.textContent = SwalCSS;
shadowRoot.appendChild(swalStyle);
4 技术难点与解决方案
4.1 事件穿透问题
问题:Shadow DOM 内的元素需要接收事件,但宿主元素设置了 pointer-events: none。
解决方案:
// 宿主元素:不阻止事件
shadowHost.style.pointerEvents = 'none';
// 子元素:恢复事件接收
panel.style.pointerEvents = 'auto';
swalContainer.style.pointerEvents = 'auto';
4.2 焦点事件处理
问题:SweetAlert2 弹窗需要接收焦点事件,但可能被 pointer-events: none 影响。
解决方案:
#m3u8-capture-swal-container,
#m3u8-capture-swal-container * {
pointer-events: auto !important;
}
4.3 CSP 安全策略
问题:使用 eval() 执行动态代码会被 CSP 拦截。
解决方案:使用 GM_addElement API:
// ❌ 会被 CSP 拦截
eval(SwalJS);
// ✅ 绕过 CSP 限制
GM_addElement(document.head, 'script', {
type: 'text/javascript',
textContent: SwalJS
});
4.4 样式作用域
问题:确保 Shadow DOM 内的样式不影响外部。
解决方案:
:host {
all: initial; /* 重置所有样式 */
font-family: system-ui, -apple-system, sans-serif;
}
5 完整示例
以下是一个完整的 Shadow DOM 样式隔离实现:
// 创建 Shadow DOM 容器
function createShadowHost() {
if (shadowHost) return shadowRoot;
// 1. 创建宿主元素
shadowHost = document.createElement('div');
shadowHost.id = 'm3u8-capture-shadow-host';
shadowHost.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;';
document.body.appendChild(shadowHost);
// 2. 创建 Shadow DOM
shadowRoot = shadowHost.attachShadow({ mode: 'open' });
// 3. 加载 CSS 到 Shadow DOM
// 在 Shadow DOM 内加载 Tailwind CSS
addCssOrScript('TailwindCSS', shadowRoot, 'css');
// 在 Shadow DOM 内加载 SweetAlert2 CSS,并修改样式以适应 Shadow DOM
addCssOrScript(GM_getResourceText("SwalCSS").replace(':root{', `#m3u8-capture-container{`).replace(/body/g, ""), shadowRoot, 'css');
// 4. 添加基础样式
const style = document.createElement('style');
style.textContent = `
:host {
all: initial;
font-family: system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
`;
shadowRoot.appendChild(style);
// 5. 创建第三方库容器
const swalContainer = document.createElement('div');
swalContainer.id = 'm3u8-capture-swal-container';
swalContainer.style.pointerEvents = 'auto';
shadowRoot.appendChild(swalContainer);
return shadowRoot;
}
5.1 性能优化建议
- 延迟加载:CSS 文件可以延迟加载,减少初始加载时间
- 缓存优化:使用 CDN 并设置合适的缓存策略
- 按需加载:只在需要时创建 Shadow DOM
- 事件委托:在 Shadow DOM 内使用事件委托减少事件监听器
5.2 浏览器兼容性
Shadow DOM 在现代浏览器中支持良好:
- Chrome/Edge: 53+
- Firefox: 63+
- Safari: 10.1+
- Opera: 40+
对于不支持 Shadow DOM 的旧浏览器,可以考虑使用 polyfill(如 @webcomponents/shadydom),但通常油猴脚本的目标用户使用的是现代浏览器。
5.3 总结
通过 Shadow DOM 技术,我们实现了:
✅ 完美的样式隔离:Tailwind CSS 和 SweetAlert2 的样式完全不影响目标网站
✅ 灵活的组件封装:UI 组件可以自由使用现代 CSS 框架
✅ 良好的用户体验:不影响原网站的功能和样式
✅ 易于维护:不需要复杂的命名空间管理
Shadow DOM 不仅解决了样式污染问题,还为构建更复杂的油猴脚本 UI 提供了坚实的基础。如果你也在开发油猴脚本,不妨试试这个方案,相信会给你带来惊喜!
6 参考资源
- Violentmonkey 暴力猴文档
- Tampermonkey 油猴文档
- 【油猴脚本开发指南】魔改sweetalert2支持shadowRoot
- MDN: Shadow DOM
- Web Components 标准
- Tailwind CSS 文档
- SweetAlert2 文档
作者简介:专注于前端技术和用户体验,喜欢分享实用的技术解决方案。
技术交流:欢迎在评论区分享你的 Shadow DOM 使用经验,或者提出任何问题!