こんにちは。フロントエンドエンジニアの@1010realです。 昨今はコロナで完全に外界との接触をたっており、風貌が仙人のようになってきました(はやく髪切ろう。。)
今回は拡張機能の作成に関するナレッジを紹介しようと思います。
改めて説明する必要もないですが、拡張機能はブラウザの挙動をカスタマイズするためのソフトウェアです。 ユーザはChromeウェブストアChromeウェブストアから拡張機能を検索・インストールして、機能を追加することができます。
開発者目線で言うと、ChromeAPIに必要に応じてアクセスし、ブラウザの挙動に応じた処理を行うことができます。提供されているAPIは Chrome APIリファレンス で確認できます。
ブラウザ拡張機能の標準化を進めるためのコミュニティグループ(WECG:WebExtensions Community Group)が今年6月に発足しています。 今後、主なブラウザにおいて、コアなAPIについては標準化が進み、拡張機能のマルチブラウザ対応などもしやすくなるのではないかと思います。拡張機能の開発者は、このグループの動向を見守っていくと良いと思います。 参考:ウェブ関連技術の標準化推進団体「W3C」がブラウザー拡張機能の共通化に向け「WECG」コミュニティ立ち上げ
検索すれば色々と出てくるので、ここでは最低限理解しておくべき内容を記載します。
拡張機能を実装する際、最初に理解しなければならないのが、拡張機能が持つ3種類のプログラムとその(実行)コンテキスト、および互いのやり取り(メッセージング)の方法です。
以下が3つのコンテキストです。
拡張機能固有の実行コンテキストで動作するプログラムで、特定のタブに依存せず、各タブを横断した制御を行うことができます。 例えば、ブラウザのタブ移動やコンテンツ更新イベントを受け取ったり、拡張機能アイコンが押されたなどのイベントを受け取ることができます。 それらのイベントを受け取って、ChromeAPIにアクセスしたり、content scriptsにメッセージングしたりする、イベントベースのプログラムです。
Webサイトの実行コンテキストで動作するプログラムです。 もちろんタブ毎に独立しており、その実行対象(URL)やタイミングは、manifest.json(後述)やbackground scriptsで制御可能です。 Webサイトの実行コンテキストで動作しているため、ユーザのWebコンテンツに対するアクションなどをイベントとして受け取ることができます。 基本的には、background scriptsとのメッセージング経由でChromeAPIにアクセスしますが、一部のChromeAPIについては直接アクセスも許可されています。
拡張機能のアイコンを押した際に表示されるWebコンテンツで、独立したコンテキストを持っています。 拡張機能の状態表示や操作を行うUIを提供することができます。
各プログラム間でのやり取りをメッセージパッシングと呼んでおり、これは以下のように行います。
送信側(content scriptsやpopup)はchrome.runtime.sendMessage でメッセージを送信し、background scriptsはchrome.runtime.onMessageで受信します。
送信側(background scriptsやpopup)はchrome.tabs.sendMessageでメッセージを送信し、content scriptsはchrome.runtime.onMessageで受信します。(注:受信する際はどちらもruntime.onMessageを使用します。tabs.onMessageではありません) メッセージ送信時に、送信先のタブIDを指定する必要があるため、現在アクティブなタブの取得とセットで呼び出されることが多いです。
そのままでは、送信側は送信が終わると同時にportを閉じてしまうため、レスポンスを受け取れません。
runtime.onMessage内で、 return true
してあげると、メッセージを送信した側は、レスポンスを受信するまでportを開放し続けます。
上記は一回限りのメッセージのやり取りについてのみ記載しているので、必要に応じてMessage Passingを参考にしてください(コネクション貼りっぱなしにしたいケースなど)
とりあえず、作って動かしてみた方がわかると思うので、拡張機能に必要なファイルをミニマムで掲載します。
manifest.json
拡張機能に含まれるファイルや、その拡張機能に許可される権限(インストール時に確認される)を指定します
{
"name": "Sample",
"description": "Sample Extension",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "tabs"],
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["https://tech.smartshopping.co.jp/*"],
"js": ["content-script.js"]
}
]
}
content-script.js
Webページのコンテキストで実行されます。
// 実行直後に、背景をオレンジに変えています。
document.body.style.backgroundColor = 'orange'
// `SET_BG`メッセージを受け取ると、その時にstorageに保存されている値でbodyの背景色を設定します
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request !== 'SET_BG') return
const storageKey = '__sample_color'
chrome.storage.sync.get([storageKey], (items) => {
document.body.style.backgroundColor = items[storageKey]
})
})
background.js
拡張機能固有のコンテキストで実行されます。
// インストール時にstorageを初期値で初期化します。
chrome.runtime.onInstalled.addListener(({}) => {
console.log('oninstalled')
chrome.storage.sync.set({ ['__sample_color']: '#ffff00' })
})
// タブの内容が更新された際に、content scriptsに `SET_BG`メッセージを送信します。
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
console.log('tabs.onupdated')
chrome.tabs.sendMessage(tabId, 'SET_BG')
})
popup.html
ポップアップ単独のコンテキストで実行されます。popup.jsを読み込んでいます。
<!DOCTYPE html>
<html>
<head></head>
<body>
<input id="colorText" type="text" />
<button id="changeColor">change color</button>
<script src="popup.js"></script>
</body>
</html>
popup.js
popup.htmlで読み込むjsファイルです
let color
const inputColorText = document.getElementById('colorText')
const changeColorButton = document.getElementById('changeColor')
const storageKey = '__sample_color'
// ページ表示時(ユーザがアイコンクリック時)にstorageの値を取得し、テキストボックスに設定します。
window.addEventListener('load', () => {
chrome.storage.sync.get([storageKey], (items) => {
inputColorText.value = items[storageKey]
})
})
//ボタンを押したタイミングで、テキストボックスの内容をstorageに保存し、現在アクティブなタブ(content script)に `SET_BG`メッセージを送信します
changeColorButton.addEventListener('click', () => {
chrome.storage.sync.set({ [storageKey]: inputColorText.value }, async () => {
const queryOptions = { active: true, currentWindow: true }
const [tab] = await chrome.tabs.query(queryOptions)
chrome.tabs.sendMessage(tab.id, 'SET_BG')
})
})
※余談ですが、拡張機能のpopup内でインラインスクリプトは実行できないため、このように別ファイルにする必要があります)
上記のファイルを作成し、一つのフォルダにまとめ、 chrome://extensions/ にブラウザからアクセスし、デベロッパーモードをONにして、「パッケージ化されていない拡張機能を読み込む」で先ほどのフォルダを指定してください。
そして、このブログに戻ってきてリロードすると、ヘッダーがこんな感じで黄色に変わると思います。(また、一瞬オレンジの状態が見え隠れすると思います)
また、インストールした拡張機能のアイコンを固定し、アイコンをクリックすると色を指定することができます。 この色はstorageに保存されているため、Chromeを終了して起動したときにも保持されます。
各コンテキストによって、デバッグ方法が異なるため、以下にまとめておきます。
「ビューを検証 ServiceWorker 」をクリックして、serviceWorker用devToolを表示します。
Webページのコンテキストで動作するので、いつも通りChrome Dev Toolsからどうぞ。
popupを右クリックして、「検証」からpopup用devToolを表示します。
以下を押すと再度フォルダを読み直してくれます。(再インストールされます)
拡張機能について検索すると、manifestV2の情報が引っかかってくることが多いのですが、manifestV2がリリースされたのは、2012/07/04で実に9年もの月日が経っています。 その間にJavascriptは目覚ましい進化を遂げていますのでこれから拡張機能を開発しようとするなら、manifestV3で開発した方が色々と恩恵を受けられると思います。
ちなみに今回のコードはこちらに載せておりますので必要に応じてご確認ください。
拡張機能を語るには、単一記事では全然足りないため、今回は本当に初歩的な知識のみの羅列となってしまいました。 実際に開発している拡張機能は、環境から全然違ってたりしますので、熱が冷めないうちに、次の記事について執筆をしていきたいと思います。