参考:如何使用油猴插件提高测试工作效率
一、背景
在酷家乐设计工具测试中,总会有许多高频且较繁琐的工作,比如:
- 查询插件版本:需要打开Chrome控制台,输入好几个命令然后过滤出版本信息。
- 查询模型商品:需要先打开调试工具,查询得到模型商品id,然后跳转到测试平台进行加密,再去商家后台拼接url,最终访问到商品详情页。
- 修改定制高级配置:至少要点击4次页面跳转,才能开始配置。
类似的重复性工作实在太多,无形中影响工作效率与体验。并且大量的命令记忆对新手特别不友好。
仔细分析这类行为,大多都属于"数据查询"、“命令输入” 、“页面访问” 等简单操作的组合,其实非常适合“插件化”,封装成各种【一键操作】。
二、思路
基于上述背景,我们期望能开发一个插件来提高测试工作效率。
对于测试插件,主要有以下诉求:
- 开发门槛低。能让更多人参与进来,实现丰富的功能,满足各种需求。
- API 强大。便于扩展更多能力。
- 插件更新方便。便于新功能的推广。
最容易想到有两种方案: 酷家乐工具内部集成插件****、Chrome 插件。但是很明显,这两种方式都存在开发门槛高、维护成本高、使用场景有限的缺点。
所以最后选择了另一种方案---油猴插件。
什么是油猴插件?
篡改猴 (Tampermonkey) 是拥有 超过 1000 万用户 的最流行的浏览器扩展之一。它适用于 Chrome、Microsoft Edge、Safari 等主流浏览器。
它允许用户自定义并增强您最喜爱的网页的功能。用户脚本是小型 JavaScript 程序,可用于向网页添加新功能或修改现有功能。使用 篡改猴,您可以轻松在任何网站上创建、管理和运行这些用户脚本。简单说,油猴插件是一个 Chrome 插件,但是它的功能是一个脚本管理器,能将自定义的脚本注入到当前页面,让你的代码成为网页的一部分。
油猴提供的API:
Documentation | Tampermonkey
GM_*API 按功能主要分为 WEB请求类: GM_xmlhttpRequest(details) GM_webRequest(rules, listener) cookie操作: GM_cookie.list(details[, callback]) GM_cookie.set(details[, callback]) GM_cookie.delete(details, callback) tab选项卡操作: GM_getTab(callback) GM_saveTab(tab) GM_getTabs(callback) 键值对操作: GM_setValue(key, value) GM_getValue(key, defaultValue) GM_deleteValue(key) GM_listValues() GM_addValueChangeListener(key, (key, old_value, new_value, remote) => void) GM_removeValueChangeListener(listenerId) 修改dom: GM_addElement(tag_name, attributes), GM_addElement(parent_node, tag_name, attributes) 添加样式: GM_addStyle(css) 下载: GM_download(details), GM_download(url, name) 获取@resource 引入的资源文件的文本内容(比如js) GM_getResourceText(name) 获取@resource 引入的资源文件的源地址 GM_getResourceURL(name) 控制台打印 GM_log(message) 屏幕通知 GM_notification(details, ondone), GM_notification(text, title, image, onclick) 打开新选项卡 GM_openInTab(url, options), GM_openInTab(url, loadInBackground) 菜单注册 GM_registerMenuCommand(name, callback, accessKey) 菜单注销 GM_unregisterMenuCommand(menuCmdId) 设置剪切板 GM_setClipboard(data, info) windows窗体操作: 窗口地址改变 window.onurlchange 窗口关闭 window.close() 窗口聚焦 window.focus()
油猴脚本开发详解+油猴爬虫脚本实例_其它综合_脚本之家
demo1:页面上增加刷新按钮,且可以实现拖拽:
// ==UserScript==
// @name 测试插件
// @version 0.0.1
// @description 百度首页刷新
// @namespace baidu.com
// @match *://*/*
// @grant GM_addStyle
// ==/UserScript==
const addContainerDiv=()=>{
const containerDiv=document.createElement("div");
containerDiv.id="test-tool"
containerDiv.innerHTML= "<button>刷新1</button>"
GM_addStyle('#test-tool {position:fixed;right:300px;top:280px;}')
//containerDiv.addEventListener("click",()=>{window.location.reload()})
document.body.appendChild(containerDiv);
//设置可拖拽
const dragButton=document.getElementById("test-tool");
dragButton.onmousedown = function(ev){
// 获取鼠标相对于盒子的坐标
var x2 = ev.offsetX;
var y2 = ev.offsetY;
// 鼠标移动
document.onmousemove = function (ev) {
var x3 = ev.pageX;
var y3 = ev.pageY;
dragButton.style.top = y3 - y2 + "px";
dragButton.style.left = x3 - x2 + "px"
}
}
// 4.鼠标松开事件
dragButton.onmouseup = function () {
document.onmousemove = null;
}
}
(function() {
'use strict';
addContainerDiv()
})();
效果:(注意:如果加上刷新动作的话,会导致拖拽无效;所以先把这行代码注释掉了)
demo2:录制接口
可以看到接口一般有两种类型,分别是fetch和xhr
Fetch和XHR都是用于发起HTTP请求的技术,但它们有以下几点区别:
1
原生API vs ES6新增函数:XHR是浏览器提供的原生API,而Fetch是ES6中新增的全局函数。
2
使用对象差异:XHR使用XMLHttpRequest对象,而Fetch使用Promise对象。
3
Cookies默认携带:Fetch默认不会携带cookies,需要手动设置credentials属性;而XHR请求会自动携带cookies。
4
请求取消能力:XHR可以取消一个正在进行的请求,而Fetch目前没有原生的请求取消机制。
5
响应类型处理:XHR的responseType属性可以设置响应类型(text、json、blob等),而Fetch需要手动解析响应。
6
进度监听功能:XHR可以监听上传和下载的进度,而Fetch不支持此功能。
7
错误处理方式:在错误处理方面,Fetch只会在网络错误时reject Promise,其他错误都会被视为成功的响应,需要手动判断;而XHR则会在出现错误时reject Promise。
8
兼容性:XHR兼容性更好,在一些旧版本的浏览器中可能无法使用Fetch2。
9
关注分离:Fetch是一种关注分离的技术,把复杂的事情拆分成几个简单的步骤实现,并得到结果3。
10
底层抽象:Fetch API更底层,包括Request、Response、Headers、Body等原生对象,而XHR需要使用一个实例来发出请求和处理响应。
11
灵活性:Fetch API比XHR更灵活,可以明确的配置请求和响应4。
12
兼容性:XHR兼容性更好,在一些旧版本的浏览器中可能无法使用Fetch2。
综上所述,Fetch和XHR各有优缺点,开发者应当根据项目需求和兼容性要求选择合适的请求技术
针对xhr:
- 把运行时间设置为document-start,确保能拦截到较早发出的请求。
- 使用
@grant unsafeWindow
声明,授予脚本访问或修改全局窗口对象的权限。参考:油猴脚本高级应用:拦截与修改网页Fetch请求实战指南_油猴拦截请求-CSDN博客
这里有一点点无用代码,自己改改
// ==UserScript==
// @name 测试插件
// @run-at document-start
// @version 0.0.1
// @description 百度首页刷新
// @namespace baidu.com
// @match *://*/*
// @grant GM_addStyle
// @require http://code.jquery.com/jquery-1.11.0.min.js
// @grant unsafeWindow
// ==/UserScript==
const addContainerDiv=()=>{
const containerDiv=document.createElement("div");
containerDiv.id="test-tool"
containerDiv.innerHTML= "<button>刷新1</button>"
GM_addStyle('#test-tool {position:fixed;right:300px;top:280px;}')
var aweme_list=[];
containerDiv.addEventListener("click",()=>{
console.log('aba:');
console.log("aweme_list"+aweme_list);
// 定义包含名称和链接的数组
const files = [];
aweme_list.forEach((item)=>{
if(item.aweme_type==0||item.awemeType==0||item.aweme_type==61||item.awemeType==61){
try{files.push({name:item.desc,url:item.video.play_addr.url_list[0]})}catch{files.push({name:item.desc,url:item.video.playAddr[0]})}
}
if(item.aweme_type==68||item.awemeType==68){
var urlList=[]
item.images.forEach(img=>{
try{urlList.push(img.url_list[0])}catch{urlList.push(img.urlList[0])}
})
files.push({name:item.desc,urlList:urlList})
}
});
console.log(files);
})
document.body.appendChild(containerDiv);
//设置可拖拽
const dragButton=document.getElementById("test-tool");
dragButton.onmousedown = function(ev){
// 获取鼠标相对于盒子的坐标
var x2 = ev.offsetX;
var y2 = ev.offsetY;
// 鼠标移动
document.onmousemove = function (ev) {
var x3 = ev.pageX;
var y3 = ev.pageY;
dragButton.style.top = y3 - y2 + "px";
dragButton.style.left = x3 - x2 + "px"
}
}
// 4.鼠标松开事件
dragButton.onmouseup = function () {
document.onmousemove = null;
}
}
(function() {
'use strict';
addContainerDiv();
$(() => {
function addXMLRequestCallback(callback) {
// 是一个劫持的函数
var oldSend, i;
if (XMLHttpRequest.callbacks) {
// 判断XMLHttpRequest对象下是否存在回调列表,存在就push一个回调的函数
// we've already overridden send() so just add the callback
XMLHttpRequest.callbacks.push(callback);
} else {
// create a callback queue
XMLHttpRequest.callbacks = [callback];
// 如果不存在则在xmlhttprequest函数下创建一个回调列表
// store the native send()
oldSend = XMLHttpRequest.prototype.send;
// 获取旧xml的send函数,并对其进行劫持
// override the native send()
XMLHttpRequest.prototype.send = function () {
// process the callback queue
// the xhr instance is passed into each callback but seems pretty useless
// you can't tell what its destination is or call abort() without an error
// so only really good for logging that a request has happened
// I could be wrong, I hope so...
// EDIT: I suppose you could override the onreadystatechange handler though
for (i = 0; i < XMLHttpRequest.callbacks.length; i++) {
XMLHttpRequest.callbacks[i](this);
}
// 循环回调xml内的回调函数
// 由于我们获取了send函数的引用,并且复写了send函数,这样我们在调用原send的函数的时候,需要对其传入引用,而arguments是传入的参数
// call the native send()
oldSend.apply(this, arguments);
}
}
}
// e.g.
addXMLRequestCallback(function (xhr) {
// 调用劫持函数,填入一个function的回调函数
// 回调函数监听了对xhr调用了监听load状态,并且在触发的时候再次调用一个function,进行一些数据的劫持以及修改
xhr.addEventListener("load", function () {
if (xhr.readyState == 4 && xhr.status == 200) {
// 获取URL
var url = new URL(xhr.responseURL);
console.log("xhr接口:" + url);
}
});
});
})
})();
另一种写法,可以拿到url+参数:
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
var xhr = this;
// 保存原始的onreadystatechange事件处理器
var originalOnReadyStateChange = xhr.onreadystatechange;
// 重写onreadystatechange事件处理器
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // 请求已完成
// 打印URL和请求参数
console.log('Request URL:', xhr.responseURL);
console.log('Request Parameters:', body);
}
// 如果存在,则调用原始的onreadystatechange事件处理器
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};
// 调用原始的send方法
originalSend.apply(this, arguments);
};
fetch的录制下来:
const originFetch = fetch;
unsafeWindow.fetch = (...arg) => {
console.log('fetch arg', ...arg);
//console.log('通过')
return originFetch(...arg);
}
如果想要修改响应的数据:
参考:https://zhuanlan.zhihu.com/p/436757974
let oldfetch = fetch; function fuckfetch() { return new Promise((resolve, reject) => { oldfetch.apply(this, arguments).then(response => { const oldJson = response.json; response.json = function() { return new Promise((resolve, reject) => { oldJson.apply(this, arguments).then(result => { result.hook = 'success'; resolve(result); }); }); }; resolve(response); }); }); } window.fetch = fuckfetch;
完整demo要求:抓捕当前页面上的所有请求,得到fetch格式:
得到这一串代码:
fetch("http://xx/compare", {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/json",
"proxy-connection": "keep-alive",
"token": “”,
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "{\"id\":\"3862\",“\Version\":\"001420240626\"}",
"method": "POST",
});
然后可以拷贝url和body,之后可以直接粘贴到接口自动化脚本中,方便编写脚本
// ==UserScript==
// @name 测试插件
// @run-at document-start
// @version 0.0.1
// @description 百度首页刷新
// @namespace baidu.com
// @match *://*/*
// @grant GM_addStyle
// @require http://code.jquery.com/jquery-1.11.0.min.js
// @grant unsafeWindow
// ==/UserScript==
(function () {
//'use strict';
var apiList = [];
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
var xhr = this;
// 保存原始的onreadystatechange事件处理器
var originalOnReadyStateChange = xhr.onreadystatechange;
// 重写onreadystatechange事件处理器
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { // 请求已完成
apiList.push({
url: xhr.responseURL,
status: xhr.status,
body: body
})
console.log("apiList", apiList);
}
// 如果存在,则调用原始的onreadystatechange事件处理器
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};
// 调用原始的send方法
originalSend.apply(this, arguments);
};
const originFetch = fetch;
unsafeWindow.fetch = (...arg) => {
console.log('fetch arg', ...arg);
//console.log('通过')
return originFetch(...arg);
}
//控件
function addContainerDiv() {
const containerDiv = document.createElement("div");
containerDiv.id = "test-tool"
containerDiv.innerHTML = "<button>获取接口</button>"
GM_addStyle('#test-tool {position:fixed;right:300px;top:280px;}')
containerDiv.addEventListener("click", () => {
//console.log(apiList);
})
document.body.appendChild(containerDiv);
//设置可拖拽
const dragButton = document.getElementById("test-tool");
dragButton.onmousedown = function (ev) {
// 获取鼠标相对于盒子的坐标
var x2 = ev.offsetX;
var y2 = ev.offsetY;
// 鼠标移动
document.onmousemove = function (ev) {
var x3 = ev.pageX;
var y3 = ev.pageY;
dragButton.style.top = y3 - y2 + "px";
dragButton.style.left = x3 - x2 + "px"
}
}
// 4.鼠标松开事件
dragButton.onmouseup = function () {
document.onmousemove = null;
}
}
addContainerDiv();
})();
版权归原作者 6moji6 所有, 如有侵权,请联系我们删除。