Chrome Extension の作り方 (最終話: メッセージパッシング)

[Chrome Extension の作り方 \(その1: 3つの世界\) \- Qiita](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)
[Chrome Extension の作り方 \(その2: Contents Script\) \- Qiita](https://qiita.com/sakaimo/items/6b7b464de4bdcad18802)
[Chrome Extension の作り方 \(その3: Browser Action / Page Action\) \- Qiita](https://qiita.com/sakaimo/items/129a785146e5873c42a6)
[Chrome Extension の作り方 \(その4: Event Page / Background Page\) \- Qiita](https://qiita.com/sakaimo/items/edd910a770b3d5ba83e3)


の連載の最終回！！

----

ChromeExtensionを作っていると「メッセージパッシング」という言葉を見かけます。
[Message Passing \- 公式](https://developer.chrome.com/extensions/messaging)

[Chrome Extension の作り方 \(その1: 3つの世界\) \- Qiita](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)の説明の通り、**3つの世界**があるのですが、異世界とやり取りしたい状況が出てきます。

今回のサンプルでは `content scripts` と`browser action` 間でのメッセージパッシングの例を挙げます。

# イメージ図

![demoimage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/1903568a-1224-4852-1a38-2606e53ce2ba.png)


# サンプルアプリ


- メモを残せる
- メモに「現在選択してる文字列」を挿入できる

![memodemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/9251a99c-6a38-1616-3716-1ab6e315e44e.gif)



# 完成品

## フォルダ構成

```:フォルダ構成
ChromeMemo
 └ content
    └ content.js
 └ lib
    └ jquery-3.4.0.min.js
 └ popup
    └ popup.css
    └ popup.html
    └ popup-chrome.js
    └ popup-main.js
 └ icon_128.png // アイコンはなくても大丈夫
 └ manifest.json
```

※ `popup-chrome.js` と `popup-main.js` 
今回は2つのファイルに分けていますが、一つのファイルにまとめても動きます。
画面の操作のためのJS(popup-main.js)と、メッセージパッシング用(popup-chrome.js)のJSに分けてみました。



## manifest.json

```json:manifest.json
{
  "manifest_version": 2,
  "name": "ChromeMemo",
  "version": "1.0.0",
  "description": "アイコンクリックで、メモを起動します。URLボタンでURL,コピーボタンで選択範囲をメモに入れ込みます。メモを保存するときはSAVEをクリックしてください。",
  "icons": {
    "128": "icon_128.png"
  },
  "permissions": ["tabs","storage"],
  "browser_action": {
    "default_title": "ChromeMemo",
    "default_popup": "popup/popup.html"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"]
    }
  ]
}
```

↓ アイコン画像がない場合はこの部分はなくても大丈夫です。

```json
  "icons": {
    "128": "icon_128.png"
  },
```

## popup.html

Browser Actionとして、右上のアイコンを押したときに表示されるポップアップ(HTML)
ここで `popup-main.js` と `popup-chrome.js` を読み込んでる。

```html:popup.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ChromeMemo</title>
  <link rel="stylesheet" href="popup.css">
</head>

<body>
  <textarea id='memo'></textarea>
  <input type='button' id='copy' value='選択範囲をコピー'>
  <input type='button' id='save' value='メモを保存'>
  
  <script src='../lib/jquery-3.4.0.min.js'></script>
  <script src='popup-main.js'></script>
  <script src='popup-chrome.js'></script>
</body>
</html>
```


## popup.css

デザインセンスある人はもっとカッコよくしてほしい。

```css:popup.css
#memo{
  width: 400px;
  height: 300px;
  font-size: 16px;
  background-color: #ffffc4;
  border: 0;
}

/*ボタンを押したとき*/
input:active {
  -webkit-transform: translateY(4px);
  transform: translateY(4px);
  box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.2);
  border-bottom: none;
}
```

## popup-main.js

- 保存されてるメモを読み込んでテキストエリアに表示する
- 保存ボタンが押されたら保存する

の処理を書きました。

```js:popup-main.js
let memo;

loadData();

//データの読み込み
function loadData(){
  chrome.storage.local.get(['chromememo'], function(obj){
    memo = obj.chromememo;

    //データがない場合の初期化
    if(!memo){
      memo = [ {text: "", lastUpdate: new Date()} ];
      chrome.storage.local.set({chromememo:memo}, function(){});
      return;
    }
    $('#memo').val(memo.text);
  });
}

//保存ボタンが押されたとき
$('#save').on('click', function(){
  memo = {
    text: $('#memo').val(),
    lastUpdate: new Date()
  };
  chrome.storage.local.set({chromememo: memo}, function(){
    alert('保存が完了しました');
  });
});
```

[chrome\.storage \- 公式](https://developer.chrome.com/apps/storage)
Chromeが持ってる保存領域(storage)にデータを保存(set)するのと、取り出す(get)することができます。

これ(chrome.storageのAPI)を使いたいので、`manifest.json` に下記の記述があります。(tabも後で出てきます)

```json:manifest.json
"permissions": ["tabs","storage"],
```







## popup-chrome.js

**今回のポイント1: メッセージを送る！**

```js:popup-chrome.js
// 「選択範囲をコピー」が押されたら、現在アクティブなタブへ通信をして、選択範囲の情報を取得
$('#copy').on('click', function(){
  chrome.tabs.query( {active:true, currentWindow:true}, function(tabs){
    chrome.tabs.sendMessage(tabs[0].id, {message: '選択範囲ちょうだい'}, function(item){
      if(!item){
        alert('選択範囲が見つかりませんでした');
        return;
      }
      $('#memo').val($('#memo').val() + item);
    });
  });
});
```

インデントが深くてパット見て「うっ」ってなりますがもっとうまいやり方あるのかな。


まずは「内側」の `chrome.tabs.sendMessage` から見ていきます。


```js:popup-chrome.js
    chrome.tabs.sendMessage(tabs[0].id, {message: '選択範囲ちょうだい'}, function(item){
```

`sendMessage` の第一引数に「どのタブの中にあるページにメッセージを送るのか」を指定する必要があります。これをタブのidで指定します。詳しくは [chrome.tabs.sendMessage \- 公式](https://developer.chrome.com/extensions/tabs#method-sendMessage) 参照。

で、そのタブidを取得するために「外側」のこれ↓をしています。

```js:popup-chrome.js
  chrome.tabs.query( {active:true, currentWindow:true}, function(tabs){
```


`chrome.tabs.query`の第一引数に `{active:true,currentWindow:true}` を与えていますが、第一引数に何を指定できるのかは[chrome\.tabs \- 公式](https://developer.chrome.com/extensions/tabs#method-query) 参照。上記では「現在のウィンドウ(複数のウィンドウが開かれている可能性もあるので)にあるタブで、activeなタブ」という指定になります。

queryの第一引数の条件にマッチするタブが配列になって `tabs` で受け取れます。
今回の条件にマッチするタブは一つしか存在しないので、 `tabs[0]` で取り出せます。
`tabs[0].id` でこのタブのidが取得できる、と。


下記の構造がわかれば

```js:popup-chrome.js
    chrome.tabs.sendMessage(/*送信先のタブid*/, /*送るモノ*/, function(/*レスポンス*/){
      /*レスポンスを受け取ったあとの処理*/
    });
```

`popup-chrome.js`の全体像もわかりますね！

```js:popup-chrome.js
// 「選択範囲をコピー」が押されたら、現在アクティブなタブへ通信をして、選択範囲の情報を取得
$('#copy').on('click', function(){

  // 対象のタブのidを取得したい
  chrome.tabs.query( {active:true, currentWindow:true}, function(tabs){

    // 取得したタブid(tabs[0].id)を利用してsendMessageする
    chrome.tabs.sendMessage(tabs[0].id, {message: '選択範囲ちょうだい'}, function(item){

      // sendMessageのレスポンスが item で取得できるのでそれを使って処理する
      if(!item){
        alert('選択範囲が見つかりませんでした');
        return;
      }
      $('#memo').val($('#memo').val() + item);
    });
  });
});
```


## content.js

**今回のポイント2: メッセージを受け取る！レスポンスを返す！**


```js:content.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
  let selection;
  console.log(request.message); // -> 選択範囲ちょうだい が出力される

  // 画面で選択されている部分を文字列で取得する
  if(window.getSelection){
    selection = window.getSelection().toString();
  }else{
    selection = '';
  }
  sendResponse(selection);
});
```

```js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
```

で受け取ります。送られてきたメッセージの中身は `request.message` で取り出せます。
今回は ` {message: '選択範囲ちょうだい'}` は何の役にも立ってないですが、例えば

一つの `content.js` に対して、複数のパターンのメッセージが送られてくる場合はmessageの中身によって`content.js` 側で処理を分岐することができますね。(すみません、調査できてないのですが `sender` からも判別できそうですね)


処理した結果(今回は `selection`)を

```js
sendResponse(selection);
```

ってやると、メッセージを送信した人にレスポンスを返せます。



# メッセージパッシングの関係図

覚え方としては

- 「content scriptsはtab内にあるページに仕込んだスクリプト」だから、`chrome.tabs`で送る！
- あとは `chrome.runtime` だ！

![構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/4e7b2c88-6024-83f2-0475-f8b8616b9a14.png)



# おしまい

ChromeExtensionについて5回でまとめてみました。

第1回でも書きましたがこの内容は私が考えたのではなく、@ikeikeda のレクチャをもとにしたものです。スゴイのは彼です。多分そのうち彼もアウトプットしてくれると思いますYO!

[Chrome拡張とGASを連携させる例](https://qiita.com/sakaimo/items/3ee8feaf692d5e7455bb) にも書きましたが、Googleスプレッドシートをデータベース代わりにすれば、小規模ユースのアプリケーションは作れそうですね。