Chromeブラウザ拡張機能を作ってみた

一から作ってみた時の作業記録みたいなもの。

## ディレクトリを作る
適当にディレクトリを作る

## manifest.jsonを作る
以下のような内容でプラグインを作ってディレクトリに入れる。

```json:manifest.json
{
  "name": "テストプラグイン",
  "version": "0.1",
  "description": "テストプラグイン説明",
  "manifest_version": 2
}
```

manifest.xmlの詳しい書き方は[公式サイト](https://developer.chrome.com/apps/manifest)参照。

## 拡張機能を入れてみる
Chromeで`chrome://extensions/`にアクセスし、右上から「デベロッパーモード」を有効にする。
![b1.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/23e0c556-4800-40b5-34eb-0bbc6112e383.png)

「パッケージ化されていない拡張機能を読み込む」から先程作ったディレクトリを指定する。
![b2.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/33c64ea8-783e-fc16-0e37-612c56a89f20.png)

するとこんな感じで読み込まれる。
![b3.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/5ece6d67-553d-3565-c9b8-614c576df9df.png)


## 拡張のアイコンを設定してみる
manifest.jsonファイルに以下のように書き足す。

```json:manifest.json
{
  :,
  "icons": {
    "16": "images/icon16.png",
    "32": "images/icon32.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  }
}
```

アイコンはもちろん対象の場所に用意する。
その後再度「読み込む」。
するとアイコンが適用される。
![b1.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/51cd1532-1f65-bcbb-c942-02a6d1fb59be.png)


ちなみに各アイコンのサイズは以下の通りらしい。

|  **サイズ** | **使われる場所** |
|  :------ | :------ |
|  16x16 | 拡張機能ページのfavicon |
|  32x32 | Windowsの場合、変倍がかかってしまうのでこのアイコンを48x48アイコンの代わりに利用しているらしい |
|  48x48 | 拡張機能の管理ページに表示されるアイコン |
|  128x128 | Chrome ウェブストアで表示されるアイコン |

## ログだけ出力する機能を作る
適当にGitHubのユーザー名を書き換えるだけのものを作ってみる。
manifest.jsonファイルに以下のように書き足す。

```json:manifest.json
{
  :,
  "background": {
    "scripts": ["js/background.js"],
    "persistent": false
  }
}
```

そして以下のようなJavaScriptを書く。

```js:js/background.js
chrome.runtime.onInstalled.addListener(function (details) {
  console.log(details.reason);
});
```

その後再度「読み込む」。
すると「バックグラウンドページ」からDevToolsを起動すると`update`とログが出力されている。
![b1.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/eb3335d1-3f53-2822-143b-b0d1e262d34d.png)
`chrome.runtime.onInstalled`は拡張機能がインストールされた時に呼ばれるイベントで、この時に初期化をしたりする。

これは[バックグラウンドページ](https://developer.chrome.com/extensions/background_pages)と呼ばれている。
お察しの通り、配列なので複数のスクリプトを列挙することもできる。
`persistent`は基本的に`false`にして残り続けないようにする。


## GitHubのページで動くようにする
github.comのタブの情報が得られるように、manifest.jsonファイルに以下のように書き足す。

```json:manifest.json
{
  :,
  "permissions": ["tabs", "https://github.com/*"]
}
```

詳細な権限の一覧は[公式サイト](https://developer.chrome.com/apps/declare_permissions#manifest)参照。実は`tabs`が入っていないようだけど…
URLのマッチングについては[こちら](https://developer.chrome.com/apps/match_patterns)。

background.jsをこんな感じで書き換える。

```js:js/background.js
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  if (changeInfo.status === 'complete') {
    console.log(tabId);
    console.log(changeInfo);
    console.log(tab);
  }
});
```

するとページをリロードする毎にこんな感じのログが出力される。
![b2.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/6c232d2a-beb9-0d8f-ca07-6938bcb542cc.png)

## GitHubのauthorを書き換えてみる
この部分を書き換える。
![b3.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/0d41dbe7-6021-2069-5ed4-b49cfb914919.png)

authorノードの情報を取得する。
authorを右クリック→検証。
![b6.png](https://qiita-image-store.s3.amazonaws.com/0/9104/55777e42-9fdf-df84-e64d-d3553703f6a2.png)

更にDev Toolsから右クリック→Copy→Copy selector。
![b5.png](https://qiita-image-store.s3.amazonaws.com/0/9104/839ce7ae-8778-6ab4-a001-2fa69c723dd8.png)

background.jsからタブ内でbase.jsを呼ぶようにする。

```js:js/background.js
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  if (changeInfo.status === 'complete') {
    chrome.tabs.executeScript(tab.id, { file: "js/base.js" });
  }
});
```
[chrome.tabs.executeScript](https://developer.chrome.com/extensions/tabs#method-executeScript)は、そのタブで指定したスクリプトを実行させる。
バックグラウンドページのスクリプトはそのページのスクリプトとは分離されていてページ内の情報を読み書きできないので、こういうかたちになる。

base.jsでは先程取得したセレクタをそのままquerySelectorに貼り付けて利用する。

```js:js/base.js
var authorNode = document.querySelector('#js-repo-pjax-container > div.pagehead.repohead.instapaper_ignore.readability-menu.experiment-repo-nav > div > h1 > span.author > a');
if (authorNode) {
  authorNode.innerHTML = 'hoge';
}
```

できた :tada: 
![b1.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/86a0ac9f-f856-2896-2af3-59f7b6322b90.png)

## 設定画面を作ってみる
次は設定画面を作ってみる。hogeだけだと微妙なので自分で設定できるようにする（そういう問題ではないが）。

```json:manifest.json
{
  :,
   "options_page": "html/option.html"
}
```

適当にHTMLの設定画面を作る。

```html:html/option.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    <title>設定</title>
  </head>
  <body>
    <div class="container">
      <h1>設定</h1>
      <form>
        <div class="form-group">
          <label for="author">Author</label>
          <input class="form-control" type="text" name="author" id="author">
        </div>
        <div class="form-group">
          <button class="btn btn-default" name="save" type="button">更新</button>
        </div>
      </form>
    </div>
  </body>
</html>
```

再読込してから「詳細」へ移動する。
![b3.png](https://qiita-image-store.s3.amazonaws.com/0/9104/dd9d7917-6aea-0152-d2b0-b77ba8e9c467.png)

「拡張機能のオプション」ができているので、そこから「↗」マークをクリックする。
![b4.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/ccaae800-ca80-5460-db25-73c6e26dfb4a.png)

すると先程作ったページが閲覧できる。
![b2.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/38c0c287-aa94-3833-71e9-0c7ac14aff44.png)

次に、設定を読み書きしたいのでローカルストレージを利用する。
manifest.jsonに権限を追加してローカルストレージにアクセスできるようにする。

```json:manifest.json
{
  :,
  "permissions": ["tabs", "https://github.com/*", "storage"]
}
```

オプションのページでJavaScriptを読み込む。

```html:html/option.html
:
  <script type="text/javascript" src="/js/option.js"></script>
  <title>設定</title>
:
```

読み込み先のJavaScriptを書く。

```js:js/option.js
document.addEventListener('DOMContentLoaded', function () {
    chrome.storage.local.get("github_author", function (item) {
        const author = item.github_author;
        if (author) {
            document.querySelector('[name="author"]').value = author;
        }
    });

    document.querySelector('[name="save"]').addEventListener('click', function (e) {
        const author = document.querySelector('[name="author"]').value;
        chrome.storage.local.set({ "github_author": author }, function () {
            alert('saved');
        });
    });
});
```

上半分でローカルストレージに値があったらフォームに初期値として入力している。
下半分で「保存」ボタン押下時にローカルストレージに値を保存している。
これで設定画面が機能するようになった。

ちなみに`chrome.storage.local`の場合、[ローカルに保存される](https://developer.chrome.com/extensions/storage#property-local)のでマシンごとに値が異なる。
アカウントごとに同期したい場合は変わりに`chrome.storage.sync`を使うと良いらしい。

仕上げにbase.jsで値をローカルストレージから呼ぶように変更する。

```js:js/base.js
var authorNode = document.querySelector(
  "#js-repo-pjax-container > div.pagehead.repohead.instapaper_ignore.readability-menu.experiment-repo-nav > div > h1 > span.author > a"
);
if (authorNode) {
  chrome.storage.local.get("github_author", function(item) {
    const author = item.github_author;
    if (author) {
      authorNode.innerHTML = author;
    }
  });
}
```
拡張機能を再読込し、github.comのページにアクセスしてみる。
すると設定画面で設定した値に書き換わるようになる。
![b1.PNG](https://qiita-image-store.s3.amazonaws.com/0/9104/3ef45b69-5280-c1f4-dfae-d712f94c3596.png)


## 右上のメニューに出してみる
更にmanifest.jsonに書き足す。

```json:manifest.json
{
  :,
  "browser_action": {
  }
}
```

するとこんな感じでアイコンが出る。何も指定していないので無反応。
![b1.png](https://qiita-image-store.s3.amazonaws.com/0/9104/2f2a94ce-dfd0-d4df-4dfc-174ad03b756f.png)

ポップアップするHTMLを指定する。

```json:manifest.json
{
  :,
  "browser_action": {
    "default_popup": "html/popup.html"
  }
}
```

popup.htmlの中身はとりあえずほぼ同じにする。

```html:html/popup.html
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
    crossorigin="anonymous">
  <script type="text/javascript" src="/js/option.js"></script>
  <title>設定</title>
</head>

<body>
  <div class="container" style="width: 200px">
    <form>
      <div class="form-group">
        <label for="author">Author</label>
        <input class="form-control" type="text" name="author" id="author">
      </div>
      <div class="form-group">
        <button class="btn btn-default" name="save" type="button">更新</button>
      </div>
    </form>
  </div>
</body>

</html>
```

するとこんな感じでポップアップする。
![b1.png](https://qiita-image-store.s3.amazonaws.com/0/9104/4cb961aa-8fac-b865-cb03-f302773231b8.png)


## 右クリックメニューを出してみる
次に右クリックメニュー（コンテキストメニュー）に追加してみる。
更に権限を足す。

```json:manifest.json
{
  :,
  "permissions": [..., "contextMenus"]
}
```

background.jsに以下のように書き足す。

```js:background.js
var parentId = chrome.contextMenus.create({
  "id": "sampleParent",
  "title" : "サンプル親"
});
```

するとこんな感じになる。

![b2 - コピー.png](https://qiita-image-store.s3.amazonaws.com/0/9104/4b9f1e5a-a856-cc0f-e084-f95304811c84.png)

更にサブメニューを追加してみる。

```js:background.js
chrome.contextMenus.create({
  "id": "sampleChild",
  "title" : "サンプル子",
  "parentId" : parentId
});
```

するとこんな感じ。

![b2.png](https://qiita-image-store.s3.amazonaws.com/0/9104/754a5e2a-fdaf-059a-ca4d-170d0380fc88.png)

クリックした時のイベント登録はこんな感じ。
これでサンプル子をクリックした時にアラートダイアログが表示される。

```js
chrome.contextMenus.onClicked.addListener( function(info, tab)　{
  alert(info.menuItemId + "をクリック");
});
```


## 参考
[Getting Started Tutorial - Google Chrome](https://developer.chrome.com/extensions/getstarted)
`C:\Users\user_name\AppData\Local\Google\Chrome\User Data\Default\Extensions`にも自分のブラウザに入っている拡張機能がたくさんあるので、それらを参考にすると良いかもしれない。