最近对脚本开发比较感兴趣,通过JavaScript编写脚本,不仅可以加深你对JavaScript使用的熟练程度,更甚可以锻炼你的逻辑能力。
IOS上面有一些可以定时运行js脚本的工具,这些工具可以实现通过js定时京东签到,漫画签到等一系列的功能。Quantumult-X
就是其中的一个软件,我知道的还有Loon
、Surge
不过后两者我是完全没有使用过。
奇怪的是Quantumult-X
这种软件,我目前没有找到官方的API文档,翻来覆去折腾了好久,还是决定直接看一下别人写的源码,其中我参考的是京东签到这个脚本的源代码。
读这种比较长的源码一定不要在网页上面阅读,因为没有代码定位,你可以将它拷贝到本地,通过vscode这一类的编辑器打开阅读,其中它有一个比较重要的方法就是。
注:本篇文章不适合对JavaScript没有了解的小白阅读,同时阅读本篇文章之前,相信你已经知道Quantumult-X
的基础用法,并且已经可以正常使用别人的开源脚本。
1. 工具函数
下面是脚本作者封装的工具函数,里面实现了http请求、消息提醒、警告、数据持久化储存。
可以大致过一遍这些源代码,在后面的文章会单独的将上面提到的功能进行讲解。
/**
* 工具类
* @return {{read: ((function(*=): (*|null|undefined))|*), isRequest: boolean, isLoon: boolean, isQuanX: boolean, isNode: boolean, done: ((function(*=): (*|undefined))|*), notify: notify, isSurge: boolean, post: post, AnError: (function(*, *=, *=, *=, *): void), get: get, time: (function(): void), isJSBox: boolean, write: ((function(*=, *=): (*|undefined))|*)}}
*/
export function nobyda() {
const start = Date.now();
// 判断是否是重写
const isRequest = typeof $request != "undefined";
// 判断是否是Surge
const isSurge = typeof $httpClient != "undefined";
// 判断是否是QuanX
const isQuanX = typeof $task != "undefined";
// 判断是否是Loon
const isLoon = typeof $loon != "undefined";
// 判断是否是JSBox
const isJSBox = typeof $app != "undefined" && typeof $http != "undefined";
// 判断是否是Node环境
const isNode = typeof require == "function" && !isJSBox;
const NodeSet = "CookieSet.json";
/**
* 引入Nodejs中的request模块和fs模块
* @type {{request: *, fs: module:fs}|null}
*/
const node = (() => {
if (isNode) {
const request = require("request");
const fs = require("fs");
return ({
request,
fs
});
} else {
return null;
}
})();
/**
* 提示信息
* @param {string} title 标题
* @param {string} subtitle 副标题
* @param {string} message 提示信息
* @param {*} rawopts 设置
*/
const notify = (title, subtitle, message, rawopts) => {
const Opts = (rawopts) => {
//Modified from https://github.com/chavyleung/scripts/blob/master/Env.js
if (!rawopts) return rawopts;
switch (typeof rawopts) {
case "string":
return isLoon
? rawopts
: isQuanX
? {
"open-url": rawopts,
}
: isSurge
? {
url: rawopts,
}
: undefined;
case "object":
if (isLoon) {
let openUrl = rawopts.openUrl || rawopts.url || rawopts["open-url"];
let mediaUrl = rawopts.mediaUrl || rawopts["media-url"];
return {
openUrl,
mediaUrl,
};
} else if (isQuanX) {
let openUrl = rawopts["open-url"] || rawopts.url || rawopts.openUrl;
let mediaUrl = rawopts["media-url"] || rawopts.mediaUrl;
return {
"open-url": openUrl,
"media-url": mediaUrl,
};
} else if (isSurge) {
let openUrl = rawopts.url || rawopts.openUrl || rawopts["open-url"];
return {
url: openUrl,
};
}
break;
default:
return undefined;
}
};
console.log(`${ title }\n${ subtitle }\n${ message }`);
if (isQuanX) $notify(title, subtitle, message, Opts(rawopts));
if (isSurge) $notification.post(title, subtitle, message, Opts(rawopts));
if (isJSBox) $push.schedule({
title: title,
body: subtitle ? subtitle + "\n" + message : message
});
};
// 将获得的cookies信息储存起来
const write = (value, key) => {
if (isQuanX) return $prefs.setValueForKey(value, key);
if (isSurge) return $persistentStore.write(value, key);
if (isNode) {
try {
if (!node.fs.existsSync(NodeSet)) node.fs.writeFileSync(NodeSet, JSON.stringify({}));
const dataValue = JSON.parse(node.fs.readFileSync(NodeSet));
if (value) dataValue[key] = value;
if (!value) delete dataValue[key];
return node.fs.writeFileSync(NodeSet, JSON.stringify(dataValue));
} catch (er) {
return AnError("Node.js持久化写入", null, er);
}
}
if (isJSBox) {
if (!value) return $file.delete(`shared://${ key }.txt`);
return $file.write({
data: $data({
string: value
}),
path: `shared://${ key }.txt`
});
}
};
// 将获取的cookies信息读出来
const read = (key) => {
if (isQuanX) return $prefs.valueForKey(key);
if (isSurge) return $persistentStore.read(key);
if (isNode) {
try {
if (!node.fs.existsSync(NodeSet)) return null;
const dataValue = JSON.parse(node.fs.readFileSync(NodeSet));
return dataValue[key];
} catch (er) {
return AnError("Node.js持久化读取", null, er);
}
}
if (isJSBox) {
if (!$file.exists(`shared://${ key }.txt`)) return null;
return $file.read(`shared://${ key }.txt`).string;
}
};
const adapterStatus = (response) => {
if (response) {
if (response.status) {
response["statusCode"] = response.status;
} else if (response.statusCode) {
response["status"] = response.statusCode;
}
}
return response;
};
// get请求
const get = (options, callback) => {
options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)";
if (isQuanX) {
if (typeof options == "string") options = {
url: options
};
options["method"] = "GET";
//options["opts"] = {
// "hints": false
//}
$task.fetch(options).then(response => {
callback(null, adapterStatus(response), response.body);
}, reason => callback(reason.error, null, null));
}
if (isSurge) {
options.headers["X-Surge-Skip-Scripting"] = false;
$httpClient.get(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isNode) {
node.request(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isJSBox) {
if (typeof options == "string") options = {
url: options
};
options["header"] = options["headers"];
options["handler"] = function (resp) {
let error = resp.error;
if (error) error = JSON.stringify(resp.error);
let body = resp.data;
if (typeof body == "object") body = JSON.stringify(resp.data);
callback(error, adapterStatus(resp.response), body);
};
$http.get(options);
}
};
// post请求
const post = (options, callback) => {
options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)";
if (options.body) options.headers["Content-Type"] = "application/x-www-form-urlencoded";
if (isQuanX) {
if (typeof options == "string") options = {
url: options
};
options["method"] = "POST";
$task.fetch(options).then(response => {
callback(null, adapterStatus(response), response.body);
}, reason => callback(reason.error, null, null));
}
if (isSurge) {
options.headers["X-Surge-Skip-Scripting"] = false;
$httpClient.post(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isNode) {
node.request.post(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isJSBox) {
if (typeof options == "string") options = {
url: options
};
options["header"] = options["headers"];
options["handler"] = function (resp) {
let error = resp.error;
if (error) error = JSON.stringify(resp.error);
let body = resp.data;
if (typeof body == "object") body = JSON.stringify(resp.data);
callback(error, adapterStatus(resp.response), body);
};
$http.post(options);
}
};
// 异常信息
const AnError = (name, keyname, er, resp, body) => {
if (typeof (merge) != "undefined" && keyname) {
if (!merge[keyname].notify) {
merge[keyname].notify = `${ name }: 异常, 已输出日志 ‼️`;
} else {
merge[keyname].notify += `\n${ name }: 异常, 已输出日志 ‼️ (2)`;
}
merge[keyname].error = 1;
}
return console.log(`\n‼️${ name }发生错误\n‼️名称: ${ er.name }\n‼️描述: ${ er.message }${ JSON.stringify(er).match(/"line"/) ? `\n‼️行列: ${ JSON.stringify(er) }` : `` }${ resp && resp.status ? `\n‼️状态: ${ resp.status }` : `` }${ body ? `\n‼️响应: ${ resp && resp.status != 503 ? body : `Omit.` }` : `` }`);
};
// 总共用时
const time = () => {
const end = ((Date.now() - start) / 1000).toFixed(2);
return console.log("\n签到用时: " + end + " 秒");
};
// 关闭请求
const done = (value = {}) => {
if (isQuanX) return $done(value);
if (isSurge) isRequest ? $done(value) : $done();
};
return {
AnError,
isRequest,
isJSBox,
isSurge,
isQuanX,
isLoon,
isNode,
notify,
write,
read,
get,
post,
time,
done
};
}
2. 分析
2.1 判断环境
其实大部分代码我都已经在阅读源码的时候标上了注释,源码不难理解,首先进入代码一开始就是判断各种环境:
// 判断是否是重写
const isRequest = typeof $request != "undefined";
// 判断是否是Surge
const isSurge = typeof $httpClient != "undefined";
// 判断是否是QuanX
const isQuanX = typeof $task != "undefined";
// 判断是否是Loon
const isLoon = typeof $loon != "undefined";
// 判断是否是JSBox
const isJSBox = typeof $app != "undefined" && typeof $http != "undefined";
// 判断是否是Node环境
const isNode = typeof require == "function" && !isJSBox;
该作者的这个脚本不仅仅可以用在QuanX上面,还可以在Surge、Loon、JSBox甚至是Node环境上运行。
2.2 提示信息
该方法封装了消息提示功能,就跟微信来消息的弹框一样,如果你没有给该软件提示权限则收不到相关的消息提示。
/**
* 提示信息
* @param {string} title 标题
* @param {string} subtitle 副标题
* @param {string} message 提示信息
* @param {*} rawopts 设置
*/
const notify = (title, subtitle, message, rawopts) => {
const Opts = (rawopts) => {
//Modified from https://github.com/chavyleung/scripts/blob/master/Env.js
if (!rawopts) return rawopts;
switch (typeof rawopts) {
case "string":
return isLoon
? rawopts
: isQuanX
? {
"open-url": rawopts,
}
: isSurge
? {
url: rawopts,
}
: undefined;
case "object":
if (isLoon) {
let openUrl = rawopts.openUrl || rawopts.url || rawopts["open-url"];
let mediaUrl = rawopts.mediaUrl || rawopts["media-url"];
return {
openUrl,
mediaUrl,
};
} else if (isQuanX) {
let openUrl = rawopts["open-url"] || rawopts.url || rawopts.openUrl;
let mediaUrl = rawopts["media-url"] || rawopts.mediaUrl;
return {
"open-url": openUrl,
"media-url": mediaUrl,
};
} else if (isSurge) {
let openUrl = rawopts.url || rawopts.openUrl || rawopts["open-url"];
return {
url: openUrl,
};
}
break;
default:
return undefined;
}
};
console.log(`${ title }\n${ subtitle }\n${ message }`);
if (isQuanX) $notify(title, subtitle, message, Opts(rawopts));
if (isSurge) $notification.post(title, subtitle, message, Opts(rawopts));
if (isJSBox) $push.schedule({
title: title,
body: subtitle ? subtitle + "\n" + message : message
});
};
2.3 异常信息
脚本中封装的异常信息提示方法。
// 异常信息
const AnError = (name, keyname, er, resp, body) => {
if (typeof (merge) != "undefined" && keyname) {
if (!merge[keyname].notify) {
merge[keyname].notify = `${ name }: 异常, 已输出日志 ‼️`;
} else {
merge[keyname].notify += `\n${ name }: 异常, 已输出日志 ‼️ (2)`;
}
merge[keyname].error = 1;
}
return console.log(`\n‼️${ name }发生错误\n‼️名称: ${ er.name }\n‼️描述: ${ er.message }${ JSON.stringify(er).match(/"line"/) ? `\n‼️行列: ${ JSON.stringify(er) }` : `` }${ resp && resp.status ? `\n‼️状态: ${ resp.status }` : `` }${ body ? `\n‼️响应: ${ resp && resp.status != 503 ? body : `Omit.` }` : `` }`);
};
2.4 请求
在脚本的编写中因为要获取第三方网站的信息,或者模拟请求,所以需要使用到http请求,由于各个软件的请求方式有些许不同,所以该脚本中封装了一个get
以及post
请求,进行了差异化处理,直接调用这两个方法就可以进行http请求。
// get请求
const get = (options, callback) => {
options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)";
if (isQuanX) {
if (typeof options == "string") options = {
url: options
};
options["method"] = "GET";
//options["opts"] = {
// "hints": false
//}
$task.fetch(options).then(response => {
callback(null, adapterStatus(response), response.body);
}, reason => callback(reason.error, null, null));
}
if (isSurge) {
options.headers["X-Surge-Skip-Scripting"] = false;
$httpClient.get(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isNode) {
node.request(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isJSBox) {
if (typeof options == "string") options = {
url: options
};
options["header"] = options["headers"];
options["handler"] = function (resp) {
let error = resp.error;
if (error) error = JSON.stringify(resp.error);
let body = resp.data;
if (typeof body == "object") body = JSON.stringify(resp.data);
callback(error, adapterStatus(resp.response), body);
};
$http.get(options);
}
};
// post请求
const post = (options, callback) => {
options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)";
if (options.body) options.headers["Content-Type"] = "application/x-www-form-urlencoded";
if (isQuanX) {
if (typeof options == "string") options = {
url: options
};
options["method"] = "POST";
$task.fetch(options).then(response => {
callback(null, adapterStatus(response), response.body);
}, reason => callback(reason.error, null, null));
}
if (isSurge) {
options.headers["X-Surge-Skip-Scripting"] = false;
$httpClient.post(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isNode) {
node.request.post(options, (error, response, body) => {
callback(error, adapterStatus(response), body);
});
}
if (isJSBox) {
if (typeof options == "string") options = {
url: options
};
options["header"] = options["headers"];
options["handler"] = function (resp) {
let error = resp.error;
if (error) error = JSON.stringify(resp.error);
let body = resp.data;
if (typeof body == "object") body = JSON.stringify(resp.data);
callback(error, adapterStatus(resp.response), body);
};
$http.post(options);
}
};
// 关闭请求
const done = (value = {}) => {
if (isQuanX) return $done(value);
if (isSurge) isRequest ? $done(value) : $done();
};
2.5 数据读取
该脚本针对数据持久化储存进行了封装,可以将你获取到的Cookie信息存储下来,就不用每次运行脚本时都需要填写Cookie信息。
// 将获得的cookies信息储存起来
const write = (value, key) => {
if (isQuanX) return $prefs.setValueForKey(value, key);
if (isSurge) return $persistentStore.write(value, key);
if (isNode) {
try {
if (!node.fs.existsSync(NodeSet)) node.fs.writeFileSync(NodeSet, JSON.stringify({}));
const dataValue = JSON.parse(node.fs.readFileSync(NodeSet));
if (value) dataValue[key] = value;
if (!value) delete dataValue[key];
return node.fs.writeFileSync(NodeSet, JSON.stringify(dataValue));
} catch (er) {
return AnError("Node.js持久化写入", null, er);
}
}
if (isJSBox) {
if (!value) return $file.delete(`shared://${ key }.txt`);
return $file.write({
data: $data({
string: value
}),
path: `shared://${ key }.txt`
});
}
};
// 将获取的cookies信息读出来
const read = (key) => {
if (isQuanX) return $prefs.valueForKey(key);
if (isSurge) return $persistentStore.read(key);
if (isNode) {
try {
if (!node.fs.existsSync(NodeSet)) return null;
const dataValue = JSON.parse(node.fs.readFileSync(NodeSet));
return dataValue[key];
} catch (er) {
return AnError("Node.js持久化读取", null, er);
}
}
if (isJSBox) {
if (!$file.exists(`shared://${ key }.txt`)) return null;
return $file.read(`shared://${ key }.txt`).string;
}
};
3. 重写
写过脚本或者爬虫的人应该都比较清楚,http为无状态请求,也就是后端并不知道请求之前用户进行了什么操作,如果服务器要识别某一请求为哪个用户发出来的,现在最主流的有两种办法,一种是token,一种是Cookie。
而从该脚本中可以得知,京东明显是使用了Cookie判断用户信息,那么如何使用脚本来获取cookie信息呢?
根据源代码,如果const isRequest = typeof $request != "undefined";
那么该脚本即为重写。
那么我们只需要着重观察哪儿有调用isRequest
。
if (DeleteCookie) {
if ($nobyda.read(EnvInfo) || $nobyda.read(EnvInfo2)) {
$nobyda.write("", EnvInfo);
$nobyda.write("", EnvInfo2);
$nobyda.notify("京东Cookie清除成功 !", "", "请手动关闭脚本内\"DeleteCookie\"选项");
$nobyda.done();
return;
}
$nobyda.notify("脚本终止", "", "未关闭脚本内\"DeleteCookie\"选项 ‼️");
$nobyda.done();
return;
} else if ($nobyda.isRequest) {
// 如果为重写,那么就执行GetCookie函数
GetCookie();
return;
}
可以看到在$nobyda.isRequest
为true
时调用了GetCookie()
函数,于是我们就着重分析GetCookie()
函数。
// 自动获取cookie方法
function GetCookie() {
try {
if ($request.headers && $request.url.match(/api\.m\.jd\.com.*=signBean/)) {
var CV = $request.headers["Cookie"];
if (CV.match(/pt_key=.+?;/) && CV.match(/pt_pin=.+?;/)) {
var CookieValue = CV.match(/pt_key=.+?;/)[0] + CV.match(/pt_pin=.+?;/)[0];
var CK1 = $nobyda.read("CookieJD");
var CK2 = $nobyda.read("CookieJD2");
var AccountOne = CK1 ? CK1.match(/pt_pin=.+?;/) ? CK1.match(/pt_pin=(.+?);/)[1] : null : null;
var AccountTwo = CK2 ? CK2.match(/pt_pin=.+?;/) ? CK2.match(/pt_pin=(.+?);/)[1] : null : null;
var UserName = CookieValue.match(/pt_pin=(.+?);/)[1];
var DecodeName = decodeURIComponent(UserName);
if (!AccountOne || UserName == AccountOne) {
var CookieName = " [账号一] ";
var CookieKey = "CookieJD";
} else if (!AccountTwo || UserName == AccountTwo) {
var CookieName = " [账号二] ";
var CookieKey = "CookieJD2";
} else {
$nobyda.notify("更新京东Cookie失败", "非历史写入账号 ‼️", "请开启脚本内\"DeleteCookie\"以清空Cookie ‼️");
return;
}
} else {
$nobyda.notify("写入京东Cookie失败", "", "请查看脚本内说明, 登录网页获取 ‼️");
return;
}
const RA = $nobyda.read(CookieKey);
if (RA == CookieValue) {
console.log(`\n用户名: ${ DecodeName }\n与历史京东${ CookieName }Cookie相同, 跳过写入 ⚠️`);
} else {
const WT = $nobyda.write(CookieValue, CookieKey);
$nobyda.notify(`用户名: ${ DecodeName }`, ``, `${ RA ? `更新` : `写入` }京东${ CookieName }Cookie${ WT ? `成功 ?` : `失败 ‼️` }`);
}
} else if ($request.url === "http://www.apple.com/") {
$nobyda.notify("京东签到", "", "类型错误, 手动运行请选择上下文环境为Cron ⚠️");
} else {
$nobyda.notify("京东签到", "写入Cookie失败", "请检查匹配URL或配置内脚本类型 ⚠️");
}
} catch (eor) {
$nobyda.write("", "CookieJD");
$nobyda.write("", "CookieJD2");
$nobyda.notify("写入京东Cookie失败", "", "已尝试清空历史Cookie, 请重试 ⚠️");
console.log(`\n写入京东Cookie出现错误 ‼️\n${ JSON.stringify(eor) }\n\n${ eor }\n\n${ JSON.stringify($request.headers) }\n`);
} finally {
$nobyda.done();
}
}
因为Quantumult-X
的重写功能为访问到指定的url就可以触发脚本,根据该脚本来看,几个软件的重写方法没有什么差异化,都是使用的$request
对象,而Node环境下无法自动获取Cookie,必须进行手动填写。
4. 最后
我找了好久Quantumult-X
都没有提供官方文档,所以我并不清楚它的API,不过从上面的脚本来看,大致分为下面几个API:
- $prefs:持久化数据存储(读取和写入)。
- $task:网络请求。
- $done:请求完毕时需要调用。
- $request:重写网络请求,用来获取请求中的Cookie等,甚至可以用来篡改响应体。
- $notify:弹框提示信息。
看了一下该脚本封装的还是比较全的,几乎可能用到的方法都封装进去了,同时我尝试将脚本通过webpack进行压缩,事实证明即使经过webpack压缩,该脚本依然是可以正常使用。也就是说,在编写脚本的时候可以通过webpack引入一些第三方工具类。
目前来说通过webpack压缩脚本仅仅只有一点缺陷,就是在分享脚本时,别人无法直接阅读到你的源代码,而无法进行修改(修改难度高),不过如果你不想暴露自己的脚本源代码,通过webpack压缩是一个非常不错的选择。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!