エンジニアが終末に備えて技術を磨く日記の15日目。
1日目はこちら。
「エンジニアが終末に備えて技術を磨く日記」カテゴリはこちら。
TypeScriptを勉強したら、何か実際に自分の役に立つものを作りたくなったので、この4日間はChromeの拡張機能を作っていました。
以下、悩んだ部分や詰まった部分の解決法をメモしておきます。
- ReactでChrome拡張機能を作る方法
- Reactでbackground.jsをどう作るか
- executeScriptにおけるコンテキストの損失
- Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'executeScript')
- manifest.jsonで権限を変更した場合の反映方法
- 拡張機能を作ってみた感想
ReactでChrome拡張機能を作る方法
せっかくReactを勉強したばかりなので、Reactを使って拡張機能を作ろうと思いました。
ただ、調べてもまっさらなReactプロジェクトから拡張機能を作る方法はなかなか見つからず、Chrome拡張機能用のReact製ボイラープレートが出てくるばかりでした。
試行錯誤した結果、次のように作業を進めることで、Reactプロジェクトから拡張機能を作れました。
ちなみに最初に書いておくと、素直にボイラープレートを使えばよかったなと思っています。
npx create-react-app sample-app
上記コマンドでReactプロジェクトを作ると、publicディレクトリは以下のようになっているはず。
public/ ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt
これを拡張機能用に、以下のように変更します。
public ├── icons │ └── icon48.png ├── index.html └── manifest.json
manifest.jsonも拡張機能用に、以下のように書き換えます。
{ "name": "hoge name", "version": "1.0.0", "manifest_version": 3, "description": "hoge desc", "icons": { "48": "icons/icon48.png" }, "action": { "default_title": "hoge title", "default_popup": "index.html" } }
これで完成です。あとは、以下のビルドコマンドを叩きます。
npm run build
ビルドされるとbuildディレクトリが生成されるので、「chrome://extensions/」の「パッケージ化されていない拡張機能を読み込む」からbuildディレクトリを選択すると拡張機能として使えます。
Reactでbackground.jsをどう作るか
先ほど「素直にボイラープレートを使えばよかった」と書いた理由がこれです。
Reactでbackground.tsを作ってもbackground.jsにトランスパイルしてもらえないので、自分でコマンドを叩く必要がありました。
僕はpackage.jsonのscripts項目を以下のように修正し、ビルドコマンド内に無理やりトランスパイル処理を埋め込むことで対応しました。
"scripts": { "start": "react-scripts start", "build": "react-scripts build && npm run transpile", "test": "react-scripts test", "eject": "react-scripts eject", "transpile": "tsc background/background.ts && mv background/background.js build/" },
ただ、このやり方は多分あまり綺麗ではないので、最初からボイラープレート使えば良かったなあ、と、この時点で思いました。
executeScriptにおけるコンテキストの損失
任意のタブ内で処理を実行する場合などに executeScript を使いますが、ドキュメント にも書いてある通り、 executeScript でタブ内に関数を埋め込むと、その過程でコンテキストが失われてしまいます。
例えば、以下のコードを書くと、「Uncaught ReferenceError: f is not defined」というエラーが発生します。
async function likeAllTab() { const tabs = await chrome.tabs.query({}); for(let tab of tabs) { let tabId = tab.id; if(!tabId) { continue; } await chrome.scripting.executeScript( { target: { tabId }, func: clickLikeButton, } ); } } function clickLikeHButton() { // タブのlikeボタンを押下 const elm = getLikeButtonElement(); if(elm) { elm.click(); } } function getLikeButtonElement() { // likeボタンの要素を取得 // 省略 }
コンテキストが失われるとは、その関数内で定義された変数・関数や、引数しか利用できないということを意味します。
上記サンプルコードのエラーの原因は、 executeScript で実行する関数 clickLikeButton が、自身の外にある getLikeButtonElement を呼び出している点にあります。
そのため、以下のように書くことで、エラーは解消しました。
async function likeAllTab() { // 省略 } function clickLikeHButton() { // タブのlikeボタンを押下 function getLikeButtonElement() { // likeボタンの要素を取得 // 省略 } const elm = getLikeButtonElement(); if(elm) { elm.click(); } }
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'executeScript')
executeScript を使うには、manifest.jsonのpermissionに scriptingを追加しなくてはなりません。
これを追加していないと、見出しのエラーが発生しました。
manifest.jsonで権限を変更した場合の反映方法
Chromeでパッケージ化されていない拡張機能を読み込んで使う場合、いちいち削除と読み込みをしなくとも、「chrome://extensions/」からリロードボタンを押すことで、ローカルのソースを拡張機能に反映させられます。
しかし、manifest.jsonのpermissionを書き換えた後は、リロードだけでは設定が反映されませんでした。
この場合、一度拡張機能を削除し、もう一度読み込みなおすことでエラーは解決しました。
拡張機能を作ってみた感想
Chromeが便利なAPIを多数提供してくれているおかげで、拡張機能の開発は意外と簡単でした。
簡単でありながら「自分のために自分で作った拡張機能」というのはすごく便利なので、ブラウザ上で行うルーチンワークは全部拡張機能で自動化したいなと思いました。