拡張機能の作り方(サービスワーカーとメッセージ受け渡しのチュートリアル)
3 つ目に作るチュートリアルではメモ帳を作ります。機能としてはポップアップに入力欄を作り、保存ボタンを押すと入力した内容をブラウザ内に保存できるようにします。ここでは「サービスワーカー」、「メッセージ受け渡し」、「Chrome API」という新しい概念がたくさん出てくるので、前回までの 2 つよりも少し難しい内容になっています。この節を通して以下について学びます。
- サービスワーカーでイベントを処理する
-
storage
API でデータを保存する - メッセージ受け渡しの使い方を知る
拡張機能を作成する
memo-popup
という名前のフォルダを作成してください。
manifest.json
を作成する
memo-popup
フォルダの中にmanifest.json
というファイルを作成し、次のコードを追加します。
{
"manifest_version": 3,
"name": "メモ帳",
"escription": "ポップアップで使えるメモ帳",
"version": "1.0.0",
"permissions": [
"storage"
],
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "service-worker.js"
},
"icons": {
"16": "images/icon-16.png"
},
}
"background"
キーはサービスワーカーを設定する項目です。その中の"service_worker"
キーにサービスワーカーと使用する JavaScript ファイルのパスを書きます。サービスワーカーについての詳しい説明は次の章で行います。現時点では、イベントの発生を検知したら特定の処理を行う、イベントハンドラーとして機能する JavaScript ファイルだという認識で構いません。
"permissions"
キーは権限が必要な Chrome API を使うときに記述する項目です。Chrome API とは、Chrome が持つ便利な機能を拡張機能開発に使えるように提供されているライブラリ群です。Chrome API は必ずしも無条件に使えるわけではありません。Chrome API の中でも特にユーザーのプライバシー情報にアクセスできるような機能、たとえばタブ情報、ストレージ、クリップボードなどにアクセスする場合にはユーザーからの許可を得ないと使うことができません。ユーザーからの許可が必要な Chrome API を利用する場合、"permissions"
キーに記載する必要があります。今回は、許可が必要なstorage
API を使用しているので"permissions"
キーに記載しています。"storage"
キーについても次の章で詳しく解説します。現時点では、Chrome に内蔵されたストレージを使うために必要な項目だという認識で構いません。
"action"
キーの中の"default_popup"
キーではポップアップに使用する HTML ファイルを指定しています。これは 1 つ目に作った拡張機能と同じです。
popup.html
を作成
ユーザーがテキストを書き込む入力欄をポップアップに作ります。memo-popup
の中にpopup.html
というファイルを作成し、次のコードを追加します。
<html>
<head>
<meta charset="UTF-8"> <!-- ① -->
</head>
<body>
<div class="form-group">
<label for="memo-title">タイトル</label>
<input type="text" id="memo-title">
</div>
<div class="form-group">
<label for="memo-body">本文</label>
<textarea id="memo-body"></textarea>
</div>
<button id="save-button">保存する</button>
</body>
</html>
ポップアップでは文字コードを UTF-8 に指定しています(①)。文字コードを指定したいと文字化けが発生してしまいます。
次に拡張機能をインストールして、拡張機能を固定表示し、動作を確認してみましょう。アイコンをクリックするとタイトルと本文の入力欄と保存ボタンがあるフォーム画面が表示されているはずです。
次にこのフォームに適当な文字を入力してみます。その後、ポップアップ以外の画面をクリックしてポップアップを閉じます。そしてもう一度アイコンをクリックしてポップアップを開いてみてください。するとポップアップに入力した文字は全て消えてしまいました。なぜ文字が消えてしまうのかというと、ポップアップは状態を持たない UI だからです。もうすこし詳しくいうと、ポップアップを閉じるとポップアップの実行コンテキストが終了して組み立てられている DOM もすべて初期化されてしまいます。再度開いたときには何も入力されていないまっさらなフォームが表示されているので、入力した文字が消えてしまったのです。
入力直後 | 閉じて再度開いたとき |
---|---|
![]() |
![]() |
書き込んだ内容を保持したい場合は入力した文字はブラウザ内のどこかに保存する必要があります。ブラウザ内でデータを保存する機能が、先ほど紹介したstorage
API です。storage
API はブラウザや PC 内部にデータを保存することができる機能を提供します。
storage
API を含むすべての Chrome API はすべて JavaScript で書く必要があります。ところで拡張機能を開発するとき独特なルールがあり、HTML ファイルの中にインラインスクリプトで書くことはできません。storage
API を使うための JavaScript ファイルを新しく作る必要があります。そこでポップアップで利用する JavaScript ファイルのpopup.js
を作成しましょう。
popup.js
の作成
memo-popup
内にpopup.js
を作成します。popup.js
にstorage
API を使ってデータを保存したいところですが、残念なことにstorage
API はポップアップ上では直接使うことができません。というよりほとんどの便利な Chrome API はポップアップやコンテンツスクリプトで使うことができないのです。Chrome API の多くはサービスワーカーで使うことができます。そこでポップアップに入力されたデータをサービスワーカーへ送信し、サービスワーカーでstorage
API を使ってデータを保存する必要があります。
データを送ると書きましたが、具体的にはどのようにすればいいのでしょうか。一見、2 つの JavaScript ファイルで値を共有するだけなので次のようにexport
とimport
を使って書けば実現できそうな気がします。
export const titleInput = document.querySelector('#memo-title');
export const bodyInput = document.querySelector('#memo-body');
import { titleInput, bodyInput } from './popup.js';
しかしこれではうまくいきません。なぜかというと、ポップアップとサービスワーカーは実行コンテキストが異なるからです。実行コンテキストが異なるとは、単に同じフォルダにある別ファイルということだけではありません。グローバル変数やローカル変数の共有ができないことに加えて、ファイルが実行されて停止されるまでのライフサイクルまでも異なります。つまり実行コンテキストが異なる2つのファイルは完全に隔離されていると考えれます。実行コンテキストが異なるポップアップとサービスワーカーでデータを共有するにはexport
とimport
を使ったデータのやり取りとは別の方法を使う必要があります。それがメッセージ受け渡しです。
メッセージ受け渡しとは、隔離された 2 つの実行コンテキスト同士が相互通信を可能にする仕組みです。メッセージ受け渡しを行うためには、実行コンテキストが異なる 2 つのファイルのうち、一方でメッセージのリクエストを送信するコードを書き、もう一方でメッセージを受信するコードを書きます。メッセージには、他のメッセージと区別するための ID としての役割を持つ文字列の他、データを送ることもできます。
以上のことを踏まえた上で、popup.js
に次のコードを追加します。
const button = document.querySelector('.button');
const titleInput = document.querySelector('#memo-title');
const bodyInput = document.querySelector('#memo-body');
button.addEventListener('click', () => {
const title = titleInput.value;
const body = bodyInput.value;
const req = {
type: 'save',
data: { title, body }
};
button.addEventListener('click', async () => {
const title = titleInput.value;
const body = bodyInput.value;
// ②
const req = {
type: 'save',
data: { title, body }
};
try {
const res = await chrome.runtime.sendMessage(req); // ①
alert(`レスポンス: ${res}`);
} catch(error) {
alert(`エラーが発生: ${error}`);
}
})
});
① はメッセージを送信するためのコードです。ここで本書で初めて Chrome API の実例が出てきました。chrome.
から始まるコードはすべて Chrome API を表しています。ここではchrome.runtime
API のsendMessage()
を利用してポップアップからメッセージを送信しています。Chrome API についても事象で詳しく解説します。繰り返しになりますが現時点では Chrome API をChrome が持つ便利な機能を拡張機能開発に使えるように提供されているライブラリ群という認識で構いません。
sendMessage()
の第一引数では ② で定義したreq
を渡しており、リクエストの内容を書いています。今回はreq
をメッセージとデータを格納したオブジェクト型の値として定義しています。しかしreq
と名付けている、sendMessage()
の第一引数のに当たる部分はany
型で定義されています。なのでどんな型の値を渡しても構いません。
現実的には受信側でどのメッセージが送られてきたかを識別するキーと、共有したいデータを記述するキーはよく使われるでしょう。そこで今回はメッセージを区別するため"type"
キーと、共有したいデータを値にもつ"data"
キーを持つオブジェクト型として定義しました。本書では一貫してオブジェクト型でメッセージを渡しますが、コードや解説記事によってキー名や型が異なる可能性があるので、そのときは頭の中で読み替えてください。
sendMessage()
の返り値は、メッセージを受信側でsendResponse()
の引数に設定した値です。メッセージを受信するサービスワーカーのファイルを書くときに再び説明します。
popup.html
の編集
popup.js
ファイルを作成したので、popup.js
で読み込みましょう。HTML ファイルに<script>
タグで JavaScript ファイルを読み込むときは<body>
タグの最後に書きます。<script>
タグをこの位置に書くことで JavaScript が DOM 要素を確実にアクセスできるようになります。ブラウザは HTML ファイルを上から下に読み込んで画面に表示するので、まず DOM の組み立てを行い、その後 DOM の操作をする JavaScript ファイルを実行するように順番を指定できます。
<body>
...
<div class="form-group">
<label for="memo-body">本文</label>
<textarea id="memo-body"></textarea>
</div>
<button class="button">保存する</button>
+ <script src="popup.js"></script>
</body>
</html>
動作確認
動作を確認するため、この時点で拡張機能をブラウザに読み込んでみましょう。その後chrome://extensions/
へ移動し、拡張機能ページを確認すると「エラー」と書かれたボタンが出ています。
「エラー」ボタンをクリックしてみるとエラーの詳細文と、エラーが発生したファイルが書かれています。エラー文は次のように書かれています。
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
メッセージを受信するコードが見つからなかったというものです。メッセージを送信するコードと受信するコードはセットで書かなければエラーが出ます。次はサービスワーカーのファイルを作り、メッセージを受信してみましょう。
service-worker.js
の作成
memo-popup
内にservice-worker.js
というファイルを作り、次のコードを追加します。
// ①
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'save') { // ②③
const title = message.data.title; // ②
const body = message.data.body; // ②
sendResponse(`タイトル:${title}, 本文: ${body}`);// ④
}
})
runtime
API は Chrome API の 1 つです。chrome.runtime.onMessage.addListener()
によりメッセージを受信することができます。onMessage.addListener()
の引数にはコールバック関数を入れます。このコールバックはメッセージを受信した後に実行されます。
メッセージ送信にasync
/await
が使えるのに対して、メッセージ受信にはasync
/await
は使えません。つまり次のようにしてメッセージを受信する処理はかけません。
const res = await chrome.runtime.onMessage()
メッセージ受信にはコールバック関数で対処する必要があります。なお、コールバック関数自体にはasync
/await
は使えます。コールバック関数にasync
/await
を使う例はこのあとに出てきます。
コールバック関数には 3 つの引数があります。第一引数のmessage
にはpopup.js
でメッセージを送信したときに引数として渡したreq
がそのまま格納されています。第二引数のsender
にはタブ ID や URL などのメッセージを送信した側に関する情報が格納されています。第三引数のsendResponse
はメッセージの送信元にデータを返却する関数です。
② ではメッセージとして受け取ったオブジェクトを取り出してmessage
に格納しています。メッセージ送信側では次のような値をメッセージとして渡しました。
const req = {
type: 'save',
data: { title, body }
};
受信側ではこのメッセージをそのまま第一引数のmessage
で受け取ることができます。"type"
キーを取り出すときにはmessage.type
のようにかき、"title"
キーを取り出すときはmessage.data.title
のように書きます。
③ ではメッセージの"type"
キーを取り出して、メッセージの種類がsave
であるかを確認しています。現時点ではメッセージ送信は一箇所しか実装していないため、このようなメッセージを特定する場合わけは必要ありません。しかし複数のメッセージ送信を実装する場合、受け取ったメッセージをタイプ分けし、それらに対応した処理をする必要があります。そこでメッセージ送信の実装が 1 つしかない時点で場合分けすることをお勧めします。
メッセージは複数定義できるので識別できるように一意にする
④ で使っているsendResponse()
は、コールバック関数の第三引数です。sendResponse()
の引数に指定した値はメッセージ受信側で使用したsendMessage()
の返り値になります。メッセージ受信側であるpopup.js
で記述したとおり、sendResponse
に渡した値を返り値として受け取ってres
に格納しています。
const res = await chrome.runtime.sendMessage(req);
動作確認
これでメッセージ受信側の処理が書けたので先ほど出たエラーが解決しました。拡張機能を再度読み込み、ポップアップを起動し、タイトルと本文にテキストを入力し、最後に「保存する」をクリックしてください。すると入力した内容がアラートに表示されました。入力した内容はpopup.js
を通してサービスワーカーに送られ、その後処理を行った後popup.js
に返却されました。メッセージ受け渡しを通して、データが 2 つの JavaScript ファイルを行き来していることが確認できました。
servie-worker.js
の編集
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (req.type === 'save') {
const title = req.data.title;
const body = req.data.body;
- sendResponse(`タイトル:${title}, 本文: ${body}`);//
// ①
+ chrome.storage.local.set({ memo: memoData }, () => {
+ if (chrome.runtime.lastError) { // ②
+ sendResponse({ status: 'error', message: chrome.runtime.lastError.message }); // ③
+ return;
+ }
+
+ sendResponse({ status: 'success' }); // ③
+ });
+ return true; // ④
}
});
① でようやくstorage
API が出てきました。storage
API ではデータを読み書きすることができます。ただし保存できるデータの形式はキーと値のペアであるオブジェクト型に限られます。オブジェクト型はそのまま JSON に対応するので、要するにストレージとは JSON 保管庫のようなものだと考えるといいです。
storage
API を扱うときは「保存領域」と「操作」に分けて考えるとわかりやすいです。storage
API で提供される保存場所はlocal
、session
、managed
、sync
の 4 種類があります。操作はデータを保存するset
、データを読み込むget
、データを削除するremove
などがあります。今回はデータをlocalへ保存するためchrome.storage.local.set()
と書きました。逆にデータをlocalから読み込む場合はchrome.storage.local.get()
と書きます。ここで利用している保存領域のlocal
はlocalStorage
とは別物であることに注意してください。4 つの保存領域については次の章で詳しく解説します。
②ではストレージを扱うときにエラーが発生したときにキャッチしています。runtime.lastError
はコールバック関数のある非同期APIでエラーメッセージを伝えるときに使われます。今回の例ではストレージに値を保存するときにエラーが発生したらruntime.lastError
にError
オブジェクトが代入されます。このError
オブジェクトにはmessage
プロパティがあるので、エラーが発生したときにchrome.runtime.lastError.message
を使いエラー文を取り出しています。もしエラーが発生していない場合はruntime.lastError
はnull
が格納されていますので、if文は実行されません。エラーハンドリングについては次章で詳しく解説します。
③でsendResponse()
にメッセージを格納しています。sendResponse()
の引数に格納した値はそのままメッセージ送信側で返り値として受け取れます。ここではstorage
APIを使い、エラーが発生した場合のことも考える必要があります。そこで"status"
キーを持たせて、サービスワーカーで処理が成功したか失敗したかを、メッセージ呼び出し側でもわかるようにしています。エラー発生時にsendResponse()
を呼び出さずにreturn
だけを書くと、エラーが投げられ、呼び出し元のcatch
文が実行されます。本書ではsendResponse()
を使いエラーを投げず、メッセージにエラーが発生したことを含むように実装します。このエラーの発生については次章で検証します。
④でコールバック関数内部でsendResponse()
を使用するときはreturn true;
と書く必要があります。これはブラウザにコードバック関数が終了してもポートを閉じないようにするためです。sendResponse()
を使わないときはreturn true;
を書く必要はありません。これについても詳しくは次章のメッセージパッシングで解説します。現時点ではメッセージパッシングのポートを閉じないために必要なコードだという認識で構いません。
メッセージ受信側の編集
次にpopup.js
の内容を編集します。
動作確認
入力したデータがブラウザに保存されているか確認します。
まずはツールバーからアイコンをクリックします。次にポップアップ上で右クリックし、「検証」をクリックします。
すると DevTools が現れるので「アプリケーション」パネルをクリックします。もし「アプリケーション」パネルが表示されてなければ、タブを横に広げると隠れているパネル名が出てきます。次にサイドバーに表示される「拡張機能ストレージ」をダブルクリックし、「ローカル」をクリックします。すると拡張機能で保存した値が表示されていることが確認できます。
現在までで、メッセージ受け渡しと、storage
を用いたデータの保存が実装できました。しかしポップアップを閉じてしまうと入力したテキストは消えてしまいます。次にstorage
API を使ってデータを取得してポップアップを開いたら保存したデータを表示させるようにしましょう。
popup.js
の編集
ブラウザに保存したデータを取り出すためにはstorage
API を使う必要があります。しかしstorage
API はサービスワーカーでしか使用することができません。なのでポップアップとサービスワーカーでメッセージ受け渡しを行う必要があります。
まずはポップアップでメッセージを送信する処理を書いていきます。ページの読み込みが完了したときにメッセージを送信したいので、画面読み込みが完了したことを知らせるDOMContentLoaded
イベントが発生したとき処理を行います。popup.js
に次のコードを追加してください。
const button = document.querySelector(".button");
const titleInput = document.querySelector("#memo-title");
const bodyInput = document.querySelector("#memo-body");
+document.addEventListener('DOMContentLoaded', async () => {
+ const res = await chrome.runtime.sendMessage({ type: "load" }); // ①
+})
...
sendMessage()
の引数がそのままメッセージの内容になるのですが、今回は{type: "load"}
というメッセージ ID しか含んでいません。いま行いたい処理はストレージからデータを持ってくることであり、なにかデータを処理したり保存したいわけではありません。このメッセージではサービスワーカーに処理を行うタイミングを伝えるのが目的なので、メッセージの内容としてメッセージ ID だけで十分です。
次にサービスワーカーでメッセージを受信する処理を書いていきます。
Discussion