思うところあって今後ちゃんと形にしようと思っているんだけど、その前に下書きというか叩き台くらいの感じで一度世に出してみます。

概要

  • すごいかんたん!
  • ビルドシステム類のない牧歌的構成でもいける
  • API互換性はそこそこ。ポリフィル入れてFirefoxに寄せて作るのが吉か
  • Edgeは闇を感じた
  • Safariは未調査

基本的な作り方

  1. フォルダを作る
  2. manifest.json を用意
  3. それに合わせて各種スクリプト等を用意
  4. ブラウザーで開発中のものを読み込み開発
  5. 公開

動く場所

  • ツールバーのボタン
  • 任意のサイト内
  • バックグラウンド
  • 独自ページ

他に開発者ツールにタブを追加したり、Firefoxならサイドバーを作ったりすることもできます。

できること

拡張用のAPIがいっぱいあるのでそれらを駆使する。

もちろん普通のウェブ開発の知見も再利用できるよ。

最新のJavaScriptを書ける

IE対応の必要がない(し、たぶんEdgeも無視されるだろう)ので、Chromeだけ、あるいはChromeとFirefoxだけが対象になります。すると最新のJavaScriptの書き方をがんがん使えます。

class とか async/await とか、関数引数の初期値とか分割代入とか。

そこら辺の勉強を兼ねて挑戦してみるのはきっと良い選択だと思います。

Hello Worldをインストールしてみる

最初のブラウザー拡張を自作するまえに、一度サンプルをインストールしてみましょう。どうせ自作したらやる作業です。

サンプルはこちら。zipファイルの中身をどこかに展開しておいてください。

Chrome

  1. メニュー(ウィンドウ右上の三つの点) → その他のツール → 拡張機能
  2. 右上「デベロッパーモード」をオンに
  3. 「パッケージ化されていない拡張機能を読み込む」から manifest.json があるディレクトリーを選択

その後一覧に表示されるカードから以下の操作が可能です

  • 再読み込み(くるっとしたボタン)
  • 削除
  • バックグラウンドスクリプト用開発者ツールを開く

開発用に読み込んだ拡張機能は、次回ブラウザー起動時にも有効ですが、警告が表示されます。

Firefox

普通の拡張機能一覧とは違う場所で操作します。

  1. about:debugging を開く(アドレスバーに入力)
  2. 右上 “Load Temporary Add-on” 的なボタンから manifest.json を開く

その後一覧に表示されるカードから以下の操作が可能です。

  • 再読み込み
  • 削除
  • 拡張機能用コンソールを開く

開発者ツールは、ポップアップで尋ねられる “Incoming Connection” を受け入れる必要があります。

それとポップアップをデバッグする場合、開発者ツール右上の、四角が四つ並んだ “Disable popup auto hide” をオンにします。

開発用に読み込んだ拡張機能は、次回ブラウザー起動時には削除されるため、都度同じ手順で読み込む必要があります。

Edge

  1. about:flags を開く(アドレスバーに入力)
  2. 開発者向け設定の「拡張機能の開発者向け機能を有効にする(これにより…)」をオンに
  3. メニュー(ウィンドウ右上の三つの点) → 拡張機能
  4. 「拡張機能の読み込み」から manifest.json があるディレクトリーを選択

その後一覧から詳細を開いて以下の操作が可能です。

  • 再読み込み
  • 削除
  • バックグラウンドスクリプト用開発者ツールを開く

開発用に読み込んだ拡張機能は、次回ブラウザー起動時に警告が表示され、無効化されます。

Safari

そのうち調べます。

Hello Worldを作る

自作のもののインストールのやり方がわかったところで、安心して自分のHello Worldを作り始めます。

あ、作り始める前に、さっきインストールしたサンプルはもう消しちゃってください。

サンプルと同じもの、ボタンを押すとハローするやつから始めましょう。以下のように hello ディレクトリと、その下にファイルを作ります。

  • hello/
    • manifest.json
    • popup.html

本章で学べること

  • ブラウザー拡張の基本的な作り方、開発の方法
  • manifest.json 入門
  • ポップアップ

manifest.json の用意

{
  "manifest_version": 2,
  "name": "Hello World!",
  "version": "1.0.0",
  "author": "Ginpei",
  "browser_action": {
    "default_popup": "popup.html"
  }
}

HTMLの用意

限界HTMLです。お好みでちゃんとしたやつに書き換えてくださって結構。

<!doctype html>
<html lang=en>
<title>Hello World!</title>
<h1>Hello World!</h1>

できあがり

とりあえずここまででできあがり。さあインストールしましょう。(前項参考)

Hello Worldにもうちょっと足す

インストールできました? 動きましたね?

とりあえず動いたら、少しずつ足していきましょう。

本章で学べること

  • ポップアップ関係の設定を少々
  • ポップアップHTMLでCSSを読み込む
  • ブラウザー拡張でのパスの扱い

アイコンを足す

まずは簡単なところから。ボタンのアイコンを設定します。

manifest.json に、以下のように "default_icon" を足します。アイコン画像は適当にご用意ください。

"browser_action": {
  "default_icon": "icon-90.png",
  "default_popup": "index.html"
}

書き足して上書き保存したら、ブラウザーの方で拡張を再読み込みします。しないと反映されません、当然。

タイトルを足す

同様に "default_title" を足します。

マウスカーソルをボタンに乗せた際にツールチップとして表示されるようです。

"browser_action": {
  "default_icon": "icon-90.png",
  "default_title": "Hello World!",
  "default_popup": "index.html"
}

CSSを足す

ただHTMLから読み込むだけです。かーんたーん。

まず、今いる hello 配下に popup.css を以下の内容で作成します。

body {
  align-items: center;
  display: flex;
  justify-content: center;
  margin: 0;
  min-height: 300px;
  min-width: 300px;
}

そいつを読み込むよう、既存の index.html に <link> を足します。

<!doctype html>
<html lang=en>
<title>Hello World!</title>
<link rel="stylesheet" href="/popup.css">
<h1>Hello World!</h1>

ポップアップを開きなおすと、見た目も変わっているはずです。

ちなみに今回変更したのはポップアップの中身だけなので、再読み込みは不要です。(不安ならしてください。)

パスについて

manifest.json のある位置がルートになります。なので / から始めることができる。

../../../../../ とか書いても、プロジェクトのディレクトリより上が見えたりはしないみたい。

(仕様は調べていない。)

簡単なJavaScriptを書いてみる

本格的な拡張の機能を作り始める前に、ポップアップ内で完結するごく簡単なスクリプトから始めたいと思います。わかる人はさらっと流してください。

本章で学べること

  • ポップアップHTMLでJSを読み込む

作るもの

何にしようかなー。やっぱり最初はボタンを押したら何かするやつからですかねー。

というわけで、ボタンを押したら “Hello World!” が出てくるやつにします。

HTMLの用意

JSファイルもCSSと同様、ただHTMLから読み込むだけです。

index.html ちょっと書き換えて、さらに <script> を追加します。まだ読み込む先のJSファイルは作ってないけど。

<!doctype html>
<html lang=en>
<title>Hello World!</title>
<link rel="stylesheet" href="/popup.css">
<div>
  <h1 id="yabaiMessage" style="display: none">Hello World!</h1>
  <button id="sugoiButton">Push me!</button>
</div>
<script src="/popup.js"></script>

スクリプトを実装する

hello 配下に popup.js を作成します。さっきHTMLに追加した <script> で読み込むやつです。

const sugoiButton = document.querySelector('#sugoiButton');
sugoiButton.addEventListener('click', () => {
  sugoiButton.style.display = 'none';
  yabaiMessage.style.display = '';
});

これでどうでしょうか。ポップアップを開くと “Push me!” のボタンが表示され、それを押すと “Hello World!” の文字に置き換わるはずです。

よくできました

というわけで、ポップアップ内で完結するものを作る分には、普通のHTML、普通のCSS、そして普通のJavaScriptを書くだけだということがおわかり頂けたかと思います。

簡単でしょう?

といってもそれだけじゃ何も面白くはないので、もうちょっと何かしてみましょう。

jQueryが恋しい?

ポップアップについては前述の通り「普通のHTML」なので、普通にjQueryを読み込んで使うこともできます。まあそちらが良ければそちらでも構いません。

もしjQueryから離れたコードを書き始めたいのでしたら、良い機会かもしれません。ただ、脱jQueryがゴールなのか、とにかくブラウザー拡張を作りたいのか、目標を決めておきましょう。勢い付けて二つ同時に目指すと、スッ転んだときに痛いです。

jQuery使うにしろ使わないにしろ、難しいところはそこら辺じゃないです。

ブラウザー拡張のAPIをひとつ使ってみる

※本章の内容はChromeとFirefoxで動きます。Edgeでは動きません。

ポップアップは出てきたけれど、書いたのは普通のHTML、普通のCSS、そして普通のJavaScriptでした。次はブラウザー拡張だけの機能を使ってみましょう。

何にしようかな、雰囲気に慣れるための簡単なものが良いな。

notifications APIを使ってみましょうか。

本章で学べること

  • ブラウザー拡張用のAPIの概要
  • browser.notifications
  • ブラウザー拡張用のパーミッションの設定
  • manifest.jsonpermissions
  • API仕様の調べ方
  • API互換性の調べ方

ブラウザー拡張用のAPI群 chrome

Chrome、Firefox共にグローバルの chrome オブジェクト以下にAPI群が用意されています。例えば今回利用するAPIだと chrome.notifications.create() というものを利用します。

Edgeは browser オブジェクトにそれが実装されているようです。なお chrome オブジェクトも存在していて、しかしこれは全く別物のようです。(すみません、詳細はよくわかりません。) Edgeで動かしたい場合はコード例の chrome を browser に置き換えてください。もしかしたら動くかもしれません。動かないかもしれません。今回(notifications API)は動きません。

互換性は現時点ではまだ不完全で、標準化も完了していません。有用なAPIでも一部環境でしか使えないものがあったりします。

APIについてはもうちょっと、互換性とかFirefoxの browser オブジェクトとかの話もあるんだけど、後回しにします。

お知らせAPIの使い方

とまあそういうわけでして、notification APIを使います。ウェブ標準の方のNotification APIとは別物で、許可を求めるポップアップも表示されません。(ブラウザー拡張インストール時に情報は表示されます。)

さっそくこいつをポップアップで試してみましょう。コードは簡単です。 hello 内の popup.js を開いて、以下の内容に書き換えます。

const sugoiButton = document.querySelector('sugoiButton');
sugoiButton.addEventListener('click', () => {
  chrome.notifications.create(null, {
    type: chrome.notifications.TemplateType.BASIC,
    iconUrl: '/icon-90.png',
    title: 'Hello World!',
    message: 'お元気ですか',
  });
});

このコードは正しいので、コピペで一度試してみて頂きたいのですが、実は、動きません。繰り返しますが、このコードは正しいけど、動かないんです。

つまりこのコードじゃないところが正しくないということですね。

ともあれ、うまく動かないときはすぐコンソールにエラーが出ていないか確認してみてください。(ポップアップの中で右クリック→要素の検証とかそういうので出てきます。)

Chromeであればこんなエラーが出ているはずです。

popup.js:3 Uncaught TypeError: Cannot read property 'create' of undefined
    at HTMLButtonElement.sugoiButton.onclick (popup.js:3)

Firefoxならこう。

TypeError: chrome.notifications is undefined [Learn More]  popup.js:3:5

謎ですね、正しいコードのなずなのに chrome.notificatiions が undefined だぞと言われています。

実はこのオブジェクトは、事前にパーミッションの設定をしておかないと使えないのです。

パーミッションの設定

というわけで、設定します。

manifest.json を開いて、 "permissions" を追加します。

{
  "manifest_version": 2,
  "name": "Hello World!",
  "version": "1.0.0",
  "author": "Ginpei",
  "permissions": [
    "notifications"
  ],
  "browser_action": {
    "default_icon": "icon-90.png",
    "default_title": "Hello World!",
    "default_popup": "index.html"
  }
}

作業としてはこれだけです。更新後、ブラウザーの方では拡張を再読み込みしてください。 manifest.json を更新したので、ポップアップを開きなおすだけじゃ足りないです。

上記のように "permissions" の設定を適切に行い、再読み込みして、もう一度ボタンを推せば、ほらちゃんと何かが出てくるはずです。

APIの使い方を調べる

動いて良かった良かったというところですが、もうちょっとここをこうしたいなーとか思うかもしれません。今思わなくても、将来思うかもしれません。なので、APIの仕様を軽く調べておきましょう。

Firefoxの製造元であるMozillaが管理する、MDNというサイトにリファレンスがあります。現時点ではあまり日本語版がないようですが、まあAPIについて調べるくらいなら英語よりJavaScript語を読めば良いので、なんとかなるでしょう。

今回のnotification APIについては、「JavaScript API 群」に並んでいるのを見つけられます。

書式はこうなっているそうです。

var creating = browser.notifications.create(
  id,                   // optional string
  options               // NotificationOptions
)

引数や戻り値がどうなってるのかわかりますね。良かった良かった。

ページの下の方には「ブラウザ互換性」の項があります。ここを見れば、自分で実装して試すことなく確認することができます。

どうやらEdgeでは実装されていないようです。ウェブ標準のNotificationの方はある程度実装が進んでいるようなので、こちらでフォールバックする必要があるかもしれません。まあ対象外にしちゃっても良いんだろうけど。

タイマーを作る

お知らせできるようになったので、何かお知らせしたいですね。何をお知らせしましょうか。時間でしょうか。三分でしょうか。カップラーメンのできあがりでしょうか。

よし、タイマーを作りましょう。

本章で学べること

作るもの

とりあえず簡単な方が良いよね。またポップアップを使うとして、HTMLには同じようにボタンがひとつだけ。押すと3分後にお知らせ。これでいきましょう。

書いてみる

またこれじゃ動かないんだけど、いったん実装します。

sugoiButton.onclick = () => {
  const delay = 3 * 1000;  // 3秒
  setTimeout(() => {
    chrome.notifications.create(null, {
      type: chrome.notifications.TemplateType.BASIC,
      iconUrl: '/icon-90.png',
      title: 'Hello World!',
      message: 'お元気ですか',
    });
  }, delay);
};

何のひねりもなく setTimeout() でした。あ、3分だと開発中は長すぎるので、いったん3秒にしておきます。はい。

で、ポップアップを開いて、ボタンを押して、そのまま3秒待ってください。出ましたか?

できた?

……出ましたね、今回は。

あれれ~じゃあこれで終わりかな~? いいえ、これじゃ駄目です。

次はボタンを押したら、すぐにポップアップを閉じてください。ポップアップの外をクリックすれば消えます。

そうすると、……お知らせが出てこなくなります。なんてこった!

ポップアップのライフサイクル

ライフサイクルという表現で良いのかわからないんだけど、ポップアップの中身は普通のHTMLです。普通のHTMLなので、開いたらいろいろ始まって、閉じたら全て終わります。

普通のウェブページで setTimeout() を使って実装した場合を想像してほしいんですが、その場合、ページを閉じたらもうタイマーは動かないですよね。ポップアップも同様、閉じた時点でタイマーは消えてしまいます。

3分後にお知らせしようとしたら、3分間ずっとポップアップを開きっぱなしにしないといけなくなります。えー。

ずっと動き続けるスクリプト

そこで使うのがバックグラウンドです。ブラウザー拡張の読み込みと同時に開始され、その後ずっと動き続けるスクリプトです。

書いてみましょう。まずは manifest.json です。ポップアップの設定のように、バックグラウンドの設定 "background" を記述します。

{
  "manifest_version": 2,
  "name": "Hello World!",
  "version": "1.0.0",
  "author": "Ginpei",
  "permissions": [
    "notifications"
  ],
  "browser_action": {
    "default_icon": "icon-90.png",
    "default_title": "Hello World!",
    "default_popup": "index.html"
  },
  "background": {
    "scripts": [
      "background.js"
    ]
  }
}

久しぶりなので全部載せました。追加した "background" は最後です。

続いて background.js を新規作成します。

const delay = 3 * 1000;  // 3秒
setTimeout(() => {
  chrome.notifications.create(null, {
    type: chrome.notifications.TemplateType.BASIC,
    iconUrl: '/icon-90.png',
    title: 'Hello World!',
    message: 'お元気ですか',
  });
}, delay);

こう書いて新規作成して、再読み込みして、3秒待つと……、ポップアップを触らなくてもお知らせが出てきます! やった!

やってねえ!

起動の3秒後にお知らせしても仕方がないですね。ポップアップを閉じても動かせることはわかったので、次の課題は、こいつをポップアップから操作してタイマー起動することです。

ポップアップからバックグラウンドへ通信

通信は chrome.runtime.sendMessage() と chrome.runtime.onMessage でやりとりできます。

まず送る方。これは popup.js です。

const sugoiButton = document.querySelector('#sugoiButton');
sugoiButton.onclick = () => {
  const message = {
    delay: 3 * 1000,  // 3秒
    title: 'Hello World!',
    message: 'お元気ですか',
  };
  chrome.runtime.sendMessage(message);
};

続いて受ける方。こちらが background.js です。

chrome.runtime.onMessage.addListener((message) => {
  setTimeout(() => {
    chrome.notifications.create({
      type: chrome.notifications.TemplateType.BASIC,
      iconUrl: '/icon-90.png',
      title: message.title,
      message: message.message,
    });
  }, message.delay);
});

さらっとやりましたが、 message オブジェクトを通じて情報を送ることができます。

さてバックグラウンドのスクリプトは、前述の通り動きっぱなしです。ので、変更したら一度ブラウザー拡張を再読み込みする必要があります。お忘れなく。

というわけで

書き換えて、再読み込みして、ポップアップのボタンを押して、閉じて、3秒待つと、お知らせが出てくるはずです。

やったね!

互換性

最近こちらに書いたのでどうぞ。

さて次は

タイマー自体は動くようになったので、もうちょっとポップアップの見た目を整えたりしましょう。さくっと。

あと機能追加として、時間を3分で固定ではなく任意に入力できるようにしたり、どうにかして残り時間を表示したり、途中でやめたり再開したりなんかができると良いかもしれません。

そういえば最近タイマーといえば「ポモドーロ」というのが流行です。30分ごとに一区切り付けるやつです。これ目指してみてはどうでしょうか。さらにポモドーロのタイマーができて仕事で使えるようになったら、今度は仕事の進捗や能率を管理したくなるかもしれません。

という感じでちょっとアイディアだけ。何も作りたいものが思いつかない方は、そんな感じで練習がてら作ってみてはいかがでしょうかというご提案でございます。

あ、それから作るものじゃなくて作り方の方で、本格的にNPMパッケージを諸々導入して脇を固めるのも良さそうです。

公開する

作ったものは公開しましょう。ブラウザーごとにストアが用意されています。

Chromeの方で開発者登録するのに、初回$5かかります。身分証明みたいなもんですかね。Firefoxの方は無料です。

いずれも指定サイズの画像とか、諸々の情報を入力する必要があります。

Edgeは1,847円かかります。でもその後の手順がよくわからなくて、まだ公開できてません。ううむ。

使えそうなAPIの紹介

API一覧を眺めておもしろそうなものを見つけましょう。

そのうちの一部を簡単に紹介だけ。

情報を保存する

storage.local.get() と storage.local.set() を。

"permissions" の設定をお忘れなく。

3種類あるけど、とりあえず storage.local で良さそう。

  • storage.local – 端末単位に保存される情報。何も考えずに使える
  • storage.sync – ログインして端末間で共有される情報っぽい。Firefoxはひと手間ある
  • storage.managed – 実質 manifest.json で定義する情報を読む専用っぽい。別途 runtime.getManifest() もある。

あと await 使う場合はその外側が async になっている必要があります。

(async () => {
  const message = document.querySelector('#message');

  // 最初に読み込み
  const result = await browser.storage.local.get(['message']);
  message.value = '2';
  message.value = result.message || 'Hello World!';

  // 変更あれば書き込み
  message.addEventListener('input', async () => {
    browser.storage.local.set({ message: message.value });
  });
})();

見ているページに挿入する

特定のページ、あるいは全ページにJSファイルやCSSファイルを挿入する場合は、 manifest.json の content_scripts を使う。

スクリプトからコードを生成して実行する場合、あるいはJSファイルやCSSファイルを挿入する場合は tabs.executeScript() や tabs.insertCSS() を使う。いずれも manifest.json の "permissions" で "activeTab" を許可する。

挿入したファイルからブラウザー拡張が持つファイルにアクセスする場合(画像を表示するとか)、 manifest.json で web_accessible_resources を使ってアクセス権の設定が必要。

アイコンの脇にテキストを表示

数字とか英単語とかなら、 browserAction.setBadgeText() でアイコンの脇に出力して常時見せることができる。

アイコンを動的に操作

オンとオフでアイコン画像を切り替えるみたいなのには browserAction.setIcon() を使う。

画像は事前に用意したものに限らず、Canvasとかで動的に生成したものも設定できそう? 未調査。

設定画面

manifest.json の options_ui を使う。各ブラウザーの拡張機能管理ページから開けるし、スクリプトからも runtime.openOptionsPage() で開ける。

ちなみに options_page というのもある。

拡張専用の任意ページ

tabs.create() で、 url の指定を / から始めて任意のHTMLファイル等を指定する。

manifest.json の情報を得る

runtime.getManifest() を使う。

スクリプト間でやりとりする

本文でもやったけど、 runtime.sendMessage() と runtime.onMessage でやりとりする。バックグラウンドからその他へ、あるいはその他からバックグラウンドへのメッセージ送受信になるっぽい。

コンテンツスクリプトだと受信できない?

おしまい

っていう話をVuePressを使ってやってみたんだけど、どこで公開しようとか考えてる間に先に内容をある程度出しておこうと思ってここに置いておきます。もうちょっとページが分かれたりスクリーンショットが追加されたりする予定です。

あとMDNが全然日本語になってなくて、翻訳作業したい気はあるんだけど、更新とかじゃなくて新規にやった経験はなくて、どなた様かに一度レビューしてもらいたいなと思いつつ、どこで誰に頼めばよくわからないので、なんかあれば教えてほしいです。(受け身)

以上です。今後とも何卒宜しくお願い致します。