LoginSignup
9
12

More than 5 years have passed since last update.

chrome開発者ツールを作る

Posted at

開発者ツールを作るシチュエーションはめったに無いと思いますが、そういうシチュエーションになったので書いています。
クローム拡張のアーキテクチャの概要にさらっと触れて、開発者ツールとページ(content script)の相互通信を確立するところまで。

chrome拡張のアーキテクチャ

普通のクローム拡張では、表示しているページと同じコンテキストで動きDOMにアクセスできるがAPI制限のあるcontent scriptと、それとは独立なコンテキストで動作するbackground pageを使いますが、開発者ツールを作る場合はこれにプラスしてdevtools_pageというやつも使います。
これは開発者ツールを開いたときに開始される上の2つとは独立したコンテキストです。
content scriptbackground 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を経由することになります。

コネクションの実装

以下のようにしてみました。

  1. devtoolが開かれたら、content scriptをinjectして、そいつにコネクションの待受をさせる。
  2. background pageがコネクションの待受をしているようにして、devtoolからコネクションを張る。
  3. background pagedevtoolとのコネクション確立後、content scriptにコネクションを張って2つのコネクションを直結する。

エントリーポイントがdevtoolなので、開発者ツールを開かない限り不要なcontent scriptが実行されないのがミソです。

content_script.js
var _port;
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
    _port = port;
    port.onMessage.addListener(function(){
        //do domething
    })
})

これをdevtoolからページにinjectします。・

devtool_page.js
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
    });
});

これを待ち受けるのは、

background.js
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をコネクション名に乗せて雑に通知していますがあまりいい使い方ではなさそうです。
  • また、コネクションが解除(=開発者ツールが閉じた)ときにコネクションを片付ける処理も本来は必要です。
9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12