OXY NOTES

Firefox機能拡張のポップアップとコンテンツスクリプトについて

ポップアップ表示を通してアドオンスクリプトとコンテンツスクリプトを学ぶ

Firefox機能拡張の解説」第3弾。

この投稿ではpanelモジュールを利用してポップアップする機能拡張を作成します。

その流れで、アドオンスクリプトコンテンツスクリプトの違いも解説します。少し混乱しやすい部分なので、イラストと実際の動作例を紹介しつつ解説します。


このページの目次


ポップアップを表示する

今回はポップアップを表示するためにpanelモジュールを使います。

index.js

var data = require("sdk/self").data;

var panelEntry = require("sdk/panel").Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

require("sdk/ui/button/action").ActionButton({
	id: "show-panel",
	label: "Show Panel",
	icon: {
		"16": "./icon-16.png",
		"32": "./icon-32.png",
		"64": "./icon-64.png"
	},
	onClick: handleClick
});

function handleClick(state) {
	panelEntry.show();
}

1行目require(“sdk/self”).dataで機能拡張のdataフォルダを参照します。selfは機能拡張自身に関する情報を得るためのモジュールです。

selfの公式ドキュメント

参照するディレクトリが複数ある場合は、以下のように定義しておくと流用できます。

// selfには機能拡張のディレクトリが入っている(uri: "addon:@toolbaraddon"のような感じで)
var self = require("sdk/self"); 

self.data.url() // dataディレクトリを参照
self.img.url() // imgディレクトリを参照

3行目panelモジュールの読み込みと定義を同時にしている例です。
ただ省略して書いているだけで、以下のように書いても同じです。

var panels = require("sdk/panel");

var panelEntry = panels.Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

panelモジュールの詳細は公式ページを見てもらうとして、簡単に解説します。

panelモジュールの公式ページ

contentURLはポップアップで表示するhtmlファイルを指定します。

contentScriptFileは後で詳しく解説しますが、上記のhtmlファイルと同時に実行されるJavaScriptファイルです。

19行目~21行目、定義したpanelshow()で表示します。

popup.html

とりあえず理解しやすくするために今回はh1でテキストを表示するだけにします。

<html>
<head>
</head>
<body>
<h1 id="title">テキスト表示するよ</h1>
</body>
</html>

content-script.js

今回はコンソールにログを流すだけにします。

console.log("hoge");

機能拡張の実行

機能拡張のフォルダで実行します。

$ jpm watchpost --post-url http://localhost:8888/

ブラウザを起動してツールバーボタンをクリックするとパネルが現れます。

またCtrl + Shift + Jブラウザコンソールを開いて「hoge」が表示されていることも確認してください。


コンテンツスクリプトについて

panelモジュール定義の際、popup.htmlcontent-script.jsを以下のように指定しました。

var text_entry = panels.Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

popup.html」内でJavaScriptを読み込んで実行することもできるのですが、あえてcontentScriptFileで「content-script.js」を追加しているのには理由があります。

これからFirefoxの機能拡張を作る上で重要になるため、アドオンスクリプトと、コンテンツスクリプトの特徴を説明します。

アドオンスクリプトとコンテンツスクリプトの特徴

公式の解説ページがありますが、いまいちわかりにくいのでイラストを加えて補足解説します。
後で実際に動作テストをするので最初はざっと把握しておいてください。

コンテンツスクリプトについての解説

アドオンスクリプトの特徴

今までの解説の通り、アドオンスクリプトSDKモジュールを利用してFirefoxの様々な機能を追加・変更することができる。
しかし、アドオンスクリプト直接Webコンテンツにアクセスすることはできない

Webコンテンツにアクセスするにはtabs APIactiveTab.attachなどを使う。

その他、WebコンテンツにアクセスすることができるAPIはpage-modtabspage-workercontext-menuなどがある。

コンテンツスクリプトの特徴

コンテンツスクリプトは今回利用したpanelモジュールのように、SDKモジュールで指定する。

コンテンツスクリプトではSDKモジュールを使えない。(グローバルオブジェクトにアクセスするためのrequireを使うことができない)

panel APIcontentURLを利用して指定したhtmlファイルにアクセスするには、同じpanel APIで指定したコンテンツスクリプトのみ、直接アクセスできる。

コンテンツスクリプトは「contentScript」とすることで、文字列として記述することができるが、記述ミスが起こりやすいので「contentScriptFile」で外部ファイルを取り込むのが推奨されている
※過去のバージョンではdataフォルダに配置するのが一般的だったが、JPMではdataフォルダは作成されなくなった。

Ajaxを使う上での注意

panel APIcontentURLや、sidebar APIurlで指定するhtmlファイルからはクロスオリジン対策でAjaxを使った外部との通信はできない
そのためAjax等はアドオンスクリプトか、ひも付けされたコンテンツスクリプトで行う。

ドオンスクリプトとコンテンツスクリプトの通信

アドオンスクリプトコンテンツスクリプトportオブジェクトを使って通信を行う。SDK APIを使って取得したデータコンテンツスクリプトで使いたい場合はport.emitを使って送信する。
逆にコンテンツスクリプトからWebページのデータを受け取りたいといった場合はport.onを使う。
アドオンスクリプトコンテンツスクリプトどちらも同じ仕組でデータを送受信できる。

この説明を読んだだけで理解できる人は稀だと思いますw
解説だけではわかりにくいと思うので、実際に動作確認をします。


コンテンツスクリプトでWebコンテンツを変更する

コンテンツスクリプトでWebコンテンツを操作してみます。

今回は短いコードなのでcontentScriptStringを利用しましたが、ミスの元なので実際にはcontentScriptFileを利用してください。

index.js

var tabs = require("sdk/tabs");
var contentScriptString = 'document.body.innerHTML = "<h1>コンテンツスクリプトで置き換えたよ</h1>";'

tabs.activeTab.attach({
	contentScript: contentScriptString
});

変更したら機能拡張を実行してください。

無事にWebコンテンツが置き換えられていることがわかります。

例えば今回使用した以下のコードをアドオンスクリプトであるindex.jsに記述してもWebコンテンツに直接アクセスできないため何も実行されません

document.body.innerHTML = "<h1>コンテンツスクリプトで置き換えたよ</h1>";

panel APIで指定したhtmlファイルをコンテンツスクリプトで変更する

基本ポップアップを表示した時と同じです。

index.js

var data = require("sdk/self").data;

var panelEntry = require("sdk/panel").Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

require("sdk/ui/button/action").ActionButton({
	id: "show-panel",
	label: "Show Panel",
	icon: {
		"16": "./icon-16.png",
		"32": "./icon-32.png",
		"64": "./icon-64.png"
	},
	onClick: handleClick
});

function handleClick(state) {
	panelEntry.show();
}

popup.html

<html>
<head>
</head>
<body>
<h1 id="title">テキスト表示するよ</h1>
</body>
</html>

content-script.js

今回はコンテンツスクリプトでDOM要素を変更してみます。

document.body.innerHTML = "<h1>コンテンツスクリプトで置き換えたよ</h1>";

ツールバーボタンをクリックしてポップアップを表示してみてください。
以下の画像のように「コンテンツスクリプトで置き換えたよ」に変更されたパネルが表示されると思います。

このように、一口にコンテンツスクリプトと言っても、Webコンテンツを操作するものと、panelなどSDK APIで指定したhtmlファイルを操作するものがあります。
混同しがちなので、どちらを利用しているか意識して使い分けてください。


message-passingを利用する

アドオンスクリプトからコンテンツスクリプトportオブジェクトを使ってメッセージを送信してみます。

アドオンスクリプトからコンテンツスクリプトへメッセージを送信する。

index.js

tabs APIを利用してタイトルを取得。
アクションボタンのクリックイベントであるhandleClick内で、panelEntryに対してport.emitを利用してtabTitleという名前のメッセージを送信します。

var data = require("sdk/self").data;
var tabs = require("sdk/tabs");

var panelEntry = require("sdk/panel").Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

require("sdk/ui/button/action").ActionButton({
	id: "show-panel",
	label: "Show Panel",
	icon: {
		"16": "./icon-16.png",
		"32": "./icon-32.png",
		"64": "./icon-64.png"
	},
	onClick: handleClick
});

function handleClick() {
	var tabTitle = tabs.activeTab.title;
	panelEntry.port.emit("tabTitle", tabTitle);

	panelEntry.show();
}

popup.html

<html>
<head>
</head>
<body>
<h1 id="title">テキスト表示するよ</h1>
</body>
</html>

content-script.js

index.jsから送信されたメッセージを受け取りますが、このときに「self」を使います。portオブジェクトonメソッドを使ってtabTitleという名前のメッセージを受け取ります
そのままコールバック関数で受け取ったメッセージをh1で表示しています。

self.port.on("tabTitle", function (message) {
	document.body.innerHTML = "<h1>" + message + "</h1>";
});

ツールバーボタンをクリックしてポップアップを表示してみてください。
以下の画像はGoogleのトップ画面の例。タイトルである「Google」と表示されています。

コンテンツスクリプトからアドオンスクリプトへメッセージを送信する

今度は反対にコンテンツスクリプトからアドオンスクリプトへメッセージを送信してみます。

index.js

panelEntry.port.onでコンテンツスクリプトからのメッセージを待ち受けます。
メッセージの内容をコンソールログに流します。

var data = require("sdk/self").data;

var panelEntry = require("sdk/panel").Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: data.url("content-script.js")
});

require("sdk/ui/button/action").ActionButton({
	id: "show-panel",
	label: "Show Panel",
	icon: {
		"16": "./icon-16.png",
		"32": "./icon-32.png",
		"64": "./icon-64.png"
	},
	onClick: handleClick
});

function handleClick() {
	panelEntry.show();
}

panelEntry.port.on("popupTitle", function (message) {
	console.log(message);
});

popup.html

<html>
<head>
</head>
<body>
<h1 id="title">テキスト表示するよ</h1>
</body>
</html>

content-script.js

popup.htmlIDtitleの文字列を取得して、self.port.emitを利用してpopupTitleという名前でアドオンスクリプトに送信します。

var popupTitle = document.getElementById("title").innerHTML;
self.port.emit("popupTitle", popupTitle);

機能拡張を読み込むとpopup.htmlのh1である「テキスト表示するよ」がコンソールログに流れます。

これでアドオンスクリプトと、コンテンツスクリプトの双方向の通信ができたことになります。
今回は単純な文字列を渡しただけですが、配列やオブジェクトも渡せます。以下のようにして試してみてください。

var popupTitle = ["hoge", "hoge"]; // 配列
var popupTitle = {hoge:"hoge", huga:"hoge"}; // オブジェクト

コンテンツスクリプトでAjax通信をしてみる

Ajaxで通信するにはマニフェストファイルにパーミッションを指定する必要があります。

{
  "title": "Toolbar Addon",
  "name": "toolbaraddon",
  "version": "0.0.1",
  "description": "ツールバーをテストするAdd-on",
  "main": "index.js",
  "author": "Oxy",
  "engines": {
    "firefox": ">=38.0a1",
    "fennec": ">=38.0a1"
  },
  "license": "MIT",
  "permissions": {
    "cross-domain-content": ["https://www.google.co.jp/"]
  }
}

index.js

Ajaxを素で書くのが面倒なので「jQueryをダウロード」してdataフォルダに保存し利用します。

また、panelのサイズがデフォルトだと小さくてわかりにくいので、サイズを指定して広げています。

var data = require("sdk/self").data;

var panelEntry = require("sdk/panel").Panel({
	contentURL: data.url("popup.html"),
	contentScriptFile: [
		data.url("jquery-2.1.3.min.js"),
		data.url("content-script.js")
	],
	width: 600,
	height: 400
});

require("sdk/ui/button/action").ActionButton({
	id: "show-panel",
	label: "Show Panel",
	icon: {
		"16": "./icon-16.png",
		"32": "./icon-32.png",
		"64": "./icon-64.png"
	},
	onClick: handleClick
});

function handleClick() {
	panelEntry.show();
}

popup.html

わかりやすくするためにcontというdivを作成

<html>
<head>
</head>
<body>
<div id="cont"></div>
</body>
</html>

content-script.js

AjaxでGoogleのデータを読み込んで、popup.htmlのIDがcontの要素を書き換えています。jQueryを読み込んでいるので簡単に書けます。

$.ajax({
	url: "https://www.google.co.jp/",
	dataType: "html",
	method: "GET",
	cache: false,

	// 成功時の処理
	success: function(data, textStatus){
		$("#cont").html(data);
	},

	// エラー時の処理
	error: function(xhr, textStatus, error){
		console.log(error);
	},
});

ツールバーボタンをクリックしてpanelを表示します。
goolgeのトップ画面のhtmlが表示されています。

これはあくまでサンプルなので実際の検索はできませんが、Ajaxを利用できるということは理解していただけたと思います。
実装する場合はボタンイベントをハンドリングしてGoogleにPostすれば動作します。

またAjaxするには専用のRequest APIを利用する方法もあります。
ただ、このAPIはAjaxでできることを書き換えているだけで、あまりメリットはありません。簡単な通信をサクッと書きたいといった用途以外で利用することは無いと思います。

Request APIの解説ページ


マニフェストファイルでパーミッションに指定したサイト以外で通信を行うと以下のエラーが出ます。

クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、https://google.co.jp/?_=1437292519043 にあるリモートリソースの読み込みは拒否されます (理由: CORS ヘッダ 'Access-Control-Allow-Origin' が足りない)。


以上でポップアップ表示と、アドオンスクリプトとコンテンツスクリプトの特徴について解説しました。
この辺がはまりやすいポイントで、ここさ理解できれば、後はAPIとオプションの組み合わせだけです。

次のページは「Firefoxの機能拡張で外部モジュールを利用してメニューアイテムを追加する方法」です。