Chrome拡張機能のcontent scriptsでcustomElements.define()を使ったらUncaught TypeError: Cannot read properties of null (reading 'define')と出て困った。どうやら、Chromeのcontent scriptsの中ではカスタム要素が使えないらしい(他のブラウザは試していないので知らない)。windowオブジェクトのプロパティにcustomElementsプロパティはあるが、中身がnullであった。

ポリフィルを読み込んで解決

Chromeのcontent scritps内でカスタム要素を使う公式の解決方法はない。しかし、ポリフィル(サポート外の機能を埋めるためのコード)を使うことで同じ機能を実現できる。

Web Componentsの公式サイトにポリフィルの案内ページがあり、そこからGitHubリポジトリにたどり着ける。これを読むと、webcomponents-bundle.jsを読み込めば解決しそうである。

ただ、このファイルにはカスタム要素以外のポリフィルも含まれているため、サイズが大きい(135KB)。Web Componentsのサポート状況に応じて読み込むモジュールを最適化するために、ローダの利用も推奨されている。

content scritpsで使えないのはなぜかカスタム要素だけであるので、カスタム要素のポリフィルをピンポイントでインストールする形で解決した。ちなみにサイズは19KBである。

bash
npm install -D @webcomponents/custom-elements

あとは、customElements.define()を実行する前にポリフィルのモジュールを読み込むだけで良い。

js
import "@webcomponents/custom-elements/custom-elements.min.js";

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: "open" });
    const paragraph = document.createElement("p");
    paragraph.innerHTML = "Hello, webpages!";
    shadowRoot.appendChild(paragraph);
  }
}

window.customElements.define("my-custom-element", MyCustomElement);

const myCustomElement = document.createElement("my-custom-element");
document.body.appendChild(myCustomElement);

以上でおしまいである。

なお、バンドルツールを使っていないときは、custom-elements.min.jsの中身を丸っとコピペするか、拡張機能のパッケージ内にポリフィルのファイルをコピペし、以下のようにmanifest.jsoncontent_scripts.js配列にファイルへのパスを追加すれば同じ結果が得られる。

json
{
  "manifest_version": 3,
  //...略
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["custom-elements.min.js", "contentScript.js"]
    }
  ]
}

なぜカスタム要素だけ使えないのか

考えても調べても、確かなところはよくわからなかった。セキュリティ上のなんらかの問題があるのか、イタズラにカスタム要素が追加されることで、元のサイトの表示を崩したくないのか。それともブラウザの実装上、外さざるを得ない事情があるのだろうか。

わかったら追記したい。

参考サイト