Chrome拡張機能作成に入門！タブ検索機能を作ってみた

# はじめに
本記事は[ラクスアドベントカレンダー](https://qiita.com/advent-calendar/2023/rakus)の19日目です。
タブを開き過ぎて，見つからず同じページをいくつも開くことが最近続いていたので，タブを検索する拡張機能を作りました．
コード全体は[こちら](https://github.com/tkeshun/tabs-manager)です.
この記事ではChrome拡張を作った際に調べた情報のまとめと作ったタブ検索機能について紹介します．

# Chrome拡張の情報源

- [ドキュメント](https://developer.chrome.com/docs/extensions?hl=ja)
- [sample](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main)

# タブ検索機能のChrome拡張

今回作ったChrome拡張機能を例にして，作成に必要なファイルと手順を紹介していきます．
- 今回作った拡張機能

下の画像のように，拡張機能を選択するとポップアップが出現し，今開いているタブの一覧が出現します．入力ボックスにタイトルを入力して検索ボタンを押下するとタブを検索できます．
拡張機能チュートリアルの[タブマネージャー](https://developer.chrome.com/docs/extensions/get-started/tutorial/popup-tabs-manager?hl=ja)をベースにして作りました．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462734/ededc512-9c9a-569a-dbb6-57e640f138b0.png)


## プロジェクト構造

```
.
├── images
│   ├── icon-128.png
│   ├── icon-16.png
│   ├── icon-32.png
│   └── icon-48.png
├── manifest.json
├── popup.css
├── popup.html
└── popup.js
```

- images/
imagesは画像を保存するディレクトリです．今回の拡張機能で利用した画像が格納されています．

- manifest.json
拡張機能の機能や構成を記述するファイルです．

- popup.html
popupで表示するページのhtmlを記述するファイルです．

- popup.css
popupで表示するページのCSSを記述するファイルです．

- popup.js
popupで表示するページのJavaScriptを記述するファイルです．

これらのファイルを編集して拡張機能を実装しました．
次からファイルの中身を説明していきます．


## manifest.json

このファイルは以下の通りに記述しました．

```json
{
  "manifest_version": 3,
  "name": "Tab Manager",
  "version": "1.0",
  "icons": {
    "16": "images/icon-16.png",
    "32": "images/icon-32.png",
    "48": "images/icon-48.png",
    "128": "images/icon-128.png"
  },
  "action": {
    "default_popup": "popup.html"
  },
  "host_permissions": ["https://*/*"]
}
```

- manifest_version
パッケージが必要とするマニフェストファイル形式のバージョンを指定します．
現状は３でいいようです．
参考：[Manifest Version](https://developer.chrome.com/docs/extensions/reference/manifest/manifest-version?hl=en)

- name
拡張機能の名前です．この項目の記述は必須です．
45文字の制限があります．
参考：[Manifest - name](https://developer.chrome.com/docs/extensions/reference/manifest/name?hl=en)

- icons
拡張機能またはテーマを表すアイコンを 1 つ以上指定します．
参考：[Manifest - Icons](https://developer.chrome.com/docs/extensions/reference/manifest/icons?hl=en)


- action
Chromeで拡張機能のツールバーボタンを制御するchrome.action API を使用できるようにします．
ポップアップのファイルを指定することで，ユーザーがツールバーにある拡張機能のアクションボタンをクリックすると、アクションのポップアップが表示されるようになります．
`default_popup`にhtmlファイルを指定します．
参考：[chrome.action](https://developer.chrome.com/docs/extensions/reference/api/action?hl=ja)

- host_permissions

拡張機能のAPIの使用を許可するAPIを設定します．
ここで特定のURLのみを指定しておくと許可されていないURLのタブは検索結果に含まれません．
記述はMatch patternsに書いてある形式で行います．
今回はhttpsであればこの拡張機能の使用を許可するようにしています．

参考：[Declare permissions](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions)
参考：[Match patterns](https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns)

## popup.css

popupのCSSを記述するファイルです．
特にこだわりがなかったので，チュートリアルのCSSをそのまま使用しました．
このファイルは以下の通りに記述しました．

```css
body {
  width: 20rem;
}

ul {
  list-style-type: none;
  padding-inline-start: 0;
  margin: 1rem 0;
}

li {
  padding: 0.25rem;
}
li:nth-child(odd) {
  background: #80808030;
}
li:nth-child(even) {
  background: #ffffff;
}

h3,
p {
  margin: 0;
}
```

## popup.html

popupのhtmlを記述するファイルです．
チュートリアルをベースに一部変更しています．

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./popup.css" />
  </head>
  <body>
    <template id="li_template">
      <li>
        <a>
          <h3 class="title">Tab Title</h3>
          <p class="pathname">Tab Pathname</p>
        </a>
      </li>
    </template>

    <h1>タブ検索</h1>
    <input type="text" id="search" placeholder="タイトルを入力">
    <button>検索</button>
    <ul></ul>
  </body>
</html>
<script src="./popup.js" type="module"></script>

```

## popup.js

popupで使用するJavaScriptを記述するファイルです．
チュートリアルをベースに一部変更しています．

```js
// 一覧を取得
const tabs = await chrome.tabs.query({
  url: ["*://*/*"],
});
const template = document.getElementById("li_template");
const elements = new Set();
for (const tab of tabs) {
  // コピーして新規のテンプレを用意する
  const element = template.content.firstElementChild.cloneNode(true);
  // タイトルとパスを記述する
  const title = tab.title.split("-")[0].trim();
  const pathname = new URL(tab.url);
  element.querySelector(".title").textContent = title;
  element.querySelector(".pathname").textContent = pathname;
  // リンクを押したときの動作を仕込む
  element.querySelector("a").addEventListener("click", async () => {
    await chrome.tabs.update(tab.id, { active: true });
    await chrome.windows.update(tab.windowId, { focused: true });
  });
  elements.add(element);
}

document.querySelector("ul").append(...elements);
const button = document.querySelector("button");

// 検索
button.addEventListener("click", async () => {
  const searchText = document.getElementById("search").value;

  // 配列
  const url = [];
  for (const tab of tabs) {
    // 部分一致でタイトルを検索,一致したらURLを配列に格納
    if (tab.title.includes(searchText)) {
      url.push(tab.url);
    }
  }
  const searchTabs = await chrome.tabs.query({
    url: url,
  });
  const template = document.getElementById("li_template");
  // ulを空にする
  document.querySelector("ul").innerHTML = "";
  const elements = new Set();
  for (const tab of searchTabs) {
    // コピーして新規のテンプレを用意する
    const element = template.content.firstElementChild.cloneNode(true);
    // タイトルとパスを記述する
    const title = tab.title.split("-")[0].trim();
    const pathname = new URL(tab.url).pathname;
    element.querySelector(".title").textContent = title;
    element.querySelector(".pathname").textContent = pathname;
  
    element.querySelector("a").addEventListener("click", async () => {
      await chrome.tabs.update(tab.id, { active: true });
      await chrome.windows.update(tab.windowId, { focused: true });
    });
    elements.add(element);
  }
  document.querySelector("ul").append(...elements);
});

```

まず，一覧を取得します．

```js
const tabs = await chrome.tabs.query({
  url: ["*://*/*"],
});
```

検索するタブのURLを指定します．
指定する形式は[Match patterns](https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns)を使用します．
また複数指定することも可能です．
その場合は以下の例のように配列の内容を変更します．
```js
const tabs = await chrome.tabs.query({
  url: ["http://*/*","https://*/*"],
});
```

次にtabを表示させます．
要素はfor文で取り出しています．
for文の中身について一部解説していきます．

```js
const element = template.content.firstElementChild.cloneNode(true);
```

これはpopup.htmlで記述してあるtemplateをコピーしています．
```html:popup.htmlの一部
    <template id="li_template">
      <li>
        <a>
          <h3 class="title">Tab Title</h3>
          <p class="pathname">Tab Pathname</p>
        </a>
      </li>
    </template>
```
このテンプレートを書き換えて，タイトルとURLを追加します．



取得したtabは画像のようにtitleとurlを持っています．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462734/1f1b52c6-06c7-20bd-f5bb-56b21005c169.png)

htmlの要素に取得したtitleとurlを追加します．

```js
  // const pathname = new URL(tab.url).pathname;
  const pathname = new URL(tab.url);
  element.querySelector(".title").textContent = title;
  element.querySelector(".pathname").textContent = pathname;
```

次にタグを押下したときに対象のタブに遷移するようにします．

```js
  element.querySelector("a").addEventListener("click", async () => {
    await chrome.tabs.update(tab.id, { active: true });
    await chrome.windows.update(tab.windowId, { focused: true });
  });
```

最後に検索機能のメイン部分です．

```js
button.addEventListener("click", async () => {
  const searchText = document.getElementById("search").value;

  // 配列
  const url = [];
  for (const tab of tabs) {
    // 部分一致でタイトルを検索,一致したらURLを配列に格納
    if (tab.title.includes(searchText)) {
      url.push(tab.url);
    }
  }
  const searchTabs = await chrome.tabs.query({
    url: url,
  });
  // 省略
  for (const tab of searchTabs) {
    // 省略
  }
  document.querySelector("ul").append(...elements);
});
```
queryにはurlを与える必要があるので，取得済みのタブ情報をタイトルで検索してURLを取得します．
urlは複数指定できるので，url配列に一致したURLを格納しておいて，queryをもう一度実行します．

以上の実装でタブ検索機能が実装できました．

## 拡張機能の読み込み

[このページ](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world?hl=ja#load-unpacked)に従ってやっていけば読み込みできます．

1. タブで [chrome://extensions](chrome://extensions)を検索します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462734/2e0d28cc-bc81-6e25-0ea7-b762398307f9.png)

2. デベロッパーモードを有効化します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462734/4f0e4728-cbbf-283d-44e1-c96de61eee34.png)

3. `パッケージ化されていない拡張機能を読み込む`を選択します．
4. フォルダを選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462734/97b42d47-6f37-e009-0106-36752d916824.png)

以上で拡張機能が使用可能になります．

# まとめ

今回はChrome拡張機能を作成しました．
[github](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main)にサンプルがたくさんあったので，ぜひ興味があるかたは見てみると自分が作りたい機能の参考になると思います．
記事を見ていただきありがとうございました．