前言

本文首发于我的博客其乐论坛,未经授权禁止转载

本文全程使用原生JavaScript进行编程,不使用像jQuery这样的第三方库

视频链接

零基础油猴脚本编写指南【上】 (对应0x01~0x05)

0x00 前置知识

本节简单的介绍一下一些名词的概念,了解为主,直接跳过也是可以的

  1. HTML、CSS和JS

    HTML即超文本标记语言,它用来描述各种网页元素,类似与一份清单,浏览器读取以后根据它来加载对应的资源

    CSS即层叠样式表,它只用来描述网页元素的外观,还可以添加少许动画,但是不影响功能

    JS即JavaScript,它是一种网页脚本语言,用来让静态的网页能够响应各种动作,还可以挖矿

    如果把HTML比作木偶,那么CSS就是木偶的衣服,JS就是木偶的提线,用来操控木偶

  2. 油猴和浏览器

    当你访问链接的时候,浏览器会先下载链接对应的文件(通常是一个HTML文件),然后根据HTML描述的对象列表,来下载图片、视频等资源,有的CSS和JS内嵌在HTML文件里,有的则是单独的文件,等必要的资源加载完成以后,浏览器才能生成可以交互的网页

    油猴实际上是一个加载器,它把用户脚本安插在最开始获取到的HTML文件中,这样浏览器加载网页的时候,也会加载特定的用户脚本,这样我们就能够通过编写用户脚本来修改网页了

  3. HTML5和DOM

    HTML5是现行的HTML标准,DOM即文档对象模型,是一用对象表示HTML标签的方法,通过DOM,我们可以简单的修改文档对象的属性,来轻松修改HTML标签的内容

0x01 初识开发者工具

工欲善其事必先利其器,本节简单的介绍一下开发者工具的使用。

本节以我的博客作为演示,地址:chrxw.com

个人比较喜欢Firefox的开发者工具,以下介绍均使用Firefox进行演示,如果使用Chrome的话可能布局会有细微差异,不过功能是类似的。

Chrome自带的开发者工具貌似没有汉化,建议使用别的Chromium内核的浏览器(比如新版Edge),功能都差不多。

  • 在浏览器中按F12即可打开开发者工具

    开发者工具

  • 查看器,左侧可以查看页面的HTML内容,右侧显示的是选中的DOM元素的CSS

    查看器

  • 控制台,可以运行自己编写的JS代码。

    控制台

    也可以在设置里打开“分离式控制台”,这样就可以在所有TAB里都显示控制台了。

    分离式控制台

  • 调试器,可以用来单步调试JS

    调试器

  • 网络,可以查看浏览器的网络请求

    网络

  • 储存,在这个TAB中可以查看网站在本地保存的内容,最常用的是Cookie,其次是LocalStorge(本地储存),还有个由浏览器自动管理的Session(会话储存),JS可以访问的只有Cookie和LocalStorge。

    储存 Cookie

    储存 LocalStorge

0x02 初识DOM

本节通过使用开发者工具,简单的演示一下如何操作DOM元素,以我的博客作为演示,地址:chrxw.com

  • 首先打开开发者工具,然后激活选择器,然后点击“标题”,这样在右边的查看器中就会显示我们点击的对象

    选中“标题”元素

  • 右键高亮的元素,然后点击“在控制台使用”(如果是Chromium或者Chrome,点击“复制”/“Copy”->“复制JS路径”/“Copy JS path”,然后把复制结果粘贴到控制台中)

    在控制台使用

  • Firefox自动创建了一个temp0变量,按一下回车,可以看到控制台输出了一个DOM元素,点一下左边的箭头展开,可以看到这个对象的各种属性。

    “temp0”对象的属性

  • 接下来我们尝试修改一下它的内容

    temp0.textContent = 'hello world';

    按回车执行,然后可以看到,网页的标题已经被修改了

    标题变成了“hello world”

  • 接着我们修改一下CSS,让它居中显示

    temp0.style.textAlign='center';

    标题居中显示

  • 接下来我们创建一个新的DOM元素,然后插入到网页中

    let btn = document.createElement('button');
    btn.textContent = '按钮';
    btn.addEventListener('click',() => {
        alert('hello world');
    });
    temp0.appendChild(btn);

    点一下新出现的按钮,可以看到消息弹窗

    代码运行效果

    代码讲解:

    PS: document 也是DOM元素,它代表整个网页,其他DOM元素都是它的子集。

    • let btn = document.createElement('button');
      btn.textContent = '按钮';

      这两行代码创建了一个button对象,它内部的文本为按钮,由浏览器自动生成的HTML表达式为<button>按钮</button>

    • btn.addEventListener('click',() => {
          alert('hello world');
      });

      这三行代码为btn对象添加了事件监听器,addEventListener的第一个参数是事件监听器的名称,第二个参数是接收到指定事件后调用的函数,()=>{ ... }是匿名函数的缩略写法,匿名函数还有另一种等效写法function(){ ... },或者写成普通函数,也是可以的。

      //匿名函数写法2
      btn.addEventListener('click',function(){
          alert('hello world');
      });
      //普通函数的写法
      btn.addEventListener('click',foo);//foo后面不加()
      function foo(){
          alert('hello world');
      }

      添加好事件监听器以后,点一下按钮,就会触发click事件,然后由浏览器调用我们绑定的方法alert('hello world');,弹出消息框

    • temp0.appendChild(btn);

      A.appendChild(B)的作用是把B元素插入到A元素的子元素的末尾,也就是把btn对象插到temp0中,由浏览器自动帮我们修改了HTML文本

      //修改前
      <h1 class="m-n font-thin text-black l-h">Chr_小屋</h1>
      //修改后
      <h1 class="m-n font-thin text-black l-h">Chr_小屋
          <button>按钮</button> //这是我们新创建的DOM元素
      </h1>
  • 通过操作DOM元素的属性,就可以很方便的修改网页的显示效果,添加各种功能。

0x03 DOM进阶之获取DOM元素

通过修改DOM元素的属性,可以很方便地修改网页,但是首先我们得获取我们想要修改的DOM元素

本节将会介绍如何获通过代码获取现存的DOM元素,以我的博客作为演示,地址:about.html

1. 使用ID进行获取

WEB开发者设计网页的时候,会给一些经常需要修改的元素添加id属性,原则上每个元素的id都是不同的,所以如果某个元素具有id属性,我们就可以使用getElementById来获取

需要注意的是,如果元素没有id属性的话这个方法就行不通了

随便打开一篇博文,最下面的评论框是带有id属性的,使用开发者工具我们可以看到,它的idcomment

评论框的ID

我们可以用document.getElementById( id名称 )的方式获取DOM元素

let box = document.getElementById('comment');
box.value = '通过ID获取DOM';

代码非常好理解,先调用getElementById方法获取DOM,然后修改它的value属性,效果就是文本框的内容被修改了

PS: textContent 属性修改的是HTML文本,而 value 属性修改的是文本框中的内容,不影响HTML文本

代码运行效果

2. 使用CSS选择器进行获取

在一个网页中,具有id属性的元素不会太多,大多数时候都是没有这个属性的,这时候可以根据元素的class属性,使用querySelector(获取符合条件的第一个元素)或者querySelectorAll(获取符合条件的全部元素数组)进行获取

本节只介绍常用的方法,更详细的介绍可以参考这个:CSS 选择器参考手册

  • 根据class进行选择

    标题元素的classentry-title m-n font-thin text-black l-hclass选择器的语法为.+class名称,如果有多个class名称,则用.分隔

    所以标题元素的选择器写法为.entry-title.m-n.font-thin.text-black.l-h

    在CSS选择器语法中,空格代表子元素的意思,所以不能随意添加空格。

    “entry-title”这个类名是独一无二的

    通过搜索,我们发现entry-title在HTML文档只出现了一次,因此选择器也可以简化写作.entry-title,代码如下

    //两种写法等效
    document.querySelector('.entry-title.m-n.font-thin.text-black.l-h');
    document.querySelector('.entry-title');

    代码执行效果

    在实际运用中,没必要给出完整的class,只需要写独一无二的部分即可

  • 根据id进行选择(可以用,但还是建议用getElementById

    id选择器的语法为#+id名称,评论框的idcomment,所以选择器写法为#comment,代码如下

    //两种写法等效,推荐第一种
    document.getElementById('comment');
    document.querySelector('#comment');

    代码运行效果

  • 根据元素类型选择

    还可以根据DOM元素的类型进行选择,如果网页中只有一个特定种类的元素,那么直接写元素类型即可

    评论框的元素类型

    使用开发者工具,我们可以知道,评论框的类型为textarea,而且整个网页只有一个textarea,因此选择器可以直接写成textarea,代码如下

    document.querySelector('textarea');

    代码运行效果

  • 根据相对关系进行选择

    有的时候,想要操作的元素可能没有独一无二的属性,那么就可以根据相对关系进行选择

    A>B表示在A的一级子元素中查找B

    A B表示在A的内部查找B(查找范围不仅包括子元素,还包括孙元素等)

    A+B表示与A相邻的B元素(平级关系,非父子关系)

    这里的 A 和 B 代表的是选择器,选择器可以嵌套使用。

    A.B,不表示相对关系,它表示类型是Aclass名是B的元素,这个要注意。

    还是用标题元素举例子

    节点之间的层次关系

    先找到容易定位的元素,然后再根据相对关系寻找想要找的元素,方法有很多,下面举几个例子

    //以下语句都是等效的
    document.querySelector('main>div>div>header>h1');//一级子元素
    document.querySelector('main header>h1');//所有子元素
    document.querySelector('main h1');//这样也可以
    document.querySelector('header>h1');//多个方法定位header对象
    document.querySelector('#small_widgets>h1');
    document.querySelector('.bg-light.lter.wrapper-md>h1');
    document.querySelector('header.bg-light>h1');

    代码执行效果

    写成这个样子主要是为了举例子,实际使用中选择器越简洁越好

  • 根据其他属性名称进行选择

    [xxx]选择带有xxx属性的元素

    [xxx=sss]选择xxx属性等于sss的元素

    [xxx~=sss]选择xxx属性包含sss的元素(sss必须是整个单词)

    [xxx|=sss]选择xxx属性以sss开头的元素(sss必须是整个单词)

    [xxx^=sss]选择xxx属性以sss开头的元素(sss可以是单词的一部分)

    [xxx$=sss]选择xxx属性以sss结尾的元素(sss可以是单词的一部分)

    [xxx*=sss]选择xxx属性包含sss的元素(sss可以是单词的一部分)

    “评论”的“for”属性

    通过开发者工具可以看出高亮的DOM元素具有for属性,属性值为comment,代码如下,感受一下几种用法的区别

    document.querySelector('[for=comment]');//完整匹配
    document.querySelector('[for|=co]');//匹配开头,匹配失败,for属性中没有co这个单词
    document.querySelector('[for~=co]');//匹配任意位置,匹配失败,for属性中没有co这个单词
    document.querySelector('[for^=co]');//匹配开头
    document.querySelector('[for*=co]');//匹配任意位置
    document.querySelector('[for*=mm]');//匹配任意位置
    document.querySelector('[for$=nt]');//匹配结尾

    代码运行结果

  • 组合选择器

    上面介绍过的选择器可以组合使用,比如选择表情按钮中的矢量图片

    选择器与对象的对应关系

    代码如下,自己体会一下各个部分是什么意思,以及哪些部分是没有必要的

    //两种写法效果一样
    document.querySelector('textarea.form-control[name=text]+div.OwO>.OwO-logo svg');
    document.querySelector('.OwO-logo svg');

    代码运行效果

0x04 DOM进阶之修改DOM元素

拿到了DOM对象,接下来就可以进行各种操♂作了

本节将会介绍DOM元素的常用方法,以我的博客作为演示,地址:about.html

以标题元素为例,首先我们先获取DOM对象

let h1 = document.querySelector('h1');
console.log(h1);

标题DOM对象

使用开发者工具可以非常方便的看到DOM元素的方法和属性

枚举子元素

DOM.children

返回值为DOM元素的所有子元素数组,如果不含有子元素,返回的是空数组。

可以通过DOM.children[i]的方式获取某一个子元素

子元素列表的对应关系

获取/修改文本内容

DOM.textContent

返回值为当前DOM元素内的显示字符(没有被 < > 包起来的所有字符),不仅包括自身,还包括所有子元素

元素内没有子元素的情况

元素内有子元素的情况

DOM.innerText也有类似的效果,区别是它会去掉多余的换行符

textContent和innetText的区别

也可以修改DOM.textContent和`DOM.innerText

不过要注意的是,这两个方法会直接替换掉元素内部的内容,如果元素内部有其他子元素的话,子元素会消失。

修改textContent后,元素内部原来的子元素被替换成了文本

获取/修改元素CSS

只能修改DOM元素的内联CSS,全局CSS得修改Style元素才行。

CSS相关内容不做过多介绍,贴个简单的教程:菜鸟CSS教程

DOM.style,获取DOM元素的内联CSS样式

DOM.style.[css名称],获取DOM元素的特定CSS属性,例如textAlign

DOM.style.cssText,获取DOM元素的内联CSS样式,纯文本格式

同样的,也可以用类似的方法修改CSS样式

DOM.style.[css名称] = 'xxx',修改DOM元素的特定CSS属性为xxx

DOM.style.cssText = 'aaa: xxx;',直接修改DOM元素的内联CSS文本,可以一次设置多个CSS

如果想隐藏某些元素的话,可以把它的CSS的display属性设置成none,就会被隐藏起来了

修改元素CSS

新增子元素

DOM.appendChild( [要插入的DOM对象] )

appendChild会将新插入的对象放在所有子元素的末尾

DOM.insertBefore( [要插入的DOM对象] , [DOM对象,将要插在这个对象之前])

insertBefore可以指定新插入的对象的位置

let h1 = document.querySelector('h1');
let b1 = document.createElement('button');
b1.textContent='按钮1';
let b2 = document.createElement('button');
b2.textContent='按钮2';
h1.appendChild(b1);
h1.insertBefore(b2,h1.children[0]);

appendChild和insertBefore的区别

删除对象

DOM.remove()

这是个方法,所以要加(),调用后会在HTML文本中删除对象对应的内容

删除元素

添加/删除事件监听器

DOM.addEventListener( [事件名称] , [回调函数])添加事件监听器

DOM.removeEventListener( [事件名称] , [回调函数])移除事件监听器

也可以用以前的写法(不推荐):

DOM.on[事件名称] = [回调函数]添加事件监听器

DOM.on[事件名称] = null移除事件监听器

比如要让h1对象响应click事件,代码如下

let h1 = document.querySelector('h1');
//比较推荐这种写法
h1.addEventListener('click',() => {
    alert('响应click事件');
});
//也可以用旧风格的写法
h1.onclick = () => {
    alert('响应click事件');
};
//一些解释:
//() => { ... } 定义了一个匿名函数,相当于 function(){ ... }
//也可以使用实名函数,比如定义了function foo(){ ... }以后,也可以这么绑定:
//h1.addEventListener('click',foo);
//h1.onclick = foo;

绑定方法1

绑定方法2

删除绑定的方法也是类似的,不再赘述

模拟鼠标点击

DOM.click()

相当于鼠标点了一下DOM对象,触发它的click事件(如果有的话)

0x05 第一个油猴脚本

讲了这么久,都是在介绍前端的基础知识,接下来开始正文

首先让我们新建一个用户脚本

新建用户脚本

Ctrl+S即可保存

新的用户脚本

元信息

元信息是给油猴插件使用的,用来记录脚本的属性

新建脚本时给出的默认元信息如下:

// ==UserScript==
// @name         New Userscript               //名称
// @namespace    http://tampermonkey.net/     //命名空间
// 说明:@namespace相同,@name也相同的脚本会被油猴当做是同一个脚本,这两个属性起到区分不同脚本的作用

// @version      0.1                          //版本号
// @description  try to take over the world!  //说明
// @author       You                          //作者
// @match        http://*/*                   //在什么网址下启用该脚本
// @grant        none                         //特殊权限(调用GM_开头的函数需要先申请权限)
// ==/UserScript==

还有部分比较常见的元信息:

// @include      /https://example.com/.*/     //和@match差不多,但是支持正则表达式,我更喜欢用这个
// @connect      example.com                  //如果脚本需要访问跨域资源,需要提前申明
// @license      AGPL-3.0                     //脚本的授权许可信息
// @icon         https://blog.chrxw.com/favicon.ico //脚本的图标(显示在脚本列表里)

代码区域

(function() {
    'use strict';

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

首先定义了一个匿名函数function() { ... },也可以写成我常用的格式,都是等效的:

(() => {
    'use strict';

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

()把匿名函数包起来,再在后面加一个(),表示直接执行这个匿名函数

其实完全可以把代码写到匿名函数的外部,都可以执行,只是定义域不同而已

'use strict';是伪代码,它告诉解释器,开启严格模式

开启严格模式以后,会进行更严格的语法检查,建议开启,有利于养成良好的编码习惯

// 非严格模式,允许访问未定义变量
(() => {
    y = 3.14;      // 不报错
})();
// 严格模式,允许访问已定义变量
(() => {
   "use strict";   // 启用严格模式
   let y = 3.14;   // 不报错(y已定义)
})();
// 严格模式,不允许访问未定义变量
(() => {
   "use strict";   // 启用严格模式
    y = 3.14;      // 报错(y未定义)
})();

运行出错,原因是使用了未定义的变量 y

实现功能

接下来我们为脚本编写一些功能,以我的博客作为演示,地址:about.html

  • 首先我们需要修改元信息,让脚步只在特定的网页生效

    然后添加作者名称和脚本名称(除了@match都可以随便写)

    // ==UserScript==
    // @name         第一个油猴脚本
    // @namespace    http://blog.chrxw.com/
    // @version      0.1
    // @description  hello world
    // @author       Chr_
    // @match        https://blog.chrxw.com/about.html
    // @grant        none
    // ==/UserScript==
  • 我们先不编写脚本,我们先使用开发者工具测试一下代码

    首先我们找一个对象下手(如图)

    对象属性

    它没有id属性,但是它的class名称独一无二,所以我们可以直接用document.querySelector('.item-thumb')获取到DOM元素

    let a = document.querySelector('.item-thumb');
  • 修改元素的背景图

    a.style.backgroundImage='url(/usr/customize/logo.png)';

    修改背景图

  • 添加点击事件

    a.addEventListener('click',() => {
        a.style.background='#f0f';
    });

    点一下以后背景变成了品红色

    添加点击事件

  • 最后我们编写油猴脚本,让这一切自动完成

    // ==UserScript==
    // @name         第一个油猴脚本
    // @namespace    http://blog.chrxw.com/
    // @version      0.1
    // @description  hello world
    // @author       Chr_
    // @match        https://blog.chrxw.com/about.html
    // @grant        none
    // ==/UserScript==
    
    (() => {
        'use strict';
        let a = document.querySelector('.item-thumb');
        a.style.backgroundImage='url(/usr/customize/logo.png)';
        a.addEventListener('click',() => {
            a.style.background='#f0f';
        });
        // Your code here...
    })();

    脚本运行效果1

    脚本运行效果2

    保存脚本以后,刷新页面,可以看到元素的背景图片已经改掉了,说明脚本运行成功了。

0x06 脚本实战,Auto_Sub3

脚本获取链接:Auto_Sub3

脚本功能

脚本功能

安装脚本以后,首页可以打开一个面板,主要功能是一键-3

元信息

// ==UserScript==
// @name         Auto_Sub3
// @namespace    https://blog.chrxw.com
// @version      2.1
// @description  一键快乐-3
// @author       Chr_
// @include      https://keylol.com/*
// @license      AGPL-3.0
// @icon         https://blog.chrxw.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

元信息提供了这个脚本的基本信息,我们还可以知道,它申请了GM_setValue,GM_getValueGM_xmlhttpRequest三个特殊权限

代码总览

// 上次-3时间
let VLast = 0;
// 今天还能不能-3
let VCan3 = true;
// 自动展开
let VShow = false;
// 自动-3
let VAuto = false;
// 音效载入
let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3");

(function () { // 每次加载以后从这里运行
    'use strict';
    loadCFG();
    addBtns();
    if (VShow) {
        switchPanel();
    }
    if (VCan3 && VAuto) {
        autoRoll();
    }
})();

// 添加GUI
function addBtns() { ... }
// 显示小轮盘
function showLiteRoll() { ... }
// 自动打开面板
function fBtnShow() { ... }
// 自动-3
function fBtnAuto() { ... }
// 显示布尔
function bool2txt(bool) { ... }
// 隐藏面板
function switchPanel() { ... }
// 读取设置
function loadCFG() { ... }
// 保存设置
function saveCFG() { ... }
// 检查能否-3
function checkZP() { ... }
// 禁止-3
function disableS3() { ... }
// 自动-3
function autoRoll() { ... }

先不用太纠结代码,建议先用调试模式看一下代码流程

脚本调试模式

在设置里可以打开调试模式,开启以后在加载完脚本以后会自动暂停,可以用来动态调试脚本

开启调试模式

打开调试模式以后,先打开开发者工具,然后刷新页面,可以看到暂停在debugger语句上了

调试中断

给我们感兴趣的函数添加断点,然后点“继续运行(F8)”

添加断点

可以自己尝试跟踪一下每个函数的运行过程,不再赘述

代码详解

脚本入口

// 上次-3时间
let VLast = 0;
// 今天还能不能-3
let VCan3 = true;
// 自动展开
let VShow = false;
// 自动-3
let VAuto = false;
// 音效载入
let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3");
(function () {
    'use strict';
    loadCFG();
    addBtns();
    if (VShow) {
        switchPanel();
    }
    if (VCan3 && VAuto) {
        autoRoll();
    }
})();

首先定义了5个全局变量,然后运行了匿名函数,在这个匿名函数里完成了脚本的初始化工作

在匿名函数内部调用了这么几个函数:loadCFGaddBtnsswitchPanelautoRoll

loadCFG/saveCFG - 读写配置

// 读取设置
function loadCFG() {
    let t = null;
    t = GM_getValue('VLast');
    t = Number(t);
    if (t != t) { t = 0; }
    VLast = t;
    t = GM_getValue('VCan3');
    VCan3 = Boolean(t);
    t = GM_getValue('VShow');
    VShow = Boolean(t);
    t = GM_getValue('VAuto');
    VAuto = Boolean(t);
    // 日期变更,重置VCan3
    let d = new Date();
    let day = d.getDate();
    let hour = d.getHours();
    if (day != VLast && hour >= 8) {
        VCan3 = true;
        VLast = day;
    }
    saveCFG();
}
// 保存设置
function saveCFG() {
    GM_setValue('VLast', VLast);
    GM_setValue('VCan3', VCan3);
    GM_setValue('VShow', VShow);
    GM_setValue('VAuto', VAuto);
}

脚本保存数据的方法有很多种,可以利用原生的Cookie,也可以利用HTML5标准中的LocalStorge,但是这两个地方只能保存文本,我们还需要自行完成类型转换,非常不方便,所以我用的是油猴自带的储存空间

// 需要在元信息里提前申明,才可以使用
// @grant        GM_setValue
// @grant        GM_getValue

GM_setValue( 键名 , 键值),保存某个值

GM_getValue( 键名 ),如果键名不存在,返回null

可以在脚本编辑器的储存TAB看到当前脚本的储存空间

储存的内容

saveCFG非常简单,就是把4个全局变量保存起来

loadCFG也很简单,就是从储存空间读取4个变量,然后获取现在的时间和日期,判断日期有没有变更,如果日期变了,并且当前小时数大于8,就把Vcan3赋值成true,然后把VLast改成今天的日期,最后调用saveCFG保存4个全局变量

addBtns - 添加GUI

// 添加GUI
function addBtns() {
    function genButton(text, foo, id) {
        let b = document.createElement('button');//创建类型为button的DOM对象
        b.textContent = text;                    //修改内部文本为text
        b.style.cssText = 'margin: 3px 5px;'     //添加样式(margin可以让元素间隔开一定距离)
        b.addEventListener('click', foo);        //绑定click的事件的监听器
        if (id) { b.id = id; }                   //如果传入了id,就修改DOM对象的id
        return b;                                //返回修改好的DOM对象
    }
    function genDiv(cls) {
        let d = document.createElement('div');  //创建类型为div的DOM对象
        if (cls) { d.className = cls };         //如果传入了cls,就修改DOM对象的class
        return d;                               //返回修改好的DOM对象
    }
    function genPanel(name, visiable) {
        let p = genDiv(name);                   //创建类型为div,class为name的DOM对象
        p.id = name;                            //修改DOM对象的id为name
        p.style.cssText = 'width: 100%;height: 100%;';//修改DOM对象的样式(占满父容器)
        if (!visiable) { p.style.display = 'none'; }//修改DOM对象的样式(不可见)
        return p;                               //返回修改好的DOM对象
    }
    //使用CSS选择器获取对象
    let btnSwitch = document.querySelector('.index_left_title>div');

    if (btnSwitch == null) { return; }

    btnSwitch.id = 'btnSwitch1';
    btnSwitch.title = '点这里开启/隐藏控制面板';
    btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;';
    btnSwitch.addEventListener('click', switchPanel);

    let panelArea = document.querySelector('.index_navi_left');
    let panelOri = document.querySelector('.index_left_detail');
    panelOri.id = 'panelOri'

    let panel54 = genPanel('panel54');

    let aLP = document.createElement('a');
    aLP.href = 'https://keylol.com/t571093-1-1';
    let img54 = document.createElement('img');
    img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png';
    img54.alt = '总之这里是54的名言';
    img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;'
    aLP.appendChild(img54);

    let btnArea = genDiv('btnArea');
    btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;';

    let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3');

    if (!VCan3) {
        btnS3.style.textDecoration = 'line-through';
        btnS3.textContent = '今天已经不能-3了';
    }

    let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow');
    let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto');

    btnArea.appendChild(btnS3);
    btnArea.appendChild(btnShow);
    btnArea.appendChild(btnAuto);

    panel54.appendChild(aLP);
    panel54.appendChild(btnArea);
    panelArea.insertBefore(panel54, panelArea.children[1]);
}

看着很长,实际上很简单,首先定义了3和内部函数genButtongenDivgenPanel,用来生成我们需要的DOM元素

然后尝试用选择器获取DOM对象

let btnSwitch = document.querySelector('.index_left_title>div');//使用CSS选择器获取DOM对象

if (btnSwitch == null) { return; }       //如果页面中不存在这个对象,就直接返回,结束运行

btnSwitch.id = 'btnSwitch1';             //添加id属性
btnSwitch.title = '点这里开启/隐藏控制面板'; //添加title属性(鼠标悬停的时候显示的文本)
btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;';//添加CSS样式
btnSwitch.addEventListener('click', switchPanel);//绑定click事件的回调函数为switchPanel

可以利用开发者工具看一下是哪个对象

查看对象

剩下的部分也很好理解,创建3个button对象,1个img对象,依次添加到名为panel54div对象里,最后再把panel54添加到panelArea

    let panelArea = document.querySelector('.index_navi_left');
    let panelOri = document.querySelector('.index_left_detail');
    panelOri.id = 'panelOri'

    let panel54 = genPanel('panel54');

    let aLP = document.createElement('a');
    aLP.href = 'https://keylol.com/t571093-1-1';
    let img54 = document.createElement('img');
    img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png';
    img54.alt = '总之这里是54的名言';
    img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;'
    aLP.appendChild(img54);

    let btnArea = genDiv('btnArea');
    btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;';

    let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3');

    if (!VCan3) {
        btnS3.style.textDecoration = 'line-through';
        btnS3.textContent = '今天已经不能-3了';
    }

    let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow');
    let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto');

    btnArea.appendChild(btnS3);
    btnArea.appendChild(btnShow);
    btnArea.appendChild(btnAuto);

    panel54.appendChild(aLP);
    panel54.appendChild(btnArea);
    panelArea.insertBefore(panel54, panelArea.children[1]);

通过switchPanel,可以切换显示原来的元素和脚本的面板

// 隐藏面板
function switchPanel() {
    let panel1 = document.getElementById('panel54');
    let panel2 = document.getElementById('panelOri');
    if (panel1.style.display == 'none') {
        btnSwitch1.textContent = '今天你【-3】了吗 - By Chr_';
        panel1.style.display = 'block';
        panel2.style.display = 'none';
    } else {
        btnSwitch1.textContent = '关注重点';
        panel1.style.display = 'none';
        panel2.style.display = 'block';
    }
}

运行效果:

关闭的状态

开启的状态

autoRoll - 自动-3

最后我们看一下核心功能,自动-3

// 自动-3
function autoRoll() {
    try {
        sound.play();
    } catch (e) {
        console.error(e);
    }
    let v = 0;
    gethash();
    function gethash() {
        GM_xmlhttpRequest({
            method: "GET",
            url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43',
            onload: function (response) {
                if (response.status == 200) {
                    let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/);
                    let hash = m ? m[1] : null;
                    console.log(hash);
                    if (hash != null) {
                        roll(hash);
                        roll(hash);
                        roll(hash);
                    } else {
                        disableS3();
                        saveCFG();
                    }
                } else {
                    console.error('出错');
                }
            }
        });
    }
    function roll(hash) {
        GM_xmlhttpRequest({
            method: "GET",
            url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll',
            onload: function (response) {
                if (response.status == 200) {
                    console.log(response.responseText);
                } else {
                    console.error('出错')
                }
                if (++v == 3) {
                    disableS3();
                    saveCFG();
                }
            }
        });
    }
}

乍一看非常复杂,实际上也很好理解

  • try {
        sound.play();
    } catch (e) {
        console.error(e);
    }
    let v = 0;
    gethash();

    首先调用sound.play(),播放蒸汽泄漏的音效,sound是全局变量,会在网页载入的时候自动加载音频资源,如果遇到错误就忽略错误(比如网络问题加载音频失败的话就会报错)

    然后调用了gethash方法

  • function gethash() {
        GM_xmlhttpRequest({
            method: "GET",
            url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43',
            onload: function (response) {
                if (response.status == 200) {
                    let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/);
                    let hash = m ? m[1] : null;
                    console.log(hash);
                    if (hash != null) {
                        roll(hash);
                        roll(hash);
                        roll(hash);
                    } else {
                        disableS3();
                        saveCFG();
                    }
                } else {
                    console.error('出错');
                }
            }
        });
    }
    • 这个函数使用了GM_xmlhttpRequest,这个东西是一个网络请求器,深入了解可以参考官方文档(英语)GM_xmlhttpRequest类似于xmlHttpRequest,两者用法几乎一样,只是GM_xmlhttpRequest不存在跨域的问题

      // 需要先在元信息里申明,才可以使用
      // @grant        GM_xmlhttpRequest

      简单的用法如下

      GM_xmlhttpRequest({
        method: '', //请求方法,可以是 GET POST HEAD 等
        url: '',    //请求路径,简单的说就是url
        onload: (response) => {
            //请求被响应时执行,response是接受到的响应的对象
            //response.status,获取响应代码(200代表请求成功,4xx和5xx代表请求出错)
            //response.responseText,获取响应文本,本例中获取到的是HTML文本
            //response.responseXML,获取XML格式的响应文本
        }
      });
    • 看到onload的回调函数部分

      onload: (response) => {
          if (response.status == 200) {
              let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/);
              let hash = m ? m[1] : null;
              console.log(hash);
              if (hash != null) {
                  roll(hash);
                  roll(hash);
                  roll(hash);
              } else {
                  disableS3();
                  saveCFG();
              }
          } else {
              console.error('出错');
          }
      }

      首先判断状态码是不是200(代表请求成功),请求成功才会处理响应的内容

      可以在浏览器中打开链接,用开发者工具模拟一下处理过程

      网络请求的响应内容

      我们把响应的内容当做纯文本处理,使用正则表达式提取想要的内容

      正则表达式深入学习可以参考这个:正则表达式 - 语法

      let a = document.body.innerHTML;
      console.log(a.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/));

      简单的讲一下上面的正则表达式是什么意思

      /plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/
      // 两边的 / 就是告诉解释器里面是正则表达式,没别的意思
      // 按照不同元素拆分,如下:
      // plugin   - 匹配“plugin”这个字符串
      // \.       - 匹配“.”,“.”原来有别的意思,在前面加“\”转义以后就只有字面意思了
      // id=steamcn_lottery:view&lottery_id=43&hash=   - 匹配一整串字符串
      // (.+)     - “()”是子匹配的意思,“.+”意思是匹配至少一个字符
      //          - “(.+)”的意思是不仅要匹配至少一个字符,而且匹配到的文本需要返回
      // &roll    - 匹配“&roll”这个字符串

      简单点说就是先找到plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这一串字符串,然后提取中间的XXXXXXX

      执行效果如下,可以看到返回了一个数组,第一个元素是匹配到的字符串,第二个元素是子匹配项,我们只需要子匹配项,也就是8b51a2d3(这个值不同用户是不一样的,所以需要动态获取)

      查看网络请求

      然后就是赋值操作,用到了三目运算符号

      let hash = m ? m[1] : null;
      // D = A ? B : C
      // 等价于:
      if(A){
          D = B;
      }else{
          D = C;
      }
      // 当A的逻辑意义为true时,返回值为B
      // 当A的逻辑意义为false时,返回值为C
      // 类似这样
      let a = true ? 1 : 2;  // a = 1
      let a = false ? 1 : 2; // a = 2

      如果正则表达式没有匹配到合适的字符串,返回的内容是null,逻辑意义等于false,这时候hash的值就取null

      如果正则表达式匹配到了合适的字符串,返回的内容是数组,逻辑意义等于true,这时候hash的值就取数组的第2个元素

      然后根据hash决定是不是要继续运行

      if (hash != null) {
          roll(hash);
          roll(hash);
          roll(hash);
      } else {
          disableS3();
          saveCFG();
      }
  • 最后是roll函数,核心也是一个网络请求器

    function roll(hash) {
        GM_xmlhttpRequest({
            method: "GET",
            url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll',
            onload: function (response) {
                if (response.status == 200) {
                    console.log(response.responseText);
                } else {
                    console.error('出错')
                }
                if (++v == 3) {
                    disableS3();
                    saveCFG();
                }
            }
        });
    }

    实际上就是访问https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这个网站,然后把响应结果打印出来

    可以根据上一步得到的hash在浏览器中试一下

    参与轮盘

    调用以后,会自增v,如果v = 3,就调用disableS3saveCFG,修改了按钮的名称,然后保存全局变量

    执行效果

最后修改:2021 年 06 月 02 日
Null