【轻松上手】油猴脚本开发

24,195 阅读12分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

注:本文的油猴特指 Tampermonkey。

油猴脚本运行于油猴插件之上,油猴插件本质上对浏览器能力的再封装。既然如此,我们先来简单了解一下浏览器插件。

浏览器插件(Browser Extension):浏览器的扩展应用

说的直白一点,就是拿着浏览器开放的能力(插件 API),去实现一些小型应用。

浏览器插件主要由四部分构成:background scripts、content scripts、全局 UI 元素、options page。

  • background scrips: 后台脚本,一个后台脚本是一个独立线程,是游离于各个页面之外的“上帝之眼”。具有访问各类插件 API 的能力,但同时也丧失了直接操作页面的能力;
  • content scripts:内容脚本,具有直接操作页面的能力。其实就是在页面中运行 js 脚本,可以使用 DOM API。content script 只能直接访问少量插件 API,但能和 background script 进行双向通信完成数据交换;
  • 全局 UI 元素:浏览器层的 UI 交互,包括
    • 在 Toolbar 显示 icon,定义点击 icon 后显示的 Popup 或其他效果
    • 增加右键选项
    • 增加全局快捷键
    • 改造新 Tab 页、历史记录页、书签页
  • options page:插件配置页

浏览器插件的核心机制可以用下图简单概括

50c4c582f8f1968c5699220d1c2fc0c99a1212a171bcefa37d1aecdaccbaed5e.png

想必,大家最好奇的还是有哪些 API 以及能用这些 API 做什么,这里例举几个:

  • contextMenus:增加右键选项
    • 使用选中文本,例如:划词翻译、文本收集
    • 快速调用插件功能,例如:打开 DevTool,页面剪藏
  • cookies:增删改查 cookie(任意域名),直接拿着本地 cookie 发送请求,不必再做授权。同时由于后台脚本不是 Web 页面,在发送请求时没有跨域限制。:
    • 多平台信息聚合
    • 多平台信息分发
  • devtools.panels:增加 Devtool 面板,这个对前端开发者来说应该很熟悉,React Developer Tools、Vue.js devtools
  • notifications:浏览器通知,未打开页面的情况下进行通知,可以辅助一些工具类应用
  • storage:全局保存数据,可跟随浏览器账户同步

这里例举的只是我常用的一些,只是沧海一粟,更多 API 可以查阅

浏览器插件就简单介绍到这里,如果有兴趣继续了解,推荐:ChromeEdgeMozilla 三家的文档。

油猴插件(Tampermonkey)

浏览器插件可以实现各式各样的功能,但有时候开发者只是想对某一个站点加一点点小功能,如果这也要构建环境打包上架分发,未免就太麻烦了一些;从应用市场角度来看,充斥着颗粒化的应用,市场也会拥挤繁杂不堪。

油猴插件为轻量化脚本提供了一个平台,在线编辑器中编写油猴脚本即时生效,通过 Github、GreasyFork 快速分发。

在油猴插件中, content script 起到非常重要的角色,它将用户编写的代码运行在页面中,同时提供 GM_xxxx 函数封装浏览器的部分能力。封装的内部实现是和 background script 通信,驱动 background script 调用插件 API。

对油猴插件简单了解之后,来看看如何编写油猴脚本。

Tampermonkey API

油猴脚本由头部和核心逻辑两部分组成

// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://www.tampermonkey.net/documentation.php?ext=dhdg
// @icon         https://www.google.com/s2/favicons?domain=tampermonkey.net
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Your code here...
})();

头部是脚本的一些元信息、更新方式、指定运行页面、权限声明,逐一解释一下

配置名作用使用技巧
@name脚本的显示名称加后缀实现国际化,例如,@name:zh-CN 指定在浏览器语言为中文时显示的名称
@namespace脚本的命名空间,可以理解为脚本的标识为了避免冲突一般使用 github 仓库地址
@version与更新相关,当前版本
@updateURL检查脚本是否更新地址配合 @version 和自动更新使用
@downloadURL检测到更新时,去哪下载脚本
@supportURL遇到问题时,用户去哪反馈
@include脚本在哪些页面运行可使用正则,不支持 hashtag,多个页面的地址声明多个 @include 即可
@match与 @include 类似
@exclude脚本禁止在哪些页面运行,优先于 @include
@require在脚本运行前引入外部 JavaScript 文件例如,引入 jQuery
@resource声明外部资源文件,搭配 GM_getResourceText 使用例如引入 html、icon
@connect声明 GM_xmlhttpRequest 可访问的域必须指定才能正常请求
@grant声明 GM_xxx 函数的使用列表必须先指定权限才能正常使用
@run-at指定脚本运行时机document-start: 尽快执行
document-body: 当 body 挂载时执行
document-end: DOMContentLoaded 触发时执行
document-idle: DOMContentLoaded 触发后执行,也是默认设置项
context-menu: 右键菜单项被点击时执行
@author作者名
@description简短介绍同样可以加后缀实现国际化
@homepage主页地址如果未设置并且 @namespace 是仓库地址,默认导向仓库地址
@icon脚本 icon
@icon6464x64像素的脚本 icon
@antifeature脚本是否有广告、挖矿、数据收集等商业行为
@noframes声明脚本不在 iframe 中运行

核心逻辑通过一个立即执行函数包裹,避免和全局作用域相互干扰。Tampermonkey 将浏览器的部分能力封装为 GM_XXX 函数以供调用。

API作用使用技巧
unsafeWindow访问页面的 Window 对象
GM_addStyle(css)创建全局样式的快捷方式,向页面插入 style 元素也可以用 DOM 操作手动创建
GM_addElement(tag_name, attributes)
GM_addElement(parent_node, tag_name, attributes)
向 DOM 新建元素的快捷方式也可以用 DOM 操作手动创建
GM_log(message)在 console 中打印信息console.log 的快捷方式
GM_setValue(name, value)持续化存储数据
GM_getValue(name, defaultValue)从存储体中获取数据
GM_deleteValue(name)从存储体中删除数据
GM_listValues()列举存储体中所有数据项
GM_addValueChangeListener监听数据更新例如要使 Tab 间数据同步,可以用监听 value 达成同步
GM_removeValueChangeListener移除监听
GM_getResourceText(name)获取 @resource 中已声明的资源
GM_getResourceURL(name)获取 @resource 中已声明的资源(base64 URI 形式)
GM_registerMenuCommand(name, fn, accessKey)在 Tampermonkey 的 popup 中增加选项
GM_unregisterMenuCommand(menuCmdId)移除选项
GM_openInTab(url, options)新开一个 tab 页
GM_xmlhttpRequest(details)使用后台脚本进行请求,自动带上 cookie,无跨域问题,目标域需要在 @connect 中提前声明
GM_download(details)下载资源到本地
GM_getTab(callback)获取当前 tab 的 object 对象
GM_saveTab(tab)通过 tab 的 object 对象重新打开一个 tab
GM_getTabs(callback)获取当前存活的所有 tab 的对象,以便和其他脚本实例偶同学
GM_notification使用插件 notification API 弹出桌面通知
GM_setClipboard复制内容到剪贴板
GM_info获取脚本的油猴插件的信息

完整的说明文档:Tampermonkey documentation

实践:打印 Hello, World

做一个非常简单的小练习:创建一个名为 "Hello" 的脚本,当进入掘金和知乎页面时,在 Console 中打印 "Hello, World"。

image.png

  1. 新建脚本
  2. 修改脚本名称
  3. 指定运行地址 @match@include
  4. 直接使用 console.log 或者声明权限调用 GM_log
// ==UserScript==
// @name         Hello
// @namespace    http://tampermonkey.net/hello
// @version      0.1
// @description  try to take over the world!
// @author       You Name
// @match        https://zhihu.com/*
// @match        https://juejin.cn/*
// @grant        GM_log
// ==/UserScript==

(function() {
    'use strict';
    GM_log("Hello World");
})();

搭建舒适的开发环境

使用在线编辑器小试牛刀之后,或许你也发现在线编辑器

  • 缺少语法补全和自动提示
  • 难以格式化代码

不免怀念起 VSCode。

或许你还会有更深远的考虑,在线编辑器编辑完成后:

  • 怎么同步到远程仓库,怎么做代码分发
  • 如果要用到新语法,怎么保证跨浏览器兼容性
  • 如果代码越写越多,没有模块化怎么管理
  • 没有 TS,很难保证长期维护

这些坑我已经踩过了,并且抽出一个脚手架工具 create-tampermonkey - npm (npmjs.com),一键搭建舒适的油猴脚本开发环境。

Screen Shot 2021-10-24 at 9.54.53 PM.png

脚手架集成 rollup + babel + eslint + typescript,支持:

  • 自动生成 UserScript Header
  • 语法和类型系统:ESNext、ES Module、TypeScript
  • 样式系统:CSS Modules,以及 scss、sass、less、stylus(需安装对应依赖)
  • 静态资源:导入图片、SVG 转换为 Base64,同时支持 SVG Sprite
  • 多语言
  • 扩展:基于 Rollup,可以按需安装插件进行扩展

create-tampermonkey 启动项目

初始化项目

npx create-tampermonkey demo-userscript
或者
npm init tampermonkey demo-userscript
或者
yarn create tampermonkey demo-userscript

初始化完毕后,进入目录安装依赖 Screen Shot 2021-10-24 at 9.54.53 PM.png

npm run dev 跑起开发模式

Screen Shot 2021-10-24 at 10.07.27 PM.png

到浏览器中打开 dev.user.js,自动进入 Tampermonkey 脚本安装界面

Screen Shot 2021-10-24 at 10.10.21 PM.png

最后一步:访问 chrome://extension,找到油猴插件的卡片,点击 Details 进入配置界面

Screen Shot 2021-10-25 at 9.15.10 AM.png

勾选 Allow acess to file URLs

image.png

刷新页面,出现弹窗,一切就绪。

Screen Shot 2021-10-24 at 10.12.15 PM.png

用 VSCode 打开项目,这时右下角会推荐一些辅助插件,建议安装。

代码中使用到的 GM_xxx 会自动提取到 UserScript Header 中,当然也可以在 src/meta.json 中自定义。

代码的默认入口是 src/main.js 文件。

实践:掘金签到功能

基于上面初始化的项目 demo-userscript 做一个小功能:掘金签到功能。

1. 定位请求

“掘金签到”本质是调用接口,我们的实现思路是追踪点击“立即签到”按钮时请求发送情况,定位到

Screen Shot 2021-10-24 at 10.21.16 PM.png

2. 调试接口

打开 Postman 做一下调试,这里有一个导入小技巧。

右键拷贝 cURL

Screen Shot 2021-10-24 at 10.24.44 PM.png

到 Postman 中通过 curl 导入整个请求:点击左侧面板中的 import 按钮,选择 Raw text 粘贴上一步复制的内容即可。

Screen Shot 2021-10-24 at 10.25.09 PM.png

3. 获取参数

Screen Shot 2021-10-24 at 10.27.42 PM.png

在 Postman 中发现请求需要 aiduuid_signature 三个参数,试试看不带参数能否请求成功,先确定好必不可少的参数和请求头。

简单尝试后,发现这里并不需要带 aiduuid_signature 三个参数,主要是依赖 cookie,使用 GM_xmlhttpRequest 会自动带上对应的 cookie,事情变得简单。

修改 src/main.js 的代码

GM_xmlhttpRequest({
  url: "https://api.juejin.cn/growth_api/v1/check_in",
  method: "POST",
  headers: {
    "content-type": "application/json",
    "user-agent": navigator.userAgent,
  },
  responseType: "json",
  onload(response) {
    if (response.status === 200) {
      const data = response.response;
      if (data.data === "success") {
        alert("签到成功");
      } else {
        alert(data.err_msg);
      }
    }
  },
});

刷新页面测试一下。在其他站点刷新一下居然也可以发送请求,这就是插件没有跨域限制的优势了。

再做一下节流优化。利用 GM_setValueGM_getValue 做持续存储。

const storageKey = "last_sign_timestamp";
// 获取上一次签到的日子
const lastSignNumberOfDays = GM_getValue(storageKey, 0);
// 计算现在所在的日子
const currentNumberOfDays = Math.floor(
  new Date().valueOf() / 1000 / 60 / 60 / 24
);

// 如果今天已经请求过,不再请求
if (currentNumberOfDays !== lastSignNumberOfDays) {
  GM_xmlhttpRequest({
    url: "https://api.juejin.cn/growth_api/v1/check_in",
    method: "POST",
    headers: {
      "content-type": "application/json",
      "user-agent": navigator.userAgent,
    },
    responseType: "json",
    onload(response) {
      if (response.status === 200) {
        const data = response.response;
        if (data.data === "success") {
          alert("签到成功");
        } else {
          alert(data.err_msg);
        }
        // 更新最近一次签到的日子
        GM_setValue(storageKey, currentNumberOfDays);
      }
    },
  });
}

难免会遇到需要获取数据的情况,可访问的数据一般有三种:

  1. 页面中包含数据,通过 DOM 获取
  2. 通过接口请求得到
  3. 存储在本地存储中,localStorage 或 cookie 之类

确定方式很粗暴:

  1. 复制参数或参数值到 Element 中搜索;
  2. 查看前面几个请求,看看是否有迹可循;
  3. 到 localStorage 或 cookie 中搜索。

分发脚本

在本地开发完脚本之后,npm run build 构建生产版本并上传代码到 Github 或 Gitee。

用 Github/Gitee 上文件的 Raw URL 就能直接实现分发。如果在 package.json 中设置好 repositorycreate-tampermonkey 会自动生成 Raw URL 并赋给downloadURLupdateURL

但这样分发存在的问题是无法统计下载量、从网络访问的角度考虑同时维护 Github 和 Gitee 两个仓库。另一种分发方式是上传到脚本平台 greasyfork.org/ , 登录后即可发布新脚本,如果代码托管在 Github 或 GitLab 还可以使用 Webhooks 实现自动更新。

开发技巧

调试油猴脚本

油猴脚本的运行依托于 background scriptcontent script,在调试前需要对运行环境有所区分,例如 GM_xmlhttpRequest 请求是 background script 发出的,DOM 处理和脚本逻辑是 content script 执行的。

确定环境之后,就可以使用对应的调试方式进行调试了。

调试 background script

还是访问 chrome://extension,找到油猴插件的卡片

Screen Shot 2021-10-25 at 9.15.18 AM.png

inspect views 后面有个 background.html,点击一下弹出 background script 的调试弹窗。

调试 content script

在网页 inspect -> Sources -> Page 下找到 Tampermonkey 目录,页面中运行的油猴脚本代码都在这了,选择目标,断点调试即可。

image.png

获取 userId 等信息

有时候需要拿一些额外信息做请求,一般有三种方式:

  1. 看看能不能在页面中搜索到,通过 DOM 获取
  2. 看看有没有接口可以调用获取
  3. 看看本地存储里有没有

目标 DOM 节点未挂载怎么办?

如果节点是在首屏加载的,粗暴的方法是使用 setTimeout 做一下延时。

但如果是在交互过程中有 DOM 更新,就只能引入监听机制了,使用 MutationObserver 来实现。

具体的实例可以看 【开发记录】掘金 “破圈行动” 辅助脚本 - 掘金 (juejin.cn)

查看插件源码

浏览器插件安装之后,插件包被下载到本地目录中,可通过下述方法访问。

访问 chrome://version,找到 Profile path(存放用户数据的路径) image.png

访问 chrome://extensions/,找到目标插件的 ID

image.png

将 Profile Path 和插件 ID 拼装在一起 ${Prifile Path}/Extensions/${Extension Id},便是插件包的路径了。友情提示,通过命令行访问时需要在空格前加个 \ 转义一下。

总结

浏览器插件利用浏览器能力进行功能扩展,具有跨域请求、读取 cookie、管理历史记录、注册右键项等能力。

浏览器插件的能力很丰富,能够实现复杂的功能。但如果只是做一些针对页面的操作,只需要依赖基础能力,完全可以使用油猴脚本实现,开发更便捷分发更迅速。

开发油猴脚本,主要是使用 Tampermonkey API 和 JavaScript。

create-tampermonkey 脚手架提供一个全面的油猴脚本开发环境,依托这个环境,可以使用最新的 ES 语法、TypeScript、CSS Modules,在 VSCode 中进行模块化开发,大大提高开发效率。

开发完毕的油猴脚本可通过 Github/Gitee Raw URL 或 Greasy Fork 平台分发。

浏览器插件的主要分工为 background scriptcontent script 两部分,在调试油猴脚本时需要思考清楚是哪一部分出现的问题,再采用对应的调试方式。

实现了两个小实践,走出第一步,接下来尽情发挥创造力吧,玩得开心~