chromeの拡張機能を書いてみた

# はじめに
今年もアドベントカレンダーの時期になりましたね！
結構前から気になってたchromeの拡張機能を自作してみたので、それについて書こうと思います

# 何を作るのか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001524/d365870b-629b-8830-9d1d-84d017a636d1.png)
（今年のアドベントカレンダーは何を書こう...）
（Qiitaのアドベントカレンダーのヘッダーがとても可愛いな...）
（どうせなら雪でも降らしたろか！）

というわけで、ページに雪を降らせるchromeの拡張機能を作りたいと思います！

# ファイル構成
```
src/
 ┣ manifest.json・・・拡張機能の設定ファイル
 ┣ popup.html・・・拡張機能のアイコンをポチったときに表示されるやつ
 ┣ style.css・・・htmlの装飾用
 ┣ index.js・・・ブラウザに対して操作する用のjs
 ┣ index.css・・・ブラウザに対して操作する用のcss
 ┗ event.js・・・backgroundで動く用のjs
```

細かいファイル構成の情報は@sakaimoさんがまとめている記事がすごくわかりやすいので、そちらを読んでください
https://qiita.com/sakaimo/items/416f36db1aa982d8d00c


# 仕様
コンテキストメニューにボタンを追加して、そこで雪を降らせたり止ませたりできるようにしたい

※なお雪を降らせるソースについては、いろいろな人が書いているので省略します
今回は下記の記事を参考にさせていただきました
https://web-dev.tech/front-end/javascript/snow-falling-effect/

# ソース
```json: manifest.json
{
  "manifest_version": 3,
  "name": "test app",
  "version": "1.0",
  "description": "test app",
  "action": {
    "default_title": "test app",
    "default_popup": "popup.html"
  },
  "content_scripts": [{
    "matches": ["http://*/*", "https://*/*"],
    "js": [
      "index.js"
    ],
    "css": [
      "index.css"
    ]
  }],
  "background": {
    "service_worker": "event.js"
  },
  "permissions": [
    "contextMenus",
    "activeTab",
    "tabs",
    "scripting"
  ]
}
```
manifestファイルのバージョンが3になってからbackgroundの記述が他と変わってきたようなので注意です

---

```html: popup.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
  <body id="body">
    <div class="container">
      <h1>何が降るかな</h1>
    </div>
  </body>
</html>
```
まじで大したこと書いていないです

---

```css:style.css
body {
  width: 300px;
}

.container {
  text-align: center;
}
```
ただサイズ指定して中央寄せしただけです

---

```js:index.js
const container = document.createElement('div');
container.classList.add('snow-container');
document.body.appendChild(container);
```
雪を降らせる用のdivを用意してあげます
無理やりbodyに追加します

---

```css:index.css
/* ===== 雪を表示するコンテナのスタイル ===== */
.snow-container {
  position: fixed;
  top: 0;
  left: 0;
  background-color: transparent;
  height: 100vh;
  width: 100%;
  overflow: hidden;
  z-index: 9999;
  display: none;
}

/* ===== 雪のスタイル ===== */
.snow {
  background-color: #fff; /* 雪の色 */
  border-radius: 50%;
  position: absolute;
  animation: animate-snow 10s linear;
}

/* ===== 雪のアニメーション ===== */
@keyframes animate-snow {
  0% {
    opacity: 0;
    top: 0;
  }

  10% {
    opacity: 1;
  }

  90% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    top: 100vh;
  }
}
```
雪関係のスタイルです

---

```js:event.js
chrome.runtime.onInstalled.addListener(function(){
  // 親階層のメニューを生成
  const parent_menu = chrome.contextMenus.create({
    type: "normal",
    id: "parent",
    title: "雪を降らせるメニュー"
  });
  //子階層のメニューを親(parent_menu)に追加
  chrome.contextMenus.create({
    id: "0",
    parentId: parent_menu,
    title: "降らせる"
  });
  chrome.contextMenus.create({
    id: "1",
    parentId: parent_menu,
    title: "止める"
  });
});

/**
  * メニューが選択されたときの処理
  * 選択されたアイテムはこちらの関数の引数に入ってくる(今回は item)
  * menuItemIdでmenuのidが文字列で取得できる
  */
chrome.contextMenus.onClicked.addListener(function(item, tab){
  function fallingSomething(item) {
    const snowConteiner = document.querySelector('.snow-container');
    // 雪を生成する関数
    const createSometing = () => {
      snowConteiner.style.display = 'block';
      const snowEl = document.createElement('span');
      snowEl.className = 'snow';
      const minSize = 10;
      const maxSize = 30;
      const size = Math.random() * (maxSize - minSize) + minSize;
      snowEl.style.width = `${size}px`;
      snowEl.style.height = `${size}px`;
      snowEl.style.left = Math.random() * 100 + '%';
      snowConteiner.appendChild(snowEl);
      // 一定時間が経てば雪を消す
      setTimeout(() => {
        snowEl.remove();
      }, 10000);
    }
    // 繰り返し処理の制御
    function exec_interval() {
      let timerArray = (JSON.parse(localStorage.getItem('timerArray')) !== null) ? JSON.parse(localStorage.getItem('timerArray')) : [];
      const timerId = setInterval(createSometing, 100);
      timerArray.push(timerId);
      localStorage.setItem('timerArray', JSON.stringify(timerArray));
    }
    function stop_interval() {
      let timerArray = (JSON.parse(localStorage.getItem('timerArray')) !== null) ? JSON.parse(localStorage.getItem('timerArray')) : [];
      if (timerArray.length > 0) {
        for (let i = 0; timerArray.length > 0; i++) {
          clearInterval(timerArray.shift());
        }
        snowConteiner.style.display = 'none';
      }
      localStorage.setItem('timerArray', JSON.stringify(timerArray));
    }

    // コンテキストメニューの選択によって処理
    switch(item.menuItemId) {
      case "0":
        exec_interval();
        break;
      case "1":
        stop_interval();
        break;
      default:
        stop_interval();
    }
  }

  // scriptを実行する
  if(!tab.url.includes("chrome://")) {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      function: fallingSomething,
      args: [item]
    });
  }
});
```
ここが一番のキモです
簡単に説明します

1. `chrome.runtime.onInstalled.addListener`は拡張機能がインストールされたときに呼ばれるイベント
ここでコンテキストメニューを追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001524/773403e0-b681-01ed-9c31-484d58d5c1bd.png)
（追加したコンテキストメニュー）


2. `chrome.contextMenus.onClicked.addListener`はコンテキストメニューがクリックされたときに呼ばれるイベント
自作したコンテキストメニューの選択によって、雪を降らせるかどうかを処理しています



3. コンテキストでスクリプトを実行したい場合は、`chrome.scripting.executeScript`を使います
`tabId`は`chrome.contextMenus.onClicked.addListener`の引数、functionは自作の関数を指定、argsに引数を指定してあげることで選択したコンテキストメニューに応じた処理を作ることができます
```javascript
  // scriptを実行する
  if(!tab.url.includes("chrome://")) {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      function: fallingSomething,
      args: [item]
    });
  }
```

---

こうすることで雪を降らせることができるようになりました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001524/f889296c-94f1-f4c7-d30f-0a439c3fb778.png)


# さいごに
本当は月ごとに降ってくるものを変えようと思ったのですが、時間が足りず...
後ほど更新するか、来年のアドベントカレンダーの課題にしようと思います（😇）


# 参考文献
https://developer.chrome.com/docs/extensions/mv3/
https://qiita.com/sakaimo/items/416f36db1aa982d8d00c
https://web-dev.tech/front-end/javascript/snow-falling-effect/