はじめに
本記事では表題の通りの Chrome 拡張機能を作った解説を行います。
対象者は以下の全てに当てはまる人です。
- どうしても p5.js Web Editor で編集したコードをローカルに送付する必要がある
- でも JavaScript を毎回貼り付けるのは面倒
別にスケッチを作るたびに 適当なJavaScript をページにコピペすればいいので、その手間すら惜しい(私のような)人向けです。
なお、「CodeMirror(5系)が含まれるページ」 や 「クライアントサイドレンダリングのページ」 や 「別オリジンでのiframeが含まれるページ」 での拡張機能の書き方についての言及もあるので合わせてお読みください。
コード
背景
もともと Gemini + A.I.VOICE を使ったAIチャットボットを作っていました。
構成はおよそ以下みたいな感じです。
p5.js Web Editor(以下、エディタ) で作業するときの通話に使いたいと思い、コードを共有しようと考えました。
A.I.VOICE Editor APIの都合上、ローカルに送付する必要があるので、拡張機能を実装するのが必要なのでした。
そして
今回の拡張機能で重要になってくるポイントは3点です。
まず
- 「ローカルへのデータ送信方法」
それから「はじめに」でも述べた、
- 「CodeMirrorが含まれるページ」
- 「クライアントサイドレンダリングのページ」
- 「別オリジンでのiframeが含まれるページ」
です。
ローカルへのデータ送信方法
まぁデータ送信については標準機能ですからそんなに難しいことはないです。
およそHTTP通信かWebSocket 通信が考えられますが、今回は通信量が多いことから WebSocketとしました。
// 受け取る側 (ローカルで動いているプログラムの方)
const wss = new WebSocket.Server({
port: 8080
});
wss.on("connection", (ws) => {
console.log("クライアントが接続");
ws.on("message", async (msg, isBinary) => {
// ...
}
});
// content.js (拡張機能の方)
const WS_URL = "ws://localhost:8080";
let ws = new WebSocket(WS_URL);
// ...
ws.send(JSON.stringify(
{/* ... */}
));
CodeMirror(5系) が含まれるページ
Code Mirror とは、HTML上にプログラムコード編集用の要素を設置するためのJavaScript ライブラリです。現在はバージョン6系が最新なようですが、p5.js WebEditor では5系を使っています。
5系では、CodeMirror
という名前の変数がJavaScript上で格納され、それにアクセスする形でコードを取得します。
TypeScript で言うなら...
const code:string = document.querySelector(".CodeMirror").CodeMirror.getValue();
です。
発生問題
さて、拡張機能でCodeMirror
にアクセスする場合の問題として、以下があります。
「このCodeMirror
変数は、拡張機能のJavaScirptからはアクセスできない」
どういうことかというと、拡張機能のJavaScriptはページのJavaScriptと分離された環境で実行されるので、変数にアクセスができないのです。
以下のコードはエラーとなります。
// CodeMirror が undefined なのでエラーとなる
const code:string = document.querySelector(".CodeMirror").CodeMirror.getValue();
解決
これを解決するためには、JavaScriptをページ上に注入 してやる必要があります。
// inject the page script
const script = document.createElement("script");
script.src = chrome.runtime.getURL("injected.js");
document.getElementsByTagName("body")[0].appendChild(script);
// inject.js からはCodeMirror変数にアクセス可能
ついでに manifest.json
の設定を少しいじるのも必要です。
{
// ......
// "injected.js" を登録する必要がある
"content_scripts": [{
"matches": ["https://editor.p5js.org/*"],
"js": ["content.js", "injected.js"],
"run_at": "document_idle"
}]
// ......
// "injected.js" を "content.js" から見えるようにする
"web_accessible_resources": [{
"resources": ["injected.js"],
"matches": ["https://editor.p5js.org/*"]
}],
// ......
}
Scripting API を使うことも可能そうですが、現在content.js
側から直接現在TabのIdを得ることはできない ことから、処理が煩雑になるゆえ、この方が楽でしょう。
クライアントサイドレンダリングのページ
発生問題
p5.js Web Editorのindexのソースを見てもらうと分かるように、p5.js は React で書かれています。
そのため、通常のcontent_scripts
が走るタイミングでは、中身がありません。
// この時点では、まだReact描画中
const cm = document.querySelector(".CodeMirror");
// false
console.log(cm && cm.CodeMirror);
解決
これを解決するためには、MutationObserver を使って、完全にデータがセットされるまで待ちます。
const observer = new MutationObserver(() => {
const cmEl = document.querySelector(".CodeMirror");
if (cmEl && cmEl.CodeMirror) {
observer.disconnect();
// ...cmEl.CodeMirror を使った処理...
}
});
observer.observe(document.body, { childList: true, subtree: true });
別オリジンiframeへのアクセス
p5.js WebEditor では、CodeMirror の実行結果を<iframe>
に投影します。
ここに投影されるCanvas の表示もローカルに送りたいところです。
発生問題
実は、この <iframe>
の src は https://editor.p5js.org/
ではなく、https://preview.p5js.org
です。
この場合、この<iframe>
の中身は外から参照できません。
どうやら SOP (Same Origin Policy)というものに引っかかるようです。(完全に調べ切れてはいません)。
解決
これを解決するためには、manifest.json
を編集することで、https://preview.p5js.org
ページ上で直接JavaScriptを実行します。
{
// ......
"content_scripts": [
{
"matches": ["https://editor.p5js.org/*"],
"js": ["content.js", "injected.js"],
"run_at": "document_idle"
},
// 追加
{
"matches": ["https://preview.p5js.org/*"],
"js": ["content_iframe.js"],
"run_at": "document_idle",
// これがないとiframeを見てくれない
"all_frames": true
}
],
// ......
}
// キャンバスを取得
// あ、そうそう。なぜかiframeは二段重ねになっています。
// だけど、内側のiframeは同一オリジンなのでアクセス可能です。
let canvas =
window.document.body.querySelector('iframe')
.contentDocument.body.querySelector('canvas');
canvas.toBlob(blob => {
// Canvasの描画内容 (blob) を取得
});
おわりに
以上が、本拡張機能を実装するのにあたって山となった部分です。
他の詳しい実装は GitHubをご覧ください。
もっと良い実装方法があればぜひコメントなどで教えてください。
余談
Canvasの画像をGeminiに送ったら、目に見えて応答が悪くなった。
トークン数の減りも早くなるしやめようかな。