🧰

WXT(Web Extension Toolkit)でShadow DOM のレイヤーを任意要素に重ねる

に公開

概要

今回は WXT を使って最小構成の拡張機能を作成し、Content Script で Shadow DOM のレイヤーを任意要素に重ねるまでを試してみました。インストール時の特定バージョンでのエラーや、オプションの比較なども行なっているので参考になれば嬉しいです。

WXTとは?

https://wxt.dev/

WXTは、Web拡張機能を構築するための最新のオープンソースフレームワークです。Nuxtに触発され、以下の目標を掲げています。

  • 素晴らしいDXを提供する
  • すべての主要ブラウザにファーストクラスのサポートを提供します

特徴

  • サポートブラウザ
    • Chrome, Firefox, Edge, SafariなどのChromiumベースのブラウザ用拡張機能を構築
  • MV2とMV3
    • 同じコードベースで任意のブラウザ用のManifest V2 / V3 を構築
  • HMRサポート
  • ファイルベースのエントリポイント
  • TypeScriptがデフォルト
  • 自動インポート
    • unimport を使用した自動インポートサポート
  • 自動公開

プロジェクト作成

今回は wxt-example という名前でプロジェクトを作成して進めて行きたいと思います。

$ mkdir wxt-example
$ cd wxt-example
$ pnpm --version
10.18.3
$ pnpm init

※ 今回 wxt@0.20.11 のバグに遭遇したので package.json に以下を追加してインストール実施してます。今後バグが解消されれば pnpm i -D wxt で済む話かもしれません (バッドノウハウ参照)

package.json に以下を追加

{
  "devDependencies": {
    "wxt": "0.20.11"
  },
  "pnpm": {
    "overrides": {
      "@wxt-dev/storage": "1.2.0"
    }
  }
}

pnpm install 実施し、entrypoints/background.ts を以下内容で作成します。

export default defineBackground(() => {
  console.log("Hello world!");
});

package.jsonscripts に以下を追加します。

  "scripts": {
    "dev": "wxt",
    "dev:firefox": "wxt -b firefox",
    "build": "wxt build",
    "build:firefox": "wxt build -b firefox",
    "zip": "wxt zip",
    "zip:firefox": "wxt zip -b firefox",
    "postinstall": "wxt prepare"
  },

pnpm dev を実施すると Chrome が起動され拡張機能がインストールされている状態になっていればOKです。

image1.png

※ コンソールログを見る場合は右上の「デベロッパーモード」をONにして「ビューを検証 Service Worker」のリンクを開くと見れます。

簡単なサンプル実装

今回は特定のサイト上でとある要素の上に被せるLayerを追加する様なサンプルを作成してみたいと思います。実装するにあたり以下のExampleを参考にしました。

https://github.com/wxt-dev/examples/tree/main/examples/vue-overlay

Entrypoints

https://wxt.dev/guide/essentials/entrypoints.html#entrypoint-types

WXTでは機能毎に👆のリンク先にある様な複数のエントリポイントが用意されています。今回はこの中の Content Scripts を使用します。Content Scripts では以下のUIを差し込むメソッドが用意されています。

  1. Integrated
    • createIntegratedUi
    • コンテンツが直接差し込まれて、ページのCSSの影響を受けます
  2. Shadow Root
    • createShadowRootUi
    • Shadow DOMとして差し込まれます
  3. IFrame
    • createIframeUi
    • iframeとして差し込まれます

今回は Shadow Root を使ってLayerを差し込むようにしてみたいと思います。

実装

新規に entrypoints/content.ts を以下内容で作成します。

export default defineContentScript({
  matches: ["*://*/*"],
  cssInjectionMode: "ui",

  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      name: "example-overlay",
      position: "inline",
      anchor: ".main",
      onMount(container) {
        const app = document.createElement("p");
        app.textContent = "Hello world!";
        app.style.color = "red";
        container.append(app);
      },
    });
    ui.mount();
  },
});
  • cssInjectionMode
    • 差し込まれた要素のスタイルをコンテンツスクリプトに挿入する方法をカスタマイズします
      • manifest
        • マニフェスト内のコンテンツ スクリプトのcss配列の下に CSS を含めます
      • manual
        • マニフェストからCSSを除外します。ページに手動で読み込む必要があります
      • ui
        • マニフェストからCSSを除外します。CSSは、呼び出し時にUIに自動的に追加されます
  • position
    • 以下の3種類の中から設定します
      • inline
      • overlay
      • modal
  • anchor
    • CSSセレクタ、XPath式、要素、またはこれら3つのいずれかを返す関数を渡す
    • ページ内のUI追加位置を決定する

今回は https://wxt.dev/ ページの .main 要素に対して赤字で Hello world! の文字を position を変えて実行してみました。

  • inline
    image2.png

    • 差し込まれたShadow DOM
      image3.png
  • overlay
    image4.png

    • 差し込まれたShadow DOM
      image5.png
  • modal
    image6.png

    • 差し込まれたShadow DOM
      image7.png

差し込まれたShadow DOM を比較してみるとpositionの違いが一目瞭然ですね!

Layer追加

次に .main 要素の上に背景色がついたLayerを重ねて表示してみたいと思います。先ほどの entrypoints/content.ts を以下に修正します。

export default defineContentScript({
  matches: ["*://*/*"],
  cssInjectionMode: "ui",

  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      name: "example-overlay",
      position: "inline",
      anchor: ".main",
      append: "last",
      css: `
        :host{
          display: block !important;
          position: absolute !important;
          inset: 0 !important;
        }
        html, body { width: 100%; height: 100%; margin: 0; }
        #layer { width: 100%; height: 100%; background: red; opacity: 0.5; }
      `,
      onMount(container) {
        const root = document.createElement("div");
        root.id = "layer";
        container.append(root);

        // もし .mainが position:relative でない場合は付与
        const main = document.querySelector(".main");
        if (main) {
          (main as HTMLElement).style.position = "relative";
        }
      },
    });
    ui.mount();
  },
});

👆ではoptionのcssで差し込まれた要素を、親要素のサイズ一杯に広げて表示するようにしてます。この時に親要素が position: relative である必要があります。あとは id: layer の div 要素のスタイルも設定してます。

実行すると👇のように .main 要素の上にLayerが被さって表示されている見た目になります。

image8.png

Loadingなどで遅れて表示されるパターン

次にとある要素がLoadingなどで遅れて表示されるパターンの場合を試してみたいと思います。

分かりづらいかもですが、👇zennの筆者の記事一覧の全Cardの上に同じく赤い半透明のLayerを被せる様にしてみたいと思います。

image9.gif

👆は既に先ほどの実装で全CardにLayerを被せるようにしているのですが、Loadingが挟むので anchor で指定した要素が見つからずうまくいきません。

この対応はそんな難しい事はなく ui.mount(); の部分で autoMount を使うようにしてあげます。名前の通りアンカーが動的に追加/削除される際に、UIを自動的にマウントおよび削除してくれます。

export default defineContentScript({
  // ...
  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      // ...
    });
    ui.autoMount(); // ← mount() ではなく autoMount() を呼ぶ
  },
});

👇実行結果

image10.gif

まとめ

今回はたまたまインストール時のエラーでハマりましたが、それ以外は 素晴らしいDXを提供する と謳っているだけありサクッと開発に着手できる体験は良かったです。また今後別の機能も色々試してみたいと思います。

バッドノウハウ

pnpm i -D wx 時に以下のエラーが発生する

 ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In : "@wxt-dev/browser@workspace:^" is in the dependencies but no package named "@wxt-dev/browser" is present in the workspace
 This error happened while installing the dependencies of wxt@0.20.11
 at @wxt-dev/storage@1.2.5

https://github.com/wxt-dev/wxt/issues/1933

👆のIssueによると wxt@0.20.11 が依存している @wxt-dev/storage@1.2.5 が原因でエラーが起きているよう。wxtのバージョンを下げるか、本記事でもやっている様に @wxt-dev/storage1.2.0 でoverrideする。

参考URL

https://zenn.dev/daichan132/articles/0a2889173e1797

https://zenn.dev/cybozu_frontend/articles/introduction-browser-extensions-tools

https://eiji.page/blog/crx-wxt-content-anchor/

Discussion