為Chrome插件Google Dictionary進(jìn)行的一次小手術(shù)

1. 起因

經(jīng)常需要閱讀英文文檔,冷不丁的會(huì)碰到一些不懂的單詞,之前的做法是打開一個(gè)谷歌翻譯的網(wǎng)頁在一旁放著,有需要就切換過來查單詞。但是來回的切換著實(shí)有點(diǎn)麻煩,就想著有沒有一些Chrome翻譯插件,一番搜尋,找了兩款比較心儀的Chrome插件:一個(gè)是good word guide的Instant Dictionary,它的優(yōu)點(diǎn)是直接顯示英文的釋義,因?yàn)楹芏鄷r(shí)候直接看英文的釋義更容易理解一個(gè)單詞。例如對(duì)于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,另一個(gè)就是Google的Google Dictionary,因?yàn)槲揖捅容^中意谷歌翻譯。可是心儀歸心儀,這兩款插件都有個(gè)致命的缺陷:由于眾所周知的原因,他們倆都沒法鏈接到他們的服務(wù)器。
對(duì)于Instant Dictionary,這顯然已經(jīng)沒救了,但是對(duì)于Google Dictionary,我覺得還可以搶救一下。為什么呢?因?yàn)楣雀璺g在中國是可以直接打開的,對(duì)應(yīng)的域名是translate.google.cn,既然都是Google家的東西,連不上translate.google.com,那是否可以讓它去連translate.google.cn?查看了Google Dictionary配置選項(xiàng),并沒有切換到國內(nèi)服務(wù)器的選項(xiàng),這樣一來,想在國內(nèi)用,就只能手動(dòng)改一改了。

胡適曾經(jīng)說過:大膽假設(shè),小心求證。我們的假設(shè)就是最終單詞翻譯的請(qǐng)求是通過HTTP進(jìn)行的,并且域名使用的是translate.google.com。

2. 行動(dòng)

好,說干就干,只要思想不滑坡,方法總比困難多。

2.1. 獲取插件

Chrome插件的后綴名是.crx,其實(shí)就是一個(gè)壓縮包。常用的壓縮軟件一般都能解壓,解壓出來的是一堆JavaScript文件以及其他相關(guān)的一些文件。我有想過它為什么不直接用.zip做后綴?最后得到了一個(gè)我自己比較信服的答案,使用.zip等常用后綴就相當(dāng)于在挑釁:“你來解壓我?。 ???倳?huì)有好事者解壓一探究竟,并且這樣逼格也就不那么高了。當(dāng)然,最后.crx依舊沒能阻擋好事者。
但是想解壓,那也要先拿到。遺憾的是插件商店在中國正常情況下也是沒法訪問的,好在你不能訪問,別人也不能訪問。但是不能訪問并不代表需求就消失了,需求始終都在,只有愛會(huì)消失。使用必應(yīng)搜索chrome extension downloader就能找到一堆下載插件的網(wǎng)站,例如https://crxdown.com/。緊接著,雖然我們不能直接訪問到插件,但我們可以網(wǎng)上搜索該插件,得到它的確切地址,這樣我們就能將它下載下來。例如在搜索Google dictionary后找到來源為Chrome Web Store的結(jié)果,右鍵選擇復(fù)制鏈接地址就能得到對(duì)應(yīng)的插件地址。

搜索Google Dictionary的結(jié)果

2.2. 找入口

得到了插件,下面我們要做的就是找到入口。正常情況下,出于安全的考慮Chrome是沒法安裝我們下載好了的插件的,即便是來源正經(jīng)也不行,我們要進(jìn)入開發(fā)者模式。在瀏覽器地址欄輸入chrome://extensions/,在打開的界面中勾選開發(fā)者模式。

打開開發(fā)者模式

然后,點(diǎn)擊load unpacked加載我們已經(jīng)解壓好了的插件,只需要選擇包含manifest.json這層的文件夾就行。
加載解壓后的Chrome插件

想要改代碼,那就必須先理解代碼,想要理解代碼,首先要找到一個(gè)合適的切入點(diǎn)。理論上,manifest.json是整個(gè)插件的元文件,里面肯定會(huì)有描述整個(gè)插件的入口文件之類的。但這個(gè)門檻有點(diǎn)高,使用這種方法應(yīng)該是對(duì)Chrome插件的開發(fā)比較熟悉的,我這種門外漢算了。除此之外,還有另一個(gè)方法,那就是直接搜索我們能看到的東西。

Google Dictionary彈窗界面

運(yùn)行該插件之后我們發(fā)現(xiàn),觸發(fā)翻譯的條件是我們輸入需要翻譯的內(nèi)容后回車或者點(diǎn)擊藍(lán)色按鈕,正常情況下兩種方法最終都會(huì)調(diào)用同一個(gè)函數(shù)。因?yàn)槲覀兛梢詮倪@兩個(gè)動(dòng)作入手。通過在所有文件中搜索Define這個(gè)單詞,最終我們?cè)?code>browser_action.html發(fā)現(xiàn)了這個(gè)按鈕的標(biāo)簽。

    <div id="form">
      <input type="text" id="query-field"><button id="define-btn" class="btn btn-primary" value="Define">Define</button>
    </div

從代碼中看到,這里有個(gè)叫qeury-field的輸入以及一個(gè)叫define-btn的按鈕,和我們看到的一致。這個(gè)按鈕標(biāo)簽定義了id屬性卻沒有定義點(diǎn)擊的回調(diào)函數(shù),那么很大概率在JavaScript代碼中會(huì)使用類似get_element_by_id()這類的函數(shù)獲取該標(biāo)簽并為其綁定回調(diào)函數(shù),因此我們接著使用它的id define-btn進(jìn)行搜索。

搜索define-btn結(jié)果

一共搜索到三條內(nèi)容,一條在html文件中,也就是我們剛看到的,一條位于css文件中,說明是設(shè)置顯示樣式的不用管,剩下一條在js文件中,果然和我們想的一樣。打開該文件,返現(xiàn)代碼已經(jīng)經(jīng)過混淆擠作一團(tuán)了,正常人估計(jì)沒幾個(gè)能這么讀代碼,因此需要稍加處理。
簡直亂碼

處理方式很簡單,隨便找個(gè)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ǎn)擊按鈕,最終會(huì)調(diào)用一個(gè)名為r的函數(shù)。好吧,讓我們看看這個(gè)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)
        },

從代碼中看到,首先這個(gè)r函數(shù)對(duì)輸入框中的字符做了簡單的處理,最終傳遞給了chrome.runtime.sendMessage()。代碼到此戛然而止,在源代碼中再也找不到chrome.runtime.sendMessage()的定義了,既然源代碼中找不到,那么只能是別的庫中的API或者是系統(tǒng)API。從字面上我們知道它把參數(shù)發(fā)了出去,但是發(fā)給誰了呢?發(fā)給了服務(wù)器?沒道理啊。我們可以斷定的是變量a中肯定只包含了需要查詢的字符串,而c的值是數(shù)字1,這些參數(shù)不足以告訴別的API你的目的。

既然猜測(cè)是外部API,那就去搜索引擎搜索chrome.runtime.sendMessage吧。
最終搜索得到完全匹配的結(jié)果都位于Google域名之下,很遺憾沒法訪問,但是在MDN Web Docs上看到了runtime.sendMessage的介紹。

image.png

最后顯示Chrome支持了這個(gè)API,那么八九不離十,就是它了。從介紹中我么知道,當(dāng)使用了runtime.sendMessage之后,會(huì)有一個(gè)叫runtime.onMessage的API對(duì)它進(jìn)行響應(yīng),我們需要接著搜索。

最后發(fā)現(xiàn)兩個(gè)文件使用runtime.onMessage,它們分別是backgrpund.min.js以及content.min.js。同樣的,我們對(duì)它們的內(nèi)容進(jìn)行了美化。

搜索結(jié)果

對(duì)它們一個(gè)一個(gè)的梳理,最終確定了chrome.runtime.onMessage.addListener(G)這條語句中注冊(cè)的G函數(shù)最終會(huì)響應(yīng)之前點(diǎn)擊按鈕后調(diào)用的sendMessage()函數(shù),因?yàn)槊總€(gè)注冊(cè)的函數(shù)都會(huì)先通過type參數(shù)判定這是不是他們?cè)擁憫?yīng)的,從中我們看到G函數(shù)判定的是fetch_raw以及fetch_html,恰好我們之前看到的sendMessage()函數(shù)中傳遞進(jìn)來的參數(shù)是fetch_html。

image.png

最終,在梳理G函數(shù)的過程中,見到了我們夢(mèng)寐以求一個(gè)字符串https://translate.google.com/translate_a/t?client=dict-chrome-ex&sl=,并且看到了XMLHttpRequest的使用,證明我們的假設(shè)是對(duì)的。最后一番苦尋之后,只是將.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

怎么回事?一開始就猜錯(cuò)了么?

修改了域名之后,并沒有順利的得到結(jié)果。很沮喪,很無奈,但是既然都到這份上了,不搞它一搞又心有不甘。于是乎,打開了調(diào)試窗口(鼠標(biāo)移動(dòng)到插件圖標(biāo)上右鍵選擇inspect popup)。一番調(diào)試下拉,發(fā)現(xiàn)代碼根本沒有跳轉(zhuǎn)進(jìn)入關(guān)鍵的F函數(shù)當(dāng)中,而使得代碼能夠執(zhí)行F函數(shù)最重要的一個(gè)名叫p的變量的值始終是false。問題的關(guān)鍵就是這個(gè)p什么時(shí)候會(huì)變成true。繼續(xù)梳理代碼,發(fā)現(xiàn)當(dāng)一個(gè)叫initBackgroundPageAsync的函數(shù)執(zhí)行的時(shí)候,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ù)何時(shí)被調(diào)用。最終發(fā)現(xiàn)一個(gè)名叫background.html的文件加載https://apis.google.com/js/client.js這個(gè)文件完成后會(huì)執(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這個(gè)文件用于使用Google全家桶的,在中國其實(shí)沒啥用并且會(huì)帶來麻煩,因?yàn)楦驹L問不。由于訪問不了,就不可能加載成功;而加載不成功就不會(huì)執(zhí)行initBackgroundPageAsync,因此決定手動(dòng)執(zhí)行initBackgroundPageAsync。其實(shí)也就是在background.min.js文件的末尾增加一行代碼:

window.initBackgroundPageAsync();

本以為到此大功告成,可是現(xiàn)實(shí)還是狠狠地給了一巴掌,雖然代碼終于執(zhí)行了F函數(shù),但依舊沒有得到想要的結(jié)果。嘿我這暴脾氣,跟它杠上了。

這次調(diào)試返現(xiàn),代碼在奇怪的地方卡住了,定睛一看,在一個(gè)叫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去請(qǐng)求一些奇奇怪怪的東西,然后調(diào)用一個(gè)回調(diào)函數(shù)b。但是有意思的是,當(dāng)請(qǐng)求失敗了,也就是返回碼不是200的時(shí)候,它依然會(huì)調(diào)用回調(diào)函數(shù)b,只不過傳了個(gè)空參數(shù)。既然如此,有一種可能是傳遞進(jìn)去的這個(gè)參數(shù)是錦上添花的。那么我們就假設(shè)他次次都請(qǐng)求失敗,所以我們將代碼改成了下面的樣子:

D = function(a, c, b) {
            b(null);
            return;
}

重新加載插件運(yùn)行,呀,成功了!

image.png

3. 總結(jié)

我為什么一開始就沒想到直接搜索translate.google.com這個(gè)字符串呢?以為抄了近路,最后回過頭看還是拐了拐。
代碼見文后鏈接。

歡1迎2關(guān)3注4個(gè)5人6微7信8公9眾0號(hào): 愛碼士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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容