はじめてChrome拡張機能を作ったので、作り方と公開までの流れをまとめる

# はじめに：Google Meetでのミーティング時間を自動で計測するChrome拡張機能を作ってみた
ミーティング時間をちゃんと工数として計上するのって、地味にすごく面倒じゃないですか？
毎日あるものもあれば、隔日、週1、隔週、単発などなど、頻度もばらばらだし所要時間もさまざま。
押したり巻いたりして予定通り終わらないことも間々ありますし、ミーティング内容によって工数計上先もいろいろですし。

そんな私の叫びをキャッチしてくださった同僚から[toggl](https://toggl.com/)を教えていただいて使ってみたけれども、面倒臭がりなゆえ、そもそもボタンすら押したくない（というか忘れる）・・・

もうGoogle Meetのミーティング時間だけでいいから、よろしく計測してくれよ！そしてあわよくば日次で、工数計上先別で合計してくれよ！
と心の中で叫んでサービスを探しましたが、意外と見つからない・・（ご存じの方いらっしゃったらご教示ください）

ということで、Google Meetに入ってから出るまでのミーティング時間を自動で計測し、いい感じに集計できるChrome拡張機能を作ってみました。

https://chrome.google.com/webstore/detail/google-meet-stopwatch/ogiljjcopgbhmmmpaamcalpcpbpjobgl

所感としては、ミーティング時間の計測がすごく楽になった！という満足感はさることながら、
それ以上に**Chrome拡張機能を公開するのってこんなに手軽だったんだ！** ということでした。

今回はじめてChrome拡張機能を作ったので、初心者目線で拡張機能を作るためにやったこと、詰まったことを実装の流れに沿ってまとめていこうと思います。


# やりたいこと

大きくは以下です。

1. Google Meetの時間を自動で計測する
1. 各Meet時間を一覧する
1. Meet時間を日次で集計する
1. タグを自由に設定できるようにする
1. タグに対してキーワードを設定できるようにする
1. ミーティング名＝キーワードが紐づくタグごとに、Meet時間を日次で集計する

これを実現する場合、画面としては3つの実装が必要です。また、レコードデータの保存先も必要です。
こんなイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/b1c3fd6b-9a7a-8801-71bd-b86dc9626349.png)

下記の順番で手順を書いていきます。
1. [特定の画面を開いた際に処理を実行する](#特定の画面を開いた際に処理を実行する)
1. [計測した時間を保存する](#計測した時間を保存する)
1. [ミーティングの計測時間をポップアップ表示する](#ミーティングの計測時間をポップアップ表示する)
1. [設定画面でタグやキーワードの設定ができるようにする](#設定画面でタグやキーワードの設定ができるようにする)
1. [Chromeウェブストアへアプリを登録する](https://qiita.com/moromi25/items/dada185e5f1e07fc78fb#chrome%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%83%88%E3%82%A2%E3%81%B8%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B)

また、`やりたいことをChrome拡張機能としてどう作っていくか`をメインに書きます。
Google Meetの計測のための具体的な処理内容の記載はほぼ割愛しています（需要があれば公開します）。


# 特定の画面を開いた際に処理を実行する
そもそも拡張機能をどのように作ればいいか右も左もわからない状態で参考にしたのがこちら。
とてもわかりやすかったです。

https://qiita.com/RyBB/items/32b2a7b879f21b3edefc

まずはこの部分から着手。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/50c42d2b-3f87-aa83-c2f4-803b9948e79f.png)

やりたいことはGoogle Meetのミーティング時間を計測することですが、まずは[Chrome拡張の作り方（超概要）#特定の画面でアラートを出す](https://qiita.com/RyBB/items/32b2a7b879f21b3edefc#%E7%89%B9%E5%AE%9A%E3%81%AE%E7%94%BB%E9%9D%A2%E3%81%A7%E3%82%A2%E3%83%A9%E3%83%BC%E3%83%88%E3%82%92%E5%87%BA%E3%81%99)を参考に、今回の時間計測の対象画面である `Google Meet画面上でアラート`を出してみます。

### manifest.json
Chrome拡張機能は、マニフェストファイル（manifest.json）を書くことでChromeに仕様を伝えます。
そのためまずは、manifest.jsonを作成します。
```manifest.json
{
    "name": "Google Meet Stopwatch",
    "version": "1.0.0",
    "manifest_version": 3,
    "description": "Calculate the time of the meeting on Google Meet",
    "content_scripts": [{
      "matches": ["https://meet.google.com/*"],
      "exclude_matches": ["https://meet.google.com/?hs=*"],
      "js": [
        "js/main.js"
      ]
    }]
}
```
なお、今回は各章での必要最低限しか記載していきません。
manifest.jsonに書くべきこと、書けることについては下記が詳しいので参照してください。

https://qiita.com/mdstoy/items/9866544e37987337dc79

今回のポイントとしては以下。

#### manifest_version
2022年5月現在、manifest_versionは `3` 推奨だそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/ea399e9f-7ae3-4d26-8e7e-acf74d0651a5.png)
2でも動きますが、**Manifest V3にすることで公開時の審査にかかる時間を短縮し、新しいAPIや機能が使えるそうです。**
これから新しく拡張機能を作るなら、V3で問題ないですね。

#### content_scripts

##### matches
今回はすべてのGoogle Meetでのミーティングを計測するという最終目的があるため、`matches`には `["https://meet.google.com/*"]`を記載しました。これでGoogle MeetのURLを開いた際にスクリプトが実行されるようになります。

##### exclude_matches
Google Meetのうち、ホーム画面**以外のすべてのページ**でスクリプトを実行させるため、ホーム画面のURLを除外対象として記載しています。
ただし下記サイトを見ると、`exclude_globs`の方が適切かもしれません。

https://oxynotes.com/?p=8960

##### js
`js`はルートフォルダからの相対パスを記載します。content_scriptsに限らず、manifest.json内に記載するファイルのパスは、すべてルートフォルダからの相対パスで記載してください。
今回は、jsフォルダの中にmain.jsを入れています。

main.jsの中身は以下の通り。
```main.js
window.addEventListener('load', () => {
    window.alert('Meetを開きました！');
});
```
実装できたところで、拡張機能のテストをしていきます。


### 実装中の拡張機能をChrome上で表示する

ここでは、テスト目的で自端末のChrome上でのみ表示する方法を記載します。
ストアに公開する場合の手順は[Chromeウェブストアへアプリを登録する](https://qiita.com/moromi25/items/dada185e5f1e07fc78fb#chrome%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%83%88%E3%82%A2%E3%81%B8%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B)で書いています。

1. Google ChromeのURLバーに`chrome://extensions`と入力
1. デベロッパーモードをONに変更（下記添付画像①）
1. `パッケージ化されていない拡張機能を読み込む`をクリックし、自分の作ったソースフォルダを選択（下記添付画像②）
圧縮などは不要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/c053ea99-4a75-8239-76be-887cc5e11e41.png)

これで拡張機能がChrome上で表示され、テストできるようになりました。


### ソース修正を反映させる
前項で読み込んだ実装中の拡張機能に修正を加えた場合のソース更新方法です。
以下を参考にしました。

https://note.affi-sapo-sv.com/chrome-extent-test.php

ソースを修正したら、`chrome://extensions`ページ上部の`更新`ボタン、または作成中の拡張機能をパネル内右下クリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/0bf87037-a49f-20f5-fac9-2b73bd9b8ba3.png)
その後、対象のページを再読み込みすることで、ソース修正が反映されます。
再読み込みだけで反映されない場合、キャッシュの消去とハード再読み込み（リロードアイコン長押しで出てきます）をしてみてください。


### デバッグ

普段フロント開発をしている場合、開発者ツール→Sourcesでデフォルトで表示される`Page`タブでデバッグすることが多いかと思います。
Chrome拡張機能のデバッグの場合、[自作Chrome拡張機能をテストする方法#コンテンツスクリプトのデバッグ](https://note.affi-sapo-sv.com/chrome-extent-test.php#title4)にあるとおり、Pageタブの横にある`Content scripts`タブでデバッグできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/d38a9c2e-6d10-623b-e4de-957debb2b1a1.png)


### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/7328ae5a-1dbd-263b-84d8-c3f910af0407.png)

無事にアラートが表示されました！
あとはMeetの時間を計測する処理を書くだけです。ざっくり書くとこんな感じです。
```main.js
window.addEventListener('load', () => {
    googleMeetStopWatchApp.startTime = new Date();
});
window.addEventListener('beforeunload', () => {
    googleMeetStopWatchApp.endTime= new Date();
    let data = {};
    data.actualTime = Math.abs(googleMeetStopWatchApp.endTime - googleMeetStopWatchApp.startTime);
    ...
    saveToStorage(data);
});
```
最終行にある`saveToStorage`をどう実装するか。
次章で計測した時間情報を保存する方法を説明します。


# 計測した時間を保存する

この部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/6dd66390-6c04-fb20-e0ad-a16ffe99f690.png)

わざわざDB用意するほどでもないし、localStorageかなあ・・と思って調べたところ、localStorageはbackground経由でないと使えない模様。

https://mae.chab.in/archives/2861

うーんもう少しライトに使いたい・・・
と調べてみると、`chrome.storage API`なるものがあるらしい。

https://qiita.com/shimutaya/items/e8835d6ce794ef6c73cf

しかもvalueにString以外を入れられる。
今回は計測時間＝数値を入れたいので、まさにもってこいの仕組みです。採用します。

## chrome.storage APIを使う

[公式](https://developer.chrome.com/docs/extensions/reference/storage/)がわかりやすかったです。

chrome.storageにはlocalとsyncの2種類があります。
localはデバイス内完結、syncはGoogleアカウントと同期し、同一アカウントであればデバイス間でもデータを同期することが可能です。
今回はデバイス内完結でいいか、ということでlocalを利用することにしました。

manifest.jsonに下記を追記。
```manifest.json
    "permissions":[
      "storage"
    ]
```
### storageへの登録と取得
`set/get`を使います。
```main.js
chrome.storage.local.set({'meyKey': 'my value'}, () => {
    chrome.storage.local.get('myKey', function(result){
        console.log(result['myKey']);
    });            
});
```

#### 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/239aceec-f261-09fe-a47b-1c9d37272160.png)

### storageからの削除
`remove`または`clear`を使います。
* remove：特定のキーに紐づくレコードを削除
* clear：すべてのレコードを削除
```js
chrome.storage.local.remove('myKey');

// 複数のキーを渡すことも可能
chrome.storage.local.remove(['myKey1', 'myKey2'], ()=>{
    ...
});

chrome.storage.local.clear();
```
clearはstorageの中をまるまる削除してしまうことから、あまり使いどころなさそうだなという印象です。

### 実際に登録しているデータサンプル
今回の拡張機能では、こんな感じのレコードをchrome.storageに登録しています。
localStorageと違い、特にJSON.stringify/JSON.parseせずとも登録できるので使い勝手がいいです。
```js
    const KEY = 'googleMeetStopWatchRecords';
    const records = {
        googleMeetStopWatchRecords: {
         '20220415': [
             {
                 date: 20220415,
                 formattedDate: '2022年4月15日(金)',
                 elapsedTime: 5,
                 meetingTitle: 'チームDaily'
             },
             {
                 date: 20220415,
                 formattedDate: '2022年4月15日(金)',
                 elapsedTime: 43,
                 meetingTitle: '設計レビュー'
             },
         ],
         '20220414': [
             {
                 date: 20220414,
                 formattedDate: '2022年4月14日(木)',
                 elapsedTime: 5,
                 meetingTitle: 'チームDaily'
             },
             {
                 date: 20220414,
                 formattedDate: '2022年4月14日(木)',
                 elapsedTime: 25,
                 meetingTitle: '修正内容レビュー'
             }
            ]  
        }
    };
    chrome.storage.local.set({[KEY]: records});
```

### 参考）chrome.storage内に登録したデータを確認する方法
下記を参考にして確認していました。
あとはset後のfunctionにget→コンソール出力の処理を書いて確認するとかもやりました。

https://dackdive.hateblo.jp/entry/2017/07/27/100000


# ミーティングの計測時間をポップアップ表示する
続いてこの部分。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/a8f70a74-bcd9-8003-acbe-689b3a504861.png)

拡張機能をインストールすると、こんな感じでURLバーの横にアイコンを表示できます（`固定`しておくと常にアイコンが表示されます）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/807866f9-6fe3-b914-968d-e5abbf55022a.png)
このアイコンをクリックしたときに、ポップアップを表示し、計測したMeetの時間をこんな感じでポップアップが表示できるようにしたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/97a9964a-cc1d-cc90-a038-249bd733fc7f.png)

[公式](https://developer.chrome.com/docs/extensions/reference/action/)を参考に、manifest.jsonに下記を追記。

```manifest.json
    "action":{
      "default_title": "Google Meet Stopwatch",
      "default_popup": "html/popup.html"
    }
```

注意点としては、**manifest_version:2まではbrowserActionやpageActionを利用できますが、V3ではactionに変更**になっているようです。

## default_title
アイコンにカーソルを合わせた際に表示される文章を指定できます。任意項目です。

## default_popup
アイコンクリック時に表示されるhtmlファイルを指定します。
```popup.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Google Meet Stopwatch</title>
  <link rel="stylesheet" href="./../fontawsome/css/all.css">
  <link rel="stylesheet" href="./../css/popup.css">
</head>
<body>
  <div id="app" class="container">
     <div class="header-area">
        <div class="title-area">
          <span>Google Meetの利用時間</span>
        </div>
        <div class="header-buttons-area">
          <button id="button-clear-all" class="clear-button">CLEAR ALL</button>
          <i id="button-open-config" class="fa-solid fa-gear" title="設定画面を開く"></i>
        </div>        
      </div>
      <div class="data-area">
          <div class="meeting-list">
              <!-- ここにMeet情報をごりごり詰めていく -->
          </div>
      </div>
  </div>
  <script src="./../js/popup.js"></script>
</body>
</html>
```

## jsファイルの指定は？
content_scriptsではjsファイルを指定できますが、actionではhtmlのみ指定できます。
jsファイルは、html内でスクリプトタグを記載することで指定可能です（通常のhtmlと同じです）。


## デバッグ方法

アイコン右クリック→`ポップアップを検証`をクリックで、描画されているhtmlや読み込まれたjsファイルの確認は可能ですが、描画時のjsファイルのデバッグはできません。

下記の方法でデバッグができます。

https://dev.classmethod.jp/articles/google-chrome-extension-2/

私はhtmlフォルダ以下にpopup.htmlを入れているため、
`chrome-extension://{エクステンションID}/html/popup.html`
でポップアップを開いてテストしました。

注意点としては、アイコンからポップアップを開く場合とは当然ながらにサイズが違うこと。
最終的なレイアウトは、アイコンからポップアップを表示して確認してください。


# 設定画面でタグやキーワードの設定ができるようにする
最後にこの部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/e35de62e-0c65-4e1a-5a54-5ce38b2f80f7.png)

## 設定画面＝オプションページを作る
Chrome拡張機能では設定画面はオプションという名前で呼ばれます。
今回は設定画面を1つのページにしたいため、オプションページを実装します。
オプションページ以外は何があるの？は後述。

https://developer.chrome.com/docs/extensions/mv3/options/#linking

manifest.jsonに以下を追記。
```manifest.json
    "options_page": "html/options.html"
```

action同様、jsファイルの定義はoptions.htmlに記載します。

### options_pageとoptions_ui、どちらを使うべき？
オプションを実装する場合、manifest.jsonの書き方に`options_page`と`options_ui`の2種類あります。
`options_ui`の場合、`open_in_tab: false`としているとオプションページが拡張機能の詳細画面内でポップアップ表示されます。
`options_page`を指定する、またはoptions_uiで`open_in_tab: true`にした場合、オプションページが別タブで開きます。

両者の違いは[公式](https://developer.chrome.com/docs/extensions/mv3/options/#considerations)で解説されていますが、今回は以下を満たせればOKなので`options_page`を採用しました。
* 別タブで開ける
* chrome.storage.localに設定内容が保存できる


### オプションページはどこから開ける？
オプションページは、以下の2経路から開くことができます。

拡張機能アイコン右クリック→オプション
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/d9b90398-240d-d3b7-f084-aaa8729077f8.png)

拡張機能詳細ページの`拡張機能のオプション`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/78bb18dd-55ac-9e7b-172e-140ece65e0b4.png)

ただ、正直どちらもわかりにくい。
なので、ポップアップからオプションページへの動線を作るほうがいいと思いました。

## ポップアップからオプションページを開けるようにする
ポップアップ上に以下のような設定アイコンを置いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/e75b107e-bc81-82bf-5665-8ba4109b0842.png)

このアイコンからオプションページを開くのには、[公式](https://developer.chrome.com/docs/extensions/mv3/options/#linking)のとおりですが以下のように記載します。
```popup.js
    $('#button-open-config').on('click', (e) => {
        if (chrome.runtime.openOptionsPage) {
          chrome.runtime.openOptionsPage();
        } else {
          window.open(chrome.runtime.getURL('./html/options.html'));
        }
    });

```

これで、実現したかったことは一通りできるようになりました！

# デモ
こんな感じになりました（Meet時間の計測をデモに加えるのはいまいちなので、あらかじめ登録済です）。
![demo-google-meet-stopwatch_Trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/0a672533-046d-a728-932f-90c75a0f53d5.gif)
デザインがいけてないとか、設定方法わかりにくいとかツッコミどころはあるけれど、そこは次のバージョンアップで・・！


# Chromeウェブストアへアプリを登録する
せっかく作ったので、Chrome拡張機能のリリースフローを踏んでみるため、また私と同じようなニッチな悩みを抱えている人の一助となるために、拡張機能を公開してみようと思います。

下記サイトを参考にしつつ、新しくなっている部分を更新していきます。

https://belltree.life/chrome-extension-release/

## Chrome Web Store Developer Dashboardにアクセスする

https://chrome.google.com/webstore/devconsole

### デベロッパー登録をする（初回のみ）
最初にデベロッパー登録をしていない場合は登録が必要です。5ドルかかります。
有料とはいえ、1回限りでいい、かつ安価なので一歩踏み出しやすいですね。
![Chromeウェブストア_デベロッパー登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/3cad0335-fa63-5ced-d9f2-e88f1224a8cc.png)

## 拡張機能をアップロードする
実装した拡張機能のソースをzip化し、`新しいアイテムを追加`ボタンをクリックしてファイルをアップロードします。
![ChromeWebStoreDeveloperDashboard_新しいアイテム追加1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/fb34b64b-4a45-6bea-1bf0-f4bf26445a1a.png)
![ChromeWebStoreDeveloperDashboard_新しいアイテム追加2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/66a0e3bd-095d-b207-6c6f-bd9964bd8b20.png)
このとき、利用しているGoogleアカウントの2段階認証を設定しておかないとアップロード時にエラーが発生します。
2段階プロセスの設定方法はこちら。

https://support.google.com/accounts/answer/185839?hl=ja&co=GENIE.Platform%3DDesktop

## 公開のための必要項目を入力する
こちらを参考にしました。

https://belltree.life/chrome-extension-release/


実装した機能に応じて、必要項目を入力していきます。
入力不足については、画面上部の`公開できない理由`をクリックするとエラー内容が表示されるのでそれをもとにつぶしていきます。

あらかたエラーをつぶしていった後、最後に地味に詰まった箇所がこちら。
![ChromeWebStoreDeveloperDashboard_新しいアイテム追加3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/3b88e65c-531b-7bac-66c7-a8f111de587f.png)

アカウントタブで連絡先メールアドレスの登録が必要とのこと。
エラーの解消自体は難しくなさそうだけれど、そもそものアカウントタブが全然見つけられない。

ここまでつぶしてきたエラーが今回の拡張機能の登録用のタブ内のものだったのに対し、連絡先メールアドレスのエラーだけはChrome Web Store Developer Dashboard自体の設定に関するエラーだった。

なので、以下の手順でアカウントタブを見つけ、設定を更新したところ、無事解消しました。
デベロッパー登録後初回のみ発生しそうな問題でした。

![ChromeWebStoreDeveloperDashboard_新しいアイテム追加4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/14363082-8892-0479-2cb0-3a04d37d531a.png)

無事審査に出すことができました。

ただし、今回content_scriptsのmatchesに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366737/4ff10b02-a216-852e-7ccc-fec9e2cbd6c8.png)

審査結果は数週間かかる場合があるとのことですが、2日後には審査に通過。
審査通過後に自動公開の設定としていたため、即日公開されました！

https://chrome.google.com/webstore/detail/google-meet-stopwatch/ogiljjcopgbhmmmpaamcalpcpbpjobgl


# おわりに
初めてのChrome拡張機能の実装でしたが、実装、公開ともに驚くほど手軽でした。
やったことない人はとてもおすすめです。
今回みたいに自分のために作ったものを、簡単に公開できます。
そして自分が作ったアプリがウェブストアで表示されるの、感動します！

これから作ってみようとしている人の参考になれば幸いです。


# 追記
その後の機能追加に伴って書いた記事を貼っていきます。

https://qiita.com/moromi25/items/fce144889ebb6970c890