写在开始:
本文内讨论的Listary版本号为 6.0.1,目前最高版本似乎是 6.0.5。文中提到的插件,官方也似乎将接口换成了异步的版本,但是考虑到同步调用和异步调用本质上程序逻辑没有根本的变化,代码主逻辑还是可以一样地跑通,或者直接原封不动换回同步版本也是能用的。
某天下午:
—— “NICE! Listary的字典能用了!!”
——“啥是Listary?”
——“啊这……”
Listary 是一个革命性的Windows搜索工具,借助 Listary软件,你可以快速搜索电脑文件、定位文件、执行智能命令、记录访问历史、快速切换目录、收藏常用项目等。
—— 来自某乎的回答
笔者就是嫌有道词典太臃肿,必应词典太卡,用Listary能大致以随叫随到的小搜索框功能就能实现诸如文件搜索、翻译、简单计算等实用的功能。
—— “原来你也玩原神”
—— “这是重点吗?”
总之就是一个比较方便的小工具。当然也提供字典查词的功能…… 【本来是提供的
这搜索转圈可以转到你下班都转不完,根本读不出来内容。
这么好用的功能就这么无了???不,今天就来解救这个功能!
定位问题
打开Listary的设置,看了看字典是咋肥事,好家伙,是个插件,也没啥可以直接设置查询地址的地方,看来查询地址应该是在插件里写的,没法随意改动,那就得去改插件本身了。
直接对Listary进行了一番小调查,发现它的插件加载都在
C:\Users\<<username>>\AppData\Roaming\Listary\UserProfile\Extensions
这样一个文件夹里,Good,找到了一个叫做listary-extension-dict
的文件夹,看起来就像是字典这个功能的文件夹,不错不错,直接打开,一个index.js
映入眼帘,笔者人傻了,你一个Listary程序,从头到尾C#编译,最后插件编写来了个JavaScript
?
怒翻Listary的安装文件夹,好家伙,藏了个node.js
在里面。额滴个神,笔者寻思插件也是C#呢,合着得用JavaScript
改?
打开这个index.js
,内容如下……【在6.0.5版本内容略有差异,无伤大雅,6.0.5版本的最终修改后代码也会在最后一并放出,TL,DR: 跳到最后】
const request = require('request');
const api = "http://www.iciba.com/index.php";
function search(query) {
var noResult = {
title: "没有找到相关结果"
};
return new Promise((resolve, reject) => {
var options = {
method: 'GET',
url: api,
qs:
{
a: 'getWordMean',
c: 'search',
list: '1',
word: query
},
};
request(options, function (error, response, body) {
if (error) throw new Error(error);
var json = JSON.parse(body);
if (json.errno != 0) {
resolve([{
title: json.errmsg
}]);
return;
}
// console.log(json)
if (json.baesInfo.symbols) {
var results = json.baesInfo.symbols[0].parts.map(part => {
var meaning = part.part + " ";
meaning += part.means.join(";");
var result = {
title: meaning
}
return result;
});
resolve(results);
}
else if (json.baesInfo.translate_result) {
resolve([{
title: json.baesInfo.translate_result
}])
}
else {
resolve([{
title: noResult
}])
}
});
});
}
module.exports = {
search: search
};
第一眼,就印证了我之前的想法,果不其然api地址是写死的哇。把这个iciba相关地址(PHP是世界上最好的语言)直接放到浏览器里一看,芜湖,直接跳转到了主页,这API早就不好使了,这能查到词就有鬼了。
第二眼,// console.log(json)
作者很细啊,把错误信息直接打印在了命令行里,后面一想反反正用户也看不到,注释了得了(狗头)。
第三眼,JavaScript
有一点看不懂哇,不过编程无非那几招,if
、var
、new
还是能看得懂的,跟C#那可是 完全 一样的意思,细品一下嘛,说不定就会了。
第四眼,这花括号有点多,花里胡哨的,还中括号套花括号,眼睛有亿点抗不太住……
第五眼,… 第六眼,… …… ……
经过亿点的领悟,以及百度必应谷歌几位大佬的点拨,大致能看懂这货是在干什么了:
index.js 总览
这程序大致的框架分为三段:
- 定义依赖库以及常量
- 定义一个方法
- 将方法返回
好家伙,直接返回一个方法,这不就是类似返回一个Func<>
对象嘛。Easy。依赖库和常量部分就跳过了,我们先看看这个方法里定义了什么。
function search(query) { 在里面干了什么 }
以程序的视角,这在这个search(query)
函数里,似乎就只干了两件事情:
- 定义了一个叫做
noResult
的变量 - 新构造了一个叫做
Promise
的类的实例并直接返回
noResult
变量就不说了。直接看关键点,Promise
类的实例,大致排版之后,这个实例的构造函数应该长这样:
new Promise
(
(resolve, reject) => {
var options = {
/* 省略 */
};
request(options,
function (error, response, body)
{
/* 省略 */
}
);
}
)
这不就是一个 C# 里的匿名函数(lambda函数)嘛……这我可太熟悉了,经常往什么按钮点击、菜单点击时间里挂的不就是这玩意儿
(o, e) => { /*函数体省略*/ }
既然是个函数,那么这个函数的输入和输出类型就十分关键了,lambda函数的这特点就决定了这玩意儿可以省略类型,自动推导类型,emmm,直接进行一个 JavaScript 官网搜索Promise
类的教学,哦吼,套起来了:
由
(resolve, reject) => { /* 函数体 */ }
构造的函数的输入值,也就是resolve
和reject
俩参数,他俩都是 函数。
把函数作为参数传入另一个函数,嗯,好好品。
说人话版本就是你现在如果构造一个Promise
对象给别人用的话,你需要别人提供 两个函数,然后你进行一番操作,要么 执行第一个函数【resolve】 ,或者就执行第二个函数【reject】 —— 这一番操作是在Promise
对象的构造方法里的lambda函数内执行的。
套在我们当前的环节里:我们需要提供给Listary
一个Promise
对象,它要用这个Promise
对象来执行搜索的操作。不过,我们能提供这个Promise
对象的条件是:“Listary
告诉我们两个函数”,我们搜索完了,要么执行函数一,要么执行函数二。【这里,函数一以及函数二是由Promise
构造方法里的lambda函数里的输入参数,resolve
和reject
提供的】。
也就是说,我们在这个Promise
对象的构造方法的lambda里,需要进行一波操作:
- 获取搜索关键词
- 执行搜索,获取搜索结果
- 获取结果成功之后,执行
Listary
传来的resolve
或者reject
函数
这里,JavaScript约定了,一般来说,Promise
内操作成功则需要执行resolve
(也就是第一个传入参数的函数),若失败了(网络原因或者操作错误输入了错误的单词啥的)则需要执行reject
(也就是第二个传入的参数)。笔者相信Listary
的作者是按套路出牌的人,所以应该会执行这个JavaScript的设计原则,所以,笔者相信我们在搜词成功之后,应该调用resolve
,搜索失败了应该就是调用reject
。
resolve() 的调用传参
好了,经过上面的缜密分析,笔者估计大家也十分地似懂非懂了,总而言之,我们在搜索单词成功之后呢,需要调用一下Listary
那边由作者传过来的resolve
函数。
BUT, HOW?我怎么知道要传什么参数进去才能成功调用?这里没有代码提示啊!!!
JavaScript 作为一门 动态类型语言,这种特性就决定了:即便是电脑,在真正拿到resolve
这个对象之前,它***也不知道这个函数要咋运行。
谁知道呢?
只有写代码的人和上帝知道。
(抖个机灵:程序员写完这段代码之后三个月,就只剩下上帝知道了)
所以…… 直接模仿啊!看作者咋调用resolve
的(直接对 “resolve” 关键词进行一个Ctrl
+F
的搜索):
resolve([{
title: json.baesInfo.translate_result
}])
不错不错,又要学新东西了。这***是个啥?
不过,好歹是个函数调用嘛,先把括号给刨了,看看剩什么:
[
{
title: json.baesInfo.translate_result
}
]
依据笔者对python
的理解和json
数据的理解,以及加上前面对JavaScript的学习,这玩意儿就是个装了对象的列表,[]
是列表,里面的{}
是一个对象的构造函数。这个对象包含了一个属性,属性名字为title
,赋值为json.baesInfo.translate_result
。上面针对于函数resolve
的调用类似于:
public class MyObj {
public string title;
}
resolve(new object[] {
new MyObj() { title = json.baesInfo.translate_result }
});
由于 JavaScript 动态类型的特性,压根不需要向上面C#一样定义MyObj
这个类,也不需要提前定义类有哪些属性,花括号一用,直接就成一个动态类的对象了,对象内的属性直接写直接赋值就行,对象随便创建随便玩。妙啊!!!
所以每个在列表里的对象都会被翻译回来成为一个搜索结果,就像下面的一样:
好嘛,那如果搜索失败了呢?
resolve([{
title: noResult
}])
但是前面的定义了noResult
变量是{ title: "没有找到相关结果" }
,那这里岂不是套起来了?嗯,确实可能存在这样的问题,但是无所谓了,我们想要的了解的resolve()
函数如何使用已经完成,下面就来看reject()
。
reject() 的调用传参
Listary
作者似乎没有调用reject
函数,挺好的省事儿了。
替换搜索API
终于,到了关键步骤了,我们已经知道了“如何处理搜索结果的数据”,现在的问题就是如何通过需要搜索的关键词来获取搜索结果了。这里当然是需要用到网上现有的API(笔者手里没有本地能用的查词API)。也就是说 ——
搞一个能提供翻译的API,把
Listary
原来的那个.php
的地址换成能翻译的数据的API
这里不是我打广告啊,我直接选用了 “有道智云” 的“ 自然语言翻译服务-文本翻译 ”,当时注册直接送50,到现在接近2年了,我用了多少呢?
哇哈哈哈哈哈个人使用几乎没啥门槛的嘛…
注册过程就不说了,总之搜索一下,注册一下,开启“ 自然语言翻译服务-文本翻译 ”,拿到应用ID(AppKey)和应用密钥(Key)。
剩下的嘛,就是我们怎么把从function search(query) { }
函数里的要搜索的关键词query
送给有道智云(或者whatever翻译API)。这里观察了原来的Listary
,是使用request
发起了一个对某个地址的请求。那发送什么内容呢?这就只能找翻译API的服务供应商(这里就是有道智云)了呀,直接进行一个帮助文档的查找:
想仔细看的都仔细看看啊【接口调用参数 这部分还是很值得看的】,不想仔细看的直接跳转到最后的案例代码,找到JavaScript版本(JS版本)。
然后依葫芦画瓢,改改就完事儿了。总结起来就是我们需要:
- 对 https://openapi.youdao.com/api 发起一个
GET
或者POST
请求
1.1HTTPS
协议
1.2GET
/POST
请求
1.3 字符编码UTF8
- 调用的参数格式大致如下
2.1 笔者对于源语言和目标语言一般直接就auto了
2.2 salt生成方式和签名生成方式都需要依照API文档来执行
扯一大堆没用的,直接放出笔者的请求构造:
let api = "https://openapi.youdao.com/api";
let salt = (new Date).getTime();
let curtime = Math.round(new Date().getTime()/1000);
let str1 = appKey + truncate(decodeURIComponent(encodeURIComponent(query))) + salt + curtime + key;
let sign = CryptoJS.SHA256(str1).toString(CryptoJS.enc.Hex);
var api_rq_options = {
method: 'GET',
url: api,
qs: {
q: decodeURIComponent(encodeURIComponent(query)), // 需要查询的单词
appKey: appKey, // 有道智云的“应用ID”
salt: salt, // 用时间生成的salt,为什么要加盐是个新知识,可以自行搜索
from: 'auto', // 翻译自什么语言
to: 'auto', // 翻译到什么语言
sign: sign, // 附带自己的应用密钥的加过盐的再带查询字符的一个摘要算法算完的 认证信息
signType: "v3", // 来自API说明文档,API文档说要写 v3
curtime: curtime, // 当前时间 用来送给服务器做验证
},
};
function truncate(q) {
// 查词时生成摘要认证信息时需要用到的一个临时字符串处理函数 - 作用就是削减字符串长度,避免摘要计算时间过长
// 比如翻译一篇文章时,不想直接对一整篇文章做摘要算法处理
var len = q.length;
if (len <= 20) return q;
return q.substring(0, 10) + len + q.substring(len-10, len);
}
这里操作了一大波encodeURIComponent
,然后又decodeURIComponent
,是为了确保输入的字符串可以以UTF8
的格式来发送。
然后,直接使用 JavaScript 的request
函数对该API地址发起请求:
request(api_rq_options, function(error, response, body) { /* 此处继续省略函数实现 */ })
呱?又要写一个lambda函数??这里是一个完整的lambda函数了,不再是(a,b,c) => {}
的形式了。具体就是向我们的api地址发送请求之后,针对于这个请求结果,会调用这个lambda函数,分别是
error
错误时的错误码;response
成功了的返回码;body
返回的具体消息
但是…… 有道智云 有个很有趣的地方,这个它这个API永远不会返回“错误”,它返回错误也是一个“成功”,但是在body
里会有个叫做errorCode
的属性,用来具体显示有没有成功,如果出错了,这个码会有一个从101~17001不等的一个值来代表具体是什么错误。
不过whatever了,直接挨个处理就完了:
function (error, response, body)
{
// 这里判断错误一般是系统性错误,比如连接超时了之类的,也就是检测非API错误
if (error) throw new Error(error);
var json = JSON.parse(body);
if (json.errorCode > 100) // 判断是不是有错误……
{
// 依照前面的 resolve 函数,构造一个报错的返回查询结果
resolve([
{
title : '查询出错: ' + json.errorCode
}
]);
}
// 好了,接下来就依照API的返回结果来构造结果列表,先来个空列表,然后往里挨个push翻译结果
arr = [];
if (json.basic) // 基本词义
{
for (let ind = 0; ind < json.basic.explains.length; ind++)
{
arr.push(
{
title : json.basic.explains[ind],
subtitle : '基本词义',
}
);
}
}
if (json.web) // 网络词义
{
for (let index = 0; index < json.web.length; index++)
{
let subtitle_str = '网络词义 - ' + json.web[index].key;
for (let j = 0; j < json.web[index].value.length; j++)
{
arr.push({
title: json.web[index].value[j],
subtitle: subtitle_str,
});
}
}
}
for (let i = 0; i < json.translation.length; i++) // 大段文字翻译结果
{
arr.push({
title: json.translation[i],
subtitle: '翻译结果'
});
}
resolve(arr); // 调用resolve,处理arr这个列表结果
}
至此,我们对Listary的改造就完成了,让我们来查个词儿~
于是就有了开头那一幕。
芜湖,舒服了。继续板砖 🦀
老规矩,能用的代码:【别想着嫖我的AppKey和key,自己申请去……】
p.s. 另外,下面的代码都需要依赖
crypto-js
这个npm包,自己可以搜索安装然后复制到Listary插件文件夹下面的 node_modules 文件夹内
【Listary6.0.1版本(亲测6.0.5.16版本也能用)】
这版本主要是没有使用6.0.5.16版最新的axios
包,这个包是用来做异步请求的,可以让代码看起来更简单一点。带这个包的版本详见下面6.0.5版代码。二者没有本质区别
const appKey = 【换成自己的APPID值】;
const key = 【换成自己的密钥】;
const request = require('request');
const CryptoJS = require("crypto-js");
const api = "https://openapi.youdao.com/api";
function truncate(q) {
var len = q.length;
if (len <= 20) return q;
return q.substring(0, 10) + len + q.substring(len-10, len);
}
function search(query) {
return new Promise((resolve, reject) =>
{
let salt = (new Date).getTime();
let curtime = Math.round(new Date().getTime()/1000);
let str1 = appKey + truncate(decodeURIComponent(encodeURIComponent(query))) + salt + curtime + key;
let sign = CryptoJS.SHA256(str1).toString(CryptoJS.enc.Hex);
var options = {
method: 'GET',
url: api,
qs: {
q: decodeURIComponent(encodeURIComponent(query)),
appKey: appKey,
salt: salt,
from: 'auto',
to: 'auto',
sign: sign,
signType: "v3",
curtime: curtime,
},
};
request(options, function (error, response, body) {
if (error) throw new Error(error);
var json = JSON.parse(body);
if (json.errorCode > 100)
{
resolve([ { title : '查询出错: ' + json.errorCode } ]);
return;
}
var arr = []
if (json.basic)
{
for (let ind = 0; ind < json.basic.explains.length; ind++)
{
arr.push({
title: json.basic.explains[ind],
subtitle: '基本词义',
});
}
}
if (json.web)
{
for (let index = 0; index < json.web.length; index++)
{
let subtitle_str = '网络词义 - ' + json.web[index].key;
for (let j = 0; j < json.web[index].value.length; j++)
{
arr.push({
title: json.web[index].value[j],
subtitle: subtitle_str,
});
}
}
}
for (let i = 0; i < json.translation.length; i++)
{
arr.push({
title: json.translation[i],
subtitle: '翻译结果'
});
}
resolve(arr);
});
});
}
module.exports = {
search: search
};
【Listary6.0.5版本】(唯一区别是使用了axios
包,稍微简化了一下代码,隐藏了Promise
对象的显式构造)
const appKey = 【换成自己的APPID值】;
const key = 【换成自己的密钥】;
const axios = require("axios");
const cryptojs = require("crypto-js");
const apiaddr = "https://openapi.youdao.com/api";
function truncate(q) {
var len = q.length;
if (len <= 20) return q;
return q.substring(0, 10) + len + q.substring(len-10, len);
}
async function search(query) {
let salt = (new Date).getTime();
let curtime = Math.round(new Date().getTime()/1000);
let str1 = appKey + truncate(decodeURIComponent(encodeURIComponent(query))) + salt + curtime + key;
let sign = cryptojs.SHA256(str1).toString(cryptojs.enc.Hex);
const response = await axios({
url: apiaddr,
method: 'get',
params: {
q: decodeURIComponent(encodeURIComponent(query)),
appKey: appKey,
salt: salt,
from: 'auto',
to: 'auto',
sign: sign,
signType: "v3",
curtime: curtime,
}
});
const json = response.data;
if (json.errorCode > 100)
{
return [{
title : '查询出错: ' + json.errorCode
}];
}
arr = [];
if (json.basic) // 基本词义
{
for (let ind = 0; ind < json.basic.explains.length; ind++)
{
arr.push(
{
title : json.basic.explains[ind],
subtitle : '基本词义',
}
);
}
}
if (json.web) // 网络词义
{
for (let index = 0; index < json.web.length; index++)
{
let subtitle_str = '网络词义 - ' + json.web[index].key;
for (let j = 0; j < json.web[index].value.length; j++)
{
arr.push({
title: json.web[index].value[j],
subtitle: subtitle_str,
});
}
}
}
for (let i = 0; i < json.translation.length; i++) // 大段文字翻译结果
{
arr.push({
title: json.translation[i],
subtitle: '翻译结果'
});
}
return arr; // 调用resolve,处理arr这个列表结果
}
// 当外部请求进入时,将请求的string送入“search”函数
module.exports = {
search: search
};