📸

稀に便利な、Chrome拡張機能の開発

2021/02/20に公開

本当は使いたくないブラウザ拡張機能

今さらブラウザ拡張機能なんて、と思うわけです。
拡張機能を利用するということは、Webサイトを相手に手動でブラウザ操作しながらローカルベースで作業をしているという状況だと思います。今や何でもWebアプリ的なシステムで解決したいところなので、拡張機能に頼らざるを得ないというのは、望ましい状況ではないと考えています。

しかし、稀に便利です。
今回は業務上の利点があって作成しました。ブラウザでお客様サイトを巡回したときにエビデンスとして画面キャプチャを取得していたのですが、よく忘れるし面倒くさいので自動で取得するようにしたかったのです。
これを実現するは、拡張機能以外では難しいことだと思います。ブラウザ拡張機能も選択肢として知っておく価値があると感じました。

以下では実際に即してChrome拡張機能の開発の概要を示します。具体的なソースコードは提示しないのでご承知おきください。

既製ツールの検証

Windowsのツール

一応、ブラウザ拡張機能の前にほかのツールも確認しておきます。

  • 一般的なデスクトップキャプチャツール
    • 動画キャプチャは容量の問題で採用不可
    • タイマー実行で画像キャプチャができる
    • 当然ブラウザ内のページ遷移に対応したキャプチャはできない
      (だからもともとショートカットキーで手動でキャプチャ実行していた)
  • Windowsに付属の「ステップ記録ツール」
    • クリックした箇所を示すように画面がキャプチャされる優れもの
    • Webサイトの上部が見えずどのページかわからないなどの不便さ
    • 結果レポートから画像だけを取り出すのも面倒くさそう
      (できるかどうか未確認)

プロキシツール等

手動操作するブラウザ以外でレンダリングする方法も検討しました。

  • プロキシツール
    • もともと使っていて、ログに残っているという意味ではそもそもキャプチャ不要
    • 画面状態だけを画像等の何かに変換して保存というのが難しい
    • ブラウザと同じレンダリングができないことが多い
  • Selenium関係
    • 少し検討してみたが、使う方法が思いつかなかった
    • 毎回同じサイトとかではないので、手動で巡回する必要がある

Chrome拡張機能

10種類くらいインストールして試しました。個別の名称は書きません。

  • 画面キャプチャの仕様
    • タブ内を下までスクロールしてキャプチャしてくれる便利さは共通
  • キャプチャ後の自動保存
    • 保存前に編集画面/保存画面が表示される!
    • ごく一部、保存確認を省略できるものもあった
  • 自動キャプチャ
    • ページ遷移を検知して実行してくれるものが皆無!

もともとイメージしていたのがwindow.onloadとかのイベント時にキャプチャ実行してくれるもので、その意味ではページにスクリプトを挿入するツールと合わせて怪しい技術[1]で連携させればればかろうじて実行できたのですが、怪しい技術が怪しすぎて実用したくありませんでした。

がっかりして、拡張機能を自作することにしました。

Chrome拡張機能の基本的なこと

基本的な話として、Chrome拡張機能はJavaScriptで記述され動作します。
JavaScriptをちょこっとパッケージングしているだけなので、既製ツールなんかも調べれば簡単にソースコードを確認することができます。
また、ブラウザ自体を操作するためにスクリプト内でChrome APIを使用することができます。このAPIのリファレンスはChrome Developersに公開されています。
https://developer.chrome.com/docs/extensions/reference/

気になるChrome APIをググれば個人ブログとかStackOverflowとかGitHubとかでサンプルも見つかるでしょう。

というわけで、多少のフロントエンドの知識さえあれば開発は難しくありません。

開発時に作成するもの

manifest.json

これがChrome拡張機能の構成を示す土台のファイルになります。
独自の仕様に従って記述する必要があり、これがかなり重要です。

{
    "manifest_version": 2,
    "name": "ToolName",
    "version": "1.0.0",
    "description": "",
    "permissions": ["tabs", "activeTab", "downloads"],
    "browser_action": {
        "default_icon": "icon.png",
        "default_title": "ToolName",
        "default_popup": "pupup.html"
    },
    "content_scripts": [{
        "run_at": "document_end",
        "matches": ["http://*/*", "https://*/*"],
        "js": ["content.js"]
    }],
    "background": {
        "scripts": ["background.js"],
        "persistent": false
    },
    "options_page": "options.html",
    "icons": {
        "128": "icon.png"
    }
}

permissionsは、Chrome APIを使用する際にどういうアクセス権を行使するかということです。タブに対してキャプチャ取得という操作を行うためには、activeTabを指定しておく必要がある、とかそういったこと。

browser_actionはブラウザのアドレスバー横に表示されるアイコンです。このアイコンをクリックすると、小さい窓でpopup.htmlが展開されます。popup.htmlで実行するスクリプトをpopup.jsの名前で用意するのが一般的で、ここでアイコンクリック時のキャプチャ起動を実装することができます。

content_scriptsはタブで開いたサイトに埋め込まれるスクリプトです。タブの1つ1つにcontent.jsが挿入されます。つまり、ここでwindow.addEventListener()でページロード時のキャプチャ起動を実装することができます。スクロールの制御などもここでします。

backgroundはブラウザで実行されるスクリプトで、background.jsはpupup.jsとともにタブよりも上位の実行権限を持つことになります。基本的にChrome APIを使ったブラウザの操作はcontent.jsではなくbackground.jsに記載する必要があります。

options_pageはオプション設定をするためのページで、設定をLocalStorageに保存しておくとpupup.jsやbackground.jsから読み取ることができます。

JS間の通信

上記のとおり、popup.js、content.js、background.jsという3種類のJSファイルが拡張機能の本体と言えます。
キャプチャ処理の主体はbackground.jsにしました。しかし、スクロールが必要なページではスクロールさせながらキャプチャを取得したので、スクロール操作はタブ内にあるcontent.jsに実行してもらう必要があります。また、アイコンのクリックやページ遷移をトリガーとして処理を起動させるには、background.jsへメッセージを届ける必要があります。
そういった各js間での通信は、Chrome APIのメッセージを使います。

// popup.js
const closePopup = (data) {
    //ポップアップを閉じる
}
window.addEventListener("load", () => {
    chrome.runtime.sendMessage({ name: "start", data: { auto: false } })
})
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    switch (msg.name) {
        case "closePopup":
            sendResponse(closePopup(msg.data))
            break
    }
})
// content.js
const getInfo = (data) => {
    // ページのサイズやURLなどの情報を取得して返す
}
const scrollPage = (data) => {
    // dataで指示されたとおりにページをスクロールする
}
window.addEventListener("load", () => {
    chrome.runtime.sendMessage({ name: "start", data: { auto: true } })
})
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    switch (msg.name) {
        case "getInfo":
            sendResponse(getInfo(msg.data))
            break
        case "scrollPage":
            sendResponse(scrollPage(msg.data))
            break
    }
})
//background.js
// いろいろな処理をこのファイルで書く(省略)
const setupCanvas = () => {
   // キャプチャ画像を合成して1枚にするためキャンバスを用意して処理を続ける
}
const start = (data) => {
    // オプション設定をロードする
    // 実行対象のページか判定したりする
    chrome.tabs.getSelected(null, (tab) => {
        chrome.tabs.sendMessage(tab.id, { name: "getInfo", data: null }, setupCanvas)
    })
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    switch (msg.name) {
        case "start":
            sendResponse(start(msg.data))
            break
    }
})

細かいことはいいのですが、要はsendMessageとonMessage.addListenerでメッセージをやりとりしながら処理を続けていくということです。

キャプチャの実行

background.jsの中でchrome.tabs.captureVisibleTab()というAPIを使用することで、現在タブに表示している画面を取得することが可能です。
既製のツールのようにスクロール分まで含めて1枚の画像にするためには、キャンバス上で合成してやる必要があります。このあたりの話は検索すれば出てきます。ヘッダーやサイドナビへの配慮をしてやるといいです。

付加的な機能の開発

オプション設定

フォームに入力された値をLocalStorageに保存するだけ。キャプチャ対象のドメインを指定したり、ダウンロード時のファイル名を指定したり。

スクロール時の描画遅延対策

スクロール後にキャプチャ取得すると、まだ描画が追いついてなくてちゃんと撮れていなかったりします。そのためスクロール後に0.2秒待ってから~とかの少し怪しい処理になります。

SPAへの対応

zennのようにサイトがSPAで作成されていると、ページ遷移時にwindowのloadイベントがトリガーされません。そのため、history.pushStateによるURLの追加をトリガーにして起動するようにしてやったりしました。

作ってみての感想

意外と簡単で、すぐに動くので楽しかったです。
画像を合成するのとか、パズルみたいで好きな領域です。

拡張機能の開発なんてあまり無いと思いますが、業務上のルーチンワークを効率化するには良い手段になる可能性があると思います。

脚注
  1. ブラウザ内で特定スキームへの遷移からローカルのプログラムを実行するようにレジストリを変更してそのプログラムからショートカットキーを入力するという…… ↩︎

Discussion