🤖

自分の作ったChrome拡張をManifest Version 3に対応させる

2023/01/03に公開

Chrome拡張の manifest version 2が廃止になる

Googleは9月28日(現地時間)、同社の公式ブログにてChromeにおける「Manifest V3」への移行スケジュール詳細を公開した。これまでは、2023年1月に移行が予定されていたが、早くても2023年6月にずれ込み、その後2024年1月には従来のManifest V2が利用できなくなる予定だ。

Manifest version 2 is deprecated, and support will be removed in 2023. See https://developer.chrome.com/blog/mv2-transition/ for more details.

自作Chrome拡張をいくつか作っているのだが、Chrome拡張の設定画面にManifest Versionが古いというエラーが出続けていることに心痛めていたので、年末年始のお休みを利用して一気に全部対応した。

今回はそんなVersion3対応で詰まったところ、ハマったところを紹介していきます。

manifest.json のフォーマット変更

ここはそんなに大変じゃない。
対応が必要なものがあるか、見比べて書き換えるだけ。
https://developer.chrome.com/docs/extensions/mv3/mv3-migration/

  • permissions 内で記述していた対象のパスを個別で host_permissions 内に記述する形に変更になった。
  • browser_action -> action に名前が変わるだけ
  • background キー名が変わった。
    • scriptsservice_worker に変わった。
    • background script は1つのファイルだけしか指定できなくなった。TypeScript & React で書いていたら必然的に使うものをImportして記述するので影響はない

ServiceWorker(background script), ContentScript の役割分担

ここが一番めんどくさかった…。
今まで出来ていたことが出来なくなって、どう対応すればいいのかを対案を考えることに一番時間を費やした。

Manifest version 2 まではChrome拡張設定の画面でエラーがちゃんと表示されていたのに、 version 3 にすると「エラーがちゃんと表示される」場合と「エラーがちゃんと表示されない=画面が真っ白になる」という事象が起きて、デバッグが本当に面倒だった。

「一部jQueryで記述していたものを素のJavaScriptによるDOM操作に書き換えたら動くようになる」など、原因がどこにあるのか特定できない事象が起きたりとさまざまな障害があった。
これは多分後述の「ServiceWorker 内で document 要素を操作できない」が原因なのかもしれない。
極力jQueryは使わない方向で進めていきたかったのでこれは不幸中の幸いだったと思おう。

ServiceWorker 内で document 要素を操作できない

元々、こんな感じで「アクティブなタブのURLとタイトルをクリップボードにコピーする」ショートカット機能をChrome拡張に組み込んでいたのだが、ServiceWorker内 document 要素を操作できなくなったので、対案が必要になった。

参考: 表示中のページのタイトルとURLをキーボードショートカットでクリップボードに保存するChrome拡張を作ってみた - Qiita

  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    const activeTab = tabs[0];
    // 1. 任意のテキストを格納したテキストエリアを作成
    const textArea = document.createElement('textarea');
    textArea.value = `${activeTab.title}\n${activeTab.url}`;
    document.body.appendChild(textArea);

    // 2. 作成したテキストエリアを選択し、クリップボードに保存
    textArea.select();
    document.execCommand('copy');

    // 3. テキストエリアを削除
    document.body.removeChild(textArea);

    alert(
      '現在開いているページのタイトルとURLをクリップボードにコピーしました'
    );
  });
};

で、どう対応したのかと言うと、 ServiceWorker からメッセージパッシング機能を利用して、 ContentScript にメッセージを送って、 ContentScript 内で document 要素を操作するという手順だ。

実際にはこんな感じで対応した。

background.ts
const copy = (tabId: number, text: string) => {
  console.info(text);
  chrome.tabs.sendMessage(tabId, text).catch((reason) => {
    console.error('Error occurred.', reason);
  });
};
content_script.ts
/**
 * BackgroundScriptからメッセージを受け取る
 */
chrome.runtime.onMessage.addListener((request) => {
  // 1. 任意のテキストを格納したテキストエリアを作成
  const textArea = document.createElement('textarea');
  textArea.value = `${request}`;
  document.body.appendChild(textArea);

  // 2. 作成したテキストエリアを選択し、クリップボードに保存
  textArea.select();
  document.execCommand('copy');

  // 3. テキストエリアを削除
  document.body.removeChild(textArea);

  alert('現在開いているページのタイトルとURLをクリップボードにコピーしました');
});

CORS 対策

CORS対策されているサイトが表示された場合に色々頑張って ContentScript内で通信を行っていたのだが、通信が行えなくなり、これも「メッセージパッシング機能を利用して、ContentScriptからメッセージを送り、ServiceWorker内で通信を行った結果をContentScriptに返す」という対応を行った。

chrome.tabs.query が ContentScript 内で使えない

これは割と「そりゃそうか」という話。
ContentScript内でImportしていたファイル内で chrome.tabs.query を利用して「現在アクティブなタブのURLを取得する」とやっていたけど、普通に  location.href を利用するなどして対応。
寧ろ元の処理が冗長だった。

Chrome拡張のポップアップウィンドウでも使いたいから共通化していた」ために外部のファイルに定義して chrome.tabs.query を利用していたので、ContentScriptとポップアップウィンドウのどちらでも使えるように、こちらもメッセージパッシング機能に置き換えた。

メッセージパッシング機能って便利だけど…

なんかだいたい「困ったらメッセージパッシング機能を使う」で対応したんだけど、元のコードよりも読みづらくなったので、改めてリファクタリングはやっていかないといけないなぁと思った。

そしてメッセージパッシング機能の使い方をちゃんと把握していない状態だったので結構無駄なところで詰まった。

ここで見落としていたのが、受け取る側をちゃんと指定すること。
ServiceWorker側からContentScriptにメッセージを送りたい場合は chrome.runtime.sendMessage ではなく、 chrome.tabs.sendMessage で送りたいタブのIDを指定する必要がある。

そうしないと Could not establish connection. Receiving end does not exist. というエラーがコンソールに表示される。
このメッセージが表示された場合は、受け取り側が存在しない・動いていないという状態だと思って良い。

ServiceWorkerからContentScriptにメッセージを送る場合

ServiceWorker
chrome.tabs.sendMessage(targetTab.id, message);
ContentScript
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.info('content_script', 'receive message', message, sender);
});

ContentScriptからServiceWorkerにメッセージを送る場合

ContentScript
chrome.runtime.sendMessage(message);
ServiceWorker
chrome.runtime.onMessage.addListener(
  (message: Message, sender, sendResponse) => {
    console.info(
      'background',
      message,
      sender
    );
});

Discussion