前言

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

本文全程使用原生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

    储存 Cookie

    储存 LocalStorge

    储存 LocalStorge

0x02 初识DOM

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

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

    选中“标题”元素

    选中“标题”元素

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

    在控制台使用

    在控制台使用

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

    “temp0”对象的属性

    “temp0”对象的属性

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

    temp0.textContent = 'hello world';
    JavaScript

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

    标题变成了“hello world”

    标题变成了“hello world”

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

    temp0.style.textAlign='center';
    JavaScript

    标题居中显示

    标题居中显示

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

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

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

    代码运行效果

    代码运行效果

    代码讲解:

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

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

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

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

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

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

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

    • temp0.appendChild(btn);
      JavaScript

      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>
      HTML
  • 通过操作DOM元素的属性,就可以很方便的修改网页的显示效果,添加各种功能。

0x03 DOM进阶之获取DOM元素

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

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

1. 使用ID进行获取

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

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

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

评论框的ID

评论框的ID

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

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

代码非常好理解,先调用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”这个类名是独一无二的

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

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

    代码执行效果

    代码执行效果

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

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

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

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

    代码运行效果

    代码运行效果

  • 根据元素类型选择

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

    评论框的元素类型

    评论框的元素类型

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

    document.querySelector('textarea');
    JavaScript

    代码运行效果

    代码运行效果

  • 根据相对关系进行选择

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

    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');
    JavaScript

    代码执行效果

    代码执行效果

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

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

    [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”属性

    “评论”的“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]');//匹配结尾
    JavaScript

    代码运行结果

    代码运行结果

  • 组合选择器

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

    选择器与对象的对应关系

    选择器与对象的对应关系

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

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

    代码运行效果

    代码运行效果

0x04 DOM进阶之修改DOM元素

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

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

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

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

标题DOM对象

标题DOM对象

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

枚举子元素

DOM.children

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

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

子元素列表的对应关系

子元素列表的对应关系

获取/修改文本内容

DOM.textContent

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

元素内没有子元素的情况

元素内没有子元素的情况

元素内有子元素的情况

元素内有子元素的情况

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

textContent和innetText的区别

textContent和innetText的区别

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

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

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

修改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

修改元素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]);
JavaScript

appendChild和insertBefore的区别

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;
JavaScript

绑定方法1

绑定方法1

绑定方法2

绑定方法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==
JavaScript

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

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

代码区域

(function() { 'use strict'; // Your code here... })();
JavaScript

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

(() => { 'use strict'; // Your code here... })();
JavaScript

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

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

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

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

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

运行出错,原因是使用了未定义的变量 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==
    JavaScript
  • 我们先不编写脚本,我们先使用开发者工具测试一下代码

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

    对象属性

    对象属性

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

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

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

    修改背景图

    修改背景图

  • 添加点击事件

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

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

    添加点击事件

    添加点击事件

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

    // ==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... })();
    JavaScript

    脚本运行效果1

    脚本运行效果1

    脚本运行效果2

    脚本运行效果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==
JavaScript

元信息提供了这个脚本的基本信息,我们还可以知道,它申请了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() { ... }
JavaScript

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

脚本调试模式

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

开启调试模式

开启调试模式

打开调试模式以后,先打开开发者工具,然后刷新页面,可以看到暂停在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(); } })();
JavaScript

首先定义了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); }
JavaScript

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

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

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]); }
JavaScript

看着很长,实际上很简单,首先定义了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
JavaScript

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

查看对象

查看对象

剩下的部分也很好理解,创建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]);
JavaScript

通过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'; } }
JavaScript

运行效果:

关闭的状态

关闭的状态

开启的状态

开启的状态

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(); } } }); } }
JavaScript

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

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

    首先调用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('出错'); } } }); }
    JavaScript
    • 这个函数使用了GM_xmlhttpRequest,这个东西是一个网络请求器,深入了解可以参考官方文档(英语)GM_xmlhttpRequest类似于xmlHttpRequest,两者用法几乎一样,只是GM_xmlhttpRequest不存在跨域的问题

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

      简单的用法如下

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

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

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

      网络请求的响应内容

      网络请求的响应内容

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

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

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

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

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

      简单点说就是先找到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
      JavaScript

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

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

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

      if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); }
      JavaScript
  • 最后是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(); } } }); }
    JavaScript

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

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

    参与轮盘

    参与轮盘

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

    执行效果

    执行效果

最后修改:2021 年 06 月 02 日
如果觉得我的文章对你有用,请随意赞赏