chrome.actionの使い方

2021年10月4日

公式のgithubのサンプルプロジェクトをみていきます。

chrome.actionについて

Chrome action APIは、chromeのバージョン88以上で、 Manifestのバージョンは3以上で使用できます。

chromeのツールバーにあるアイコンの挙動を制御する。以下のアイコン。

以下のピンでツールバーに固定することができます。
パズルのピースみたいなアイコンをクリックすると表示されるようになります。

chrome.action APIを使用するためには、Manifestにactionを定義する必要があります。ただし、actionを定義していなくても、ツールバーにアイコンは表示されます。

chrome.actionについて{
  "name": "Action API Demo",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Default Title",
    "default_popup": "popups/popup.html",
    "default_icon": {
      "32": "icons/32.png",
      "72": "icons/72.png",
      "128": "icons/128.png",
      "512": "icons/512.png"
    }
  },
  "icons": {
    "32": "icons/32.png",
    "72": "icons/72.png",
    "128": "icons/128.png",
    "512": "icons/512.png"
  }
}

サンプルプロジェクトで動作を確認するため、プロジェクトの準備をする。

以下よりリポジトリをcloneしてくる。

https://github.com/GoogleChrome/chrome-extensions-samples

以下ディレクトリの拡張機能をデベロッパーモードでインストールします。
chrome-extensions-samples\api\action

拡張機能のインストール方法については以下を参考にしてください。

インストールすると、以下の画面が別タブで表示されます。
後ほど実際に動作確認をするときに使います。

こちら一度閉じてしまうとどうやって開くのかわからなくなるかもしれませんが、再度インストールするか更新ボタンを押せば開けます。
以下のボタンが更新ボタンです。

Manifest.jsonのactionで設定できる項目について

以下サンプルプロジェクトで設定されているManifest.jsonになります。
こちらを使って解説していきます。

 "action": {
    "default_title": "Default Title",
    "default_popup": "popups/popup.html",
    "default_icon": {
      "32": "icons/32.png",
      "72": "icons/72.png",
      "128": "icons/128.png",
      "512": "icons/512.png"
    }
  },

default_icon

ツールバーに表示するアイコンを設定できます。
拡張子は、パッケージ化していない場合は PNGのみ使用できます。
コードでアイコンを設定したい場合は、action.setIcon()で設定できます。

default_title

アイコンにマウスカーソルをホバーさせると表示される文字列を設定することができます。デフォルトの文字列をManifest.jsonで設定して、そこから動的に変更したい場合は、action.setTitle()で表示したい文字列を変更することができます。

default_popup

ユーザーがツールバーの拡張機能のアクションボタンをクリックすると表示されます。インストールで選択したディレクトリをルートディレクトリとして、htmlファイルを指定します。

"default_popup": "popups/popup.html",

任意のHTMLコンテンツを含めることができ、そのコンテンツに合わせて自動的にサイズが調整されます。ポップアップのサイズは、25×25より小さく、800 × 600より大きくすることはできません。

※結構ハマるポイントなんですが、ポップアップを設定しているとonClickedイベントは発火しません。

サンプルプロジェクトで動作を確認

Visual Studio Codeでサンプルプロジェクトがどういう作りになっているか確認してみます。

demoディレクトリのファイルについて

ここには、インストールした時に開かれた画面のhtml,css,javascriptファイルがあります。

インストールした時に自動的に開かれたように見えますが、これはbackground.jsでインストール時に開くように設定しているからです。

background.js

これはManifest.jsonで設定されたService_workerの実体となります。
Service_worker はマニフェストのV3で使えます。

  "background": {
    "service_worker": "background.js"
  },

background.jsをみると、インストール時(正確には直後)にさせたい動作を定義していることがわかります。

ソースコードは以下
background.js

// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

// Show the demo page once the extension is installed
chrome.runtime.onInstalled.addListener((details) => {
  chrome.tabs.create({
    url: "demo/index.html",
  });
});

chrome.runtime.onInstalledは、拡張機能が最初にインストールされたとき、拡張機能が新しいバージョンにアップデートされたとき、およびChromeが新しいバージョンにアップデートされたときに発生します。

上記コードではdetailsという引数が設定されています。
detailsのメンバーについては公式を見てください。
これをchromeのdevtoolでデバッグしようと思いましたが、うまくいきませんでした。
以下のようなコードを書いてみましたが、動かず。。

chrome.runtime.onInstalled.addListener((details) => {
  chrome.tabs.create({
    url: "demo/index.html",
  });
  setInterval((details) => {
    console.log("test");
    console.log(details);
  }, 5000);
});

index.js

このプロジェクトの一番重要な部分で、chrome.actionの具体的な使い方の例が書かれています。

実際に一つずつ使い方をデバッグしながら見ていきたいと思います。
なので、F12押して開発者ツールを立ち上げておきます。

アイコンボタンの活性制御

toggle enabled stateボタンを押すとアイコンの活性状態が変化します。

アイコンを活性化させたいときは
chrome.action.enable();
非活性にしたいときは
chrome.action.disable();
となります。

ボタンに対してclickイベントを設定していて、クリックしたらアイコンの活性状態が変化しています。

コードのコメントにも書いてますが、action APIにはアイコンの活性状態を知るすべがないので、自分で活性状態を管理する必要があります。
以下のコードでは、actionEnabledで活性状態を管理しています。

ところで、showToggleStateという変数が定義されていますが、使用されていませんでした。なんだったんだろう?実装し忘れた?

// The action API does not expose a way to read the action's current enabled/disabled state, so we
// have to track it ourselves.
// Relevant feature request: https://bugs.chromium.org/p/chromium/issues/detail?id=1189295
let actionEnabled = true;
let showToggleState = document.getElementById('show-toggle-state');
document.getElementById('toggle-state-button').addEventListener('click', (_event) => {
  if (actionEnabled) {
    chrome.action.disable();
  } else {
    chrome.action.enable();
  }
  actionEnabled = !actionEnabled;
});

Popup

アイコンクックした時に表示されるポップアップ画面を制御しています。

ポップアップの設定は 以下のようにします。
await chrome.action.setPopup({ popup });
現在ポップアップとしてどのurlが指定されているかを確認するには
let popup = await chrome.action.getPopup({});
とします。
上記の、popupの部分には'/popups/b.html'というように、ルートディレクトリからのパスが入ってます。

もう一点、ポイントとなるのが以下の部分です。

// If a popup is specified, our on click handler won't be called. We declare it here rather than in
// the `onclicked-button` handler to prevent the user from accidentally registering multiple
// onClicked listeners.
chrome.action.onClicked.addListener((tab) => {
  chrome.tabs.create({ url: 'https://html5zombo.com/' });
});

上記のコードは、クリックイベントを定義していていて、アイコンをクリックした時に別タブで指定されたページが表示されるようになっています。
英語の説明にもあるようにこちらはポップアップ画面(~html)が設定されていると動作しません。ボタンに対して直接イベントを定義していないのは、ユーザがミスって何度もイベント登録してしまわないようにするためとのことです。

というわけで、アイコンに対してクリックイベントを追加したいときは
chrome.action.onClicked.addListener
を使ってイベントを登録するようにします。

クリックするとデフォルトのポップアップページが表示されます。
これはManifest.jsonで設定した “default_popup”: “popups/popup.html”です。

プログラムでどうやってポップアップページを変更するかを説明するために作られているなと読み取れます。

コンボを変更した時にイベントが発火し、その時にポップアップページを動的に変化させています。ついでに、Current Page ValueというTextBoxの値も変更しています。

対象のソースは以下の部分となります。

index.html

    <section id="popup">
      <h2>Popup</h2>

      <p>This demo's <a href="manifest.json">manifest.json</a> file sets the value of
      <code>action.default_popup</code> to <code>popups/popup.html</code>. We can change that behavior at runtime using <a
      href="https://developer.chrome.com/docs/extensions/reference/action/#method-setPopup"><code>action.setPopup</code></a>.</p>

      <label>
        Change popup page<br>
        <select id="popup-options">
          <option value="/popups/popup.html">Hello world (default)</option>
          <option value="/popups/a.html">A</option>
          <option value="/popups/b.html">B</option>
          <option value="">onClicked handler</option>
        </select>
      </label>

      <div class="spaced">
        <label>
          Current popup value
          <input type="text" id="current-popup-value" disabled>
        </label>
      </div>

      <p>Register a handler to change how the action button behaves. Once changed, clicking the
      action will open your new favorite website.</p>
      <button id="onclicked-button">Change action click behavior</button>
      <button id="onclicked-reset-button">reset</button>
    </section>

index.js

document.getElementById('popup-options').addEventListener('change', async (event) => {
  let popup = event.target.value;
  await chrome.action.setPopup({ popup });

  // Show the updated popup path
  await getCurrentPopup();
});

async function getCurrentPopup() {
  let popup = await chrome.action.getPopup({});
  document.getElementById('current-popup-value').value = popup;
  return popup;
};
async function showCurrentPage() {
  let popup = await getCurrentPopup();
  let pathname = '';
  if (popup) {
    pathname = new URL(popup).pathname;
  }

  let options = document.getElementById('popup-options');
  let option = options.querySelector(`option[value="${pathname}"]`);
  option.selected = true;
}

// Populate popup inputs on on page load
showCurrentPage();

// ----------
// .onClicked
// ----------

// If a popup is specified, our on click handler won't be called. We declare it here rather than in
// the `onclicked-button` handler to prevent the user from accidentally registering multiple
// onClicked listeners.
chrome.action.onClicked.addListener((tab) => {
  chrome.tabs.create({ url: 'https://html5zombo.com/' });
});

document.getElementById('onclicked-button').addEventListener('click', async () => {
  // Our listener will only receive the action's click event after clear out the popup URL
  await chrome.action.setPopup({ popup: '' });
  await showCurrentPage();
});

document.getElementById('onclicked-reset-button').addEventListener('click', async () => {
  await chrome.action.setPopup({ popup: 'popups/popup.html' });
  await showCurrentPage();
});

見た感じ、色々やってそうですが、結局は
①ポップアップページの登録
②現在のポップアップページ(URL)を取得する
③クリックイベントを登録する
がメインの内容となります。

Badge

アイコンボタンに表示される文字列の設定ができます。

上記では、2というのがバッジになります。
見てわかるように描画範囲が少ししかないので、数文字程度の情報にとどめます。例えばメールの受信件数を表示しているケースが思いつきます。

バッジテキストの取得は、以下のようにします。
let currentText = await chrome.action.getBadgeText({});
バッジテキストの設定は、以下のようにします。
await chrome.action.setBadgeText({ text });
バッジテキストの色取得は、以下のようにします。
let color = await chrome.action.getBadgeBackgroundColor({});
バッジテキストの色設定は、以下のようにします。
chrome.action.setBadgeBackgroundColor({ color });

注意点としては、現時点(2021/10/04)でRandomize badge background colorボアんを押すとエラーが出ます。
以下の部分を修正すると正しく動きます。
ランダムの取り方が間違っているようです。
let color = [0, 0, 0].map(() => Math.floor(Math.random * 255));

let color = [0, 0, 0].map(() => Math.floor(Math.random() * 255));

// ----------------------
// badge background color
// ----------------------

async function showBadgeColor() {
  let color = await chrome.action.getBadgeBackgroundColor({});
  document.getElementById("current-badge-bg-color").value = JSON.stringify(
    color,
    null,
    0
  );
}

// Populate badge background color inputs on on page load
showBadgeColor();

document
  .getElementById("set-badge-background-color-button")
  .addEventListener("click", async () => {
    // To show off this method, we must first make sure the badge has text
    let currentText = await chrome.action.getBadgeText({});
    if (!currentText) {
      chrome.action.setBadgeText({ text: "hi :)" });
      showBadgeText();
    }

    // Next, generate a random RGBA color
    let color = [0, 0, 0].map(() => Math.floor(Math.random() * 255));

    // Use the default background color ~10% of the time.
    //
    // NOTE: Alpha color cannot be set due to crbug.com/1184905. At the time of writing (Chrome 89),
    // an alpha value of 0 sets the default color while a value of 1-255 will make the RGB color
    // fully opaque.
    if (Math.random() < 0.1) {
      color.push(0);
    } else {
      color.push(255);
    }

    chrome.action.setBadgeBackgroundColor({ color });
    showBadgeColor();
  });

document
  .getElementById("reset-badge-background-color-button")
  .addEventListener("click", async () => {
    chrome.action.setBadgeBackgroundColor({ color: [0, 0, 0, 0] });
    showBadgeColor();
  });

公式で明示的な説明は見つからなかったんですが、以下のようにして第二引数に関数を定義することで、テキストの変化や色の変化をしたあとにメソッドを呼び出すことが可能です。

await chrome.action.setBadgeText({ text }, () => {
      console.log("BadgeText Changed");
});

アイコンの画像設定

アイコンを動的に設定するには
chrome.action.setIcon({ imageData });
とします。
imageDataとなっているところには、HTML canvas elementや画像パスを設定できます。

reset action iconをクリックすると、以下のメソッドでエラーが出ます。

document.getElementById("reset-icon-button").addEventListener("click", () => {
  let manifest = chrome.runtime.getManifest();
  chrome.action.setIcon({ path: manifest.action.default_icon });
});

なので、エラーが出ないように以下のように修正しました。
エラーが出る原因はパスが正しくなかったからでした。

document.getElementById("reset-icon-button").addEventListener("click", () => {
  let manifest = chrome.runtime.getManifest();

  // 加工しやすいようにArrayに変換
  let iconArray = Object.entries(manifest.action.default_icon);
  for (let i = 0; i < iconArray.length; i++) {
    // ルートからのパスだとエラーになるので、chrome://...となるように変換する
    iconArray[i][1] = chrome.runtime.getURL(iconArray[i][1]);
  }
  // もう一度objectに変換。chrome.action.setIconには配列はセットできないから。
  const iconObj = Object.fromEntries(iconArray);
  chrome.action.setIcon({
    path: iconObj,
  });
});

マウスホバーした時にツールチップを表示する

ツールチップの設定は以下メソッド
chrome.action.setTitle({ title });
こちらも第二引数でコールバック関数を設定できます。

その他

マニフェストの内容を取得する方法

chrome.runtime.getManifest();

アイコンをクリックしたらcontent.jsをそのタブ上に埋め込む方法

個人的に重要な内容。
実際に拡張機能の公開をするときに使用する権限が少ないほうが審査が通りやすいようです。
なので、実際に拡張機能を公開するのなら、少ない権限で要件を満たしたほうが良いです。

アクティブなタブに対する権限とスクリプトを登録するための権限をManifestに登録します。

"permissions": ["activeTab", "scripting"],

background.jsには、以下を登録。
ここのタブIDはアイコンをクリックした時にアクティブとなっているタブです。
スクリプトをタブに埋め込むには以下を使用します。
chrome.scripting.executeScript

// background.js
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    files: ['content.js']
  });
});

上記例ではファイルを登録していますが、関数を登録する方法があります。
また、登録が終わった後にコールバック関数で登録後に何かすることもできます。
具体的な例は公式を確認してください。