はじめての Chrome 拡張 最小構成ハンズオン 

# はじめに
これは初めて Chrome 拡張を作る人が超基本を理解するのを手助けするための資料です。
[ユニークビジョン](https://qiita.com/organizations/uniquevision)社内のハンズオン勉強会の資料として作りました。

:::note info
この記事は Manifest V3 に準拠しています。
:::

# Chrome 拡張の全体像
Chrome 拡張のざっくりとした全体像を図にまとめてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/98bb0379-9eca-83dc-fc19-484c730221a2.png)

機能ごとに説明します。

## ポップアップウィンドウの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/0df2ffb4-943f-2224-bcb1-4323f44cc9a6.png)

拡張アイコンクリック時にポップアップウィンドウを表示できます。
**popup** と呼ばれています。

## 既存ページの DOM へのアクセス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/3eaceb01-a3dd-92bb-9593-f05057ae67d5.png)

任意のウェブサイトの DOM にアクセスし、情報を取得したり、改変したりできます。
**content_script** と呼ばれています。

## html ファイルの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/a0a9106e-5ddb-df38-7a42-e99857ce9a05.png)

通常のウェブサイトのように任意の html + js ファイルを表示、動作させることができます。

## ブラウザ自体の機能や外部 API へのアクセス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/5ef69e4a-e411-a6f8-b4ef-4ae972459756.png)

ブックマーク / クッキー / タブ など、ブラウザ自体が有する多くの機能にアクセスすることができるのが**background** です。

popup や content_script は通常のフロントエンドのように CORS などの制限があるのに対し、background はそのような制限がなく、**通常のサーバのように外部 API などを利用することができます。**

## ストレージの利用
sync / local / session / managed という 4 種類のストレージを使うことができます。通常のフロントエンドにおける local storage や session storage のようなものです。

https://developer.chrome.com/docs/extensions/reference/storage/#property

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/300e21d6-6968-971e-aed5-901430af8e2a.png)

popup / content_script / background のいずれからもアクセスできます。
**chrome 拡張の中でデータをやり取りする手段の 1 つです。**

## 各機能間の通信
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/6c2bcf32-5e17-061a-45de-365c72bf94a7.png)

popup / content_script / background は互いに messaging API を介してデータの通信ができます。

popup や content_script から直接利用できないブラウザのブックマーク情報などは、この仕組みを利用して background から間接的に取得することができます。

https://developer.chrome.com/docs/extensions/mv3/messaging/

# 最小構成ハンズオン
## ソースコード
下記で公開しています。このハンズオンの最終形です。

https://github.com/punkshiraishi/mini-ext

## 作るもの
下記のような動きの拡張を作ります。

- popup のボタンをクリックする
- 表示しているページの URL を取得する
- ページの上部に URL を表示する

実装の大まかな流れは以下です。

- popup / background / content_script それぞれを単体で動かす
- messaging API でそれぞれをつなぎこむ

それでは書いていきましょう！

## popup の表示

### プロジェクトの作成
ディレクトリを作成し、直下に manifest.json / popup.html / popup.js を作成します。

```manifest.json
{
  "name": "mini-ext",
  "description": "mini-ext",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
    "default_popup": "popup.html"
  }
}
```

```popup.html
<html>
  <body>
    <h1>mini ext</h1>
    <button id="display">display url</button>
    <script src="popup.js"></script>
  </body>
</html>
```

```popup.js
// ボタンクリック時に実行する処理を定義
const button = document.getElementById('display')
button.addEventListener('click', () => {
  // とりあえず console.log してみる
  console.log('clicked!')
})
```

### Chrome で読み込む
- 拡張機能の画面([chrome://extensions/](chrome://extensions/))を開く。
- デベロッパーモードを ON にする。
- [パッケージ化されていない拡張機能を読み込む] をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/2bada2d0-be47-b675-0b17-ce8388a057e3.png)
- 作成したプロジェクトのディレクトリを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/dd356994-e327-c9f9-b167-d80bcad2b2fb.png)
- アイコンをピン留めしておくと楽です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/ad6695bd-1d8e-b7f8-7995-e65427f18aa0.png)
- アイコンをクリックすると popup が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/0e0a88b2-718f-87e7-a516-f737a050f4d1.png)
- popup を右クリック → 検証で検証画面を開きます。
- popup のボタンをクリックするとログが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/c894d73a-7672-bc54-8499-cf18eb56f3f2.png)

## background でタブの情報を取得
- background.js をプロジェクトのルートに作成します。

```background.js
// tabs.query は指定した条件に当てはまるタブを全て取得する。
// この場合はアクティブなタブが一つ取得できる。
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
  console.log({ tabs })
});
```

- manifest.json の permission を追加します。

```diff_javascript:manifest.json
{
  "name": "mini-ext",
  "description": "mini-ext",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
    "default_popup": "popup.html"
  },
+  "background": {
+    "service_worker": "background.js"
+  },
+  "permissions": ["tabs"]
}
```

- 拡張を再読み込みし、 [Service Worker] をクリックして検証画面を開きます。
- タブの情報がログとして出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/ef44bd63-8ed0-c48d-f413-a08f7f906e92.png)


## content_script で文字を挿入
- content.js をプロジェクトのルートに作成します。
```content.js
const body = document.querySelector('body');
const addElement = document.createElement('h1');
addElement.textContent = 'Hello!!';
body.prepend(addElement);
```
- manifest.json に content_scripts の設定を追加します。
```diff_javascript:manifest.json
{
  "name": "mini-ext",
  "description": "mini-ext",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
    "default_popup": "popup.html"
  },
  "background": {
    "service_worker": "background.js"
  },
+  "content_scripts": [
+    {
+      "js": [
+        "content.js"
+      ],
+      "matches": [
+        // 対象とするウェブサイトの URL のパターンを記述する。この場合は https であればすべて対象となる。
+        "https://*/*" 
+      ]
+    }
+  ],
  "permissions": ["tabs"]
}
```

- 拡張機能を再読込します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/fa013821-9e0e-4b56-f10f-14bd84a2a642.png)

- 適当なウェブサイトを開くと、上部に「Hello!!」が表示されます。（既に開いていたページはリロードしないと反映されません。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/ae8c78f8-8125-4064-a850-883a4886316b.png)

## messaging API でをつなぎこむ
### 実装イメージ
次のように実装して、それぞれの機能を繋ぎこみます。
- popup click で background にメッセージを送る。
- background で tab の URL を取得し、content_script に送る。
- content_script は、受け取った URL をウェブサイトに差し込む。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/32a84a2e-bf8f-8c6d-a720-8ca7b8683e23.png)

### popup -> background

```diff_javascript:popup.js
const button = document.getElementById('display')
button.addEventListener('click', () => {
-  console.log('clicked!')
+  chrome.runtime.sendMessage({ name: 'displayUrl:background' })
})
```

`chrome.runtime.sendMessage` の引数には任意の文字列またはオブジェクトが渡せます。
今回は `{ name: '関数名:送り先'}` というオリジナルのフォーマットを使っています。
関数名や送り先を指定しているのは、`sendMessage` したデータはこの拡張の中であればどこでも拾えてしまうためです。（例外あり）
受け取った側で期待しているリクエストかどうかを判別するための情報として使用します。

### background -> content_script

```diff_javascript:background.js
chrome.runtime.onMessage.addListener((request) => {

  // 期待通りのリクエストかどうかをチェック
  if (request.name === 'displayUrl:background') {
    let url;
    let id;

    chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
      console.log({ tabs })
      url = tabs[0].url;
      id = tabs[0].id

      // content_script へデータを送る
      chrome.tabs.sendMessage(id, { // content_script はタブごとに存在するため ID 指定する必要がある
        name: 'displayUrl:contentScripts',
        data: {
          url
        }
      })
    });
  }
});
```

`chrome.runtime.onMessage.addListener` で message イベント時の処理を定義できます。
`chrome.tabs.sendMessage` は content_script にメッセージを送る関数です。tab ID を指定する必要があります。

### content_script

```content.js
chrome.runtime.onMessage.addListener((request, options) => {
  if (request.name === 'displayUrl:contentScripts') {
    const body = document.querySelector('body')
    const addElement = document.createElement('h1');

    // 受け取った URL を画面に表示する
    addElement.textContent = `URL is ${ request.data.url }`;
    
    body.prepend(addElement)
  }
});
```
これですべてのコードが出揃いました！

### 動かしてみる
- 拡張を再読込します。
- 任意のページを開きます。
- popup のボタンをクリックします。
![chromeextdemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/a92a9f39-b73a-9341-2c70-0b560abcdff3.gif)

**完成～！！** :tada:



※動かない場合は、拡張の再読込と、ページのリロードを試してみてください。


# 次のステップ
ここまで、シンプルな javascript + html で最小構成のデモンストレーションをしてきました。
更に複雑なものを作ってみたい、という方には、**ブラウザ拡張開発のためのフレームワーク**を使ってみることをオススメします。
面倒な設定がテンプレート化されていたり、TypeScript で開発できたりします。

vue ベースだと [vitesse-webext](https://github.com/antfu/vitesse-webext) などが著名なものとして挙げられます。

vitesse-webext については過去に解説記事を書いたので、興味があれば御覧ください。

https://qiita.com/punkshiraishi/items/7ec1448984599d0ec40f

# 終わりに
以上でハンズオンは終了です。Chrome 拡張の大まかな理解の助けになれば幸いです。
分かりづらい点や、フィードバックなどあれば、是非コメントしていただけると助かります。

# おしらせ
フロントエンド周りのことをよく呟いています。よければフォローお願いします。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ユニークビジョンという会社のエンジニアです。<br>フロントエンド開発と品質向上に取り組んでいるので、そのあたり興味ある方はフォローしていただけると嬉しいです。<br><br>品質の高いサービスを提供し続ける会社であるために／品質向上チーム <a href="https://t.co/vI7YsKL6QW">https://t.co/vI7YsKL6QW</a></p>&mdash; シライシ@ユニークビジョン (@punksy2) <a href="https://twitter.com/punksy2/status/1576934123670630404?ref_src=twsrc%5Etfw">October 3, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>