Webに好きにリンクを追加するクロム拡張を作ってみた

# ツールの特徴
- Web上の好きな場所にノートを張り付けられる
- 同じノートを複数の場所に貼り付けることもできる
- ノートを介してWeb上の複数の場所を相互リンクできる

Web上での調べもののまとめや、よく一緒に使う複数の場所をまとめたりに活用できます。
Webのブックマーク・クリッピング・アノテーションツールでしっくりくるのが無かったため、自作してみました。

https://largetownsky.wixsite.com/worth-hole

![chromeShopDescJa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747214/cc4a742f-adfa-ea35-6b47-84599f128131.png)

# 利用技術と参考になった情報源
Webフロントエンドは素人なため、いろんな情報源をあさりながら手探りでの開発でした。今回採用した主な環境・ライブラリと参考資料は以下の通りです。

## TypeScript
最初にJavaScriptで開発しましたが、複数クラスの処理が入り混じると効率が落ちていきました。効率を落とさずに開発を継続できたのは、TypeScriptに切り替えたからが一番大きいです。

TypeScriptの習得で一番参考になったのは、[サバイバルTypeScript](https://typescriptbook.jp/)で、素人にも非常に読みやすかったです。

## JQuery UI
メンテナンスモードのため、長期的な利用を考えると迷いました。が、Webページを見ながら、その上にノートを重ねてメモできるという使用感を、[モードレスダイアログ](https://jqueryui.com/dialog/)で簡単に実現できたため、採用しました。これについては、公式のAPI文書やデモコードが一番参考になりました。

また、[JQueryとの併用に難がある](https://recruit.gmo.jp/engineer/jisedai/blog/react-with-jquery/)ため、今流行りのReactは見送りました。モードレスダイアログ対応ライブラリがあればReactでも良いのですが、調べ方の問題か見つけられませんでした。表示に制約のあるモバイル対応を考えると、モードレスダイアログは筋が悪いという事なのでしょうか。

## IndexedDB
ノートの記録場所は、ローカルとクラウドの二つの選択肢があります。クラウドだと、複数のマシンや人の間で簡単に共有ができるメリットがありますが、

- 一人でも便利に使えないとクラウド以前の問題
- クラウド版のメリットが出てくる規模だと、維持費がばかにならない
- プライバシー・セキュリティや迷惑行為などの対策が追加で必要
- 今回ただでさえ慣れないフロントエンドで不確定要素を増やしたくない

という事情を考慮し、まずはシンプルにローカルで完結するIndexedDBに記録する事にしました。IndexedDBで参考になったのは、[MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/API/IDBDatabase)です。普通に使うとコールバック地獄で、正常系の処理が追いにくくなるため、Promiseで浅くラップして利用しました。例えば癖のあるカーソルの処理だと、こんな感じです。
```IdCursor.ts
export class IdCursor {
  private promise__: Promise<IDBCursor | null>
  constructor(readonly req__: IDBRequest<IDBCursor | null>) {
    this.promise__ = new Promise((resolve, reject) => {
      req__.onerror = ev => reject(ev2Err(ev))
      req__.onsuccess = () => resolve(req__.result)
    })
  }

  // 管理する状態の数を減らすため、カーソルを進めるのと情報の取得をまとめて処理。
  // 本当はｎ個スキップ→要素の取得としたいが、IDBCursorの仕様上最初の要素は必ず取得してしまうため、
  // やむなく要素の取得→ｎ個スキップというまとめ方にした。
  async next__(afterSkip: number = 0): Promise<IDBValidKey | undefined> {
    // カーソル探索終了を待つ
    const cursor = await this.promise__
    if (!cursor) {
      return undefined
    }
    const key = cursor.key
    // 次回探索のためのカーソル設定
    this.promise__ = new Promise((resolve, reject) => {
      this.req__.onerror = ev => reject(new Error(JSON.stringify(ev)))
      this.req__.onsuccess = () => resolve(this.req__.result)
      if (afterSkip > 0) {
        cursor.advance(afterSkip + 1)
      } else {
        cursor.continue()
      }
    })
    return key
  }
}
```

## クロム拡張リリースの参考
通常のJavaScriptと異なる点は、以下のようなサイトが参考になりました。

- [公式](https://developer.chrome.com/docs/extensions/)は良くも悪くも情報量が多いので、まとめ記事などで概要を把握してから役立ちました
- [Chrome Extension の作り方 (その1: 3つの世界)](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)は概要把握に良かったです
- [個人サービスを公開するまでに必ずやるべきこと](https://qiita.com/ikuosaito1989/items/61b285ce200257047cef)は、リリース直前のチェックリストとして、大変参考になりました

## リリース後の感想
Webをさらに便利に使えるツールにまとめられた上、目的があるため技術の理解・習得が早く、フロントエンド技術も毛が生えた位には持って行けました。

一方、改善点もたくさん見えてきたので、徐々に対応していきたいと考えています。例えば

- デザインをもっと洗練したい
- PDFにもノートやリンクを付けたい
- 複数のマシンや人で簡単に共有したい
- ノートをUI表示するためDOMに追記する以上、他のクロム拡張などのスクリプトから見えるのは仕方ない~~と思っていたのか？~~
  - クロム拡張内の[別オリジンページをIFRAME経由](https://ja.javascript.info/cross-window-communication)で読み込めば、他のスクリプトから隔離できる
  - が多少[UI調整が面倒](https://qiita.com/T_sa/items/7d5285b420698ea8ca15)になる
  - 個人的には機密情報を書き込むつもりはないので、今のままでも困らないが…