1. 起因
经常需要阅读英文文档,冷不丁的会碰到一些不懂的单词,之前的做法是打开一个谷歌翻译的网页在一旁放着,有需要就切换过来查单词。但是来回的切换着实有点麻烦,就想着有没有一些Chrome翻译插件,一番搜寻,找了两款比较心仪的Chrome插件:一个是good word guide的Instant Dictionary,它的优点是直接显示英文的释义,因为很多时候直接看英文的释义更容易理解一个单词。例如对于a bank of memory might be assigned to each CPU
这句话,bank这单词,不管你套用“银行”、“湖畔”、“岸”等意思,感觉都怪怪的,而如果直接看它的英文释义a set or series of similar things, especially electrical or electronic devices, grouped together in rows
,一下子就能明白他说的是一组特性相同的内存;除了Instant Dictionary,另一个就是Google的Google Dictionary,因为我就比较中意谷歌翻译。可是心仪归心仪,这两款插件都有个致命的缺陷:由于众所周知的原因,他们俩都没法链接到他们的服务器。
对于Instant Dictionary,这显然已经没救了,但是对于Google Dictionary,我觉得还可以抢救一下。为什么呢?因为谷歌翻译在中国是可以直接打开的,对应的域名是translate.google.cn
,既然都是Google家的东西,连不上translate.google.com
,那是否可以让它去连translate.google.cn
?查看了Google Dictionary配置选项,并没有切换到国内服务器的选项,这样一来,想在国内用,就只能手动改一改了。
胡适曾经说过:大胆假设,小心求证。我们的假设就是最终单词翻译的请求是通过HTTP进行的,并且域名使用的是translate.google.com
。
2. 行动
好,说干就干,只要思想不滑坡,方法总比困难多。
2.1. 获取插件
Chrome插件的后缀名是.crx
,其实就是一个压缩包。常用的压缩软件一般都能解压,解压出来的是一堆JavaScript文件以及其他相关的一些文件。我有想过它为什么不直接用.zip
做后缀?最后得到了一个我自己比较信服的答案,使用.zip
等常用后缀就相当于在挑衅:“你来解压我啊!”。总会有好事者解压一探究竟,并且这样逼格也就不那么高了。当然,最后.crx
依旧没能阻挡好事者。
但是想解压,那也要先拿到。遗憾的是插件商店在中国正常情况下也是没法访问的,好在你不能访问,别人也不能访问。但是不能访问并不代表需求就消失了,需求始终都在,只有爱会消失。使用必应搜索chrome extension downloader
就能找到一堆下载插件的网站,例如https://crxdown.com/。紧接着,虽然我们不能直接访问到插件,但我们可以网上搜索该插件,得到它的确切地址,这样我们就能将它下载下来。例如在搜索Google dictionary后找到来源为Chrome Web Store的结果,右键选择复制链接地址就能得到对应的插件地址。
2.2. 找入口
得到了插件,下面我们要做的就是找到入口。正常情况下,出于安全的考虑Chrome是没法安装我们下载好了的插件的,即便是来源正经也不行,我们要进入开发者模式。在浏览器地址栏输入chrome://extensions/
,在打开的界面中勾选开发者模式。
然后,点击
load unpacked
加载我们已经解压好了的插件,只需要选择包含manifest.json
这层的文件夹就行。
想要改代码,那就必须先理解代码,想要理解代码,首先要找到一个合适的切入点。理论上,manifest.json
是整个插件的元文件,里面肯定会有描述整个插件的入口文件之类的。但这个门槛有点高,使用这种方法应该是对Chrome插件的开发比较熟悉的,我这种门外汉算了。除此之外,还有另一个方法,那就是直接搜索我们能看到的东西。
运行该插件之后我们发现,触发翻译的条件是我们输入需要翻译的内容后回车或者点击蓝色按钮,正常情况下两种方法最终都会调用同一个函数。因为我们可以从这两个动作入手。通过在所有文件中搜索
Define
这个单词,最终我们在browser_action.html
发现了这个按钮的标签。
从代码中看到,这里有个叫qeury-field
的输入以及一个叫define-btn
的按钮,和我们看到的一致。这个按钮标签定义了id属性却没有定义点击的回调函数,那么很大概率在JavaScript代码中会使用类似get_element_by_id()
这类的函数获取该标签并为其绑定回调函数,因此我们接着使用它的id define-btn
进行搜索。
一共搜索到三条内容,一条在html文件中,也就是我们刚看到的,一条位于css文件中,说明是设置显示样式的不用管,剩下一条在js文件中,果然和我们想的一样。打开该文件,返现代码已经经过混淆挤作一团了,正常人估计没几个能这么读代码,因此需要稍加处理。
处理方式很简单,随便找个JavaScript代码美化网站进行下格式调整,这里我使用的是https://beautifier.io/。进过美化,代码变成了下面的样子。
d = document.getElementById("define-btn");
e = document.getElementById("query-field");
f = document.getElementById("status-box");
g = document.getElementById("status-msg");
h = document.getElementById("status-search-link");
k = document.getElementById("usage-tip");
n = document.getElementById("meaning");
k.display = "block";
k.innerText = "Tip: Select text on any webpage, then click the Google Dictionary button to view the definition of your selection.";
document.getElementById("year").innerText = (new Date).getFullYear();
p(h);
p(document.getElementById("options-link"));
e.focus();
d.addEventListener("click", r, !1);
e.addEventListener("keydown", function(a) {
13 === a.keyCode && r()
}, !1);
可以看到,在这里它找到了输入框,将它命名为e
,找到了按钮将它命名为d
。
这样,我们就算摸到门了。
2.3. 代码梳理
经过前面的探寻我们已经找到了代码入口,可以看到,当我们点击按钮,最终会调用一个名为r
的函数。好吧,让我们看看这个r
长啥样。
r = function() {
var a;
if (a = e.value.replace(/^\s+|\s+$/g, "")) g.innerHTML = "Searching...", f.style.display = "block", h.style.display = "none", k.style.display = "none", n.style.display = "none", d.disabled = !0, c++, chrome.runtime.sendMessage({
type: "fetch_html",
eventKey: c,
query: a
}, q)
},
从代码中看到,首先这个r
函数对输入框中的字符做了简单的处理,最终传递给了chrome.runtime.sendMessage()
。代码到此戛然而止,在源代码中再也找不到chrome.runtime.sendMessage()
的定义了,既然源代码中找不到,那么只能是别的库中的API或者是系统API。从字面上我们知道它把参数发了出去,但是发给谁了呢?发给了服务器?没道理啊。我们可以断定的是变量a
中肯定只包含了需要查询的字符串,而c
的值是数字1,这些参数不足以告诉别的API你的目的。
既然猜测是外部API,那就去搜索引擎搜索chrome.runtime.sendMessage
吧。
最终搜索得到完全匹配的结果都位于Google域名之下,很遗憾没法访问,但是在MDN Web Docs上看到了runtime.sendMessage
的介绍。
最后显示Chrome支持了这个API,那么八九不离十,就是它了。从介绍中我么知道,当使用了
runtime.sendMessage
之后,会有一个叫runtime.onMessage
的API对它进行响应,我们需要接着搜索。
最后发现两个文件使用runtime.onMessage
,它们分别是backgrpund.min.js
以及content.min.js
。同样的,我们对它们的内容进行了美化。
对它们一个一个的梳理,最终确定了chrome.runtime.onMessage.addListener(G)
这条语句中注册的G
函数最终会响应之前点击按钮后调用的sendMessage()
函数,因为每个注册的函数都会先通过type
参数判定这是不是他们该响应的,从中我们看到G
函数判定的是fetch_raw
以及fetch_html
,恰好我们之前看到的sendMessage()
函数中传递进来的参数是fetch_html
。
最终,在梳理G
函数的过程中,见到了我们梦寐以求一个字符串https://translate.google.com/translate_a/t?client=dict-chrome-ex&sl=
,并且看到了XMLHttpRequest
的使用,证明我们的假设是对的。最后一番苦寻之后,只是将.com
改成.cn
。暗自祈祷,希望能成功。
F = function(a, c, b) {
a = "https://translate.google.cn/translate_a/t?client=dict-chrome-ex&sl=" + c + "&tl=" + q.language + "&q=" + encodeURIComponent(a);
var d = new XMLHttpRequest;
d.open("GET", a, !0);
d.onload = function() {
var f = null;
if (200 === this.status) try {
f = JSON.parse(d.response)
} catch (l) {}
return b(f)
};
d.send()
},
遗憾的是,事情并没有想象的那么顺利,插件没能查出词来。
2.4. Debug
怎么回事?一开始就猜错了么?
修改了域名之后,并没有顺利的得到结果。很沮丧,很无奈,但是既然都到这份上了,不搞它一搞又心有不甘。于是乎,打开了调试窗口(鼠标移动到插件图标上右键选择inspect popup)。一番调试下拉,发现代码根本没有跳转进入关键的F
函数当中,而使得代码能够执行F
函数最重要的一个名叫p
的变量的值始终是false
。问题的关键就是这个p
什么时候会变成true
。继续梳理代码,发现当一个叫initBackgroundPageAsync
的函数执行的时候,p
就有可能被赋值true
,并且这是p
唯一变成true
的地方。
window.initBackgroundPageAsync = function(a) {
gapi.config.update("googleapis.config/root", "https://dictionaryextension-pa.googleapis.com");
gapi.client.setApiKey("AIzaSyA6EEtrDCfBkHV8uU2lgGY-N383ZgAOo7Y");
var c = function() {
2 > Object.keys(r).length || (p = !0, a && a())
},
b = function(d) {
Mustache.parse(d);
return function(f) {
return Mustache.render(d, f)
}
};
Q("templates/browser_action_dict.html", function(d) {
r.browser_action_dict = b(d);
c()
});
Q("templates/browser_action_tran.html", function(d) {
r.browser_action_tran = b(d);
c()
})
};
问题又变成了查看该函数何时被调用。最终发现一个名叫background.html
的文件加载https://apis.google.com/js/client.js
这个文件完成后会执行。
查了一下,https://apis.google.com/js/client.js
这个文件用于使用Google全家桶的,在中国其实没啥用并且会带来麻烦,因为根本访问不。由于访问不了,就不可能加载成功;而加载不成功就不会执行initBackgroundPageAsync
,因此决定手动执行initBackgroundPageAsync
。其实也就是在background.min.js
文件的末尾增加一行代码:
window.initBackgroundPageAsync();
本以为到此大功告成,可是现实还是狠狠地给了一巴掌,虽然代码终于执行了F
函数,但依旧没有得到想要的结果。嘿我这暴脾气,跟它杠上了。
这次调试返现,代码在奇怪的地方卡住了,定睛一看,在一个叫D
的函数里面出不来了。
D = function(a, c, b) {
var d = c;
"en-uk" == c && (d = "en");
var f = window["gdx.LANG_TO_CORPUS"][c];
f || (f = c);
a = {
path: "v1/dictionaryExtensionData",
params: {
term: a,
language: d,
corpus: f
}
};
(f = window["gdx.CORPUS_TO_COUNTRY"][f]) && (a.params.country = f);
gapi.client.request(a).execute(function(l) {
var e =
l.status;
if (e && 200 != e) b(null);
else {
l = H(l, "dictionaryData[0]");
if (!l) return b(null);
var m = function(g) {
if (!g.senseFamilies) return 0;
g = g.senseFamilies;
for (var h = g.length, n = 0; n < g.length; n++) g[n].senses && (h += .1 * g[n].senses.length);
return h
};
e = function(g, h) {
return m(h) - m(g)
};
l.entries && (l.entries = l.entries.sort(e));
l.webDefinitions && (l.hasWebDefinitions = !0);
b(l)
}
})
},
仔细分析了下代码,发现它又使用谷歌的API去请求一些奇奇怪怪的东西,然后调用一个回调函数b
。但是有意思的是,当请求失败了,也就是返回码不是200的时候,它依然会调用回调函数b
,只不过传了个空参数。既然如此,有一种可能是传递进去的这个参数是锦上添花的。那么我们就假设他次次都请求失败,所以我们将代码改成了下面的样子:
D = function(a, c, b) {
b(null);
return;
}
重新加载插件运行,呀,成功了!
3. 总结
我为什么一开始就没想到直接搜索translate.google.com
这个字符串呢?以为抄了近路,最后回过头看还是拐了拐。
代码见文后链接。
欢1迎2关3注4个5人6微7信8公9众0号: 爱码士1024
源码 | 原理 | 语言 | 工具
4. Resources
[1] https://github.com/zmychou/google-dictionary-chrome-extension
[2] https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage
[3] https://beautifier.io/
[4] https://crxdown.com/