油猴(Tampermonkey) 入门指北

一、前言

浏览器插件 泛指 Tampermonkey, Violentmonkey, Greasemonkey 这类给页面注入 js 的浏览器扩展。

通过在浏览器扩展在网页加载时注入特定的 js 达到更改页面布局效果等等一切 js 能做的事情。这些扩展也提供了一些网页没有的 api,使注入的 js 能突破网页默认的限制,实现其他的功能。

二、了解 Tampermonkey

早期浏览器脚本的执行机制:动态创建一个 script 元素插入到 document ,当脚本执行完毕后删除 script 元素。但是这种方式有几个缺陷:

  1. 插入 script 标签时会触发 DOMNodeInserted 事件,如下:
js 复制代码
document.addEventListener("DOMNodeInserted",()=>{
  // ...
},true)
  1. 脚本封装的 GM_API 有泄露风险。可能会被恶意网页调用 GM_API 实现跨网页窃取数据。
  2. 脚本可以可以通过 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 知识点

需要知道:选择器

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进2 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er2 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063712 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl2 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码2 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html
辛-夷3 小时前
VUE面试题(单页应用及其首屏加载速度慢的问题)
前端·javascript·vue.js
田哥coder3 小时前
充电桩项目:前端实现
前端