神代綺凛

油猴脚本入坑指南
希望可以让开始尝试自己写脚本的同学们少走一些弯路吧
扫描右侧二维码阅读全文
06
2019/11

油猴脚本入坑指南

希望可以让开始尝试自己写脚本的同学们少走一些弯路吧

Head Pic: #アズールレーン 恶毒 - 小小男爵不要坑的插画 - pixiv

基础

这部分主要是开始写油猴脚本前应当有所了解的知识

常见的用户脚本管理器

  • Tampermonkey
    应该是各位见得最多的也是最知名的,好用又稳定,多浏览器支持,我很喜欢
  • Greasemonkey
    用户脚本始祖,我们现在一直习惯说的油猴脚本的“油猴”实际上就是 Greasemonkey,只支持 Firefox
    由于与 Tampermonkey 等其它脚本管理器在 API 的使用上会有些区别,导致某些情况下你很难保持你的脚本同时对 Greasemonkey 兼容,我一般直接放弃兼容
  • Violentmonkey
    由国人开发的一款脚本管理器,界面好看,我很喜欢

元数据

即每个油猴脚本都有的,脚本开头很多行注释的内容,这是油猴脚本关键的基础部分,刚开始接触可能会一头雾水,但你绝不能忽视这部分内容

建议:

  1. 多参考别人的脚本,能对各个字段的意义了解个大概
  2. 阅读官方 wiki,有每个字段详细的介绍
    如果你觉得读鸟语实在是很头疼,你也可以阅读由他人维护的中文 GreaseMonkey 用户脚本开发手册
  3. 不同的用户脚本管理器可能会加入自己独有的 meta,开发时建议以你的脚本打算主要支持的脚本管理器为主,例如这是 Tampermonkey 的文档

GM API

油猴提供了很多强大的 API,它们可以使很操作变得相当简单

注意每个 API 在使用前需要在元数据中用 @grant 进行声明,若你不打算使用这些 API,应当声明 @grant none

以下是一个简单的表格,帮助你了解油猴的 API 大概能做哪些事情

旧 API 新 API 说明
GM_info GM.info 返回当前脚本的元数据
GM_addStyle 为网页添加 CSS
GM_setValue GM.setValue 在本地储存值(只能是字符串),你可以将这个储存看作是 localStorage 一样的东西
GM_getValue GM.getValue 获取使用储存的值
GM_deleteValue GM.deleteValue 删除储存的值
GM_listValues GM.listValues 返回一个由所有储存值的键名组成的数组
GM_getResourceText 获取元数据中定义的 @resource 的资源内容
GM_getResourceURL GM.getResourceUrl 获取元数据中定义的 @resource 资源的 URL(base64 编码后的data:协议地址)
GM_openInTab GM.openInTab 新标签页打开指定地址(用来绕过 Chrome 会阻止所有非用户触发的window.open的限制)
GM_registerMenuCommand 向油猴插件菜单中添加脚本指令(通常用于打开自己写的设置界面或者执行代码之类的)
GM_setClipboard GM.setClipboard 复制指定内容到剪贴板
GM_xmlhttpRequest GM.xmlHttpRequest 发送网络请求,且允许跨域
GM.notification 浏览器通知

新旧 API 的区别

Greasemonkey 从版本 4 开始向性能更高的异步模型发展,旧的 API GM_* 通常是同步的,而新的 API GM.* 是异步的(采用 Promise),在使用时请参考官方 wiki 并多加留意

并且,有些 API 的名称拼写也发生了变化,在上面的表格中已经用粗体标识

想了解更多信息可以阅读官方说明文章 Greasemonkey 4 For Script Authors

unsafeWindow

如果你在写脚本的时候有尝试直接通过 window 添加或访问网页全局变量,你会发现这是没有效果的

这是因为油猴的沙箱机制,任何人都无法从 window 直接访问到油猴的 API 或脚本内的变量,保证了安全

如果你确实需要访问 window,可以使用 unsafeWindow,但在正式发布的脚本中你不应该将任何油猴 API 或者脚本中的变量通过它暴露到 window 中

unsafeWindow 在不同脚本管理器中的表现可能会有所不同,特别是 Violentmonkey,如需考虑兼容性还需要多加测试

跨域请求

在油猴脚本中你可以引用网络脚本来使用 axios 之类的网络请求模块,这很方便,但同样也产生了局限性,例如由于浏览器机制的限制,你无法直接在网页上进行没有被事先允许的跨域请求

这时建议使用 GM.xmlHttpRequest,同时你应当在元数据用// @connect <value>声明允许被 GM.xmlHttpRequest 访问的域名

<value>可以是:

  • 域名,例如example.com,这也将允许所有子域
  • 子域,例如abc.example.com
  • self,即脚本运行的网址
  • localhost
  • IP 地址
  • *

如果你习惯用 axios 之类的用 Promise 封装的请求模块,你同样可以将 GM.xmlHttpRequest 封装成 Promise 形式

const xhr = option => new Promise((resolve, reject) => {
    GM.xmlHttpRequest({
        ...option,
        onerror: reject,
        onload: resolve,
    });
});

使用自己的 IDE 编写油猴脚本

一般脚本管理器自带的编辑器功能十分单一,全程在里面写代码肯定十分不爽,那么如何使用自己的 IDE 编写脚本并随时保存随时生效呢

答案是利用元数据的 @require,它不仅能引用网络脚本,还可以引用本地脚本,所以我们只要 require 用 IDE 编辑的本地脚本就行了

在这之前我们需要允许油猴插件访问本地文件,以 Chrome 为例,在扩展程序列表chrome://extensions/进入插件的详细信息,开启“允许访问文件网址”即可,接着就可以// @require file://<本地路径>的文件网址方式引用本地脚本了

引用 CSS

引用 JS 可以采用@require,但 CSS 不行

可行的方法有两种

  1. 老办法:用 JS 往<head>插入 CSS 的<link>
  2. API 方法:在元数据中声明// @resource mycss <地址>,然后GM_addStyle(GM_getResourceText('mycss'));
    别忘了用到的这两个 API 也要@grant声明

进阶

这部分主要是写脚本的过程中有可能遇到的一些难点的较优解决方法

避免将 setInterval 用作动态监听的解决方案

初学 JS 的新手在遇到监听动态元素的问题的时候,由于缺乏经验,通常只能想到用 setInterval 去“每隔一段时间就检测一下”,当然这也包括我自己,但不管从性能上还是从实现复杂度来说,这都不是一个好选择,不够优雅

大部分类似的问题都可以在事件监听层面运用点技巧来解决

此处会列举几个常见的场景来说明一下解决思路

1. 监听动态生成的页面元素的事件

在有些时候我们可能要去监听动态生成的页面元素的事件,例如自动翻页加载的评论这类

  • 不好的思路
    setInterval 每隔一段时间检测一下有没有新生成的页面元素,然后对这些页面元素添加事件监听
  • 好的思路
    由于事件冒泡机制,我们可以监听其父级元素的点击事件,然后通过事件信息来确定被点击的元素currentTarget或其父级元素currentTarget.parentNode

不仅是动态的场景下可以这么做,当你需要针对一个很多元素的静态列表监听每个元素的事件时也可以这么做,这种方法最大的优点是你只需要添加一个事件监听,如果你对列表中的每个元素都添加事件监听,会增大内存开销,影响页面性能

有种比较特殊的情况:

<ul class="list">
    <li class="item">
        <img class="image" />
        ...
    <li>
    ...
</ul>

假设在该场景下,点击 .image 时它自身会被移除,而你需要得到被点击的 .image 所在的 .item,由于该 .image 已经被移出页面的 DOM 树,因此你无法通过点击事件的currentTarget.parentNode来得到 .item

最简单的解决方案是在事件发生时获取鼠标所在的 .item,例如使用 jQuery:$('.item:hover')

2. 对动态生成的页面元素进行修改

假设一个场景,此处借用一下 vue 的语法来说明页面元素逻辑:

<!-- Init: showA = true; showB = false; -->
<ul class="list">
    <li class="item">
        <div v-if="showA" class="item-a" @click="showA = false; doSth().then(() => { showB = true });">...</div>
        <div v-if="showB" class="item-b">...</div>
        ...
    <li>
    ...
</ul>

大致就是,当你点击 .item-a 的时候,.item-a 会被移除,并在一个异步函数doSth()完成后显示 .item-b

你当前的目标是要在 .item-b 出现的时候修改其内容

  • 不好的思路
    监听 .item-a 的点击事件,setInterval 每隔一段时间检测一下当前 .item 内有没有 .item-b,有的话就进行修改然后终止该 interval
  • 好的思路
    监听 .item-a 的点击事件,当其被点击后监视 .item 的 DOM 变化,若新增了 .item-b 就对其进行修改

是时候祭出 MutationObserver 了,利用它我们可以监视 DOM 树的改动,同时它也是过去的 Mutation Events 的替代品

上面所说的场景可以按这个思路来解决

  1. 监听 .list 的点击
  2. 当触发点击事件时,找到 :hover 状态的 .item,对其添加 MutationObserver
  3. 当 MutationObserver 监视到 .item-b 被添加时,修改 .item-b,并disconnect()该 MutationObserver

写成代码大概像这样:

const findItemB = $item => new Promise((resolve, reject) => {
    if ($item.length === 0) reject();
    // 有可能此时 .item-b 已经出现,所以先检查下
    const $itemB = $item.find('.item-b');
    if ($itemB.length > 0) {
        resolve($itemB);
        return;
    }
    // 监视 .item 的 DOM 树 childList 变化
    new MutationObserver((mutations, self) => {
        mutations.forEach(({ addedNodes }) => {
            addedNodes.forEach(node => {
                if (node.className !== '.item-b') return;
                self.disconnect();
                resolve($(node));
            });
        });
    }).observe($item[0], { childList: true });
});

$('.list').click(async ({ target }) => {
    if (target.className !== 'item-a') return;
    const $itemB = await findItemB($('.item:hover'));
    // do something with $itemB
});

补充

推荐的一些可能会常用的模块

Github BootCDN 用途
jquery-pjax Link 为页面添加 pjax 支持
jquery-mousewheel Link 为 jQuery 添加鼠标滚轮事件的支持
FileSaver.js Link 另存为任意 blob 为文件
jszip Link 读写创建压缩文件
gif.js Link 制作 gif,支持 worker 方式
clipboard.js Link 虽然油猴提供剪贴板 API,但该模块可以提供一些扩展功能,例如 tooltips 反馈等
dragula Link 提供页面元素的拖拽调序功能
toastr Link 方便的显示页内通知
搬瓦工VPS优惠套餐,建站稳如狗,支持支付宝,循环出账94折优惠码BWH3HYATVBJW
年付$47CN2线路,1核/1G内存/20G硬盘/1T@1Gbps【点击购买
季付$47CN2 GIA线路,1核/1G内存/20G硬盘/1T@2.5Gbps【点击购买
Last modification:December 29th, 2020 at 03:54 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment

25 comments

  1. Knect  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 87.0.4280.88(Google Chrome 87.0.4280.88)
    **避免将 setInterval 用作动态监听的解决方案

    啊,所以如果我想监控网址变更,还能怎么做呢?我要应用脚本的网盘站,目录变更的时候不改变 hash,只改变 query string,所以网上热门的 hashchange 法不生效……目前我只有设置 50ms 检查 document.URL 是否改变一个办法,网上搜罗了半天并没找到更好的……大概是我太菜了

    1. 神代綺凜  Mac OS X 10.15.7(Mac OS X 10.15.7) / Google Chrome 87.0.4280.88(Google Chrome 87.0.4280.88)
      @Knect 按你的说法是这个页面的 URL 变了但页面并没有刷新,那么一定是通过 pushState 或者 replaceState 实现的,这个确实没有什么优雅的方法,因为没有相应的事件,你可以通过修改原函数的方法来加入自己的回调或触发自定义事件等等

      你可以参考这里的做法 https://segmentfault.com/a/1190000022822185

  2. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
    所以什么时候也搞个stylish/stylus入坑指南呢
    1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
      @余水 我寻思这个应该不需要入坑指南,只要学了 CSS 就能写
      1. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
        @神代綺凜 这倒是,不过连嵌套文件都不能嵌套吗 本来想把base64的内容单独分离出来再到另一个样式里引入的,发现好像做不到
        1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
          @余水 单独写一个样式块不就行了,应用规则可以写好几条
          1. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
            @神代綺凜 emmmmm,后来我确实是这样做的,我一直以为是里面官方的某个方法我不知道OωO
            1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
              1. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
                @神代綺凜
                该评论仅登录用户及评论双方可见
                1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
                  @余水
                  该评论仅登录用户及评论双方可见
                  1. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
                    @神代綺凜
                    该评论仅登录用户及评论双方可见
                  2. 余水  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.132(Google Chrome 80.0.3987.132)
                    @神代綺凜
                    该评论仅登录用户及评论双方可见
  3. Haleclipse  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 78.0.3904.108(Google Chrome 78.0.3904.108)
    啊哈,FileSaver.js真棒,我之前傻傻的自己写了从canvas里转储img blob保存本地,现在才知道有这轮子
    1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 80.0.3987.122(Google Chrome 80.0.3987.122)
      @Haleclipse 所以我学会了要上什么功能之前先搜搜有没有现成的轮子
  4. blank  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 79.0.3945.29(Google Chrome 79.0.3945.29)
    失踪站主回归? 23333
    1. 神代綺凜  Mac OS X 10.14.6(Mac OS X 10.14.6) / Google Chrome 78.0.3904.87(Google Chrome 78.0.3904.87)
      @blank 最近突然有东西写,今天下午估计还会写一篇
  5. T.O.V  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 78.0.3904.70(Google Chrome 78.0.3904.70)
    来围观大佬(虽然前端我还没怎么学)
    上班摸鱼好评
    1. 神代綺凜  Mac OS X 10.14.6(Mac OS X 10.14.6) / Google Chrome 78.0.3904.87(Google Chrome 78.0.3904.87)
      @T.O.V (正在摸鱼继续写
  6. rxliuli  Android 7.1.1(Android 7.1.1) / Google Chrome 78.0.3904.62(Google Chrome 78.0.3904.62)
    best 现在也要写这个了呀,吾辈写了几个下来之后最大的感觉是写页面太麻烦了
    1. 神代綺凜  Mac OS X 10.14.6(Mac OS X 10.14.6) / Google Chrome 78.0.3904.87(Google Chrome 78.0.3904.87)
      @rxliuli 也不是,自己接触油猴其实有挺长一段时间了,虽然也没写多少脚本(

      油猴要写页面的场景应该挺少?顶多是设置之类的

      1. WeiYuan  Android 9(Android 9) / Google Chrome 78.0.3904.90(Google Chrome 78.0.3904.90)
        @神代綺凜 大学党表示,刷课院公选网课必备( ╹▽╹ )
        1. 神代綺凜  Mac OS X(Mac OS X) / Safari(Safari)
          @WeiYuan 我以前也是自己写脚本来刷,后来觉得太麻烦了还要挂网页,直接去抓了API然后自己模拟客户端发包
  7. cezdo  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 77.0.3865.120(Google Chrome 77.0.3865.120)
    啊哈,虽然看不懂,但是感觉好厉害的样子!
    :>
  8. Ekkles  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 78.0.3904.87(Google Chrome 78.0.3904.87)
    上班摸鱼好评
    1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 78.0.3904.87(Google Chrome 78.0.3904.87)
      @Ekkles 被发现了