最近開いたタブを順番に一覧表示する拡張機能を作成

## はじめに

https://qiita.com/taka-k/items/fdaebef8fe459d705a71

前回作成した、タブ整理系の拡張機能作成の続きとして、
最近開いたタブを一覧表示し、クリックするとそのタブがアクティブになる拡張機能を作成してみました。


が、自分の勘違いか丁度最近実装されたのかわかりませんが、
Chrome 本体に最近開いた順にタブの一覧を表示してくれる機能が実装されたようです...

なので今回の拡張機能は必要なくなってしまったのですが、
そのままお蔵入りは悲しいのでどのように作成したかを備忘録として残します。

## 拡張機能を作成していく
### ファイル構成

```
manifest.json
background.js
popup.html
popup.js
```

#### manifest.json

```json
{
  "name": "Tab history",
  "description": "Tab history",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "activeTab", "tabs", "storage"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  }
}
```

今回は、タブがアクティブになったタイミングでその情報を保存するようにしたいため、
serive_worker を使用できるように、以下の設定をしています。

```json
  "background": {
    "service_worker": "background.js"
  }
```

また、今回はタブの情報はもちろん、アクティブになったタブと ID を保存するためのストレージが必要となるため、
以下の許可設定を行っています。

```json
  "permissions": [
    "activeTab", "tabs", "storage"
  ]
```

#### background.js
```js
const historyLimit = 20

chrome.tabs.onActivated.addListener((activeTab) => {
  chrome.storage.local.get(["recentActiveTabIds", "lastCloseTabId"], (result) => {
    let tabIds = []

    if (result.recentActiveTabIds) {
      tabIds = result.recentActiveTabIds
    }

    if (result.lastCloseTabId) {
      tabIds = tabIds.filter((id) => id !== result.lastCloseTabId)
    }

    tabIds.unshift(activeTab.tabId)
    tabIds = [...new Set(tabIds)]
    tabIds = tabIds.slice(0, historyLimit)

    chrome.storage.local.set({ recentActiveTabIds: tabIds })
  })
})

chrome.tabs.onRemoved.addListener((removeTabId) => {
  chrome.storage.local.set({ lastCloseTabId: removeTabId })

  chrome.storage.local.get(["recentActiveTabIds"], (result) => {
    let tabIds = []

    if (result.recentActiveTabIds) {
      tabIds = result.recentActiveTabIds
    }

    tabIds = tabIds.filter((id) => id !== removeTabId)

    chrome.storage.local.set({ recentActiveTabIds: tabIds })
  })
})

```

タブがアクティブになった際(`onActivated`)、タブが閉じられた際(`onRemoved`)にそれぞれ動作する処理を用意します。

```js
chrome.tabs.onActivated.addListener((activeTab) => {
  chrome.storage.local.get(["recentActiveTabIds", "lastCloseTabId"], (result) => {
    let tabIds = []

    if (result.recentActiveTabIds) {
      tabIds = result.recentActiveTabIds
    }

    if (result.lastCloseTabId) {
      tabIds = tabIds.filter((id) => id !== result.lastCloseTabId)
    }

    tabIds.unshift(activeTab.tabId)
    tabIds = [...new Set(tabIds)]
    tabIds = tabIds.slice(0, historyLimit)

    chrome.storage.local.set({ recentActiveTabIds: tabIds })
  })
})
```

onActivated の処理は、大まかに以下の通りです。

1. ストレージから 最近開いたタブの ID のリストを取得する。
2. 取得したリストに対して今回アクティブになったタブの ID を追加する。
3. リストに対して重複削除処理と保存上限数に切り捨てる処理を行う。
4. ストレージにリストを保存する。

```js
chrome.tabs.onRemoved.addListener((removeTabId) => {
  chrome.storage.local.set({ lastCloseTabId: removeTabId })

  chrome.storage.local.get(["recentActiveTabIds"], (result) => {
    let tabIds = []

    if (result.recentActiveTabIds) {
      tabIds = result.recentActiveTabIds
    }

    tabIds = tabIds.filter((id) => id !== removeTabId)

    chrome.storage.local.set({ recentActiveTabIds: tabIds })
  })
})
```

onRemoved の処理は、大まかに以下の通りです。

1. ストレージから 最近開いたタブの ID のリストを取得する。
2. 取得したリストから、今回開いたタブの ID を削除する。
3. ストレージにリストを保存する。

また、上記に挙げた以外の処理として、タブを閉じた際にそのタブの ID をストレージに保存し、
アクティブの処理側でその ID を参照し、リストから削除する処理も行っています。

これは、現在開いているタブを削除した際に他のタブが瞬時にアクティブになることにより、
onRemoved と onActivated がほぼ同時に動き、onRemoved で削除したはずの ID が onActivated 側の処理で復活してしまうことがあったため、
onActivated 側でも削除するようにしました。

#### popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>popup</title>
  <style>
    body{
      width: 350px;
    }
    button{
      align-items: center;
      background-color: transparent;
      border-bottom: #cccccc 1px solid;
      border: none;
      cursor: pointer;
      display: flex;
      padding: 4px 0px;
      text-align: left;
      width: 100%;
    }
    button:hover{
      background-color: #eeeeee;
    }
    button > img{
      height: 16px;
      margin-right: 4px;
      width: 16px;
    }
  </style>
</head>
<body>
  <div id="tabList"></div>
  <script src="popup.js"></script>
</body>
</html>
```

拡張機能のアイコンをクリックした際に表示する、ポップアップの中身です。
タブの一覧を挿入する `tabList` を用意し、 最低限の style を指定して見た目を整えています。

#### popup.js
```js
chrome.storage.local.get(["recentActiveTabIds"], (result) => {
  targetDOM = document.getElementById("tabList")

  if (result.recentActiveTabIds) {
    result.recentActiveTabIds.forEach((id) => {
      chrome.tabs.get(id, (tab) => {
        const button = document.createElement("button")
        const span = document.createElement("span")
        span.textContent = tab.title

        if (tab.favIconUrl) {
          const img = document.createElement("img")
          img.src = tab.favIconUrl
          button.appendChild(img)
        }

        button.appendChild(span)

        button.addEventListener("click", () => {
          chrome.tabs.update(id, {active: true})
        })
        targetDOM.appendChild(button)
      })
    })
  }
})
```

ポップアップで動作するスクリプトです。

ストレージから最近開いたタブの ID のリストを取得し、その ID を使って `chrome.tabs.get` で詳細情報をそれぞれ取得します。
その詳細情報を DOM として構築し、popup.html の `tabList` に挿入しています。

文字情報だけだと視認性が悪かったため、今回は favIcon も表示するようにしました。

## 動かしてみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247154/ddd50620-de4f-00a6-b9e3-276e5445088b.png)

無事、最近開いたタブの一覧が表示されました！

## ハマったところ
### service_worker 内の変数は、永続しない
最初は、タブの ID リスト を service_worker 内に変数として持たせるように作成していました。
が、一定時間が経つと、変数の中身が空になってしまうことに気づきました。
調べてみると、service_worker は一定の条件を満たすとシャットダウンされ、その際に変数に保存した内容は失われてしまうようです。

https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle?hl=ja#idle-shutdown