💉

【chrome拡張機能】コンテンツスクリプトについて

2023/10/19に公開

コンテンツスクリプトとは

コンテントスクリプトは表示しているサイトに自分が用意したCSSファイルやJavaScriptファイルを挿入することができる機能です。コンテンツスクリプトを使うことで閲覧中のサイトのDOMを自由に編集することができ、普段利用しているサイトを自分好みにカスタマイズすることができます。あるサイトにこれがあれば便利だなと思う機能があっても、運用会社が実装するまで待つ必要があったり、そもそも運用方針と違えば実装されることなどありません。しかしコンテンツスクリプトを使うことで自由にサイトをカスタマイズすることができるので、サイトの使い勝手がよくなります。例えばコンテントスクリプトを使えば次のようなことができるようになります。

  • ダークモードに対応してないサイトでも背景を暗くする
  • ブロック機能がついてないサイトでも特定のユーザーやコンテンツを非表示にできる
  • Twitterに画像のダウンロードボタンを追加する
  • 動画サイトで16倍速再生ができるようになる

コンテンツスクリプトでできることをまとめるなら

  1. 任意のCSS/JSファイルをサイトに挿入できる
  2. サイト上のDOMと通信したり編集すること
    と言えます。実際、上に書いた例はCSS/JSで実装できる機能ばかりです。
    拡張機能を作る前に、まずは「検証」を使ってブラウザのDOMにアクセスしましょう。JSを使うことでどれだけのことができるのかブラウザの「検証」を使ってコンテンツスクリプトの便利さを実感してみます。「検証」でできることはコンテンツスクリプトと比べて限られています。しかし「検証」でJavaScriptを実行するとリアルタイムでサイト上に反映されますし、「検証」でできることは大体コンテンツスクリプトでできます。このことから拡張機能の開発する際はいきなりコードに書かず、まずは「検証」で動作確認をすることが多いです。
    このサンプルではYoutubeで実験をするので、Youtubeのお好きな動画を開いてみてください。特にこだわりがなければを開いてみてください。

動画を開いた画面上で右クリックをし「検証」メニューをクリックします。次に、出てきた検証ウィンドウの「Console」もしくは「コンソール」と書かれたタブをクリックしてください。するとコンソールパネルが開かれます。コンソールパネルではJavaScriptが実行でき、表示しているサイトのDOMにアクセスすることもできます。まずは以下のコード貼り付け、コンソールの一番下のエディタに貼り付け、Enterを押してください。すると動画が16倍再生されることはわかります。

document.querySelector('video').playbackRate = 16;

ここでやっていることは単にDOMの編集です。video要素のplaybackRateプロパティの値を16に変更したとも言えます。動画の再生速度を変えるのは運営しかできないことのように思えますが、標準で提供されている機能の1つなので<video>タグを使っているサイトなら使える機能です。次は低評価ボタンを非表示にしてみましょう。以下のコードを入力し、実行してください。

document.getElementById("segmented-dislike-button").style.display = "none"

すると低評価ボタンが消えました。CSSでDOMの表示形式を設定するdisplayというプロパティの値をnoneに変更したので、低評価ボタンを作るDOMが非表示になりました。やはりここでもDOMを編集しただけということに注意してください。コンテンツスクリプトはサイト上のDOMを編集したり通信する機能だからです。またコンソールパネルではJSしか実行できないので、CSSの値を編集するためにJS経由で行いました。次はDOMを追加してみましょう。Youtubeのタイトルに別の文字を追加してみます。以下のコードを入力し、実行してみてください。

let area = document.getElementsByClassName("style-scope ytd-watch-metadata");
let h1 = document.createElement('h1');
h1.innerText = "Hello Youtube";
area[1].appendChild(h1);

するとタイトル画面に「Hello Youtube」という文字列が追加されました。areaという

コンテンツスクリプトは
コンテントスクリプトを適用するサイト、挿入したいCSS /JSファイル、挿入したいタイミングなどは全てmanifest.jsonで定義します。

で定義した指定のサイトやパターンマッチングで適用サイトを選択し、``

コンテントスクリプトが実行するタイミングを変えたい

コンテンツスクリプトを書いたファイルにJavaScriptのwindow.onloadや jQueryのready()といった、DOMの読み込みが完了したイベントを検知するコードを書いてないことに気づきましたか?普通、DOMを編集したいときは上記のようなイベントを検知したあと実行したい処理を書くことが多いと思います。なぜこれらなしでも動くのかというと、window.loadが発火した後にコンテンツスクリプトが実行されるからです。このようにコンテンツスクリプトが実行されるタイミングはmanifest.jsonファイルのcontent_scripts内のrun_atキーで変更することができます。

コンテントスクリプトが起動するタイミングは以下の3つがあります。

状態 説明 対応するJSのイベント
document_start DOMツリー構築中。DOMを組み立てている状態なので画面には何も表示されていない状態。document.bodydocument.headも空。 不明
document_end DOMツリーの構築完了。DOMへのアクセスができるようになる。しかし画像、CSS、JSなどの外部リソースはまだ読み込み始めてない。 DOMContentLoaded発火後
document_idle(デフォルト) 画像、CSS、JavaScriptなどの外部リソースの読み込み完了。生成される静的なDOMが完成されたタイミング。 load発火後

Chromeソース
対応するJSのイベント。MDNソース

JavaScriptのイベントについて理解を深めるためにDOMContentLoadedloadについて細く説明をします。URLをクリックし、
特別なことがなければデフォルト値の document_idleで十分なのでrun_atは書かなくていいです。DOMを編集する目的でコンテンツスクリプトが使いたいなら、HTMLで書かれた要素だけでなくJSで動的に生成されるDOMにもアクセスできるように、ページが完成してからJS/CSSを挿入するdocument_idleが確実でしょう。一方、処理に時間がかかるJSを挿入したいときはdocument_endを使うといいでしょう。document_idleを使うタイミングはわからない。

https://takuya-1st.hatenablog.jp/entry/2014/11/04/001449

https://stackoverflow.com/questions/43233115/chrome-content-scripts-arent-working-domcontentloaded-listener-does-not-execut

さらに遅く読み込む

コンテンツスクリプトを挿入するタイミングは3つあることを説明し、基本的にはdocument_idleイベントを使えば十分だと説明しました。しかしもっと遅いタイミングでJS/CSSを読み込みたいタイミングがあります。特にReactやVueなど遅延ロードでページを構築するモダンなサイトにコンテンツスクリプトを使いたい場合だとdocument_idle時でもまだアクセスしたい要素が生成されてないことがほとんどでsy。YoutubeやTwitterなど読み込む情報の多いサイトはまず画面の大枠のDOMを生成し、ページの読み込みが完了した(loadが発火されたタイミング)後でも、JSで動的に画面を構築します。これはYoutubeを使うとわかりやすいです。

このようにdocument_idleよりも遅いタイミングでJS/CSSを挿入したい。そのためにはどうすればいいだろう。選択肢は2つある。

  1. setTimerで遅らせて処理を実行する
  2. MutationObserver でDOMの変化を監視する

しかし1の方法が無難だ。なぜなら入れ物のDOMすら生成されていない場合がある。MutationObserverで監視する要素すら存在しないからどうしようもない。
例えばスクレイピングの本でも要素が出てくるまで1秒待つ sleep(1000)という処理をよくする。ずぼらに見えるが確実な方法だと思う。一方、一度限りの読み込みではなく、ページが生成された後でも不定期にDOMが変更される場合がある。例えばTwitterのように無限スクロールが使われてるサイトでは、ページの一番下までスクロールしたら新たにコンテンツが表示される。YoutubeやTwitchのようなライブ配信サイトでは、コメント欄に大量のコメントが流れてくる。このような場合はMutationObserver を使うのがいいだろう。入れ物のDOMがすでに生成されて、その中にのみコンテンツが追加されるからだ。他にも、不定期で更新されるコンテンツに対して、setIntervalのような定期的に検知する関数を使うとパフォーマンスが悪くなったり、コンテンツが現れるタイミングとそれを更新するタイミングがずれてユーザー体験が悪くなる。

こんな感じのが参考になるかも。setTimerが無難かな
https://zenn.dev/doma_itachi/articles/0289cc5b40bace

https://qiita.com/3mc/items/c3c580ca5de4a2d3990d

https://stackoverflow.com/questions/67259787/chrome-extension-i-want-the-content-script-injected-after-the-web-page-is-fully

runat

https://stackoverflow.com/questions/38771140/is-there-an-equivalent-to-run-at-start-in-a-background-page

公式ドキュメント

https://developer.chrome.com/docs/extensions/mv3/content_scripts/

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts

Discussion