読者です 読者をやめる 読者になる 読者になる

初めてのGoogleChromeExtensions。notificationを使ってみる

JavaScript Google Chrome Extensions perl

ネットで買ってる日用品がそろそろ切れそうで、そろそろ買おうかなぁと見てみたら、
ちょうど2,3日前に買ったら1000円くらい安かったことがわかり、
何か悔しくなったので「価格.comの最安値をチェックして安い時に買ってやろう」という気になりました。

まずはPerlで。Web::Scraperで。

とりあえず思いついたのはWeb::Scraperで定期的に取得。

#!/usr/local/bin/perl
use strict;
use warnings;
use Web::Scraper;
use URI;

my $url = shift;
my $scraper = scraper {
    process '#minPrice > a', 'price' => 'TEXT';
    result 'price';
};

my $price = $scraper->scrape(URI->new($url));
$price =~ s/[^\d]//g;

print $price;

こんな感じでスクリプト実行時に価格.comの該当ページのURLを渡すだけ。
'#minPrice > a'のxpathで取得できるaタグのテキストが値段で、値段の数字だけわかれば良いので無駄なものは置換で削除と。
頻繁に叩くならキャッシュした方がいいかなと思ったけど、
多分1日に2、3回だろうと思ってとりあえずいいやとなりました。



とりあえずで書き始めたけど相変わらず素晴らしいなぁWeb::Scraper。素敵すぎる。何か気持ちいいです。
で、さくっと書き終わって、「これをcronに仕掛けようかなぁ」と思ったとこで、
「ぁ、どこに出力しようかな」となりました。



メールで飛ばすとか、ファイルに落とすとか、IMに飛ばすとか、色々考えたんですけど、
色々考えるうちにその先をつくるのが面倒になりました。


いつも見ている場所はブラウザ

ということで、「こいつをCGIにしてスタートページみたいなところで表示すればいっか」となったのですが、
「スタートページなんか設定してないので新しくページ作ることになってダメだな」となり、
その結果「いつもGoogleChrome開いてるからnotificationで表示させればいっか」となりました。


Google Chrome Extensionsの作り方

以上の流れでGoogle Chrome Extensionsを作ることにしたんですけど、
前に軽く見ただけで作り方ちゃんと知らないためTutorialページを参考にしながらスタート。


まずはディレクトリ作ってmanifest.jsonを用意

googlechrome-extensions」ってディレクトリを作り、その中に入り、
manifiest.jsonってのを作ります。
ここに拡張機能の名前やら、バージョンやら、アイコンへのpathやらを書いていくわけですね。

{
  "background_page": "background.html",
  "name": "My First Extension",
  "version": "0.0.1",
  "description": "The first extension that I made.",
  "browser_action": {
    "default_icon": "icon.ico",
    "popup": "popup.html"
  },
  "options_page": "options.html",
  "permissions": [ "tabs", "http://kakaku.com/*", "notifications" ]
}

作り終わったあとなので色々埋まってますけど、

background_page: インストールして初めに実行され、そのまま裏側で動いてるページ
popup: 拡張機能のアイコンをクリックした時に実行されるページ
options_page: 拡張機能のオプションを設定するためのページ

くらいの理解でいいんじゃないかと思います。
(僕も適当に写しただけであまり理解してません)



permissionsは「どこへのアクセスを許すか」かな?
この辺はGmail Notifierを参考にしました。
(と言うより、htmlも含めほぼ丸々劣化コピーみたいなものです。)



最後のnotificationsが重要で、これがないとnotificationが出てくれません。
普通のページだとユーザにアクセス制限を許可するようにとかできるみたいなんですけど、
拡張機能からだとそれができないみたい。
(ここで結構ハマった。)



default_iconは拡張機能のiconですね。
今回はどうでもいい拡張機能なので、256 Pixels - a daily favicon design challengeに行って、適当に作りました。


background.html、options.html、popup.htmlの動き

拡張機能の動きを決めないと作りようがないので、以下のような感じでイメージしました。

  1. インストールするとオプション設定画面が開く
  2. オプション設定画面で価格.comのURLを入力して保存すると値段が表示される
  3. 12時間ごとに値段を再取得して表示する
  4. 拡張機能のアイコンをクリックした時にはオプション設定画面が開く
基盤となるbackground.htmlの作成

作りが決まったので作成。
まずは「インストールするとオプション設定画面が開く」という状態を作りたい。
background.htmlを実行しようとするので、background.htmlのHTMLを書いてしまいます。

<!DOCTYPE html> 
<html>
<head>
</head>
<body>
</body>
</html>

こんな感じ。
HTMLでは何もしないのでこれで良さそう。



オプション設定画面を開きたいので、options.htmlに遷移するJavaScriptを加えてあげます。

        chrome.tabs.create({
            url: "options.html"
        });

これでOK。
これだけで新規のタブを開いて、そのタブをoptions.htmlの画面にしてくれます。
ということで、background.htmlは下のようになります。

<!DOCTYPE html> 
<html>
<head>
</head>
<body>
</body>
</html>
<script type="text/javascript">
init();

function init() {
    chrome.tabs.create({
        url: "options.html"
    });
}
</script>

backgound.htmlを開いたらinitが実行されてoptions.htmlが開かれると。
簡単簡単。

オプション設定画面を担当するoptions.html

次にoptions.html。
この画面はGmail Notifierからいただいてきました。
結果から先に貼ると、

<!DOCTYPE html> 
<html>
<head>
<title>input kakaku.com</title>
<style type="text/css">
body {
  font-family:helvetica, arial, sans-serif;
  font-size:80%;
  margin:10px;
}

#header {
  padding-bottom:1.5em;
  padding-top:1.5em;
}

#header h1 {
  font-size: 156%;
  display:inline;
}

.section-header {
  background:#ebeff9;
  border-top:1px solid #b5c7de;
  font-size:99%;
  padding:3px 0 2px 5px;
  font-weight:bold;
  margin-bottom:1em;
  margin-top:4em;
}
.section-header.first {
  margin-top:1em;
}
#kakakuComUrl {
  width:300px;
  margin-left:2px;
}
#footer {
  margin-top:4em;
}
</style>
</head>
<body>

<div id="header"><h1>input kakaku.com</h1></div>

<div class="section-header first">Custom Domain</div>
<p>input kakaku.com url here.</p>
<p>
  <input type="text" id="kakakuComUrl">
    <em>(eg http://kakaku.com/item/K0000084168/)</em>
</p>

<div id="footer">
  <button id="saveButton" style="font-weight:bold" onclick="save()">Save</button>
</div>

</body>
</html>
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript">
init();

function init() {
  load();
}

function load() {
  $('#kakakuComUrl').val(localStorage.kakakuComUrl || "");
}

function save() {
  localStorage.kakakuComUrl = $('#kakakuComUrl').val();
  if (! localStorage.kakakuComUrl) {
      return;
  }

  localStorage.hasExecuted = true;
  chrome.extension.getBackgroundPage().init();
}
</script>

こんな感じ。
htmlの中身は「URLを入力するためのinput」と「それを保存するためのsaveボタン」があるだけです。



JavaScriptでやってることは、

  1. initを呼び出す
  2. 既にURLが入力してる場合用にloadを呼び出す
  3. loadでは、URLを既に保持している(localStorage.kakakuComURLに保存している)場合にはそれをinputに入れ、そうでない場合はから文字を入れる
  4. saveボタンがクリックされたらsave()を呼び、inputの値をlocalStorage.kakakuComURLに保持+オプション設定が終わったフラグのlocalStorage.hasExecutedをtrueにし、background.htmlのinit()を呼び出す

という感じです。
localStorageで値を使い回すと楽ってことと、
chrome.extension.getBackgroundPage()でbackground_pageが取得でき、その内部の関数が呼び出せる
ってことくらいですかね。

再びbackground.html。価格.comページのデータを取得する機能とNotificationの追加。

ということで、background.htmlに戻ります。
initが「URLを保持していなければオプション設定画面を開き、保持していればその価格.comのページに値段を取得しに行く」という動きに変わるため、
下のように書き換えられます。

function init() {
    if (! localStorage.hasExecuted) {
        localStorage.hasExecuted = true;
        chrome.tabs.create({
            url: "options.html"
        });
    }
    else {
        getHtml();
    }
}

(オプション設定画面が設定した場合はlocalStorage.hasExecutedがtrueなので。)



で、実際に値段を取得しに関数ですが、jQueryを使って下のように書けば良いと。

function getHtml() {
    var url = localStorage.kakakuComUrl;
    $.get(url, setNotification);
    setTimeout(getHtml, 12 * 60 * 60 * 1000); // semidiurnal
}

localStorageに保持しているURLを$.getに渡し、返ってきてデータをsetNotification関数に渡します。
最後とのsetTimeoutは12時間毎に実行したいので繰り返し処理です。



価格.comのページまるごとのデータがsetNotificationに渡ってくるので、
setNotificationではjQueryxpathを使ってタイトルと値段だけ抜き出します。

function setNotification(data) {
    var $doc = $(data);
    var title = $doc.find('h2').text();
    var price = $doc.find('#minPrice > a').text();
    webkitNotifications.createNotification('icon.ico', title, price).show();
}

渡ってきたdataをjQueryオブジェクトに変換し、Web::Scraperの時と同様にxpathで取得。



そして、最後の行がnotificationを表示する部分です。

webkitNotifications.createNotification('icon.ico', title, price)

でnotificationのオブジェクトを作成し、そのオブジェクトのshow()メソッドを呼び出すと、
ピロっと出てきてくれます。
(HTMLにしたい場合はcreateHTMLNotificationとかで。)



cancel()を実行すればnotificationが消えるので、Gmail Notifierではその設定もしてましたが、
そこまでやる必要もないので特に書きませんでした。



background.htmlのまとめが下のものです。

<!DOCTYPE html> 
<html>
<head>
</head>
<body>
</body>
</html>
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript">
init();

function init() {
    if (! localStorage.hasExecuted) {
        localStorage.hasExecuted = true;
        chrome.tabs.create({
            url: "options.html"
        });
    }
    else {
        getHtml();
    }
}

function getHtml() {
    var url = localStorage.kakakuComUrl;
    $.get(url, setNotification);
    setTimeout(getHtml, 12 * 60 * 60 * 1000); // semidiurnal
}

function setNotification(data) {
    var $doc = $(data);
    var title = $doc.find('h2').text();
    var price = $doc.find('#minPrice > a').text();
    webkitNotifications.createNotification('icon.ico', title, price).show();
}
</script>
拡張機能アイコンをクリックされた時に動くpopup.htmlをちょろっと。

最後にpopup.htmlはオプション設定画面を表示すれば良いだけなので、
下のように書きました。

<!DOCTYPE html> 
<html>
<head>
<style type="text/css">
body {
    display: none;
}
</style>
</head>
<body onload="openOptions()">
</body>
</html>
<script type="text/javascript">
function openOptions() {
    chrome.tabs.create({
        url: "options.html"
    });
}
</script>

Google Chromeへの組み込み

ということで、ここまでで「googlechrome-extensions」フォルダ配下には下のものが入ってます。

background.html
icon.ico
jquery-1.4.2.min.js
manifest.json
options.html
popup.html

あとはこれをGoogle Chromeに認識してもらえば良いので、
chrome://extensions/」とアドレスバーに打つか、メニューから「拡張機能」を開き、
デベロッパー モード」をONにして、「パッケージ化されていない拡張機能を読み込みます」で「googlechrome-extensions」のディレクトリを読み込めば終了。
おつかれさまでした。



実際に実行すると、macの場合はこんな感じで表示されます
http://img.skitch.com/20100705-dmqrnu9rfb4njc81m6jfdmrstx.jpg


デバッグ

初めpopup.htmlで色々試していたのですが、
作りながら「何でconsole.logが出ないんだっけかなぁ」と思って結構ハマりました。



拡張機能から通常ページのコンソールには出力されないんですね。
それぞれ以下のようにしながらコンソールログを確認しました。

background.htmlの場合は拡張機能のページのリンクを叩いて確認
options.htmlはoptions.htmlのページを開いてそこで確認
popup.htmlはアイコンを右クリックして「ポップアップを検証」で確認

コンソールログが確認できれば開発もだいぶ楽かなと。



たまたま思い立ってgoogle chrome拡張機能を作ってみましたが、
hello worldくらいはすぐできるのでみんなやるといいですよね。
ひな形みたいなのを用意しておけばgreasemonkey的な感じでさくさく細かいのが作れるんじゃないかなぁと。
要らなくなったら捨てるとかすればいいし。