Chrome拡張機能開発 Content Script備忘録

Chrome拡張機能の開発、Content Scriptの備忘録として、書き方のパターンをいくつか紹介します。

要素の出現を待つ処理

Content Scriptでは要素の出現まで待ってから処理を実行することがよくあります。そんな時に使えるテンプレートです。

N秒ごとの監視で要素の出現を待つ

1秒毎に指定した要素が出現したか確認し、10秒後に待つのをやめます。

const timer = setInterval(injectContent,1000);
setTimeout(() => clearInterval(timer), 10000);
function injectContent() {
const anchor = document.querySelector("#myroot");
if (!anchor) {
return;
}
clearInterval(timer);
// anchorが出現した時の処理を書く
}

DOMの監視で要素の出現を待つ

秒数ではなくDOMの監視によって要素の変化を待ちたい場合はMutationObserverを使います。

async function getAnchor(selector:string): Promise<Element> {
return new Promise( (resolve) => {
let elm = document.querySelector(selector);
if (elm) {
return resolve(elm);
}
const observer = new MutationObserver(() => {
elm = document.querySelector(selector);
if (elm) {
observer.disconnect();
resolve(elm);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
})
} )
}

最初から要素が見つかればすぐに要素を返して終わりです。
見つからない場合、MutationObserverを使ったDOMの監視を開始します。

CSSの影響を拡張機能のUIだけに閉じたい

Content Scriptで挿入するUIは挿入される側のサイトのCSSが適用されます。
サイトのアップデートでスタイルが変わると、拡張機能のUIも当然変わり、視認性が急に落ちることも。

逆に、Content Scriptで拡張機能のUIに適用するスタイルが、挿入される側のデザインを大幅に変えることもあります。

それが目的ならよいですが、そうでない場合はCSSの影響を抑える工夫が必要です。

Shadow DOM

Shadow DOMを使うと、隠れたDOMツリーを取り付けてカプセル化できます。

const container = document.createElement("div");
container.id = "myroot";
const shadow = container.attachShadow({ mode: "open" });

もう少し具体的に、UnoCSS・React・Shadow DOMを使った例を書いておきます。

import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
function main() {
const container = document.createElement("div");
container.id = "myroot";
const shadowRoot = container.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
@unocss-placeholder
</style>
`
const timer = setInterval(injectContent,1000);
setTimeout(() => clearInterval(timer), 10000);
function injectContent() {
const anchor = document.querySelector("#anchor");
if (!anchor) {
return;
}
clearInterval(timer);
anchor.before(container);
const root = createRoot(shadowRoot);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
}
}
main()

とはいえ、完全に分離されているわけではなく、一部のスタイルは通常のDOMから継承されます(参考:HTML Web Components とは何か | grip on minds)。

Tailwindを使うなら

Shadow DOMでTailwind CSSを使うにはかなり工夫が必要で、懸命な判断とはいえません。GitHubでもさまざまなやりとりがあります。

How to use Tailwind with shadow dom? · tailwindlabs/tailwindcss · Discussion #1935

どうしてもTailwindをContent Scriptで使いたい場合は、Shadow DOMは使わずpostcss-prefix-selectorを使いましょう。

postcss-prefix-selectorのインストール後、postcss.config.jsに次のように書くことで、CSSにprefixを付けることができます。

module.exports = {
plugins: {
tailwindcss: {},
'postcss-prefix-selector': { prefix: '#myroot' },
autoprefixer: {},
},
};

セレクタとしては.foo#myroot .fooになるイメージです。この場合、idmyrootの要素より下の要素だけにTailwind CSSを適用できます。

OptionやPopupではTailwind CSSを使い、Content Scriptでは使わない、といった検討もアリです。

ちなみに筆者はTailwind CSSのコンポーネントライブラリdaisyUIを使おうとしましたが、postcss-prefix-selectorと組み合わせるとうまくいきませんでした。

2024年6月17日追記:↓daisyUIをcontent scriptで使うための解説を別の記事に書きました
Chrome拡張機能のcontent scriptでdaisyUIを使う | えいじのサイバー備忘録

Tailwindっぽく書けるUnoCSSのShadow DOMモードを使うのもいいかもしれません。

WXTはどうなの?

Chrome拡張機能のフレームワークWXTを使うのもありです。Content Script UI周りのAPIがいくつか用意されています。

ただ、「要素の出現まで待ってからマウントする」処理を書くのが複雑になったり、筆者の環境(WSL)だとリロードがうまくいかなかったりしたため、まだ実戦投入していません。

2024年6月17日追記:めっちゃいいです!
タグ#Chrome拡張機能にいくつか記事を書きましたので興味があればどうぞ!

↓おすすめの記事
WXTとSvelteでChrome拡張機能の開発 | えいじのサイバー備忘録

React・Svelte・Solid・Vueの例も掲載されていたり、Storage APIも用意されており、とても書きやすいです。


Content ScriptとCSSのライブラリなどを組み合わせて書く時の注意点をまとめました。

Content Scriptはそもそも元のページが主体であるため、差し込むスクリプト側が神経質になるのは本末転倒ですね。ほどほどにしましょう。