一、前言
浏览器插件 泛指 Tampermonkey, Violentmonkey, Greasemonkey 这类给页面注入 js 的浏览器扩展。
通过在浏览器扩展在网页加载时注入特定的 js 达到更改页面布局效果等等一切 js 能做的事情。这些扩展也提供了一些网页没有的 api,使注入的 js 能突破网页默认的限制,实现其他的功能。
二、了解 Tampermonkey
早期浏览器脚本的执行机制:动态创建一个 script 元素插入到 document ,当脚本执行完毕后删除 script 元素。但是这种方式有几个缺陷:
- 插入 script 标签时会触发 DOMNodeInserted 事件,如下:
js
document.addEventListener("DOMNodeInserted",()=>{
// ...
},true)
- 脚本封装的 GM_API 有泄露风险。可能会被恶意网页调用 GM_API 实现跨网页窃取数据。
- 脚本可以可以通过 file:// 路径来发起请求,读取用户磁盘数据,但这也给恶意网页开了后门,一旦被反向注入会导致用户本地隐私数据泄密。
后面脚本新增了沙盒模式(iframe 方案/with+proxy方案)进行重构,不再通过注入 script 元素的方式,也不会在宿主环境全局定义 GM_API。并引入了 XPCNativeWrappers 机制(返回了一个包装过的数据,这个数据是可以信任的,确保不会被网页进行恶意篡改)。
但是这个方案也不是完美的,也有一些问题:如在设置 onclick 回调函数的时候不可以直接进行赋值;在设置自定义属性的时候没有作用。这些都是因为可信任进程通信管道返回的是一个包装后的数据,而不是原来的数据本身,这时候我们只能使用 addEventListener 来进行添加 onclick 回调函数,使用 setAttribute 来设置自定义属性等等,这很繁琐。
为了妥协,Tampermonkey 提出了 unsafewindows ,它是原网页数据的一个映射,我们操纵它就相当于操纵原网页。注意:一旦使用了 unsafewindows,代表破坏了安全模型!并且 unsafewindows 的内容并不完全可信,需要谨慎处理!
默认情况下,脚本运行在沙盒环境下,此环境无法访问到前端 dom 。若声明 @grant none,那脚本就会被直接放在浏览器环境上下文中执行,这时脚本上下文(this)就是浏览器上下文(this)。这更加方便获取 dom 节点,但会导致 GM_API 无法使用。
grant 属性可用来申请 GM_ * 函数和 unsafeWindow 权限。
- none 就是直接运行在前端页面中,此时脚本中的 this 指向宿主网页的 window 对象。
- unsafeWindow 和 GM_* 就是运行在沙盒环境,需要使用 unsafeWindow 去操作前端的元素。
常用头部信息
名称 | 描述 | 说明 |
---|---|---|
@name | 脚本名称 | |
@namespace | 脚本命名空间 | |
@version | 脚本版本 | 语义化版本规则 |
@author | 脚本作者 | - |
@description | 脚本描述 | - |
@include | 脚本匹配地址 | 可以使用正则表达式/通配符 * |
@match | 脚本匹配地址 | 允许使用通配符,更加严格 |
@exclude | 排除脚本匹配地址 | - |
@require | 引入外部 JS 文件 | 加载并执行的 js文件,如 UNPKG、jsDelivr 等 cdn 或本地资源 |
@resource | 预加载资源 | 将外部库保存成资源,可以在代码中通过 GM_getResourceURL 访问 |
@connect | 获取网站访问权限 | 允许由 GM_xmlhttpRequest 检索的子域 |
@run-at | 脚本运行时机 | 可选项:document-start、body、end、idle、menu |
@grant | 申请脚本环境/GM_API | none 表示直接注入宿主环境,unsafeWindow 表示沙盒环境,其他权限由 API 名称指定 |
@noframes | 脚本标记 | 标记使脚本在主页上运行,但不在 iframe 上运行 |
下面列举部分常用 GM_API,其它可见:官方文档
名称 | 描述 |
---|---|
GM_addStyle | 将给定样式添加到文档中并返回注入的样式元素 |
GM_addElement | 创建指定的 HTML 元素,应用所有给定的属性并返回注入的 HTML 元素,此功能是实验性的 |
GM_setValue | 存储一个给定名称的值 |
GM_getValue | 从 GM_setValue 存储的名称中获取值 |
GM_deleteValue | 将 GM_setValue 存储的名称删除 |
GM_listValues | 列出存储的所有名称 |
GM_addValueChangeListener | 侦听 GM_setValue 储存名称的值的更改并返回更改前和后的值 |
GM_removeValueChangeListener | 删除由 GM_addValueChangeListener 添加的侦听器 |
GM_log | 向控制台记录消息 |
GM_getResourceText | 获取由 @resource 预加载的资源 |
GM_getResourceURL | 获取由 @resource 预加载的 Base64 编码 URI |
GM_registerMenuCommand | 注册一个菜单,在运行此脚本的页面的中显示 |
GM_unregisterMenuCommand | 取消由 GM_registerMenuCommand 注册的菜单 |
GM_openInTab | 通过给定的 URL 打开一个新标签页 |
GM_xmlhttpRequest | 通过脚本发送的 XHR 请求 |
GM_download | 通过给定的 URL 下载文件到本地 |
GM_saveTab | 保存选项卡对象,生命周期为选项卡的打开->关闭 |
GM_getTab | 获取选项卡对象,生命周期为选项卡的打开->关闭 |
GM_getTabs | 获取所有选项卡对象,生命周期为选项卡的打开->关闭 |
GM_notification | 显示桌面通知 |
GM_setClipboard | 将数据复制到剪贴板 |
三、工程化
最基础的开发方式是通过插件自带的在线编辑器开发,但有着诸多问题:只能写原生 js、没有代码提示、没有 js model ,只能通过 @require API 引入第三方库、没有工程化概念,复杂工程管理困难、没有模块热更新等。
第二种方式:通过脚本 @require file:\C:\Users\userName\Desktop\tm_demo\xxx.user.js 引用本地资源这种方式,配合 IDE ,如 vscode 开发,但这种只解决了部分问题,复杂项目依然难以维护。
第三种是通过 puppeteer,加载 Tampermonkey 魔改插件,对插件进行了非侵入式hook,并且起了个服务器跟浏览器的插件用 socket 通信,实现了文件的自动监控,授权修改,动态更新。原理可参考:李恒道---尝试抹平Tampermonkey的VSCode开发体验。
第四种:最常见的是通过 webpack/rollup/uglify-js/esbuild/vite 等构建工具,我选择了 vite-plugin-monkey,vite 相比 webpack 的优势在于:
- 明确区分了 开发/构建 模式
- 暴露了本地开发服务器的 api 供外部使用
- 更加轻量快速的热重载
- 支持中间件,用来生成中间桥接代码,无需每次生成文件或手动填写代码
具体可参考 vite-plugin-monkey 文档
四、常见问题
1. 异步获取元素
获取 dom 节点时可能会出现元素延迟加载的问题,常见解决方案如下:
使用 settimeout 获取,缺点:实时性不足(需要等当前 tick),有性能损失
DOMNodeInserted 事件的性能也 不好
MutationObserver ,缺点:语法复杂
使用第三方库:如 ElementGetter 等,
2. 存在 iframe 框架
存在某个网页通过 iframe 内嵌了其它网页需要注入,解决方案如下:
@match 匹配到脚本内部
在同域的情况下可以获取到 iframe 元素后通过 conetentWindow 属性访问 iframe 的作用域
3. 需要掌握的 JS API
MutationObserver 参考:DOM 变动观察器(Mutation observer)
Proxy 参考:Proxy 和 Reflect
4. 需要知道的 CSS 知识点
需要知道:选择器