1. 起因
經(jīng)常需要閱讀英文文檔,冷不丁的會碰到一些不懂的單詞,之前的做法是打開一個谷歌翻譯的網(wǎng)頁在一旁放著,有需要就切換過來查單詞牡借。但是來回的切換著實有點麻煩,就想著有沒有一些Chrome翻譯插件袭异,一番搜尋钠龙,找了兩款比較心儀的Chrome插件:一個是good word guide的Instant Dictionary,它的優(yōu)點是直接顯示英文的釋義御铃,因為很多時候直接看英文的釋義更容易理解一個單詞碴里。例如對于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
,一下子就能明白他說的是一組特性相同的內(nèi)存就珠;除了Instant Dictionary寇壳,另一個就是Google的Google Dictionary,因為我就比較中意谷歌翻譯妻怎】茄祝可是心儀歸心儀,這兩款插件都有個致命的缺陷:由于眾所周知的原因蹂季,他們倆都沒法鏈接到他們的服務(wù)器冕广。
對于Instant Dictionary疏日,這顯然已經(jīng)沒救了偿洁,但是對于Google Dictionary,我覺得還可以搶救一下沟优。為什么呢涕滋?因為谷歌翻譯在中國是可以直接打開的,對應(yīng)的域名是translate.google.cn
挠阁,既然都是Google家的東西宾肺,連不上translate.google.com
溯饵,那是否可以讓它去連translate.google.cn
?查看了Google Dictionary配置選項锨用,并沒有切換到國內(nèi)服務(wù)器的選項丰刊,這樣一來,想在國內(nèi)用增拥,就只能手動改一改了啄巧。
胡適曾經(jīng)說過:大膽假設(shè),小心求證掌栅。我們的假設(shè)就是最終單詞翻譯的請求是通過HTTP進(jìn)行的秩仆,并且域名使用的是translate.google.com
。
2. 行動
好猾封,說干就干澄耍,只要思想不滑坡,方法總比困難多晌缘。
2.1. 獲取插件
Chrome插件的后綴名是.crx
齐莲,其實就是一個壓縮包。常用的壓縮軟件一般都能解壓磷箕,解壓出來的是一堆JavaScript文件以及其他相關(guān)的一些文件铅搓。我有想過它為什么不直接用.zip
做后綴?最后得到了一個我自己比較信服的答案搀捷,使用.zip
等常用后綴就相當(dāng)于在挑釁:“你來解壓我靶顷!”嫩舟∏夂妫總會有好事者解壓一探究竟,并且這樣逼格也就不那么高了家厌。當(dāng)然播玖,最后.crx
依舊沒能阻擋好事者。
但是想解壓饭于,那也要先拿到蜀踏。遺憾的是插件商店在中國正常情況下也是沒法訪問的,好在你不能訪問掰吕,別人也不能訪問果覆。但是不能訪問并不代表需求就消失了,需求始終都在殖熟,只有愛會消失局待。使用必應(yīng)搜索chrome extension downloader
就能找到一堆下載插件的網(wǎng)站,例如https://crxdown.com/。緊接著钳榨,雖然我們不能直接訪問到插件舰罚,但我們可以網(wǎng)上搜索該插件,得到它的確切地址薛耻,這樣我們就能將它下載下來营罢。例如在搜索Google dictionary后找到來源為Chrome Web Store的結(jié)果,右鍵選擇復(fù)制鏈接地址就能得到對應(yīng)的插件地址饼齿。
2.2. 找入口
得到了插件愤钾,下面我們要做的就是找到入口。正常情況下候醒,出于安全的考慮Chrome是沒法安裝我們下載好了的插件的能颁,即便是來源正經(jīng)也不行,我們要進(jìn)入開發(fā)者模式倒淫。在瀏覽器地址欄輸入chrome://extensions/
伙菊,在打開的界面中勾選開發(fā)者模式。
然后敌土,點擊
load unpacked
加載我們已經(jīng)解壓好了的插件镜硕,只需要選擇包含manifest.json
這層的文件夾就行。想要改代碼返干,那就必須先理解代碼兴枯,想要理解代碼,首先要找到一個合適的切入點矩欠。理論上财剖,manifest.json
是整個插件的元文件,里面肯定會有描述整個插件的入口文件之類的癌淮。但這個門檻有點高躺坟,使用這種方法應(yīng)該是對Chrome插件的開發(fā)比較熟悉的,我這種門外漢算了乳蓄。除此之外咪橙,還有另一個方法,那就是直接搜索我們能看到的東西虚倒。
運行該插件之后我們發(fā)現(xiàn)美侦,觸發(fā)翻譯的條件是我們輸入需要翻譯的內(nèi)容后回車或者點擊藍(lán)色按鈕,正常情況下兩種方法最終都會調(diào)用同一個函數(shù)魂奥。因為我們可以從這兩個動作入手菠剩。通過在所有文件中搜索
Define
這個單詞,最終我們在browser_action.html
發(fā)現(xiàn)了這個按鈕的標(biāo)簽捧弃。
<div id="form">
<input type="text" id="query-field"><button id="define-btn" class="btn btn-primary" value="Define">Define</button>
</div
從代碼中看到赠叼,這里有個叫qeury-field
的輸入以及一個叫define-btn
的按鈕擦囊,和我們看到的一致违霞。這個按鈕標(biāo)簽定義了id屬性卻沒有定義點擊的回調(diào)函數(shù)嘴办,那么很大概率在JavaScript代碼中會使用類似get_element_by_id()
這類的函數(shù)獲取該標(biāo)簽并為其綁定回調(diào)函數(shù),因此我們接著使用它的id define-btn
進(jìn)行搜索买鸽。
一共搜索到三條內(nèi)容涧郊,一條在html文件中,也就是我們剛看到的眼五,一條位于css文件中妆艘,說明是設(shè)置顯示樣式的不用管,剩下一條在js文件中看幼,果然和我們想的一樣批旺。打開該文件,返現(xiàn)代碼已經(jīng)經(jīng)過混淆擠作一團(tuán)了诵姜,正常人估計沒幾個能這么讀代碼汽煮,因此需要稍加處理。
處理方式很簡單棚唆,隨便找個JavaScript代碼美化網(wǎng)站進(jìn)行下格式調(diào)整暇赤,這里我使用的是https://beautifier.io/。進(jìn)過美化宵凌,代碼變成了下面的樣子鞋囊。
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. 代碼梳理
經(jīng)過前面的探尋我們已經(jīng)找到了代碼入口,可以看到欠橘,當(dāng)我們點擊按鈕矩肩,最終會調(diào)用一個名為r
的函數(shù)。好吧肃续,讓我們看看這個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
函數(shù)對輸入框中的字符做了簡單的處理始锚,最終傳遞給了chrome.runtime.sendMessage()
刽酱。代碼到此戛然而止,在源代碼中再也找不到chrome.runtime.sendMessage()
的定義了瞧捌,既然源代碼中找不到棵里,那么只能是別的庫中的API或者是系統(tǒng)API润文。從字面上我們知道它把參數(shù)發(fā)了出去,但是發(fā)給誰了呢殿怜?發(fā)給了服務(wù)器典蝌?沒道理啊。我們可以斷定的是變量a
中肯定只包含了需要查詢的字符串头谜,而c
的值是數(shù)字1骏掀,這些參數(shù)不足以告訴別的API你的目的。
既然猜測是外部API柱告,那就去搜索引擎搜索chrome.runtime.sendMessage
吧截驮。
最終搜索得到完全匹配的結(jié)果都位于Google域名之下,很遺憾沒法訪問际度,但是在MDN Web Docs上看到了runtime.sendMessage
的介紹葵袭。
最后顯示Chrome支持了這個API,那么八九不離十乖菱,就是它了坡锡。從介紹中我么知道,當(dāng)使用了
runtime.sendMessage
之后块请,會有一個叫runtime.onMessage
的API對它進(jìn)行響應(yīng)娜氏,我們需要接著搜索。
最后發(fā)現(xiàn)兩個文件使用runtime.onMessage
墩新,它們分別是backgrpund.min.js
以及content.min.js
贸弥。同樣的,我們對它們的內(nèi)容進(jìn)行了美化海渊。
對它們一個一個的梳理绵疲,最終確定了chrome.runtime.onMessage.addListener(G)
這條語句中注冊的G
函數(shù)最終會響應(yīng)之前點擊按鈕后調(diào)用的sendMessage()
函數(shù),因為每個注冊的函數(shù)都會先通過type
參數(shù)判定這是不是他們該響應(yīng)的臣疑,從中我們看到G
函數(shù)判定的是fetch_raw
以及fetch_html
盔憨,恰好我們之前看到的sendMessage()
函數(shù)中傳遞進(jìn)來的參數(shù)是fetch_html
。
最終讯沈,在梳理G
函數(shù)的過程中郁岩,見到了我們夢寐以求一個字符串https://translate.google.com/translate_a/t?client=dict-chrome-ex&sl=
,并且看到了XMLHttpRequest
的使用缺狠,證明我們的假設(shè)是對的问慎。最后一番苦尋之后,只是將.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
怎么回事?一開始就猜錯了么社证?
修改了域名之后逼龟,并沒有順利的得到結(jié)果。很沮喪猴仑,很無奈审轮,但是既然都到這份上了肥哎,不搞它一搞又心有不甘辽俗。于是乎,打開了調(diào)試窗口(鼠標(biāo)移動到插件圖標(biāo)上右鍵選擇inspect popup)篡诽。一番調(diào)試下拉崖飘,發(fā)現(xiàn)代碼根本沒有跳轉(zhuǎn)進(jìn)入關(guān)鍵的F
函數(shù)當(dāng)中,而使得代碼能夠執(zhí)行F
函數(shù)最重要的一個名叫p
的變量的值始終是false
杈女。問題的關(guān)鍵就是這個p
什么時候會變成true
朱浴。繼續(xù)梳理代碼,發(fā)現(xiàn)當(dāng)一個叫initBackgroundPageAsync
的函數(shù)執(zhí)行的時候达椰,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()
})
};
問題又變成了查看該函數(shù)何時被調(diào)用啰劲。最終發(fā)現(xiàn)一個名叫background.html
的文件加載https://apis.google.com/js/client.js
這個文件完成后會執(zhí)行梁沧。
<!DOCTYPE html>
<html>
<head>
<title></title>
<script type="text/javascript" src="lang_map.min.js"></script>
<script type="text/javascript" src="mustache.js"></script>
<script type="text/javascript" src="background.min.js"></script>
<script type="text/javascript"
src="https://apis.google.com/js/client.js?onload=initBackgroundPageAsync">
</script>
<script type="text/javascript" src="ga.js"></script>
</head>
<body>
</body>
</html>
查了一下,https://apis.google.com/js/client.js
這個文件用于使用Google全家桶的蝇裤,在中國其實沒啥用并且會帶來麻煩廷支,因為根本訪問不。由于訪問不了栓辜,就不可能加載成功恋拍;而加載不成功就不會執(zhí)行initBackgroundPageAsync
,因此決定手動執(zhí)行initBackgroundPageAsync
藕甩。其實也就是在background.min.js
文件的末尾增加一行代碼:
window.initBackgroundPageAsync();
本以為到此大功告成施敢,可是現(xiàn)實還是狠狠地給了一巴掌,雖然代碼終于執(zhí)行了F
函數(shù)狭莱,但依舊沒有得到想要的結(jié)果僵娃。嘿我這暴脾氣,跟它杠上了贩毕。
這次調(diào)試返現(xiàn)悯许,代碼在奇怪的地方卡住了,定睛一看辉阶,在一個叫D
的函數(shù)里面出不來了先壕。
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)
}
})
},
仔細(xì)分析了下代碼瘩扼,發(fā)現(xiàn)它又使用谷歌的API去請求一些奇奇怪怪的東西,然后調(diào)用一個回調(diào)函數(shù)b
垃僚。但是有意思的是集绰,當(dāng)請求失敗了,也就是返回碼不是200的時候谆棺,它依然會調(diào)用回調(diào)函數(shù)b
栽燕,只不過傳了個空參數(shù)。既然如此,有一種可能是傳遞進(jìn)去的這個參數(shù)是錦上添花的立镶。那么我們就假設(shè)他次次都請求失敗味榛,所以我們將代碼改成了下面的樣子:
D = function(a, c, b) {
b(null);
return;
}
重新加載插件運行,呀蔼啦,成功了!
3. 總結(jié)
我為什么一開始就沒想到直接搜索translate.google.com
這個字符串呢仰猖?以為抄了近路捏肢,最后回過頭看還是拐了拐。
代碼見文后鏈接饥侵。
歡1迎2關(guān)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/