前言
之前筆者一直想了解一些關(guān)于谷歌插件的相關(guān)知識,通過這個谷歌插件也可以更好的認(rèn)識到這個谷歌的調(diào)試工具,正好最近需要進(jìn)行分享,這兩個星期去學(xué)習(xí)和了解了谷歌插件,然后寫了這一篇文章,把本人所了解的和一些思考點(diǎn)寫了下來。同時(shí),也想著可以使用谷歌插件去寫一些小工具,既學(xué)習(xí)了新的東西,又有一定的趣味性。當(dāng)然,因?yàn)闀r(shí)間的原因,如果筆者對于這一塊的認(rèn)識有不對的地方,歡迎批評指正~
什么是谷歌插件
谷歌插件,全名谷歌瀏覽器擴(kuò)展程序。那什么是谷歌瀏覽器擴(kuò)展程序,官方說明如下:
擴(kuò)展程序允許您為 Chrome 瀏覽器增加功能,而不需要深入研究本機(jī)代碼。您可以使用您在網(wǎng)頁開發(fā)中已經(jīng)很熟悉的核心技術(shù)(HTML、CSS 與 JavaScript)為 Chrome 瀏覽器創(chuàng)建新的擴(kuò)展程序。
有疑惑的同學(xué)會問了,為什么人家還叫谷歌插件,那這就正如魯迅所說的那句話:世上本沒有路,走的人多了,也就有路了。谷歌瀏覽器擴(kuò)展程序本來也不是谷歌插件,谷歌插件應(yīng)該是瀏覽器更為底層的東西,奈何叫的人太多了,所以本文也使用谷歌插件來統(tǒng)稱谷歌瀏覽器擴(kuò)展程序。
基本使用
下面先介紹一下谷歌插件的主要組成部分,因?yàn)槟壳肮雀璨寮褂帽容^普遍的版本為 2.0 版本,所有以下都是基于 2.0 版本進(jìn)行使用說明,3.0 版本相較于 2.0 版本更為簡便,有興趣的同學(xué)可以點(diǎn)擊文章末尾處的鏈接了解更多相關(guān)知識。
配置文件
谷歌插件的核心文件就是配置文件--manifest.json(清單)文件。其中,manifest.json 文件最基本的 Api 如下:
{
"name": "chrome extension",
"version": "1.0.0",
"manifest_version": 2,
"description": "A litlle chrome extension demo"
}
主要是包含所寫谷歌插件的名稱,版本,以及相關(guān)描述,其中 manifest_version 表示清單文件版本。manifest.json 作為谷歌插件的核心部分,筆者認(rèn)為該文件對插件來說就相當(dāng)于一個入口配置文件,開發(fā)只需要在這個文件通過配置相應(yīng)的 js,調(diào)用谷歌瀏覽器提供的 Api,來達(dá)到完善這個插件的目的。
基本使用Api
在清單文件中還有很多 Api 就不一一列舉了,下面只介紹幾個筆者認(rèn)為比較重要的幾個 Api,通過以下幾個 Api 可以使得讀者對于谷歌插件的開發(fā)過程有一個大概的認(rèn)識。
- browser_action
{
...
"browser_action": {
"default_icon": {
"16": "images/get_started16.png",
"32": "images/get_started32.png"
},
"default_title": "谷歌劃詞翻譯",
"default_popup": "popup.html"
},
...
}
browser_action 可設(shè)置瀏覽器右上角的圖標(biāo),名稱。default_popup 可配置點(diǎn)擊圖標(biāo)后會出現(xiàn)的一個小窗口,這里可以做一些臨時(shí)性的操作。
- permissions
{
...
"permissions": [ "activeTab", "storage", "tabs", "contextMenus" ],
...
}
permissions 可配置谷歌插件權(quán)限申請,如 contextMenus(右鍵菜單), tabs(標(biāo)簽),storage(插件本地存儲)。
- content_scripts
{
...
"content_scripts": {
"matches": ["<all_urls>"],
"css": ["content/content_script.css"],
"js": ["content/content_script.js"]
},
...
}
content-scripts,其實(shí)就是谷歌插件中向頁面注入腳本的一種形式(雖然名為 script,其實(shí)還可以包括 CSS 的),借助 content-scripts 可以實(shí)現(xiàn)通過配置的方式輕松向指定頁面注入 JS 和 CSS。
- background
{
···
"background": {
"scripts": ["background.js"],
"persistent": false
},
···
}
background 是一個常駐的頁面,它的生命周期是插件中所有類型頁面中最長的,它隨著瀏覽器的打開而打開,隨著瀏覽器的關(guān)閉而關(guān)閉,所以通常把需要一直運(yùn)行的、啟動就運(yùn)行的、全局的代碼放在 background 里面。
筆者也畫了一個上面涉及到的腳本在瀏覽器中的分布,如下圖:

以上,介紹完谷歌插件清單文件基本且比較重要的一些 Api 以后,感興趣的同學(xué)就可以開始著手寫幾個簡單的工具,用來實(shí)現(xiàn)自己遠(yuǎn)大的抱負(fù)以及理想,升華自己高貴的靈魂。相應(yīng)的,筆者也分享一個有意思的谷歌插件工具實(shí)現(xiàn)過程,通過開發(fā)這個工具也可以加深對于谷歌插件的認(rèn)識。
前置條件
因?yàn)橛械耐瑢W(xué)不太知道怎么開發(fā)谷歌插件,就還是把前置條件說一下。首先需要打開管理擴(kuò)展程序,打開開發(fā)者模式。點(diǎn)擊加載已解壓的程序按鈕即可加載本地谷歌插件,開發(fā)的時(shí)候代碼如果有更新的話,需要刷新已加載插件,點(diǎn)擊關(guān)閉后再開啟,不必刷新開發(fā)頁面。

谷歌劃詞翻譯插件
筆者對于谷歌插件使用次數(shù)比較頻繁的還是翻譯工具,對于在網(wǎng)頁上看到的不懂的英文單詞或者句子,直接使用鼠標(biāo)選中,輕松快捷的翻譯出相應(yīng)的中文。那么谷歌瀏覽器插件的翻譯工具是如何實(shí)現(xiàn)的呢?
思考
如何去做一個劃詞翻譯插件,首先要考慮的有以下幾點(diǎn):
- 如何實(shí)現(xiàn)翻譯效果
- 如何選中我們需要的元素
- 選中元素之后如何展示劃詞翻譯面板
- 所有的瀏覽器 Tab 都需要支持翻譯效果
思考完上面的點(diǎn)后,帶著這幾個疑惑,筆者在下文一一解答,同時(shí)也列舉一下遇到的一些點(diǎn)。
劃詞翻譯面板
首先不去考慮該插件的功能,先寫下劃詞翻譯的面板的樣式,所達(dá)到的效果如下:

HTML 代碼如下:
<div class="translate-panel show">
<header>谷歌劃詞翻譯插件<span class="close">X</span></header>
<main>
<div class="source">
<div class="title">英文</div>
<div class="content">test</div>
</div>
<div class="result">
<div class="title">簡體中文</div>
<div class="content">...</div>
</div>
</main>
</div>
將上面的樣式簡單的寫好之后,開始考慮如何將劃詞翻譯的面板展示在瀏覽器當(dāng)前頁面。對于谷歌瀏覽器來說,在網(wǎng)頁上面去進(jìn)行的交互是屬于 content_scripts 的,需要引入劃詞翻譯面板所需要的 JS 或者 CSS,去生成當(dāng)前面板。
其次,在配置文件中配置 content_scripts,引入 JS 文件,動態(tài)的生成 DOM 元素。大致的思路就是通過監(jiān)聽到鼠標(biāo)松開后,去生成翻譯面板,在生成的元素上面添加 opacity 樣式控制顯隱,使用谷歌免費(fèi)翻譯 Api 進(jìn)行翻譯。其中代碼如下所示:
// manifest.json
{
...
"content_scripts": {
"matches": ["<all_urls>"],
"css": ["content_script.css"],
"js": ["content_script.js"]
},
"permissions": [
"activeTab"
],
...
}
// content_script.js
class TranslatePanel {
createPanel = () => {
let wrapper = document.createElement('div')
wrapper.innerHTML = `
<header>谷歌劃詞翻譯插件<span class="close">X</span></header>
<main>
<div class="source">
<div class="title">英文</div>
<div class="content">test</div>
</div>
<div class="result">
<div class="title">簡體中文</div>
<div class="content">...</div>
</div>
</main>
`
wrapper.classList.add('translate-panel')
wrapper.querySelector('.close').onclick = () => {
this.wrapper.classList.remove('show')
}
document.body.appendChild(wrapper)
this.wrapper = wrapper
}
showPanel = () => {
this.wrapper.classList.add('show')
}
translateSelect = (content) => {
const source = this.wrapper.querySelector('.source .content')
const result = this.wrapper.querySelector('.result .content')
source.innerHTML = content
result.innerHTML = '翻譯中...'
fetch(`https://translate.google.cn/translate_a/single?client=at&sl=en&tl=zh-CN&dt=t&q=${content}`)
.then(res => res.json())
.then(res => {
result.innerHTML = res[0][0][0]
})
}
locationPanel = (target) => {
this.wrapper.style.top = target.y + 'px'
this.wrapper.style.left = target.x + 'px'
### }
}
let panel = new TranslatePanel()
panel.createPanel()
window.onmouseup = (target) => {
// 獲取選中內(nèi)容
const content = window.getSelection().toString().trim()
if (!content) return
panel.locationPanel({ x: target.pageX, y: target.pageY })
panel.translateSelect(content)
panel.showPanel()
}
上面過程中,筆者使用了谷歌免費(fèi)的翻譯借口,但是這個接口按照目前的使用還是有一點(diǎn)問題,我們暫時(shí)先放下不說。那么現(xiàn)在來說,滑詞翻譯的面板就已經(jīng)基本寫好了。
腳本通信
劃詞翻譯插件開發(fā)到這里,細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn)了,每次選中單詞時(shí)都會觸發(fā)劃詞翻譯功能,此時(shí),急需一個控制翻譯功能的開關(guān),這個開關(guān)就可以放在 popup 腳本上面。具體的樣式的實(shí)現(xiàn)就不去介紹了,主要看一下 HTML 結(jié)構(gòu)。
基本效果如下:

<div class="switch-wrapper">
<div class="switch-desc">是否啟用劃詞翻譯</div>
<input type="checkbox" class="switch" />
</div>
于此同時(shí),面板和劃詞翻譯的面板都已經(jīng)有了,再考慮一下如何實(shí)現(xiàn) popup 腳本與 content_script 腳本之間的通信。首先,在 popup 腳本上面,我們在打開窗口的時(shí)候需要去查詢是否有存儲開啟劃詞翻譯的狀態(tài),同時(shí),當(dāng)狀態(tài)發(fā)生變更的時(shí)候需要將其存儲,再在當(dāng)前的 Tab 下面發(fā)送請求。
// popup.js
let switchWrapp = document.querySelector('.switch')
chrome.storage.sync.get(['checked'], (target) => {
if (target) {
switchWrapp.checked = target.checked
}
})
switchWrapp.onclick = (e) => {
chrome.storage.sync.set({ checked: e.target.checked })
chrome.tabs.query( {active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { checked: e.target.checked })
})
}
上面代碼中的 chrome.storage 可用于存儲數(shù)據(jù),追蹤數(shù)據(jù)。storage.sync 的作用是讓谷歌瀏覽器的數(shù)據(jù)同步,這使得在不同 Tab 頁上面切換的狀態(tài)也是可以同步的,同時(shí)也不用將數(shù)據(jù)保存在 background 后臺頁面中,storage 還有很多 Api 比如監(jiān)聽 storage 數(shù)據(jù)變化的 onChanged,就不一一介紹了。將開啟或關(guān)閉劃詞翻譯的狀態(tài)發(fā)送后,content_script.JS 需要添加監(jiān)聽事件,獲取到該狀態(tài)后,進(jìn)行關(guān)閉或開啟操作。
// content_script.js
let checked = false
window.onmouseup = (target) => {
···
if (!content || !checked) return
···
}
chrome.storage.sync.get(['checked'], (target) => {
if (target) checked = target.checked
})
chrome.runtime.onMessage.addListener((target) => {
if (target) {
checked = target.checked
}
})
在開發(fā)過程中呢,發(fā)先在當(dāng)前的 Tab 是可以去完成這個操作的,但是當(dāng)開啟了多個 Tab 的情況就會出現(xiàn)開啟翻譯卻不能展示翻譯面板的情況,以上筆者思考了一下,此時(shí)應(yīng)該將 checked 儲存起來,不應(yīng)該放在 content_script 腳本當(dāng)中。
// content_script.js
let panel = new TranslatePanel()
panel.createPanel()
window.onmouseup = (target) => {
// 獲取選中內(nèi)容
const content = window.getSelection().toString().trim()
if (!content) return
window.chrome.storage.sync.get(['checked'], (result) => {
if (result.checked) {
panel.locationPanel({ x: target.pageX, y: target.pageY })
panel.translateSelect(content)
panel.showPanel()
}
})
}
chrome.runtime.onMessage.addListener((target) => {
if (target.type == 'CHECKED') {
chrome.storage.sync.set({ checked: target.checked })
}
})
以上,popup 腳本和 content_script 腳本之間就實(shí)現(xiàn)了通信,翻譯插件也可以通過 popup 上面的按鈕,進(jìn)行開啟或關(guān)閉翻譯功能。同理,也可以知道其他模塊也是可以通過這種方式去進(jìn)行通信,不同的只是其他腳本向 content_script 通信是需要使用 tabs,先查找到當(dāng)前的 Tab 在發(fā)送請求。

右鍵直達(dá)翻譯頁面
當(dāng)關(guān)閉劃詞翻譯的時(shí)候,直接不能再去翻譯選中內(nèi)容也不是很友好,這個時(shí)候可以為點(diǎn)擊右鍵的時(shí)候出現(xiàn)翻譯菜單項(xiàng)。因?yàn)檫@部分內(nèi)容需要一直存在就加在 background 中。
// backgrond.js
// 當(dāng)擴(kuò)展程序第一次安裝、更新至新版本或 Chrome 瀏覽器更新至新版本時(shí)產(chǎn)生
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
"id": "SELECT_TRANSLATE",
"title": "翻譯 %s",
"contexts": ["selection"]
})
})
chrome.contextMenus.onClicked.addListener((target) => {
if (target.menuItemId == 'SELECT_TRANSLATE') {
chrome.tabs.create({url: `https://translate.google.cn/?sl=en&tl=zh-CN&text=${target.selectionText}&op=translate`})
}
})
跨域問題
開發(fā)過程中,有的同學(xué)也看出來了一個問題,比如說谷歌的這個翻譯的 Api 需要同源的情況下才能正常調(diào)用該接口,然后就只能在谷歌翻譯的頁面中使用劃詞翻譯,場面一度十分尷尬...
那么,正常來說這個劃詞翻譯使用起來也是十分不合理的,接下來就需要解決一下這個跨域的問題。

筆者當(dāng)時(shí)想要嘗試的是使用 JSONP,也就是去使用嵌入腳本去進(jìn)行跨域,發(fā)現(xiàn)還是會有一些問題,主要是谷歌的翻譯的接口不支持回調(diào)函數(shù),同時(shí)也去查閱了一些資料,發(fā)現(xiàn)是可以在 content_script 中通知 background,background 后臺去調(diào)用谷歌翻譯的 Api 是來避免這個情況的,主要因?yàn)?background 的權(quán)限非常高,幾乎可以調(diào)用所有的 Chrome 擴(kuò)展 Api,而且它可以無限制跨域,也就是可以跨域訪問任何網(wǎng)站而無需要求對方設(shè)置 CORS,具體添加的代碼如下:
// content_script.js
translateSelect = (content) => {
const source = this.wrapper.querySelector('.source .content')
const result = this.wrapper.querySelector('.result .content')
source.innerHTML = content
result.innerHTML = '翻譯中...'
chrome.runtime.sendMessage({ type: 'QUERY_TRANSLATE', queryContent: content }, (res) => {
result.innerHTML = res[0][0][0]
})
}
// background.js
chrome.runtime.onMessage.addListener((request, sender, callBack) => {
if (request.type == 'QUERY_TRANSLATE') {
fetch(`https://translate.google.cn/translate_a/single?client=at&sl=en&tl=zh-CN&dt=t&q=${request.queryContent}`)
.then(res => res.json())
.then(res => {
callBack(res)
})
return true
}
})
background 中的發(fā)送消息的監(jiān)聽事件返回 true 是為了與 content_script 的消息通道保持打開,通過異步的方式發(fā)送請求。
現(xiàn)在想想,如果使用插件的 background 就可以去跨域去進(jìn)行請求一些借口,使用不得當(dāng)?shù)脑捀杏X還是很危險(xiǎn)的,可以去獲取其他網(wǎng)站的一些信息,由此可見,還是要慎重的進(jìn)行此操作。
待完善的點(diǎn)
- 支持其他語言的翻譯.谷歌翻譯的接口有兩個 Api,sl(文本翻譯之前的語言) 和 tl(文本需要翻譯成的語言) 可通過改變對應(yīng)的值支持其他語言的翻譯;
- 樣式完善,實(shí)現(xiàn)先選中圖標(biāo)在進(jìn)行翻譯。多添加一步感覺對交互更友好,不用去進(jìn)行開關(guān)的操作;
代碼結(jié)構(gòu)

介紹完了劃詞翻譯插件,筆者原本是打算再分享一個關(guān)于谷歌 devtool 開發(fā)工具,開發(fā)一個類似于 React Developer Tools 的本地開發(fā)工具,但是由于時(shí)間也是不太夠,涉及到的點(diǎn)比較多,同時(shí)查閱了很多的資料對于這一塊的介紹也是比較的淺,所以還是決定著重于介紹劃詞翻譯,通過劃詞翻譯的開發(fā)使得讀者也能夠比較快速的認(rèn)識到谷歌插件,谷歌瀏覽器的一些,舉一反三,如果對于 devtool 工具插件開發(fā)有興趣的同學(xué)也可以去了解一下。另外,有的同學(xué)可能會認(rèn)為目前開發(fā)的效率是有一點(diǎn)低的,現(xiàn)在的話谷歌插件的開發(fā)也是可以基于 react + antd 去進(jìn)行開發(fā)的,也是可以達(dá)到高效快速的去開發(fā)一個插件的效果。
結(jié)尾
在學(xué)習(xí)谷歌插件的時(shí)候,筆者遇到了一些問題,比如說文檔比較少,官方文檔又是英文的還經(jīng)常 404,不過呢,好在谷歌瀏覽器有提供了很多 Api,筆者這邊在寫插件的時(shí)候也感覺到了很多趣味性。本篇文章還是寫的比較通俗易懂,如果有什么點(diǎn)寫的不對或著不清晰的地方,歡迎踴躍舉手發(fā)言