an odd fellow

仕事のメモ

ReactでChrome Extensionを作る

エロゲー批評空間の推薦システムを作っている。推薦アイテムをFirestoreから取ってきて推薦枠を表示するChrome Extensionになる予定。この土日にChrome Extensionだけ出来上がったので、調べたことをまとめておく。

Chrome Extensionの名前はdreamyfishにした。素晴らしき日々という作品の中で主人公がエリック・サティの夢見る魚を引いていたのを思い出したから。

GitHub - roronya/dreamyfish-fe

Chrome Extensionの種類

大きく分けて2つある。

  • Brouser Action
    • 開いているページに関わらず動かすExtension
  • Page Action
    • 開いているページに特化したExtension

Chrome Extensionのアーキテクチャ

公式のOverviewのアーキテクチャの箇所が分かりやすかった。

ref: Overview - Google Chrome

ExtensionはJavascriptが動く箇所が3つあり、それぞれで「この場所はこういうことができますよ」というのが決まっている。そして、これらの3つのJSはMessageを送り合うことでデータを受け渡しできる。

それぞれの特性は以下のようになっている。

  • popup
    • URLの右側にアイコンが表示されていてクリックすると動くHTMLとそこで動くJS
    • Extension独自のUIを作るときに使う
  • content script
    • ブラウザで表示しているページに埋め込まれて動くJS
    • 今表示しているページが持っているContextを引き継いで動く
      • どういうことかというと、例えばcontent scriptでAjax通信をすると、そのページのドメインがOriginとして使われる(サーバーでallow originしてなければ通信できないということ)
      • どこかのAPIを叩いてそのページに埋め込む、みたいなExtensionを作るときはbackground
      • chrome APIが一部たたけないなど制約が多い
  • background
    • UIを持たずに裏側で動かすJS
    • content scriptのように、ページのContextは持たないので自由な処理ができる
    • chrome APIがフルで叩ける
    • どこかのAPIを叩いてcontent scriptやpopupにデータを渡すのに使うことが多いかな?と思った

公式のOverviewにあるように、backgroundでAPIを叩き、Messageでデータは受け渡し、content scriptやpopupはUIに専念する、という責務分割になっているようだ。

create-react-appでChrome Extensionは作れる?

popupであれば簡単に作れる。デフォルトで生成されるmanifest.jsonを書き換えればすぐにできるようだ。

ref: create-react-app で TypeScript × React での Chrome Extension 開発を始める - Qiita

ref: create-react-appはなぜmanifest.jsonを作るの? - an odd fellow

しかし、content scriptやbackgroundで動かしたい場合はejectしてwebpackの設定を弄る必要があったので諦めた。create-react-appは npm run build すると index.html をエントリーポイントに build ディレクトリ配下にファイルを吐くので、popupの場合は都合が良い。しかし、content scriptやbackgroundはHTMLは持たない生のJSとして出力される必要があり、そういう設定はwebpackの設定を直接いじらなければならない。

webpackでChrome Extensionを作る

create-react-appに頼りきっていたのでwebpackから逃げていたが、良い機会だったので本腰を入れて勉強した。詳細は省く。結局webpackを理解せずコピペで進もうとしたら遠回りになることが多かった。

Rails本やJSの本など多数執筆している山田祥寛さんの速習シリーズのwebpackがかなり為になった。2018年の販売だが、十分新しい内容で、これで勉強すれば基礎がわかるのでwebpack4やbabel v7.4なども差分が分かってついていけるようになった。

速習webpack 速習シリーズ

速習webpack 速習シリーズ

webpackのconfigはこのようになった。content scriptとbackgroundを作るのでentryをふたつ用意し、loaderでJSにする必要のないmanifest.jsonをCopyPluginで直接buildディレクトリにコピーしている。

dreamyfish-fe/webpack.config.js at master · roronya/dreamyfish-fe · GitHub

content scriptでFirestoreを叩けなかった

content scriptでFirestoreを叩いたら以下のようなエラーメッセージで動かなかった。

Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=unavailable]: The operation could not be completed This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.

詳細な原因は分からなかった。content scriptはページのContextを引き継ぐので、Content Security Policy周りで引っかかったのだろうか。以下のようなページもあり、Content Security Policyの設定も試したがだめだった。

ref: Google Developers Japan: Chrome 拡張機能で Firebase を使う方法

データ取得はbackgroundでやれということなのだな、と理解した。

backgroundとcontent script間のメッセージのやりとりはLong-lived connectionsで行う

content scriptからbackgroundにメッセージを送り、backgroundはメッセージに即したデータをFirestoreから取得して、content scriptに返答する、という流れをやろうとした。

ref: Message Passing#Simple one-time requests - Google Chrome

この公式にあるSimple one-time requestsで行ったところ、Firestoreのデータ取得を待たずにチャンネルが閉じてしまい通信できなかった。メッセージを返答するまで長時間要する場合はLong-lived connectionsを使えとあったので、使ったらうまくいった。

ref: Message Passing#Long-lived connections - Google Chrome

Reactを埋め込む

埋め込みたい箇所にdivをぶちこんでReactDOM.renderするだけでおk。こんなかんじ。

dreamyfish-fe/content.js at master · roronya/dreamyfish-fe · GitHub