Firefox Add-on SDK入門
Pasta-K
📧pastak@kmc.gr.jp
こんにちは😺
こんにちは😺
で、誰?
🍣 KMCでの活動
KMC 37代目 広報
OSC Kyoto 2014 出展 etc
JavaScriptで世界平和2014
🍣 インターネット活動
! twitter.com/pastak
id: Pasta-K
pastak.hatenablog.com
pastak-diary.hatenadiary.com
" github.com/pastak
はてなインターン 2013
アルバイトエンジニア ←イマココ
最近では、はてなのインターンを
経てウチへ来るというのが定番の
ようになっているんです
!
休学して社員となった杉本氏をは
じめ、現在5人いる学生アルバイ
トのメンバーも、そのほとんどが
「京大マイコンクラブ」の所属。
アニメファン
🐾目次
• Mozilla Firefox について
• ブラウザー拡張について
• Firefox Add-on SDKでアドオンを作ってみる
" サンプルコード
https://github.com/pastak/
firefox-addon-sdk-sample
Mozilla Firefox
について
About Mozilla Firefox
https://www.mozilla.org/firefox/
Mozillaによって提供されているブラウザ
Win / Mac / Linux / Android
現行最新バージョン 36.0.1 (2015/03/05)
次回37は2015/03/31にリリース予定
Netscapeがうにゃむにゃみたいな歴史に興味
がある人は自力でググッてください
ブラウザシェア (2015/02)
http://news.mynavi.jp/news/2015/03/04/084/
?
iceweasel
iceweasel
Iceweasel(アイスウィーズル)とは、ウェブブラウザ。または
その開発プロジェクト。Mozilla Firefox とほぼ同一のものであ
るが、商標の関係により同プロジェクトから独立したもの。
Debian によるものと、GNU によるもの(GNU IceWeasel)が
存在したが、現在では GNU IceWeasel は GNU IceCat に名称
変更されている。
http://ja.wikipedia.org/wiki/Iceweasel
ブラウザ拡張
現状確認
Google Chrome
Opera
(Based Chromium)
Google Chrome / Opera
JavaScript + HTML + CSS で書ける
Chromium由来の共通のAPIを利用する
→同じExtensionがそのまま動作する
Chrome ExtensionをOperaにインストールするための拡張
https://addons.opera.com/ja/extensions/details/download-
chrome-extension-9
ちなみにOpera側のドキュメントの方が少しだけ充実してる
!
ExtensionとAppの2種類がある
Safari
Safari
https://extensions.apple.com/
Chromiumと同じくHTML + JS + CSSで書ける
大体同じ世界観で書ける
ドキュメント
https://developer.apple.com/library/safari/
documentation/Tools/Conceptual/
SafariExtensionGuide/Introduction/Introduction.html
!
Internet Explorer
Internet Explorer
MSDNにチュートリアルなどのページがある。
(寧ろそれ以外どこにも載ってない)
Browser Extensions Overviews and Tutorials
https://msdn.microsoft.com/en-us/library/
aa753616%28v=vs.85%29.aspx
C#で書くっぽい。
あんまり書いてる人見たこと無い気がする
Mozilla Firefox
Mozilla Firefox
ダウンロード
https://addons.mozilla.org/
ドキュメントはとにかくMDN
https://developer.mozilla.org/ja/docs/Extensions
開発手法は2通り
XULベースのAdd-on
HTML + JS + CSSベースのAdd-on
Addon SDKで開発できるのは後者
このあと詳しく
Google Chrome
VS
Mozilla Firefox
操作可能範囲
表示領域
右上ポップアップ
オムニバー
表示領域
サイドバー
ツールバー
URLバー
ポップアップ
ステータスバー
表示領域
サイドバー
ツールバー
URLバー
ポップアップ
ステータスバー
Add-on SDKでステータスバーにボタンなど
を追加するためのwidgetモジュールがFirefox 29で削除
公開方法
https://chrome.google.com/webstore/
無審査
開発者登録に$5
https://addons.mozilla.org/
審査あり
開発者登録無料
審査あり
開発者登録無料
人が確認するのではない
セキュリティ上の問題などを自動確認
💖
Firefox
Add-on SDK
について
改めてFirefoxのアドオンについて
XULベースの開発とAdd-on SDKでの開発
配布形式は xpi ( その正体はzip )
!
開発に関しては困ったらとにかくMDNを見る
https://developer.mozilla.org/ja/docs/Extensions
http://www.slideshare.net/skeevs/mozilla-firefox-extension-development
XULベースの開発について
これ以降は特に触れない
Firefoxの初期からあった伝統的な方
FirefoxアプリケーションのXULを上書きして機能を提供し
たり、XPCOM コンポーネントを通じて操作をしたりす
る。
!
より詳細な比較は https://developer.mozilla.org/en-US/
Add-ons/SDK/Guides/SDK_vs_XUL を
2009年 Jetpackリリース
アドオンとして提供開始
当時JSだけでFirefoxのアドオンが作れると話題に
XULとか触らなくて良い
JSとHTMLとCSSでなんとかなる
jQueryも標準装備
Firebugでデバッグ出来る!
大喜び
– https://developer.mozilla.org/ja/docs/Jetpack
“Jetpack で、開発者は拡張機能を高速に
作り出すことができると同時に、強化さ
れた体験を与えられたユーザーは、Web
とのふれあいが変わるでしょう。”
それから
2010年 初頭
Jetpack(Prototype)から Jetpack Rebootへ
アドオンからSDKへ移行
開発環境もPythonで書かれたコマンドラインツールに
https://dev.mozilla.jp/2010/03/shifting-from-jetpack-
ptototype-to-jetpack-reboot/
!
2010年末名称を Add-on SDK へ名称変更 🎉
Add-on SDKについて
FirefoxだけではなくThumderbird向けのアド
オンも作れる
一部のAPIはAndroid向けのFirefoxにも対応
デバッグのためにUSBでインストール可能
Add-on SDKで扱えるAPI一覧
座学
ここまで
ここから
実践編
とにかく
数が多い
スライドに書いて
あること一覧
ボタンを出す
バッジを操作する
パネルを出す
サイドバーを出す
通信する
その他諸々
🚀
Add-on SDKのインストール
Mac OSXの人はhomebrewで一発
!
Git cloneでも入手可
!
!
https://developer.mozilla.org/en-US/Add-
ons/SDK/Tutorials/Installation
$ brew install mozilla-addon-sdk
$ git clone git@github.com:mozilla/addon-sdk.git
Add-on SDKのインストール
homebrew以外の方法で入れた場合は
./bin/ をPATHに追加する
と使えるようになる。
SDK仮想環境をシェル上で起動することが一
応推奨されているっぽい
$ bash ./bin/activate
$ cfx --version
Add-on SDK 1.17(12f7d53e8b5…)
cfxコマンドの基本的な使い方
Add-onのスケルトンを生成
!
Add-onをFirefox上で実行
!
Add-onをxpiとして生成
!
テストを実行
$ cfx init
$ cfx run
$ cfx xpi
$ cfx test
Add-on用のスケルトンを生成
!
!
!
!
!
この状態で      で実行可能
(ただし機能は何もない)
$ cfx init
.
├── data
├── lib
│   └── main.js
├── package.json
└── test
└── test-main.js
$ cfx run
$ cfx init
$ mkdir firefox-addon-sample
// cfx initは空のディレクトリでのみ実行可能
$ cd firefox-addon-sample
$ cfx init
* lib directory created
* data directory created
* test directory created
* generated jID automatically: *********
* package.json written
* test/test-main.js written
* lib/main.js written
!
Your sample add-on is now ready.
Do "cfx test" to test it and "cfx run" to try it. Have fun!
これを実行するとFirefoxが立ち上がる
実行する度にprofileを生成する
普段の環境を汚さない
profileを指定する場合は
$ cfx run
$ cfx run -p <ProfilePath>
<ProfilePath>
Mac OSXの場合は /Library/Application Support/Firefox/Profiles/以下にある
https://support.mozilla.org/ja/kb/profiles-where-firefox-stores-user-data
デバッグについて
JSで console.log すると標準出力に出る
デバッガーも利用可能
about:addons で [デバッグ]ボタンを押す
配布用のxpiファイルを生成
ドラッグアンドドロップでインストール
自前サーバで配布する場合でもアップデートを配信することも出来る
!
!
!
!
HTTPで配信する場合はこの方法では署名できない
詳細: Add-on SDKで作ったFirefox拡張をHTTPで配布、アップデートする方法 - Pastalablog in はてな
http://pastak.hatenablog.com/entry/2013/10/03/213040
!
※AMO以外からは配布出来なくなる予定があるので注意(おまけ参照)
$ cfx xpi
$ cfx xpi 
--update-url=https://hogehoge.com/hoge.update.rdf 
--update-link=https://hogehoge.com/hoge.xpi
ファイルの紹介
.
├── data <- 静的htmlや画像ファイルなど
│ アイコンなどもここに置く
├── lib
│   └── main.js <- エントリーポイント
│ になるJSファイル
├── package.json <- 設定などを書く
└── test <- ここに置いておけば
│ cfx test で実行してくれる
└── test-main.js
package.json
Add-onに関する情報を書いておくファイル。
$ cat package.json
{
"name": "sample",
"title": "sample",
"id": "jid1-ZaoDi2iCqJeZVg",
"description": "a basic add-on",
"author": "",
"license": "MPL 2.0",
"version": "0.1"
}
package.json
name:
アドオンの名前を入れる。
ドット(.)やスペースを含むことが出来ない。
title や fullName があればそちらが利用される
version:
アドオンのバージョンを書く。
記法は npm などと同じくsemver に準拠する
http://semver.org/
package.json
permissions:
クロスドメインXHRを許可するホスト名の指定
private browsing mode での許可
icon, icon64:
アイコンのパスを指定する
iconは48x48、icon64は64x64
無ければデフォルトアイコンを利用する
main.js を編集してみる
右上にボタンを追加してみる
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
モジュールを読み込む
モジュールの実体は
build時にxpiに含まれる
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
モジュールのパス
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
ボタンにidとか
アイコンとか
ハンドラを設定する。
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
相対パスで書くと
./data以下を参照する
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
ツールバー
18x18 (px)
メニューパネル
32x32 (px)
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
ツールバー
18x18 (px)
メニューパネル
32x32 (px)
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: "./icon-16.png",
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
省略可
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
}
クリック時に呼ばれる関数
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
!
function handleClick(state) {
tabs.open("http://www.mozilla.org/");
} 新しいタブを開く
badgeを出す
New feature for Firefox 36
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
}
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
}
変更箇所
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
}
badgeの値と色を指定
値: Number or String
色: CSS color value
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: "KMC!!",
badgeColor: "#00AAAA"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
}
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: "KMC!!",
badgeColor: "blue"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
}
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
!
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
function handleClick(state) {
button.badge = state.badge + 1;
} badgeの値を変更
ボタンを押したらパネルが出る
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
panelモジュール
main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
パネルを記述
main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
width, height
大きさ
contextMenu
コンテキストメニューの表示の許可
onShow / onHide / onMessage
各々のイベントハンドラー main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
表示するHTMLファイルを指定
main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
パネルを表示
main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show({
position: button
});
}
button.state('window', {checked: !state.checked});
}
object
top / bottom / left / right
main.js
var buttons = require('sdk/ui/button/action');
var panels = require("sdk/panel");
!
var button = buttons.ActionButton({
id: "panel-sample",
label: "Panel Button",
icon: "./icon-16.png",
onClick: handleClick,
badge: 0,
badgeColor: "#00AAAA"
});
!
var panel = panels.Panel({
contentURL: "./panel.html",
position: button,
onHide: function(){
button.state('window', {checked: false});
}
});
!
function handleClick(state) {
if (!state.checked) {
panel.show();
}
button.state('window', {checked: !state.checked});
}
ここで設定しておいてもOK
main.js
パネル内のイベントを掴む
パネル内のHTML上のボタンが押されたら
badgeの内容をアップデートする
Messaging(1) - port
emit(name, message)
on(name, handler)
removeListener(name, handler)
once(name, handler)
一度だけmessageを受ける
事前にmessageに対してhandlerを設定しておく
https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts/using_port
Messaging(2) - postMessage
messageイベントを発火させる
context-menuモジュールなどはportがサポートさ
れていないのでこちらを使うしかない
panelなどには onMessage があるので、こっちを
使っておくと吉
https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts/using_postMessage
!
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
},
onMessage: function(){
button.badge = button.badge + 1;
}
});
!
main.js(抜粋)
!
!
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
},
onMessage: function(){
button.badge = button.badge + 1;
}
});
!
メッセージを受取ったら
badgeを++
main.js(抜粋)
<h1>Panel!!!!!!!</h1>
<button id='increament'>++</button>
<script>
!
!
!
</script>
var btn = document.getElementById('increament');
btn.addEventListener('click', function(){
addon.postMessage("increament");
})
./data/panel.html
この場合はpostMessageは
addonのメソッド
JSをcontentScriptとして
読み込ませた場合はselfのメソッドになる
var btn = document.getElementById('increament');
btn.addEventListener('click', function(){
addon.postMessage({
type: 'increament'
});
})
var color = document.getElementById('color');
color.addEventListener('change', function(){
addon.postMessage({
type: 'color',
value: color.value
});
})
<h1>Panel!!!!!!!</h1>
<button id='increament'>++</button>
<input type='color' id='color'/>
<script>
!
!
!
!
!
!
!
!
!
!
!
!
</script>
./data/panel.html
input[type=color]を使って
badgeの色を変える
var btn = document.getElementById('increament');
btn.addEventListener('click', function(){
addon.postMessage({
type: 'increament'
});
})
var color = document.getElementById('color');
color.addEventListener('change', function(){
addon.postMessage({
type: 'color',
value: color.value
});
})
<h1>Panel!!!!!!!</h1>
<button id='increament'>++</button>
<input type='color' id='color'/>
<script>
!
!
!
!
!
!
!
!
!
!
!
!
</script>
./data/panel.html
messageはJSONserializeされる
var panel = panels.Panel({
contentURL: "./panel.html",
onHide: function(){
button.state('window', {checked: false});
},
onMessage: function(message){
switch(message.type){
case 'increament':
button.badge = button.badge + 1;
break;
case 'color':
button.badgeColor = message.value;
break;
}
}
});
main.js(抜粋)
message.typeで挙動を切替
サイドバーを表示する
サイドバー
Chrome Extensionだとこれが出来ない
扱い方の要領はpanelと同じ
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html'
})
main.js
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html'
})
main.js
main.js
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
worker.port.emit('ping', '');
}
})
main.js
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
worker.port.emit('ping', '');
}
}) サイドバーが表示されたら
messageを送る
<script>
!
!
!
</script>
addon.port.on('ping', function(){
alert('pong');
})
./data/sidebar.html
<script>
!
!
!
</script>
addon.port.on('ping', function(){
alert('pong');
})
./data/sidebar.html
alertを出す
サイドバーでAjaxを実装する
サイドバーからはCross-Originの制約でXHRなどで
外部に通信できない
panelなどはJSをファイルに分けて、
contentScriptFileなどで読込めばXHR出来る
→main.jsで代わりに通信して結果を
 messageで送信する
package.jsonに追記
通信先のプロトコルとドメインを記述する
Wildcardは使えないので注意
"permissions": {
"cross-domain-content": [
"http://example.org/",
"https://example.com/"
]
}
Request = require("sdk/request").Request;
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
Request({
url: "http://api.openweathermap.org/data/2.5/weather?
q=Kyoto,jp",
// 天気情報を取得するAPIを利用してみる
onComplete: function(response){
worker.port.emit('ping', response.json)
}
}).get()
}
})
main.js
Request = require("sdk/request").Request;
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
Request({
url: "http://api.openweathermap.org/data/2.5/weather?
q=Kyoto,jp",
// 天気情報を取得するAPIを利用してみる
onComplete: function(response){
worker.port.emit('ping', response.json)
}
}).get()
}
})
main.js
外部と通信するために
Requestモジュールを利用
Request
XMLHttpRequestをベースにしたオブジェクト
!
option:
url: 通信先のURL
onComplete: 通信成功後に呼ぶ関数
content: GETやHEADのquery、POSTやPUTのbody
contentType: request headerのContent-Typeの内容
header: request header
require("sdk/request").Request(option)
Request methods
例: GET
!
get()
head()
post()
put()
delete()
require("sdk/request").Request(option).get()
Request = require("sdk/request").Request;
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
Request({
url: "http://api.openweathermap.org/data/2.5/weather?
q=Kyoto,jp",
// 天気情報を取得するAPIを利用してみる
onComplete: function(response){
worker.port.emit('ping', response.json)
}
}).get()
}
})
main.js
成功したらportで
サイドバー内のJSに送る
Request = require("sdk/request").Request;
sideBar = require("sdk/ui/sidebar").Sidebar({
id: 'sample-sidebar',
title: 'MiracleSidebar',
url: './sidebar.html',
onAttach: function(worker){
Request({
url: "http://api.openweathermap.org/data/2.5/weather?
q=Kyoto,jp",
// 天気情報を取得するAPIを利用してみる
onComplete: function(response){
worker.port.emit('ping', response.json)
}
}).get()
}
})
main.js
通信結果を保持しているオブジェクト
text: plain text
json: JSON.parse()の結果
status: status code ( ex: 200 )
statusText:
headers: HTTP response headerのK/Vオブジェクト
<script>
!
!
!
!
!
!
!
!
!
!
!
</script>
addon.port.on('ping', function(data){
var weather = data.weather[0];
document.body.innerHTML = `
<ul>
<li>Weather: ${weather.main}</li>
<li>Description: ${weather.description}</li>
<li>Sunrise: ${Date(data.sys.sunrise)} </li>
<li>Sunset: ${Date(data.sys.sunset)}</li>
</ul>
`;
})
./data/sidebar.html
<script>
!
!
!
!
!
!
!
!
!
!
!
</script>
addon.port.on('ping', function(data){
var weather = data.weather[0];
document.body.innerHTML = `
<ul>
<li>Weather: ${weather.main}</li>
<li>Description: ${weather.description}</li>
<li>Sunrise: ${Date(data.sys.sunrise)} </li>
<li>Sunset: ${Date(data.sys.sunset)}</li>
</ul>
`;
})
./data/sidebar.html
メッセージを受け取って
HTMLとして出力
諸々モジュール紹介
clipboard, context-menu, hotkeys,
notification, page-mod, simple-prefs …
clipboard
var clipboard = require("sdk/clipboard");
clipboard.set("KMC is great!");
var contents = clipboard.get();
//set some HTML
clipboard.set("<marquee>KMC KMC KMC</marquee>", "html");
context-menu
コンテキストメニューを表示する
Chromeと違って細やかに設定できる
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
メニューアイテム
を生成
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
表示するテキスト
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
アイコンを出すことも
出来る
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
表示するコンテキストを
指定する
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
こういう指定も出来る
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
PreficateContextは
引数の関数がtrueを返す場合に
メニューアイテムを表示する
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
PreficateContextは
引数の関数がtrueを返す場合に
メニューアイテムを表示する
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
documentType
text/html, image/jpeg
documentURL
ページのURL
targetName
タグ名
targetID
id属性
isEditable
contenteditable?
selectionText
選択中の文字列
srcURL
<img>のsrc属性
linkURL
<a>のhref属性
value
<input><textarea>のvalue属性
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
MenuはItemを
子に持つことが出来る
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
ページ上のあらゆる箇所
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
child, sp, child2を
子メニューとして持つ
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
セパレータを生成
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
親メニューの無いアイテム
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
メニューを表示している状態で
このキーを押すと選択したことになる。
キーの組み合わせ不可
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
}); URLでマッチさせる
var cm = require("sdk/context-menu");
var sp = cm.Separator();
var child = cm.Item({
label: "Child Item 2 show only on <img>",
image: require("sdk/self").data.url("icon-16.png")
});
child.context.add(cm.SelectorContext('img'));
var child2 = cm.Item({
label: "This item show only on some context",
context: cm.PredicateContext(function(context){
return (
(context.documentURL.match(/kmc/)) ||
(context.selectionText && context.selectionText.length > 3) ||
(context.srcURL && context.srcURL.match(/png$/))
)
})
});
var menu = cm.Menu({
label: "sample menu",
context: cm.PageContext(),
items: [child, sp, child2]
})
var menu2 = cm.Item({
label: "Welcome KMC",
accesskey: '1',
context: cm.URLContext("*.kmc.gr.jp"),
contentScriptFile: './welcome-kmc.js'
});
選択された際に実行されるJS
var { Hotkey } = require("sdk/hotkeys");
!
var myPanel = require("sdk/panel").Panel({
...
});
!
var showHotKey = Hotkey({
combo: "accel-shift-o",
onPress: function() {
myPanel.show();
}
});
hotkeys
hotkeys
alt: [alt]、Macでは [option] キー
meta: [Meta] ([#] キー)、Macでは [⌘]
accel: [ctrl]、Macでは[⌘]
shift, control, pageup, pagedown
notification
var notifications = require("sdk/notifications");
!
notifications.notify({
title: "Mailer",
text: "Got new 2 mails!",
onClick: function(){
console.log('clicked')
},
iconURL: "./myIcon.png"
});
page-mod
user.jsなどのように閲覧しているページ内に
JSを埋め込む。
content script と呼ばれることが多い
page-mod
var pageMod = require("sdk/page-mod");
!
pageMod.PageMod({
include: "*",
exclude: "*.kmc.gr.jp",
contentScriptFile: [
"./jquery-1.7.min.js",
"./my-script.js"
]
});
page-mod
content script 内で port や postMessage を使
う場合は window.self のメソッドとして利用
できる
simple-prefs
設定画面にボタンなどを配置する
package.json に記述しておく
"preferences": [{
"name": "somePreference",
"title": "Some preference title",
"description": "description text",
"type": "string",
"value": "this is the default string value"
}]
simple-prefs
type: 種別を指定(後述)
name: JS内で利用するための名前
   (propaty name として valid な必要がある)
title: 設定画面でラベルに使われる
description: 設定に関する説明
hidden: 非表示にする ( boolean )
value: デフォルト値
simple-prefs
値を取り出す時は name を使う
"preferences": [{
"name": "somePreference",
"type": "string",
"value": "this is the default string value"
}]
var preferences = require("sdk/simple-prefs").prefs;
// Get
console.log(preferences.somePreference);
// Update
preferences.somePreference = "this is a new value";
package.json
main.js
type (1)
bool: <input type='checkbox'>
boolint: checkbox が表示されるが値はT/Fで
はなくて on off の値が利用される
"preferences": [{
"type": "boolint",
"on": "1", // value for true must be string
"off": "2", // value for false must be string
"value": 1
}]
type (2)
integer: <input type='number'>
string: <input type='text'>
color: <input type='color'>
file: <input type='file'> フルパスを得る
directory: ディレクトリのパスを得る
type (3)
menulist: ドロップダウンリストが表示される
radio: <input type="radio">
{
"type": "menulist", // or "radio"
"options": [
{
"value": "0", //must be string
"label": "nona"
},
{
"value": "1",
"label": "prime"
}
]
}
type (4)
control: <button> {
"type": "control",
"label": "Click me!",
"name": "sayHello",
"title": "Say Hello"
}
var sp = require("sdk/simple-prefs");
sp.on("sayHello", function() {
console.log("hello");
});
package.json
main.js
type (4)
control: <button> {
"type": "control",
"label": "Click me!",
"name": "sayHello",
"title": "Say Hello"
}
var sp = require("sdk/simple-prefs");
sp.on("sayHello", function() {
console.log("hello");
});
package.json
main.js
type (4)
control: <button> {
"type": "control",
"label": "Click me!",
"name": "sayHello",
"title": "Say Hello"
}
var sp = require("sdk/simple-prefs");
sp.on("", function() {
console.log("hello");
});
package.json
main.js
空にすると全てのcontrollの
clickを受け取る
まとめ
ズサッと一気に Add-on SDK の世界観を紹介しました。
サンプルコードも用意したので、MDNなどと合わせて
上手く活用して良いAdd-onを作って下さい。
Firefoxをより便利にして
最高のインターネットライフを
手に入れよう😍😍😍😍
Do you have any questions?🙉
おまけ
※時間が余った時など用
野良アドオン
について
公式配布サイト以外に関する対応
Chrome
2014/05 から Windows 向けの Stable と
Beta チャンネルのみ外部サイトからインス
トールした Extension は無効化
Firefox
AMO以外で配布する場合は要署名
Nightly と Developer Edition は除外
http://googledevjp.blogspot.jp/2014/03/chrome-chrome.html
https://dev.mozilla.jp/2015/02/extension-signing-safer-experience/
Firefoxにおける影響
アドオンのテストを行う場合はDeveloper Edition、
Nightly あるいはいずれかのノーブランド版で行う必
要がある。
AMOで配布しない場合も一度署名のために AMO へ
拡張機能ファイルをアップロードするという必須手
順が導入される。

Firefox Add-on SDK 入門