GreaseMonkeyとDOM/prototype.js入門
目標
- GreaseMonkeyで実際のサイトを書き換えてみよう
- DOM理解してprototype.jsを使ってみよう
intro
GMとは
直訳すると「整備士」。特定のページを見たときに自分で用意したJavaScriptを実行することが出来る。おもな使い道としては、ページのデザインなどの改善(デザインの改善ならStylishでも出来る)と、ちょっとした機能追加。
GMの導入
firefox のアドオン : GreaseMonkey - Firefox addon
※safariやIEにも同様のものがある (参考:GreaseMonkeyとは@hatena)
便利なスクリプト紹介
- AutoPagerize
- JavaAPI検索
- サイト内検索
実際には、個人の利用に応じた機能追加・改造を行うことが多いので、これといったものはあまり多くない。
情報源
まとめ系
- Greasemonkey - Mozilla Firefox まとめサイト
- →大体一般的な情報はここから辿れる
入門
- Dive Into Greasemonkey
- mozdev.org - greasemonkey: authoring(日本語訳)
- 特集:Greasemonkeyによるアプリケーション開発|gihyo.jp … 技術評論社
- →チュートリアル
リファレンス的
- Gmail Greasemonkey API リファレンスを翻訳しました - WebOS Goodies
-
- →JavaScriptやDOM,CSSなどで知りたいとき
TIPS
-
- →実際に開発を始めるにあたって便利
- Greasemonkeyで永続的に外部スクリプトを利用する - 技術メモ帳
- →外部ライブラリの使いかた
- GreaseSpot
- →開発する上でかなり重要な情報が集まっている
その他
- Userscripts.org
- →スクリプトがいろいろ集まっている
- 「Greasemonkeyスクリプティング TIPS&SAMPLES」と言う本を書きました
関連ツール
- IE7Pro - The Ultimate Add-On for Internet Explorer
- GreaseKit - User Scripting for all WebKit applications
GMを始める
基本的なGM開発の流れを確認しましょう。
GMでの新規スクリプト
GMのアイコンからメニューを出して新規スクリプトを作ってみましょう。
初回の場合、ここでエディタ設定を行う。後で変更するには、 about:config から greasemonkey.editor の値を変更する。
名前空間はGM_setValueやスクリプトの入れ替えのときに使われるもの。個人でひとつ持っておいて、hatenaや自分管理のURLにしておくことが多い。
特定のサイトで以下のようなコードを書いて hello world を出す。
document.body.innerHTML = "<h1>hello world</h1>";
議論
抑えなければいけないポイント:エラーはエラーコンソールを見る。メニューの「Tool」→「Error Console」。
デバッグのポイントとしてはログを一番最初に押さえておく GM_log, console.log。
スクリプトはUTF8で書く。昔は書けなかったのでエスケープしていたことがある。(このあたり、いろいろ変わってきた歴史があるので、人のコードを参考にする場合は気を付ける。変わってきた原因としては機能追加とセキュリティー対応)
コツとしては自分コードを集めておいてコピペする。ライブラリとかでまとめていくよりはコピペが早いかも。
書き換え、サイトの再構築実習
実際にサイトを書き換える練習問題をやってみて、DOMとprototype.jsについて理解しましょう。
GoogleのロゴをYahooにしてみる
GoogleのロゴをGMでYahooのロゴに入れ替えてみましょう。
ポイント:
- firebugのinspectで調査
- consoleでコードの確認
某新聞社のニュースサイトをシンプルにしてみる
記事のみを一覧表示させて、素早くニュースを確認できるように見やすく整形してみる。
1:右側の記事でないエリアを削除
邪魔だと思う箇所を消す方法で、整形をやってみましょう。
- firebugのinspectで、全体のHTMLの構成を確認してください。
- 記事でないエリアの要素のIDを調べてください。
- console上で、そのIDで document.getElementById して、 style.display = "none" して消してみましょう。
- GMでやってみましょう。
議論
- 消す前に画面に一瞬出てしまう → DOMcontentloadedのイベントでGMが動くから。
- display: "none" するだけなら stylish の方が楽。
2:記事一覧の内容だけ表示する
上とは逆に必要なものを取ってくる方法を考えてみましょう。
- firebugで記事の一覧を格納している要素を探してみてください。
- firebugでクリックした後、$0.innerHTML で中身を確認してみてください。($0でクリックした要素にアクセスできる)
- document.body.innerHTML に、その中身を入れてみて表示を確認してみましょう。
- GMでやってみましょう。
3:a要素を集める
上ではまだ元のレイアウトが残っていました。今度はもっと積極的に、必要なタグのみ集めてみましょう。
- document.getElementsByTagName でa要素を列挙して、それぞれの中身を確認してみてください。
- 欲しい記事のリンク以外もたくさん含まれています。
- 良く見ると、liの直下のa要素が記事へのリンクになっているようです。
- DOM APIを使って、aの親がliであるものだけを集めるプログラムをconsole上で書いてみましょう。
- 集めたaを使って、ulのリストを作ってみましょう。
- GMでやってみましょう。
DOMとDOM APIの簡単な説明
- タグの入れ子とツリー構造
- 良く使うプロパティとメソッド(tagName, length, childNodes, parentNodeなど)
- 今回は tagName と parentNode
- MDCやその他ドキュメントでAPIを調べる
- Firebugで調べる方法の確認
プログラム例:
var tags = document.getElementsByTagName("a");
// liが親のaを集める
var list = [];
for(var i=0;i<tags.length;i++) {
if (tags[i].parentNode.tagName == "LI") {
list.push(tags[i]);
}
}
// 集めたaからリストを作る
var html = ["<ul>"];
for(var i=0;i<list.length;i++) {
var a = list[i];
html.push("<li>");
html.push("<a href='"+a.href+"'>"+a.textContent+"</a>");
}
html.push("</ul>");
document.body.innerHTML = html.join("\n");
議論
- tagNameは大文字か小文字か? → doctypeで決まっている。参考:DOM:element.tagName « Gecko DOM リファレンス
- プログラムがアレ過ぎないか? → とりあえず黙って続きを待て。
4:prototype.js の Array.select, mapを使ってみる
ちょっと上のプログラムは洗練されていないため、prototype.jsを使ってもう少しモダンな書き方をしてみましょう。
プログラム上の問題点の整理:
- 似たようなループが続く
- →ループは頻出するためもう少し短く書きたい。
prototype.jsについて
- prototype.jsについての簡単な説明
- Arrayのいくつかのメソッド(each, select, map)について説明
- 対象サイトには既にprototype.jsが組み込まれている。
解決方針:
- document.getElementsByTagNameで取ってきた要素の配列を$Aでprototype.jsのArrayにする
- if文でa要素を選択している部分をselectにする
- a要素の配列を文字列の配列に変換している所をmapにする
プログラム例:
// サイトのライブラリを使いたい
var $A = unsafeWindow.$A;
var tags = document.getElementsByTagName("a");
tags = $A(tags);
// liが親のaを集める
var list = tags.select(function(elm){
return elm.parentNode.tagName == "LI";
});
// 集めたaからリストを作る
var html = list.map(function(a) {
return "<li><a href='"+a.href+"'>"+a.innerHTML+"</a>";
});
html.unshift("<ol>");
html.push("</ol>");
document.body.innerHTML = html.join("\n");
もう少し短くまとめられる:
$A(document.getElementsByTagName("a")).select(function(elm){
return elm.parentNode.tagName == "LI";
}).map(function(a) {
return "<li><a href='"+a.href+"'>"+a.innerHTML+"</a>";
});
html.unshift("<ol>"); html.push("</ol>");
ここで行ったような「集めて→フィルター→加工→結果」というパターンはWebの整形だけでなく、いろいろなプログラムに応用できるパターン。関数的。
議論
- unsafeWindow とは?
- 対象サイトのグローバルコンテキストであるwindowオブジェクト。対象サイトで定義してある関数やライブラリにアクセスするために必要。
- セキュリティ上問題がある。このオブジェクトを通してWeb側からGMの実行環境で任意のコードを実行することが出来る。GMの環境はセキュリティ的に緩いので問題がある。
- 関数を使うだけではなくて、触れるだけでもだめ(getter setterで実装が関数だったりするので)
- 特にどのサイトでも有効になるスクリプトは危ない。逆に特定のサイトだけ有効になるスクリプトは、そのサイトの信頼性と同程度のリスク。
- セキュリティの問題はGM自身の設計・実装の問題のように思われる。グローバルに関数がいろいろあるし、もっといいフレームワークが設計できるのではないか?
- まだプログラムに納得がいかないんですが・・・ → まだ続きがあります。もう少し待ちなさい。
5:$$でもっとスマートに集めてくる
上で作ったリストを良く見ると、まだ必要ないa要素があります。prototype.jsの$$関数を使って、もっと複雑な条件で要素を集めてくる方法を試してみましょう。また、文字列では無くDOMオブジェクトで結果を組み立ててみましょう。
- $$関数、CSSセレクタについて確認してください。
- prototype.jsの便利関数のひとつ。クラス名で集めてくる時に良く使う。
- CSSで要素を特定する書き方とほぼ同じ
- console上で $$('ul.list li a')とやって、結果の中身を確認してみましょう。
- $$を使って、GMのプログラムを書き変えてみましょう。
- 文字列では無く、DOMを使ってHTMLを組み立ててみましょう。
- 文字列はわかりやすいが、拡張しづらく複雑な組み立てが難しい。
- DOM API の確認: createElement, appendChild
プログラム例:
var $$ = unsafeWindow.$$;
var tags = $$("ul.list li a");
var ul = document.createElement("ul");
tags.each(function(a) {
var li = document.createElement("li");
li.appendChild(a);
ul.appendChild(li);
});
document.body.innerHTML = "";
document.body.appendChild(ul);
議論
- document.body.innerHTML = "";
- 手っ取り早くbodyの中身をクリアする。たぶん間違いでは無いらしい。
- xpathも良く使う。
6:MashUpしてみる
別のサイトから情報を取ってきて機能を追加してみましょう。
- hatenaブックマーク数を表示する方法を確認してください。
- 記事のリンクの横に、はてなブックマーク数を入れてみましょう。
プログラム例:
var $$ = unsafeWindow.$$;
var tags = $$("ul.list li a");
var ul = document.createElement("ul");
tags.each(function(a) {
var li = document.createElement("li");
li.appendChild(a);
var img = document.createElement("img");
img.src = "http://b.hatena.ne.jp/entry/image/normal/"+a.href;
li.appendChild(img);
ul.appendChild(li);
});
document.body.innerHTML = "";
document.body.appendChild(ul);
議論
- 記事の数が多いとリクエストアクセスが集中して凶悪かもしれない。
- callLaterなどを使って一度にたくさんアクセスが集中しないような仕組みを入れるべき
- 特定の文字列を含む記事を選んだり、記事の数でソートなどを行うと楽しそう。
- →少しずつでもやってみると、いろいろな発展が見えてくる。
7: XMLHttpRequestを使ってScraping
XHRを使ったページの切り取り手法を使ってさらに機能追加してみましょう。
- やりたいこと:クリックしなくても記事のサマリーを確認したい
- →マウスでリンクを触れるとポップアップで記事の第1パラグラフを表示する
- 実装方針:
- 上のリンク一覧で、a要素にmouseover, mouseoutイベントを付ける
- イベントの中でポップアップ表示、削除を行う
- ポップアップ表示のタイミングでXHRでリンク先のページの内容を取得して、必要な箇所を切り取って表示する
実際にやってみる
- XMLHttpRequest(いわゆるAjax)について仕組みと使いかた例を確認してください。
- 文字化けは overrideMimeType で文字コードを指定する。
- DOM API でイベントの扱いかたを確認してください。
- DOM:element.addEventListener « Gecko DOM リファレンス
- addEventListenerの最後の引数のフラグは必ず必要。無いと動かない。
- capture phaseとbubbling phaseの確認。参考:Event dispatch and DOM event flow
- イベントフロー検証ツール | Diaspar Journal
- リンク先のページで、記事の一部を抜き出す関数・正規表現をconsoleで確認してください。
- 以上を組み合わせてGMで実現してみてください。
プログラム例:
var $$ = unsafeWindow.$$;
var tags = $$("ul.list li a");
var ul = document.createElement("ul");
tags.each(function(a) {
var li = document.createElement("li");
li.appendChild(a);
var img = document.createElement("img");
img.src = "http://b.hatena.ne.jp/entry/image/normal/"+a.href;
li.appendChild(img);
ul.appendChild(li);
//ポップアップ表示、消滅
li.addEventListener("mouseover",function(event){ showDigest(event,li,a.href); },true);
li.addEventListener("mouseout",function(){hideDigest();},true);
});
document.body.innerHTML = "";http://diaspar.jp/node/61
document.body.appendChild(ul);
//ポップアップ表示
function showDigest(event,elm,href) {
hideDigest();
var div = document.createElement("div");
with(div.style) {
//classでやろう!
position = "absolute";
border = "3px dashed gray";
left = event.clientX+"px";
top = (event.clientY+unsafeWindow.scrollY+10)+"px";
background = "cornsilk";
padding = "5px";
zIndex = "1000";
}
div.id = "gm_digest";
retrieveDigestText(href,function(text){
div.innerHTML = text;
});
elm.appendChild(div);
}
function hideDigest() {
var elm = document.getElementById("gm_digest");
if (elm) {
elm.parentNode.removeChild(elm);
}
}
function retrieveDigestText(href,setter) {
GM_xmlhttpRequest({
method: 'get',
url: href,
overrideMimeType: document.contentType+"; charset="+document.characterSet,
onload: function(details){
setter(scrapingKiji(details.responseText));
}
});
}
function scrapingKiji(wholeText) {
var m = wholeText.match(/<div class="kiji"><p>([^<]*)/);
return (m) ? trim(m[1]) : "Not available...";
}
function trim(t) {
return t.replace(/^\s*/,"").replace(/\s*$/,"");
}
議論
- 「scrapingKiji」のKijiって何よ? → 抜き出すdivのclassが「kiji」なので・・・。
- CSSスタイル直書きなの? → とりあえずわかり易さ優先のサンプルなので・・・。
- 正規表現なの? → DOMを解析するより速い。複雑な場合はDOMを解析するほうが良い。
- 「.」は改行を含まない。改行を含む正規表現について議論。
- 相変わらずアクセスが凶悪そう。 → サンプル(ry。実際には一定時間待った方がいいでしょうね。
- show, hide とかではなくて、ポップアップのクラスを作って、ステートパターンで実装するべき。 → いや、だからサンプル(ry。
- 意外に速い。 → ページ読み込みの時間の8割以上はHTMLのロード以外に消費されている。前回復習事項。
まとめ
GreaseMonkeyでDOM操作とprototype.jsの簡単な使いかた講座をしようとしました。
→来場者のレベルが高すぎて、講演者のサンプルプログラムのツッコミ大会になってしまいました。