20
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Chrome拡張機能の開発Tips (2019)

Last updated at Posted at 2019-11-02

この記事ではChrome拡張機能を書くときに得られた知見をまとめておく。拡張機能を書くにあたっての基本的な知識は持っていることを前提とする。

なお、今回の記事の元となった拡張機能はChromeウェブストアから入手可能で、ソースコードも公開しているので、適宜参照していただきたい。

開発環境の準備

parcelparcel-plugin-web-extensionを使うことで、簡単に拡張機能の開発環境を構築することができる。また、開発時にブラウザで拡張機能を再読込みするためにChrome拡張機能のExtensions Reloaderが便利。

parcelのインストールは次のコマンドを叩く。

yarn -D parcel-bundler parcel-plugin-web-extension

例えばTypeScriptとStylusを使う場合、manifest.jsonを次のように書いておくだけでparcelがJSとCSSにビルドしてくれる。

"content_scripts": [
  {
    "matches": ["*://*/*"],
    "js": ["scripts/content.ts"],
    "css": ["styles/content.styl"]
  }
]

ビルド時には次のようにmanifest.jsonの場所を指定する。

yarn parcel build src/manifest.json

また次のようなコマンドで、コードの変更を監視して自動でビルドできる。

yarn parcel watch --no-hmr src/manifest.json

バンドルサイズを小さくする

ビルドされるスクリプトのサイズはなるべく抑えたいもの。とくに、各ページに読み込まれるContent Scriptでは、無闇に多くのリソースを消費してしまうことにもなりかねない。

そこで活用できるのがTree Shakingや Scope Hoistingといったバンドラの機能。これらを用いることで、バンドルサイズを小さくして実行速度も向上させることができる。

parcelでは次のようにオプションを付けることで、これらの機能を有効にしたビルドができる。

yarn parcel build --experimental-scope-hoisting src/manifest.json

またTypeScriptを用いている場合、tsconfig.jsonでモジュール関連の設定を適切にしておく必要もある。次のようにES Modulesを有効にしておく。

"target": "es2016",
"module": "es6",
"lib": ["dom", "esnext"],
"moduleResolution": "node"

ただし、外部ライブラリがES Moduleでエクスポートしてないとサイズ削減できなかったりもするので注意。

Content Scriptから可能な限り早いタイミングでdocument.bodyを操作する

ブラウザが開いているページに拡張機能が何か表示したい場合などは、Content Scriptからdocument.bodyを操作することが多いと思う。本稿を書く元となった拡張機能でも、タブ一覧をポップアップで画面内に出すために、document.bodyにDOM要素を追加している。

その際に使い心地を良くするため重要だと感じたのが、document.bodyに要素を追加するタイミングだ。document.bodyを操作する場合、次のようにmanifest.jsonでrun_atdocument_endとすることで、DOMの読み込みを待ってContent Srciptを走らせるのが一般的な方法とされる。

"content_scripts": [
  {
    "matches": ["*://*/*"],
    "js": ["scripts/content.ts"],
    "run_at": "document_end"
  }
]

この方法の問題は、サイトによってはDOMの読み込みまで数秒かかり、それまでContent Scriptが実行されないという点だ。この問題を解決するには、run_atdocument_startにすればよい。しかしそうすると、Content Scriptが走りだしたタイミングではdocument.bodynullのことがあるという新たな問題が生じる。そこで、すぐにContent Scriptを読み込み、Mutation Observerを用いることで、document.bodyが非nullになった直後からDOMを操作することができるのだ。

manifest.jsonではrun_atdocument_startとして、

"content_scripts": [
  {
    "matches": ["*://*/*"],
    "js": ["scripts/content.ts"],
    "run_at": "document_start"
  }
]

Content Scriptの読込み直後にMutationObserverでDOMの変更を監視し、document.bodyが非nullになった直後にアクセスする。

const insertApp = () => {
  // manipulate document.body!
};

if (document.body != null) {
  insertApp();
} else {
  const observer = new MutationObserver(() => {
    if (document.body != null) {
      observer.disconnect();
      insertApp();
    }
  });

  observer.observe(document, {
    attributes: false,
    attributeOldValue: false,
    characterData: false,
    characterDataOldValue: false,
    childList: true,
    subtree: true
  });

  document.addEventListener(
    "readystatechange",
    event => {
      switch (event.target.readyState) {
        case "interactive":
        case "complete":
          insertApp();
      }
    },
    { capture: true }
  );
}

拡張機能をWebサイトのCSSから分離する

Content ScriptでDOMに追加した要素には、Webサイトに読み込まれているCSSのスタイルが適用される。これはDOMに要素を追加している以上は当然なのだが、拡張機能の独自のUIを表示したい場合には、WebサイトのCSSが影響して開発者の意図とは違う見た目になってしまうことがあり問題となる。

Shadow DOM

Content Scriptが追加する要素がWebサイトのCSSの影響を受けないようにするために活用できるのがShadow DOMだ。Shadow DOMにより通常のDOMツリーからは隠され分離されたDOMツリーを作ることができ、Content Scriptが追加する要素はShadow rootの下に追加していく。

const element = document.createElement("div");
element.attachShadow({ mode: "open" });
element.id = "myawesomeapp";
document.body.prepend(element);
// Append DOM elements to element.shadowRoot

Shadow DOMとして追加した要素に拡張機能のCSSを適用させるには、二通りの方法がある。

方法1: style要素に書く

ひとつはShadow rootの下にstyle要素を追加して、そこにスタイルを書く方法である。本稿を書く元となった拡張機能ではこの方法を使っている。ただし、Stylusで書きたかったので、ビルドされたCSSをBackground Script経由で読み込んでstyle要素に書き出している。もしくは、Background Scriptを経由せずとも、manifest.jsonでweb_accessible_resourcesを指定することでContent ScriptからCSSを直接読むことができる。

方法2: CSS Shadow Parts

もうひとつの方法は、CSS Shadow Partsが定義する::part()疑似要素を用いてShadow DOMの要素にスタイルを適用する方法だ。~~この機能はFireFoxではまだデフォルトで有効になっていないので、拡張機能をFireFoxのアドオンとしても提供したい場合は注意が必要だろう。~~また、スタイルを適用したい各DOM要素にpart属性をつけないといけないのが個人的には少し手間だと感じた。さらに、Shadow partを適用するShadow DOMはWeb Componentsとして提供されていなければならないが、Chromeは普通は拡張機能からWeb Componentsを作れないので、polyfillを用いて何とか作れるようにする必要もある。

all: initialで親要素からも分離

拡張機能のShadow rootなどに適用するスタイルでall: initialを指定しておくのも忘れてはならない。というのも、Shadow DOMはサイトのCSSからは隔離されるが、Shadow rootの親のスタイルの影響は受けるからだ。all: initialを指定することで、その影響を消すことができる。

拡張機能のAPIでPromiseを使う

chrome.*で提供される拡張機能のAPIでは、Promiseを返すメソッドは用意されておらず、非同期な処理はコールバック関数で書くことになる。asyncawaitを用いた書きかたでAPIを使いたければ、webextension-polyfill、TypeScriptで書いている場合はwebextension-polyfill-tsを利用するとよい。

Content ScriptでReactを使う

Reactはとても有用なので、Content Scriptでも使いたいことがあると思う。その際に、reactのかわりに軽量な互換ライブラリのpreactを使うことで、フットプリントを小さくすることができる。preactはHooksなど最近の機能も実装しており、Reactを使って書かれたコードをそのまま動かすことができる。

preactをTypeScriptと使う場合、tsconfig.jsonには次の設定を加える。詳しくは本家のドキュメントも参照してほしい。

"jsx": "react",
"jsxFactory": "h"

参考URLs

Bundler

Mutation Observer

CSS Isolation

Others

20
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?