開発者ツールを作るシチュエーションはめったに無いと思いますが、そういうシチュエーションになったので書いています。
クローム拡張のアーキテクチャの概要にさらっと触れて、開発者ツールとページ(content script
)の相互通信を確立するところまで。
chrome拡張のアーキテクチャ
普通のクローム拡張では、表示しているページと同じコンテキストで動きDOMにアクセスできるがAPI制限のあるcontent script
と、それとは独立なコンテキストで動作するbackground page
を使いますが、開発者ツールを作る場合はこれにプラスしてdevtools_page
というやつも使います。
これは開発者ツールを開いたときに開始される上の2つとは独立したコンテキストです。
content script
やbackground page
と同じように、相互にメッセージを送り合うことができます。
まずは、devtools_page
を作ります。
devtools_page
manifest.json
に
{
"devtools_page": "devtool.html"
}
と書くと開発者ツール起動時に指定したファイルがロードされてdevtool_page
のコンテキストが生成されます。
そのコンテキストで、
chrome.devtools.panels.create("My Panel",
"icon.png",
"main.html",
function (panel) {
// callback
}
);
とすると、devtoolにMy Panel
というタブが追加されます。
このタブで表示する内容はmain.html
です。
これで開発者ツールにタブを増やせました。
ちなみに、chrome.devtools.panels.elements.createSidebarPane
でElementsパネルの右の方のメニューを追加したりもできるそうです。
content scriptの追加(inject)
ページの情報を取得するために、content script
のコンテキストにスクリプトを送り込む必要があります。
chrome.tabs.executeScript
を使います。
chrome.tabs.executeScript({
code: 'document.body.style.backgroundColor="red"'
});
chrome.tabs.executeScript(null, {file: "content_script.js"});
別のコンテキストから動的にcontent script
にコードを送り込めます。(cross-origin permissionsが必要です)
chrome.tabs.executeScript
は第一引数にタブIDを受け取りますが省略するとアクティブなタブが使われます。普通このまま動きますが 開発者ツールが独立Windowで起動するときは開発者ツール自体がアクティブタブになってしまう ので注意が必要です。
それを防ぐには対象タブを指定します。chrome.devtools
はdevtoolsのコンテキストで使えるAPIです。
chrome.tabs.executeScript(chrome.devtools.inspectedWindow.tabId,{
file: "./dist/content_script.js"
});
メッセージパッシング
ページの状態を監視して逐一devtoolと通信するためにはメッセージパッシングが必要です。
別のコンテキストで動くスクリプト同士が通信するためのものです。
単発の通信は以下のようにします。
// 送信
chrome.runtime.sendMessage({hoge:"hoge"},response=>{
console.log(response.fuga);// "hogehoge"
});
// 受信
chrome.runtime.onMessage.addListener((request, sender, sendResponse)=>{
sendResponse({fuga:request.hoge+request.hoge});
// return true;
});
これでコンテキストをまたいだ通信ができます。
受信側の最後のreturn true
は、sendResponse
を非同期に呼ぶときにないとうまくコールバックできません。(この例は同期処理なので不要)
この通信は、 content script
からbackground page
への一方通行で、逆から開始することはできないので注意が必要です。
逆向きの通信がしたいときは、chrome.tabs.sendMessage
を使います。
chrome.tabs.sendMessage(tabId,{hogehoge:123},function(){
//callback
})
当然ですがこの場合はタブIDの指定が必須です。
コネクション
何度も通信を繰り返すならコネクションを張ったほうが便利です。
コネクションをbackground pageで待ち受けます。
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
port.onMessage.addListener(function(){
port.postMessage({hoge:"aaaa"})
})
})
コネクションにcontent script
から接続します。
var port = chrome.runtime.connect({
name: "hogehoge"
});
port.postMessage({hoge:"bbbb"})
port
を介して無限に会話ができます。
この場合も、逆方向の場合は少しAPIが変わり、
let c = chrome.tabs.connect(tabId, {
name: "hogehoge:"+tabId
})
という感じになります。
さて、devtool page
からcontent script
にコネクションを張りたいなら、background page
を経由することになります。
コネクションの実装
以下のようにしてみました。
-
devtool
が開かれたら、content script
をinjectして、そいつにコネクションの待受をさせる。 -
background page
がコネクションの待受をしているようにして、devtool
からコネクションを張る。 -
background page
はdevtool
とのコネクション確立後、content script
にコネクションを張って2つのコネクションを直結する。
エントリーポイントがdevtool
なので、開発者ツールを開かない限り不要なcontent script
が実行されないのがミソです。
var _port;
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
_port = port;
port.onMessage.addListener(function(){
//do domething
})
})
これをdevtool
からページにinjectします。・
chrome.tabs.executeScript(chrome.devtools.inspectedWindow.tabId, {
file: "./content_script.js"
}, () => {
var port = chrome.runtime.connect({
name: `${chrome.devtools.inspectedWindow.tabId}`
});
port.onMessage.addListener(function (message) {
// do something
});
port.postMessage({
hoge:123
});
});
これを待ち受けるのは、
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
let tabId = Number(port.name);
let c = chrome.tabs.connect(tabId, {
name: "hogehoge:"+tabId
})
port.onMessage.addListener((message, port) => {
c.postMessage(message);
});
c.onMessage.addListener((message, port) => {
port.postMessage(message);
})
})
さいごに
- 今回はtabIdをコネクション名に乗せて雑に通知していますがあまりいい使い方ではなさそうです。
- また、コネクションが解除(=開発者ツールが閉じた)ときにコネクションを片付ける処理も本来は必要です。