Chrome拡張とGASを連携させる例(改)


2019年に書いた [Chrome拡張とGASを連携させる例](https://qiita.com/sakaimo/items/3ee8feaf692d5e7455bb) が今現在、ちゃんと動いていないことを上記記事のコメントでご指摘いただき、確認したところ確かにCORSのエラーが出てしまいます。

時を同じくして、業務でも「Googleスプレッドシートにかかれている情報を元にしてChrome拡張の動作を変える」というツールを作る機会が訪れました。


**2022/04/14 追記** : `manifest_version` の3に対応した書き方を追記しました。


# やりたいこと

* Googleスプレッドシートにあるデータを取ってきて、
* そのデータを元に画面に対して操作する(色を変えるとか、ハイライトするとか、表示内容を変えるとか)

# 整理してみました

(正直なところ、「仕組み」についてはあまり自信が無いので、間違っているとか、もっと良い説明の仕方があるよ、という場合はご指摘お願いします)

## うまくいくパターン

content.js の中に書いたコードで、画面を操作することはできます。

![ex1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/93b27b62-6918-e04c-906f-9392700a25c7.png)

やりかたは [Chrome Extension の作り方 (その2: Contents Script)](https://qiita.com/sakaimo/items/6b7b464de4bdcad18802) 参照。


## うまくいかないパターン

content.js から別ドメインへのリクエストはCORSエラーになります。

![ex2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/4bf29432-87f2-7b51-ec65-6ca38a8aa24a.png)


 [Chrome拡張とGASを連携させる例](https://qiita.com/sakaimo/items/3ee8feaf692d5e7455bb) と同じですね。


## これならうまくいく

![ex3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/22e1162b-c68b-0bfe-6d5c-64116a10115b.png)

background.js は content.js とは「別の世界」にいます。この「別の世界」というのが大事なようで、background.js は example.com のドメインにいるわけじゃない(Googleのどこかの世界にいる？)ので、script.google.com へのリクエストがうまくいくようです。

↑ この説明、自信ない。


# サンプルコード

* GoogleAppsScript
    * リクエストを受けるとレスポンスを返す。(後述の `doGet()`)
* 拡張機能では
    * example.com にアクセスすると、GASにリクエストをする
    * GASからレスポンスをもらう
    * レスポンスの中身を画面に書き出す

というサンプルを用意しました。


## GoogleAppsScriptのコード

下記のGASを「ウェブアプリ」としてデプロイします。
スプレッドシートにバインドしたスクリプトにしておくと、スプレッドシートとのやり取りができてステキです。(スプレッドシートをデータベースとして使えるイメージ)

今回のサンプルでは `doGet()` が実行されますので、`doPost()` は使われませんが、拡張機能から何かデータをもらうときは `doPost()` で受け取れます。

```js:GASでウェブアプリとしてデプロイするやつ
// GASが何か受け取るときはこっち
function doPost(e) {
  // エクステンションから受け取ったデータを取り出す
  const params = JSON.parse(e.postData.getDataAsString());

  // ここで処理をする
  const res = { sucsess: true };

  // エクステンションにレスポンスを返す
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify(res));
  return output;
}

// GASから何かを返すだけのときはこっち
function doGet(e){

  // ここにGASから返す処理を書く
  const res = { text: "GASからのレスポンス" };

  // エクステンションにレスポンスを返す
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify(res));
  return output;
}
```

ウェブアプリとして公開し、URLを控えておいてください。
(新しいデプロイをするたびにURLが変更されます)

(2023-04-06 追記) デプロイし直してもURLが変わらない方法があることを知りました!!
詳しくはこちら → [GASでWebアプリをデプロイする](https://www.shanaidx.com/webapp_deploy/)

![ex4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/a130bac5-a508-249a-ca1c-5125f14f911c.png)



## Chrome拡張のコード

### フォルダ構成

```
extension
  └ manifest.json
  └ content.js
  └ background.js
```

### manifest.json

今回は http://example.com/ にアクセスしたときに動くエクステンションにします。


**2022/04/14 追記**
manifest_versionの3に対応した書き方を追記しました。
`background` と `permissions` の書き方が変わったんですね。

↓ version 3
```json:manifest.json
{
  "manifest_version": 3,
  "name": "ChromeエクステンションとGASを連携させるやーつ",
  "version": "1.0",
  "host_permissions": [
    "https://script.google.com/*"
  ],
  "content_scripts": [
    {
      "matches": [
          "http://example.com/*"
        ],
        "js": [
           "content.js"
        ]
    }
  ],
  "background": {
    "service_worker": "background.js"
  }
}
```

↓ こちらは version2。(これから書く人は v3 推奨です)

```json:manifest.json
{
  "manifest_version": 2,
  "name": "ChromeエクステンションとGASを連携させるやーつ",
  "version": "1.0",
  "permissions": [
    "https://script.google.com/*"
  ],
  "content_scripts": [
    {
      "matches": [
        "http://example.com/*"
      ],
      "js": [
        "content.js"
      ]
    }
  ],
  "background": {
    "scripts": [
      "background.js"
    ],
    "presistent": false
  }
}
```

### content.js

http://example.com/ にアクセスするとこれが起動します。ログもブラウザのコンソールに出力されます。

```js:content.js
console.log("content.js開始");

chrome.runtime.sendMessage({
  message: "contentからbackgroundに送るもの(無くてもいい)"
}, response => {

  console.log(`backgroundからの戻り値: ${JSON.stringify(response)}`);

  // ここで画面への処理を書く
  const new_element = document.createElement('p');
  new_element.textContent = response.text;
  document.getElementsByTagName("div")[0].appendChild(new_element);
});
```

やっていることは `chrome.runtime.sendMessage()` をして、`background.js` にある `chrome.runtime.onMessage.addListener` に向けてメッセージ・パッシングを行っています。

ここではサンプルとして、sendMessage の第一引数としてオブジェクトを渡しています。

```js
chrome.runtime.sendMessage({
  message: "contentからbackgroundに送るもの(無くてもいい)"
}, response => {
```

渡す必要がなければカラにしておいてOKです。

```js
chrome.runtime.sendMessage("", response => {
```







### background.js

`content.js` からメッセージを受け取ります。メッセージ自体があってもなくてもいいですが、ともかく処理がここに来ます。


```js:background.js
chrome.runtime.onMessage.addListener(
  function (request, sender, callback) {
    console.log(`バックグラウンドで受け取ったもの: ${request.message}`);

    // 「ウェブアプリ」としてデプロイしてるGASのURL
    const gasUrl = "https://script.google.com/macros/s/xxxxxxxxxx/exec";

    fetch(gasUrl)
    .then(response => {
      return response.text();
    })
    .then(json => {
      console.log(`GASからのレスポンス: ${json}`);
      callback(JSON.parse(json));
    });
    
    // 非同期を同期的に扱うためのtrue
    return true;
  }
);
```

ここからGASに向けてリクエストを送っています。
GASからのレスポンスは `response` として受け取れます。 

`background.js` のログは `content.js` のログとは別のところに出力されます。
拡張機能の画面に行って「バックグラウンドページ」をクリックするとウィンドウが開き、そこにログが出力されます。
![ex5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/7919b38d-5cd7-6898-bd55-79c527b0c212.png)



# 使ってみる

extensionフォルダを 拡張機能としてChrome にインストールします。
その後、http://example.com/ にアクセスすると下記のように「GASから取得した `GASからのレスポンス` という文字列を画面内に表示させることができます。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8612/b97bdebf-1e21-6265-8ee1-6376c15be16b.gif)

どうでしょうか？これ、うまく使えばいろんなことに応用できると思います！

# 課題と対策

課題としては「GASからのレスポンス速度」が気になりますね。何度か実験してみましたが、**たまにGASからのレスポンスが5~10秒くらいかかることもありました。**

↓

これの対策としては

* 起動時にGASから情報(スプレッドシートの値を返すとする)を取ってくる
* 取ってきた値を `chrome.storage` に保存しておく
* その後は `chrome.storage` の値を見て判断する

のであれば、時間がかかるのは起動時の一回だけになりますね。

↓

ただしこれだと、スプレッドシートの値が変わった時に、拡張機能を再起動しないと反映されない問題が起こる。

↓

ということは、`background.js` が、定期的にGASにリクエストをして chrome.storage の中身を更新する。

みたいな？


**2024-02-27追記**
上記の課題を解決した記事を書きました。
https://qiita.com/sakaimo/items/252038ede143ab1fc340