「タブ以上ブックマーク未満」を実現するChrome拡張機能を作りました。

## TL;DR
![Group 2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201590/e63c71f0-f8d0-45fb-c4d1-324953719ba4.jpeg)


- ブラウザのタブはなるべく増やしたくない。
- 「数時間後に開くことが明確だけど、今は閉じたいタブ」をどうにかしたい。ブックマークにはしたくない。
- 一時的にリンクを保存し、開きたい時に参照できる拡張機能を作った。

 
 ![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201590/5c97d1d0-d7ae-c412-ea24-8622ba09c6de.gif)


「LinkMemo」と名付けたこの拡張機能。
インストールは[こちらのページから](https://chrome.google.com/webstore/detail/link-memo/ldjlfegijibnmliabfelcjpkbklicbhi?hl=ja)できます。

## 提供機能/使い方

- 「＋」ボタンで、現在開いているタブのタイトルとURLが保存できる。
- 各アイテムをクリックすると、そのページが新規タブで開く。
- ゴミ箱ボタンをタップすると、各アイテムの削除ボタンが出てくる。「cancel」ボタンで通常画面に戻る。

機能はとてもシンプルです。地味に便利です。
なにかご意見あれば[twitter](https://twitter.com/rabspice)でお声がけください！:muscle:


## 工夫しました
### vue-cliで作成
経験がある方はご存知の通り、Chromeの拡張機能はWebページと同じ要領で作ることが出来ます。
そこで、Vueを用いて開発をするためにvue-cliを使用しました。ビルドしたjsファイルをindex.htmlで読み込むようにしています。
index.htmlに、直接scriptタグでvueを埋め込むこともできましたが、その方法だと、拡張機能を起動するたびに外部からvueのスクリプトを引っ張ってくることになります。
ローカルで完結する拡張機能なのに外部と通信するのは嫌だったので、リソースの読み込みはローカルで完結するようにしました。


ただ１つ注意点があります。

拡張機能を公開する際には、Chrome Storeのダッシュボードからzipファイルをアップロードするのですが、
そこに余計なファイル(node_moduleなど)が含まれているとエラーが出てしまうため、公開に必要なファイルだけをまとめたディレクトリが生成されるよう、webpackの設定を少しいじる必要がある、という点です。

そこまで難しいことではないのですが、初めて開発する方は注意が必要かもしれません。
[LinkMemo](https://chrome.google.com/webstore/detail/link-memo/ldjlfegijibnmliabfelcjpkbklicbhi?hl=ja)では、webpackのoutputを以下のように設定して

```js
  output: {
    path: path.resolve(__dirname, './release/dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  }
```

`release/dist`配下にindex.htmlを配置するようにしています。
こうすれば、releaseディレクトリをzip化するだけで、拡張機能のアップロードが可能となります。

### Chromeの独自APIを叩くためにPromiseでラップした

[LinkMemo](https://chrome.google.com/webstore/detail/link-memo/ldjlfegijibnmliabfelcjpkbklicbhi?hl=ja)で保存しているデータは、ページのタイトルとURLだけで、ブラウザ上で保持しています。
ただ、Chromeの拡張機能でデータを保存する場合、それ専用の[Chrome APIs](https://developer.chrome.com/extensions/api_index)を叩く必要があります。
今回は[storage API](https://developer.chrome.com/extensions/storage)を使用しましたが、少し手間取りました。
データの保存/取得をするためのコード自体は、以下のように単純です。

```js
        chrome.storage.local.set({key: value}, function() {
          console.log('Value is set to ' + value);
        });
      
        chrome.storage.local.get(['key'], function(result) {
          console.log('Value currently is ' + result.key);
        });
```

僕は開発当初、これらのメソッドを通常のSPA開発における外部APIのように使おうとしました。

```js
// 別ファイルに定義
export const fetchMemoList = async () => {
  const result = await chrome.storage.local.get(["memo"], result => {
      return result
    });
  return result;
};

// Vueの中で呼び出す
const result = await storageApi.fetchMemoList();
```

しかしこれでは動きません。
その理由は、ChromeAPIは単純なコールバック関数だからです。
上のようにasync/awaitで使いたい場合は、Promiseを返すようにラップする必要がありました。
修正後のコードは以下のとおりです。

```js
// 別ファイルにPromiseを返すようラップして定義
export const fetchMemoList = async () => {
  return new Promise((resolve, reject) => {
    chrome.storage.local.get(["memo"], result => {
      resolve(result);
    });
  });
};

// Vueの中で呼び出す
const result = await storageApi.fetchMemoList();
```
このようにAPIをラップすれば、async/awaitを使うことが出来ます。
これに気づかずかなりの時間を消費してしまいました・・・。

雰囲気でAPIを使っていたおらが悪かっただ・・・。
まぢリスカしょ・・・。:cry:

## 後悔しました
この拡張機能で最も後悔したのは、
初めは生のJavaScriptで開発をしていた、という点です。

「アイテムの追加/削除をするためのイベント管理とDOM操作」
をしながら、
「Chromeに保存するデータとの整合性を保持し続ける」
には苦しみしかありませんでした。

例えば、アイテムを削除するだけでも

- 削除ボタンのクリック
- クリックされた要素のidを元に、DOMから概要要素を削除
- 残った要素で配列を作り直してChromeに保存
- 削除のクリックと実際の要素削除はidで紐付いているため、新たな配列
を元に、存在するアイテムにidを振り直す

という処理を自前で書く必要がありました。
この点、Vueで記述した場合にはデータをループしてDOMが生成されるように記述をすれば、データとDOMの整合性を意識する必要はなく、非常に楽でした。
生JSは、それにこだわるよっぽどの理由がない限り、なるべく使わないほうが良いですね、やはり・・・。

（自分の設計の問題でもある）


### おわりに
この拡張機能のデザインは、[@prp_papi](https://twitter.com/prp_papi)さんがやってくださいました・・・！
初めは自分で使うためだけにクソダサいデザインで開発したのですが、
いつのまにかリデザインしてくれてた。
超絶感謝です。

拡張機能は比較的気軽に作成/公開ができるのでよいですね。
願わくばマネタイズとかしてみたいです。