Chrome拡張機能でSPAにcontent scriptの挿入

Chrome拡張機能でSPA(Single Page Application)のページにUIを挿入するとうまく動かない場合があります。本記事ではその問題と解決方法を解説します。

問題点

SPAの特性上同じページ内でUIが切り替わるため、URLの変更があってもcontent script側だけでは検知できません。
そのため、UIが意図したタイミングで更新されない場合があります。

たとえばYouTubeで動画のページにcontent scriptを挿入したとします。別の動画をクリックしても挿入したUIの再描画は行われません。

解決方法

特定の要素を監視し、変更があれば「URLが変わった」と判断して再描画するのが一番楽です。
具体的にはMutationObserverを使います。

実際の解決例

MutationObserverを使った例を書いてみました。
YouTubeの動画のIDを取得して描画するコンポーネントです。

例はSvelteですが、他のフレームワークでも似たように書けるはずです。

<script lang="ts">
import { onMount } from "svelte";
let videoId = "";
let previousURL = location.href;
function updateVideoInfo() {
const urlParams = new URL(location.href).searchParams;
videoId = urlParams.get("v") ?? "";
}
onMount(() => {
updateVideoInfo();
const observer = new MutationObserver(() => {
const nowURL = location.href;
if (previousURL !== nowURL) {
updateVideoInfo();
}
});
observer.observe(document.getElementsByTagName("title")[0], {
childList: true,
});
return () => observer.disconnect();
});
</script>
<div class="">
<!-- ... -->
</div>

MutationObserverを使って要素の変更を監視し、変更があった場合は前後のURLを比較します。URLが変化していたら再描画するための関数を走らせます。

監視する要素は要注意

observer.observeの第一引数で指定する要素はよく考えて選びましょう。あくまでも「変更」が想定される要素だけを指定します。

たとえば、SPAの切り替えの前後で属性だけが変わったタグについて考えてみましょう。
もしかしたら「フレームワークによって一度削除され、完全に別のノードとして挿入されているタグ」かもしれません。

今回の例ではtitleタグを指定しました。

別の解決方法

別の解決方法もあります。chrome.tabs.onUpdated.addListenerを使ってタブの更新を監視する方法です。

ただし、chrome.tabsのAPIはcontent scriptでは使えないため、background(service worker)に書く必要があります。

さらに、backgroundとcontent scirptを連携する必要があって面倒です。この連携はメッセージのやりとりやスクリプトの挿入など手間がかかるため、おすすめできません。


以上、SPAのサイトにcontent scirptでUIをぶっ込むときの注意点でした。

SPAでMutationObserverを使う方法は自前で要素の監視が必要ですが、その分再描画させる範囲を自由に決められるメリットもありますね。
SPAではない場合、URLが変わったらcontent scriptも全部再描画ですから。