Chrome拡張の開発方法まとめ　その1：概念編

Chrome拡張の開発に必要な知識とかの覚書です。
この記事では開発の前に知っておくべきChrome拡張の全容について解説していきます。
「実際に開発しながら学ぶ」形式の解説記事は多く見られるのですが、概念についてちゃんとまとめてある記事は見当たらず、そこらへんの理解で結構苦労した覚えがあるので、そういった人達の手助けになればと思います。

## 対象

- Chrome拡張が作りたい人
- Javascriptがまあまあわかる人
- Chrome拡張の各概念がいまいちわかってない人


# 概要

まずはChrome拡張の大まかな構造について説明します。
Chrome拡張は**HTML**と**CSS**と**Javascript**で作られています。
必要なhtml,css,jsファイルなどと後述のManifest Fileを一つのフォルダにまとめたものが一つの拡張機能のまとまりとなり、zipで圧縮してアップロードすることでストアで公開することができます。
また、`chrome://extensions/`のページに上記フォルダをD&Dすることで、自身のChromeに自作の拡張機能をインストールすることができます。

オプションのページやボタンのポップアップなどのGUIの部分はHTMLとCSSで、処理の部分はJavascriptで作ります。
拡張機能内ではchrome.*APIと呼ばれるAPIを利用することができます。
[JavaScript APIs - Google Chrome](https://developer.chrome.com/extensions/api_index)
具体的にAPIを用いてどのような事ができるかは、公式で用意されているサンプルを見ると良いでしょう。ソースコードを見ることができるので、勉強にも役立ちます。
[Sample Extensions - Google Chrome](https://developer.chrome.com/extensions/samples)


# [Manifest File](https://developer.chrome.com/extensions/manifest)
Chrome拡張には必ず**Manifest File**が存在します。Manifest Fileとはその拡張機能に関する情報を書くものであり、json形式で書きます。
具体的にはその拡張機能の名前と説明、使用するスクリプトなどのパス、chrome.* APIを用いる為のパーミッションの設定などを書きます。
拡張機能のルートフォルダにmanifest.jsonという名前で配置します。
細かい記法については、[公式ドキュメント](https://developer.chrome.com/extensions/manifest)や、こちらの記事が参考になると思います。
[Chrome 拡張機能のマニフェストファイルの書き方 - Qiita](http://qiita.com/mdstoy/items/9866544e37987337dc79)


# [Content Script](https://developer.chrome.com/extensions/content_scripts)
Chrome拡張は**Content Script**と呼ばれるスクリプトを任意のページで実行することができます。
例えば、Manifest Fileに以下の記述を付加すると

```js:manifest.json
"content_scripts": [
  {
    "matches": ["http://www.google.com/*"],
    "css": ["mystyles.css"],
    "js": ["jquery.js", "myscript.js"]
  }
]
```

URLが`"http://www.google.com/*"`にマッチするページで、指定したCSSとJSが読み込まれます。
Content Scriptは特殊な性質を持っており、

- chrome.*APIが一部のものしか用いることが出来ない（[公式ドキュメントを参照](https://developer.chrome.com/extensions/content_scripts)）
- ページ内で定義されている変数や関数にアクセスができない（**DOMにはアクセスできる**）

という制約を持っています。
特に「ページ内の変数や関数にアクセスができない」というのがなかなか厄介で、要するにContent Scriptはページ上のスクリプトから隔離されたスコープで実行されているということです。
この特殊な実行環境は公式ドキュメントによると**isolated world**と呼ばれています。
但し、**DOMの取得・操作は出来る**ため、例えばページ内で定義されている関数を実行したい場合など、Webページのスコープ内でスクリプトを実行したい場合は、以下のようにscript要素を挿入してやることで実現できます。

```content_script.js
var script = document.createElement("script");
script.textContent = "hoge();";
document.body.appendChild(script);
```

# [Background Page]と[Event Page]

[Background Page]:https://developer.chrome.com/extensions/background_pages
[Event Page]:https://developer.chrome.com/extensions/event_pages

Content Scriptでは一部のAPIしか使えない制約がありました。そこで登場するのが**[Background Page]**です。
Chrome拡張はBackground Pageと呼ばれるバックグラウンドで動くページを持つことができ、そこで様々な処理を行うことができます。
Manifest Fileに以下のように記述すると、指定したスクリプトがBackground Pageで動作します。

```js:manifest.json
"background": {
  "scripts": ["background.js"]
}
```

「ページ」と言っても目に見えないのでイメージし辛いかもしれないですが、Chromeの内部にWebページのような物が存在していて、そこでJavascriptが動いているという感じです。
ちなみに`chrome://extensions/`からデベロッパーモードをオンにした状態で、「ビューを検証」からBackground PageのDeveloper Toolsを開く事ができます。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/6f002d4e-0b7e-5485-bcb9-fec0fd93a1f9.png)

但し、Background Pageは常にバックグラウンドで動き続けるため、**メモリを常に占有してしまうという欠点があります。**
そこで用意されたのが**[Event Page]**で、Background Pageと同じようにバックグラウンドで動作するものの、必要な時だけ立ち上がって、動作が完了すると閉じるという性質を持っています。
Event Pageの動作状況は`chrome://extensions/`で確認することができます。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/cafb3eee-4020-79e7-e03e-cb37c806be09.png)

現在ではEvent Pageの利用が推奨されています。

# [Browser Action]と[Page Action]

[Browser Action]:https://developer.chrome.com/extensions/browserAction
[Page Action]:https://developer.chrome.com/extensions/pageAction

Chrome拡張は**[Browser Action]**または**[Page Action]**と呼ばれるボタンをChromeのツールバーに追加することができます。
~~どちらを選んでもできる事は変わらないのですが、Page Actionは一時的にしかボタンが表示されず、Browser Actionは常にツールバー上にボタンが表示されるという性質があります。~~

![image](https://qiita-image-store.s3.amazonaws.com/0/97014/9295095b-9e31-8067-f071-101af080a42e.png)

これ、Chrome48以前ではPageActionはURLバーの中に表示される仕様だったのですが、48からは拡張機能のボタンの仕様が変わって、Page Actionも常にツールバー上に表示されるようになりました…
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/391a857c-1d93-94bc-3289-bd4c4cbdde01.png)
利便性の為にわざわざこうして分けられていたのに、何故か一本化され、さらにActionの存在しない拡張機能までボタンが表示されるようになったなど、UIとしても全くダメだし、本当に何の意図の仕様変更なのか全くもって理解できず憤慨しているのですが…それはまあ、とりあえず置いておきましょう。

## [Browser Action]

**[Browser Action]**はURLバーの右にボタンが表示されます。
常にボタンが表示されているため、**特定のWebページを対象としない**ような拡張機能はこちらが良いでしょう。

## [Page Action](https://developer.chrome.com/extensions/pageAction)

~~**[Page Action]**はURLバー内の右端にボタンが表示されます。~~
~~Browser Actionと違い、常にボタンが表示されている訳ではなく、拡張機能側から任意のタイミングで表示させることができるため、**特定のWebページを対象とする**ような拡張機能に向いています。~~
~~例えば、特定のURLパターンのページでのみ表示する、Webページ内に特定の要素が含まれている時にのみ表示するといったような事が可能です。~~

前述の意味不明な仕様変更により、何故かボタンが常に表示されるようになりました。
その代わり、Page Actionが有効になった時はボタンに青いバッジが付きます。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/f2f978e9-8fd4-c1f8-292d-1e4008aa8c74.png)←こんな感じ

## Actionの機能

Browser ActionまたはPage Actionには**ポップアップ**を設定でき、ボタンがクリックされた時に用意したhtmlファイルをポップアップで表示させることができます。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/2823c978-eb44-a5cb-bde1-a42323e98780.png)

Manifest Fileの記述は以下のようになります。ここで設定したhtmlファイル（popup.html）がポップアップで表示されます。（Page Actionの場合はbrowser_actionがpage_actionに変わります）

```js:manifest.json
"browser_action": {
    "default_icon": {
        "19": "icon19.png"
    },
    "default_popup": "popup.html"
}
```
または、Content ScriptやBackground Page側から[browserAction.setPopup](https://developer.chrome.com/extensions/browserAction#method-setPopup)でポップアップを設定することも出来ます。（Page Actionも同様）
Page Actionの~~表示・非表示~~（Chrome48～はアクティブ・非アクティブ）は[pageAction.show,hide](https://developer.chrome.com/extensions/pageAction#method-show)でコントロール出来ます。

ポップアップも一つのページとしてできていて、アイコンを右クリックし、「ポップアップを検証」から、ポップアップページのDeveloper Toolsを開く事ができます。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/0a009205-5b01-7f1f-0476-d7b40508b0df.png)

また、ポップアップを表示する以外にも、[chrome.browserAction.onClicked.addListener](https://developer.chrome.com/extensions/browserAction#event-onClicked)を用いて、クリックしたタイミングで任意のスクリプトを実行するといったことも出来ます。


# 番外編：[Override Pages](https://developer.chrome.com/extensions/override)

上記の他に**Override Pages**という物があり、例えば`chrome://bookmarks`などを拡張機能で置き換える事ができます。
使う機会は限られていると思うのでここでは解説しませんが、そういうものがあるという事は知っておいて損は無いでしょう。


# [Message Passing](https://developer.chrome.com/extensions/messaging)

ここまで、Webページで動くContent Scripts、Browser(Page) Actionのポップアップ、Background(Event) Pageと、3つのJavascriptの実行コンテキストが出てきました。これらはそれぞれ独立して動いており、**異なるコンテキストを跨いで互いに変数や関数を参照することは出来ません**。
そこで、それぞれの環境から互いにメッセージを送り、スクリプト間の連携を実現する**Message Passing**という概念が登場します。
Message Passingはchrome.*APIによって実現します。これは実際に例を見てみたほうが理解できると思うので、まずは例を見てみましょう。

例えば、以下のようなContent ScirptとBackground Pageのスクリプトがあったとしましょう。

```content_script.js
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});
```

```background.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.greeting == "hello")
      sendResponse({farewell: "goodbye"});
  });
```

content_script.jsの**chrome.runtime.sendMessage**が実行されると、Background Page側で**chrome.runtime.onMessage**イベントが発火します。
chrome.runtime.sendMessageの第一引数の`{greeting: "hello"}`が、chrome.runtime.onMessage.addListenerのコールバックの第一引数`request`に入ります。
コールバックの第二引数の`sender`には、メッセージを送ってきたページの情報が入った[MessageSender](https://developer.chrome.com/extensions/runtime#type-MessageSender)オブジェクトが入ります。これと[chrome.tabs](https://developer.chrome.com/extensions/tabs)APIを用いて色々できるのですが、それについては次回詳しく説明する予定です。
第三引数`sendResponse`はレスポンスを返す為の関数で、これに引数を渡して実行することでsendMessageのコールバックが実行され、コールバックの引数`response`に、sendResponseで渡した引数（ここでは`{farewell: "goodbye"}`）が入っています。
`console.log`で`response.farewell`を参照し、コンソールに「goodbye」が表示されます。

このようにして、**sendMessage**と**onMessage**の組み合わせでオブジェクトをやり取りすることにより、異なるコンテキスト同士での連携が実現できます。


# [chrome.storage](https://developer.chrome.com/extensions/storage)APIと[Option Page](https://developer.chrome.com/extensions/options)

拡張機能内で特定の値を保存しておいて、次回起動時にも使いたいという場合があると思います。例えば、拡張機能のユーザー設定項目などです。
また、ユーザー設定項目を入力するための設定画面も必要になります。
これらを実現するのが**chrome.storage**APIと**Option Page**です。

## chrome.storageAPI

chrome.storageAPIは、拡張機能内で特定の値の保存と読み取りを実現するAPIです。
chrome.storageAPIを利用するには、Manifest Fileにpermissionの設定を記述する必要があります。

```js:manifest.json
"permissions": [
  "storage"
]
```

chrome.storageの保存領域には**sync**と**local**の2種類があります。
**sync**を利用すると、ユーザーが同期を有効にしている場合、Googleアカウントを用いてデータが同期され、同じアカウントでログインしている異なるPCのChrome上で同じデータを共有することができます。
**local**はコンピューター上にデータが記録されます。
syncはlocalに比べて記録できる量や書き込みの頻度などが制限されています。（[公式ドキュメント参照](https://developer.chrome.com/extensions/storage#property-sync)）
また、**managed**という読み取り専用の領域もあるようです。これの使い方は知らないです…。教えて偉い人！

storageAPIの使い方は簡単で、保存するときはset、読み取る時はgetを用います。

```set.js
chrome.storage.sync.set(
  {
    "value1": "string1",
    "value2": "string2"
  }
);
```

```get.js
chrome.storage.sync.get(["value1", "value2"], function(items) {
    console.log(items.value1); // -> "string1"
    console.log(items.value2); // -> "string2"
});
```
getでは第一引数に読み取りたい値のstringの配列を渡すと、コールバックの引数`items`に読み取った値が入ります。
localを利用する場合は、それぞれchrome.storage.local.set、chrome.storage.local.getになります。

ちなみに、一部の記事では値の保存に[localStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage)を用いると説明しているものもあります。
確かにlocalStorageでも同じような事は可能なのですが、Content Scripts内でのlocalStorageはContent Scriptsが実行されたサイトのlocalStorageとなってしまうので、Content Scriptsから拡張機能のlocalStorageを読み取ることが出来ないという問題があります。

## Option Page

Option Pageは拡張機能に関する設定変更を行うページです。
![image](https://qiita-image-store.s3.amazonaws.com/0/97014/d52d3973-effd-4542-c0bc-a97d753dcb02.png)

htmlファイルを用意し、Manifest Fileに以下の記述をすることで、拡張機能にオプションページを実装することができます。

```js:manifest.json
"options_page": "options.html"
```
options.html内で必要なのは

- 設定項目を入力するフォーム
- chrome.storageからgetしてフォームにセットする処理
- 保存ボタンを押した時にフォームの値をchrome.storageにsetする処理

です。
フォーム処理はJSerとしては慣れ親しんでいると思われるので、ここでは実例は省いてしまいます。

また、Chrome40から[新しいタイプのオプション](https://developer.chrome.com/extensions/optionsV2)が搭載されたようです。これについてはまた調べてみて記事にしたいと思います。


# まとめ

- Content Scriptsで任意のページでスクリプトを実行できる
- Browser(Page) Actionでツールバー上にボタンを追加できる
- Background(Event) Pageでバックグラウンドで処理を行う
- Chrome拡張上にはContent Scripts、Actionのポップアップページ、Background(Event) Pageなど複数のJS実行コンテキストが存在する
- Message Passingで複数コンテキスト間の連携を実現する
- chrome.storageAPIとOption Pageでユーザー設定項目を実現する

この辺りを理解していれば、Chrome拡張は作れるようになるでしょう。
この記事では概念の説明を中心としたので、実際に作る時は他の記事や公式ドキュメントを見ると良いと思います。

次回はMessage Passingについてもう少し詳しい解説をしたいと思っています。