🪤

Chrome Extension を円滑に作るためのポイント

2021/08/20に公開

Chrome Extension を作った際にいくつかハマったことがあったのでメモします。

https://chrome.google.com/webstore/detail/url-canonicalizer/kifnlgpidelbjfmcekcpfafaakjflgil?hl=ja

ページ上での JavaScript 実行結果を取得する

scripting.executeScript を使うのですが、関数を指定する形式のみしか値が返ってきません。ファイルを指定する形式と同じページに書かれており、制限が明確に書かれていないため紛らわしいので注意が必要です。

NG

次は実行結果が null として返ってきます。

injection.js
function getTitle() {
  return document.title;
}

// returns null
getTitle();

// same result
// (() => getTitle())();
background.js
async function walkDocumentTitle() {
  const tabId = getTabId();
  const injectionResults = await chrome.scripting.executeScript({
    target: { tabId },
    files: ["injection.js"],
  });

  for (const frameResult of injectionResults)
    console.log('Frame Title: ' + frameResult.result);
  }
}

walkDocumentTitle();

OK

次は意図通りに動きます。

background.js
function getTitle() {
  return document.title;
}

async function walkDocumentTitle() {
  // snip

  const injectionResults = await chrome.scripting.executeScript({
    target: { tabId },
    func: getTitle,
  });

  // snip
}

walkDocumentTitle();

ページ上で実行する関数は文脈が欠落する

関数をページへ inject するために、 Google Chrome は関数をシリアライズします。そのため、まわりの文脈と切り離されてしまいます。

ドキュメントに書いてありますが、実際に遭遇すると何故かわからず時間を使ってしまうので突起します。

This function will be serialized, and then deserialized for injection. This means that any bound parameters and execution context will be lost.

https://developer.chrome.com/docs/extensions/reference/scripting/#type-ScriptInjection

NG

次のコードは isHtmlLinkElement が見つからず、エラーとなります。

function isHTMLLinkElement(el) {
  return "href" in el;
}

function getCanonicalUrl() {
  const canonical = document.querySelector('link[rel="canonical"]');
  if (
    canonical === null ||
    !isHTMLLinkElement(canonical) ||
    canonical.href === undefined
  ) {
    throw new Error("canonical URL is unspecified");
  }
  return canonical.href;
}

chrome.scripting.executeScript({
  const tabId = getTabId();
  const injectionResults = await chrome.scripting.executeScript({
    target: { tabId },
    func: getCanonicalUrl,
  });

  // snip
})

OK

いくつか対処法がありますが、一番確実なものは inject したいコードをひとつの関数にまとめてしまう方法です。

function injectedCodes() {
  function isHTMLLinkElement(el) {
    return "href" in el;
  }

  function getCanonicalUrl() {
    const canonical = document.querySelector('link[rel="canonical"]');
    if (
      canonical === null ||
      !isHTMLLinkElement(canonical) ||
      canonical.href === undefined
    ) {
      throw new Error("canonical URL is unspecified");
    }
    return canonical.href;
  }

  return getCanonicalUrl()
}

chrome.scripting.executeScript({
  const tabId = getTabId();
  const injectionResults = await chrome.scripting.executeScript({
    target: { tabId },
    func: injectedCodes,
  });

  // snip
})

isHTMLLinkElement のような簡単な関数を内部で使いたいだけであれば webpack でバンドルする際にインライン化するという方法が使えます。 modeproduction としてバンドルすると、 webpack は最適化をかけてくれます。その際に内部で使われている terser において、簡単な関数はインライン化してくれるようです。

また、ファイルを指定する形式での executeScript 呼び出しが許される状況であれば、同様に webpack を使ってひとつのファイルにバンドルすることでこの制限を回避できます。複数のファイルを出力したい場合は entry に Object を指定する形式が使えます。

host_permission を指定する代わりに activeTab を使えないか検討する

manifest.json に書く permission として <all_urls> は強すぎるので、ユーザーが見ているページ上で動かすだけで良ければ activeTab を指定します。

https://developer.chrome.com/docs/extensions/mv3/manifest/activeTab/

This serves as an alternative for many uses of >all_urls>, but displays no warning message during installation

GitHubで編集を提案

Discussion