本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
注:本文的油猴特指 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:插件配置页
浏览器插件的核心机制可以用下图简单概括
想必,大家最好奇的还是有哪些 API 以及能用这些 API 做什么,这里例举几个:
contextMenus
:增加右键选项- 使用选中文本,例如:划词翻译、文本收集
- 快速调用插件功能,例如:打开 DevTool,页面剪藏
cookies
:增删改查 cookie(任意域名),直接拿着本地 cookie 发送请求,不必再做授权。同时由于后台脚本不是 Web 页面,在发送请求时没有跨域限制。:- 多平台信息聚合
- 多平台信息分发
devtools.panels
:增加 Devtool 面板,这个对前端开发者来说应该很熟悉,React Developer Tools、Vue.js devtoolsnotifications
:浏览器通知,未打开页面的情况下进行通知,可以辅助一些工具类应用storage
:全局保存数据,可跟随浏览器账户同步
这里例举的只是我常用的一些,只是沧海一粟,更多 API 可以查阅
浏览器插件就简单介绍到这里,如果有兴趣继续了解,推荐:Chrome,Edge,Mozilla 三家的文档。
油猴插件(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 | |
@icon64 | 64x64像素的脚本 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"。
- 新建脚本
- 修改脚本名称
- 指定运行地址
@match
或@include
- 直接使用
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),一键搭建舒适的油猴脚本开发环境。
脚手架集成 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
初始化完毕后,进入目录安装依赖
npm run dev
跑起开发模式
到浏览器中打开 dev.user.js
,自动进入 Tampermonkey 脚本安装界面
最后一步:访问 chrome://extension
,找到油猴插件的卡片,点击 Details 进入配置界面
勾选 Allow acess to file URLs
刷新页面,出现弹窗,一切就绪。
用 VSCode 打开项目,这时右下角会推荐一些辅助插件,建议安装。
代码中使用到的 GM_xxx
会自动提取到 UserScript Header 中,当然也可以在 src/meta.json
中自定义。
代码的默认入口是 src/main.js
文件。
实践:掘金签到功能
基于上面初始化的项目 demo-userscript
做一个小功能:掘金签到功能。
1. 定位请求
“掘金签到”本质是调用接口,我们的实现思路是追踪点击“立即签到”按钮时请求发送情况,定位到
2. 调试接口
打开 Postman 做一下调试,这里有一个导入小技巧。
右键拷贝 cURL
到 Postman 中通过 curl 导入整个请求:点击左侧面板中的 import 按钮,选择 Raw text 粘贴上一步复制的内容即可。
3. 获取参数
在 Postman 中发现请求需要 aid
、uuid
、_signature
三个参数,试试看不带参数能否请求成功,先确定好必不可少的参数和请求头。
简单尝试后,发现这里并不需要带 aid
、uuid
、_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_setValue
和 GM_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);
}
},
});
}
难免会遇到需要获取数据的情况,可访问的数据一般有三种:
- 页面中包含数据,通过 DOM 获取
- 通过接口请求得到
- 存储在本地存储中,localStorage 或 cookie 之类
确定方式很粗暴:
- 复制参数或参数值到 Element 中搜索;
- 查看前面几个请求,看看是否有迹可循;
- 到 localStorage 或 cookie 中搜索。
分发脚本
在本地开发完脚本之后,npm run build
构建生产版本并上传代码到 Github 或 Gitee。
用 Github/Gitee 上文件的 Raw URL 就能直接实现分发。如果在 package.json 中设置好 repository
,create-tampermonkey
会自动生成 Raw URL 并赋给downloadURL
、updateURL
。
但这样分发存在的问题是无法统计下载量、从网络访问的角度考虑同时维护 Github 和 Gitee 两个仓库。另一种分发方式是上传到脚本平台 greasyfork.org/ , 登录后即可发布新脚本,如果代码托管在 Github 或 GitLab 还可以使用 Webhooks 实现自动更新。
开发技巧
调试油猴脚本
油猴脚本的运行依托于 background script
和 content script
,在调试前需要对运行环境有所区分,例如 GM_xmlhttpRequest
请求是 background script
发出的,DOM 处理和脚本逻辑是 content script
执行的。
确定环境之后,就可以使用对应的调试方式进行调试了。
调试 background script
还是访问 chrome://extension
,找到油猴插件的卡片
inspect views
后面有个 background.html,点击一下弹出 background script
的调试弹窗。
调试 content script
在网页 inspect -> Sources -> Page 下找到 Tampermonkey
目录,页面中运行的油猴脚本代码都在这了,选择目标,断点调试即可。
获取 userId
等信息
有时候需要拿一些额外信息做请求,一般有三种方式:
- 看看能不能在页面中搜索到,通过 DOM 获取
- 看看有没有接口可以调用获取
- 看看本地存储里有没有
目标 DOM 节点未挂载怎么办?
如果节点是在首屏加载的,粗暴的方法是使用 setTimeout
做一下延时。
但如果是在交互过程中有 DOM 更新,就只能引入监听机制了,使用 MutationObserver
来实现。
具体的实例可以看 【开发记录】掘金 “破圈行动” 辅助脚本 - 掘金 (juejin.cn)
查看插件源码
浏览器插件安装之后,插件包被下载到本地目录中,可通过下述方法访问。
访问 chrome://version
,找到 Profile path(存放用户数据的路径)
访问 chrome://extensions/
,找到目标插件的 ID
将 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 script
和 content script
两部分,在调试油猴脚本时需要思考清楚是哪一部分出现的问题,再采用对应的调试方式。
实现了两个小实践,走出第一步,接下来尽情发挥创造力吧,玩得开心~