Sample RSS
以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。このウィジェットは、BlankWidget で解説したことは繰り返しませんので、そちらを参照してください。
1 ウィジェット
Sample RSS.wdgt は、以下のようなウィジェットです。
Apple サイトが提供している最新ウィジェット RSS を調べ、最新のウィジェットを表示します。一番上のタイトルをクリックすれば、その RSS をブウラザで表示します。また、各項目のタイトル上にマウスを移動させると下線が表示され、クリックされると RSS でのリンク先へと移動します。
2 ファイル構成
このサンプルのファイル構成は以下のようになります。
AppleClasses 内には、AppleScrollArea.js と AppleScrollbar.js という 2 つの JavaScript ファイル、そして Images という画像フォルダがあり、これらのファイルがスクロール動作をになっています。これらについては、Scroller で解説しているので、そちらを参照してください。
Info.plist では、ネットワークにアクセスするため、AllowNetworkAccess キーが値を true として設定されていることに注意してください。Apple クラス関連については、Scroller を見てください。主要 HTML は SampleRSS.html に指定されています。
Default.png は、ウィジェットの背景が含まれている画像です。images 内の background.png も同じ背景画像です。残りの画像については、後の説明を見てください。
3 SampleRSS.html
それでは、主要 HTML ファイルを見てみます。
SampleRSS.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<head>
<meta http-equiv='content-type' content='text/html; charset=utf-8'>
<style type='text/css' title='AppleStyle'>
@import "SampleRSS.css";
</style>
<script type='text/javascript' src='SampleRSS.js' charset='utf-8'/>
<!-- Pre-10.4.3 より前の互換性:
ウィジェットの一番上に AppleClass ディレクトリを含め、相対パスを使用 -->
<script type='text/javascript'
src='AppleClasses/AppleScrollbar.js' charset='utf-8'/>
<script type='text/javascript'
src='AppleClasses/AppleScrollArea.js' charset='utf-8'/>
</head>
<body onload='load();'>
<div id='front'>
<img src='Images/background.png'></img>
<div id='feed' onclick='clickOnFeedTitle(event);'>
Dashboard Widget Downloads</div>
<div id='contents'></div>
<div id='ohnoes'>An error occurred retrieving the feed.</div>
<div id='myScrollBar'></div>
<div id='line'></div>
</div>
</body>
</html>
後で JavaScript ファイルを見ればわかるように、スクロールバーは id が myScrollBar の div 要素に、スクロール領域は id が contents の div 要素に設定されています。このサンプルでは表示内容をネットワークで取得するので、contents 内が空になっていることに注意してください。
body には読み込み完了時のハンドラが設定されています。その下に front という id の div 要素があります。これはウィジェットの表面で、環境設定がある場合、back という id の div 要素や情報ボタンが置かれます。このサンプルでは必要ではありませんが、通常から、他の要素を除いた形で作られています。
表面の div 要素内に、このウィジェットの要素が配置されています。まず背景画像が置かれています。つぎにタイトルが置かれています。このタイトルはクリックされれば RSS をブラウザで表示するため、クリック時のハンドラが設定されています。つぎにスクロール領域として設定される内容が置かれ、エラーの場合に表示されることになる div 要素があります。この文字列 An error occurred retrieving the feed. は「フィードの取得においてエラーが起こりました。」です。それからスクロールバーとして設定される div 要素があり、最後に line という id の div 要素があります。
4 SampleRSS.css
スタイルシートを見てみましょう。注目すべき所だけを示しています。
SampleRSS.css
...
#feed {
...
cursor: pointer;
}
.row {
position: relative;
width: 354px;
height: 24px;
}
.light {
background: url(Images/light.png) repeat-x top left;
}
.dark {
background: url(Images/dark.png) repeat-x top left;
}
.title {
...
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
.title:hover {
text-decoration: underline;
cursor: pointer;
}
#ohnoes {
...
display: none;
}
...
ここには示していませんが、#feed-bottom という ID セレクタがありますが、このサンプルでは使用されていないことに注意してください。タイトル部分の feed の id 要素で、カーソル形状が pointer になっていることに注意してください。これによってクリックした時に動作が起こることを示しています。このプロパティには多くの値があるので、知りたい方は解説しているサイト等を検索してみてください。
row クラスは、取得した RSS 内容それぞれに対して作られる要素です。また、各行ごとに色分けをするため、交互に light と dark というクラスが指定されますが、これによって背景の画像を切り替えています。各行内のタイトルである title クラスには、text-overflow プロパティが ellipsis に指定されています。これによりタイトルが長すぎる場合、省略文字を使うようにしています。このプロパティは、他に clip という値があり、その場合はみ出したものは単に表示されません。このプロパティを設定するには、ブラウザによって white-space:nowrap を指定するか、<nobr> タグを使わないとダメなことがあることに注意してください。ここでは white-space:nowrap が指定されています。タイトル上にマウスボインタがきたときの疑似クラス .hover に下線とカーソル形状が設定されています。これにより、ウィジェット内のタイトル部分に下線が表示されています。
エラー表示を行う div 要素が、最初の状態では表示しないように設定されています。
5 SampleRSS.js
SampleRSS.js ファイル内では、まず読み込み完了のハンドラが呼ばれます。スクロール領域関連は、Scroller で解説しているので、そちらを参照してください。
SampleRSS.js > グローバル変数と loaded
var feed = {title:"Dashboard Widgets", top:"Dashboard Widgets", bottom:"US",
url:"http://www.apple.com/downloads/dashboard/home/recent.rss"};
var scrollArea, scrollbar;
function load ()
{
scrollbar = new AppleVerticalScrollbar(document.getElementById("myScrollBar"));
scrollArea = new AppleScrollArea(document.getElementById("contents"), scrollbar);
scrollArea.scrollsHorizontally = false;
scrollArea.singlepressScrollPixels = 15;
window.onfocus = function () { scrollArea.focus(); }
window.onblur = function () { scrollArea.blur(); }
if (!window.widget)
{
show ();
}
}
まず、feed 変数が定義されています。ここで取得する RSS の URL を設定しているので、使用 URL を変更したい時はここを修正すればいいです。たとえば、日本版のウィジェットを取得するなら、bottom:"Japan" で、url:"http://www.apple.com/jp/main/rss/downloads/hot_downloads.rss" とすればいいでしょう。後の 2 つの変数はスクロール領域用です。
body 要素の読み込み完了ハンドラでは、スクロール領域を設定しています。このあたりは Scroller サンプルと同じです。その後でウィジェット内で起動された場合でない時だけ show() を呼び出しています。ウィジェット内で呼び出された場合、これは呼び出されません。どのようにして内容を表示しているのでしょう。これは show の実装の後にある、関数外コードで行っています。これは関数外なので、スクリプトが読み込まれた時に実行されます。
このように、ウィジェット内で読み込まれた場合だけ、ウィジットが表示される場合のハンドラを設定しています。この onshow ハンドラはウィジェット独自のものです。そのため、loaded 関数内でウィジェット以外で読み込まれた場合の対応を行っていたわけです。さて、こうして呼ばれる show について見てみましょう。
SampleRSS.js > 変数と show
var last_updated = 0;
var xml_request = null;
//-----------------------------------------------------------------------------------
// show - ウィジェットが表示される時、Apple の RSS ホットニュース RSS を取得する要求をポスト
// サーバーに頻繁にアクセスしすぎないよう、少なくとも 15 分要求間隔を空ける
// また、新しいものをポストする前に、未解決の要求をキャンセルしたほうがいいことに注意
//-----------------------------------------------------------------------------------
function show ()
{
var now = (new Date).getTime();
// 現在時を取得
// 15 分が経過した場合だけチェックする
if ((now - last_updated) > 900000)
{
if (xml_request != null)
// 未解決の要求があるなら
{
xml_request.abort();
// 強制終了する
xml_request = null;
// 実行中変数をクリア
}
xml_request = new XMLHttpRequest();
xml_request.onload = function(e) {xml_loaded(e, xml_request);}
xml_request.overrideMimeType("text/xml");
xml_request.open("GET", feed.url);
xml_request.setRequestHeader("Cache-Control", "no-cache");
xml_request.send(null);
}
}
まず現在時を取得して、それと保存しておいた時刻との差を計算し、15分たっているか調べます。そうでなければ、終了です。15分たっていれば、最初に実行中の要求があるか調べます。あるなら中止させます。
つぎに XMLHttpRequest オブジェクトを作っています。これは JavaScript にあるオブジェクトで、もともとは Microsoft が作ったものですが、その後流布しました。これについては、『Web Kit DOM プログラミングトピック』(AppleApplications>Conceptual>SafariJSProgTopics)で説明されています。『Dynamic HTML and XML: The XMLHttpRequest Object』(正規日本語訳)でも説明されていますのでそちらも参照してください。また W3C の定義は『The XMLHttpRequest Object』にあります。
XMLHttpRequest は、オプションを設定し、onload または onreadystatechange を定義して、それから送信を開始します。前者は内容が読み込まれた場合のハンドラで、後者は状態変更時のハンドラです。
onreadystatechange ハンドラを使った場合、ハンドラ内でプロパティ readyState を使って通信状態値を取得します。この状態値は、0(uninitialized 読み込み前の初期状態)、1(loading 読み込み中)、2(loaded 読み込み終了)、3(interactive 読み込んだデータの解析)、4(complete 読み込んだデータの解析完了)となっています。onload ハンドラは、4 の場合だけに呼ばれることになります。ここでは、完了した後のデータにのみ興味があるので onload ハンドラを使っています。
つぎに overrideMimeType で取得するものの MINE タイプを強制的に設定しています。サーバーが正しい Content-Type ヘッダを送信してこない場合でも、これを行うことでデータを正しく扱えます。このサンプルではハンドラ内で responseXML というプロパティを使っていますが、これは正しい Content-Type ヘッダでないとうまくいきません。そのため、こうしています。
それから要求を初期化するために open を行っています。ここでは取得メソッドであることと URL を設定しています。メソッドには、GET(最終内容取得)、HEAD(ヘッダのみ取得)、POST(処理されるデータ送信)、PUT(URL に対するリソース転送)、DELETE(URL に対するリソース削除)、OPTIONS(通信オプション問い合わせ-どんな HTTP メソッドに応答できるか調べられる)、TRACE(要求連鎖の調査-接続先変更などプロシクの状態を調べる)、CONNECT(プロクシへのトンネル接続確立要求)などがあります。また、URL は、ここでは絶対ですが、相対ならウインドウオブジェクトに関連付けられている基準 URL にしたがって解決されます。ウィジェットの場合、絶対を使うほうがいいでしょう。また、file:/// や ftp:// などとすることで HTTP 以外のプロトコルも使用可能です。ただし、HTTP 状態が返らないなどプロトコルによって制限があるので気をつけてください。ftp なら status は 0 です。ここで引数は 2 つですが、このメソッドは引数を 5 つ持っています。3 番目の引数 async は非同期かどうかを決めるもので、true(非同期)または false です。省略された場合は true になります。残りの引数は user と password で、認証が必要な場合に渡します。ここでは認証は必要なく、非同期実行なので、3 番目以降の引数は省略されています。
次に setRequestHeader で、オプションのヘッダを追加しています。ヘッダ情報にはさまざまなものがあります。たとえば、HTTP 入門 など解説サイトを参考にしてください。ここではキャッシュを行わない指示をヘッダに追加しています。
それから HTTP 要求を送信しています。ここでは null が渡されていますが、内容の送信を行うメソッドが選ばれていれば、ここでエンティティ本体 (entity body) を渡します。
さて、取得を完了した時のハンドラである xml_loaded を見てみましょう。
SampleRSS.js > 変数と show
//-----------------------------------------------------------------------------------
// xml_loaded - Apple RSS Hot News フィードの内容を引き出し、結果配列内に項目データを置く
// 各項目について、タイトル、リンク、発行日付を抽出する
//-----------------------------------------------------------------------------------
function xml_loaded (e, request)
{
xml_request = null;
// 要求は引数で渡されるため、保存していた要求を空に
if (request.responseXML)
// 1
{
var contents = document.getElementById('contents');
// 内容要素の参照を取得
while (contents.hasChildNodes())
// 現在の内容を空にする
{
contents.removeChild(contents.firstChild);
}
// 一番上のレベルの <rss> 要素を取得
var rss = findChild(request.responseXML, 'rss');
if (!rss) {
// 取得できなければエラー表示して脱出
alert("no
element");
document.getElementById("ohnoes").style.display = "block";
return;
}
// 単一の従属 channel 要素を取得
var channel = findChild( rss, 'channel');
if (!channel) { // 取得できなければエラー表示して脱出
alert("no element");
document.getElementById("ohnoes").style.display = "block";
return;
}
document.getElementById("ohnoes").style.display = "none";
var results = new Array; // 2
// channel 要素に従属するすべての item 要素を取得
// 各要素に対してタイトル、リンク、発行日付を取得
// 項目の全要素はオプションであることに注意
for( var item = channel.firstChild; item != null; item = item.nextSibling)
// item 要素は channel 要素の子なので子を順番に処理する
{
if( item.nodeName == 'item' ) // 実際に item 要素の場合だけ
{
var title = findChild (item, 'title'); // タイトル取得
// リストに項目を含めるためにタイトルを必要とする
if( title != null ) // タイトルがあった場合だけ
{
var link = findChild (item, 'link'); // リンク取得
var pubDate = findChild (item, 'pubDate'); // 発行日付取得
results[results.length] = {title:title.firstChild.data,
link:(link != null ? link.firstChild.data : null),
date:new Date(Date.parse(pubDate.firstChild.data)) // データ格納
};
}
}
}
// 日付で並べ替える
results.sort (compFunc);
// 表示のためタイトルと日付を行にコピーする
// ユーザーがタイトルをクリックした時に使えるようリンクを格納
nItems = results.length; // 3
var even = true;
for (var i = 0; i < nItems; ++i) // 配列内の各項目で繰り返し
{
var item = results[i];
var row = createRow (item.title, item.link, item.date, even);
even = !even;
contents.appendChild (row);
}
// スクロールバーが新しいデータにあうようスクロールバーを更新
scrollArea.refresh();
// 要求がポストされた最後の時刻を追跡するよう last_updated を現在時刻に設定
last_updated = (new Date).getTime();
}
}
まず引数で結果の要求が渡されるので、要求送信時に使った変数をクリアしています。これで後片付けは気にしなくて大丈夫です。
つぎに、1 で responseXML を確認しています。これがなければ、このメソッドは何も行わず、そのまま終わります。これは XmlDocument オブジェクトで、DOM 操作が行えます。サーバーが整形式の XML を送信したとしても、Content-Type ヘッダが指定されていない場合 は null になってしまいますが、要求作成の時に overrideMimeType を使ったので、それについては大丈夫です。サーバーが整形式の XML を送信しなければ、結果は null になります。この responseXML は読みとり専用プロパティであることに注意してください。DOM 操作は既にある内容を取得するものに制限されます。また、ここでは responseXML を使っていますが、応答の取得には DOMString 型の responseText も使えます。
responseXML があるなら、それで現在の内容を置き換えることになるので、まず古い内容を削除しています。id が contents の要素が子を持っているなら、それを削除します。
それから、子を探すことで、rss 要素を取得しています。 このメソッドは以下のようになっています。
SampleRSS.js > findChild
//-----------------------------------------------------------------------------------
// findChild - nodeName に一致するノードを探して与えられた DOM 要素の子を走査する
// 何を探しているかわかっている場合、
// 標準の DOM メソッド(getElementsByTagNameなど)より非常に効率的
//-----------------------------------------------------------------------------------
function findChild (element, nodeName)
{
var child;
for (child = element.firstChild; child != null; child = child.nextSibling)
{
if (child.nodeName == nodeName)
return child;
}
return null;
}
簡単なのでわかると思います。最初の子から始めて for 文で順番に子を調べ、名前が一致したら戻っているだけです。このメソッドは子だけを探しているので、子孫を探すメソッドより効率的です。
rss 要素が取得できなければ、エラーを出力し、エラー表示用の div を表示しています。このサンプルではこれ以上を行っていませんが、以前の内容を何らかの形で保存するか、まだ削除しないようにして、エラー回復オプションによって復元するなどの操作を行うことも可能でしょう。
さらに channel 要素を探します。構造は先の rss の場合と同じなので、すぐわかると思います。
2 以降から、新しい項目の取得が始まります。まず結果を格納するための配列を作成しています。それから channel 要素の子要素を最初からすべて調べます。
まずそれが実際に item 要素かどうかを調べ、item ならタイトルを取得します。タイトルが取得できた場合だけ次に進みます。リンクと発行日付を取得し、日付はデータ文字列から新しく日付オブジェクトを作成して格納しています。
項目の取得が終了したら、配列内の項目を並べ替えています。この compFunc は、このファイルで実装されています。
SampleRSS.js > compFunc
//-----------------------------------------------------------------------------------
// sortFunc - 日付を並べ替えるために使われる比較関数
//-----------------------------------------------------------------------------------
function compFunc (a, b)
{
if (a.date < b.date)
return 1;
else if (a.date > b.date)
return -1;
else
return 0;
}
単純に項目の日付を取り出して比較しているだけです。
3 からは、取得したデータを HTML 要素として変換して、ウィジェット内に表示できるようにしています。
配列内の項目の個数を取得し、配列の最初の項目から順番にすべてを処理します。処理を開始する前に、even(偶数)を true にしています。これは背景画像を切り替えるために使います。
まず配列内の項目を取り出し、それを createRow で HTML 要素に変換し、その後で even を切り替えます。そして変換した結果を、子要素として追加しています。
SampleRSS.js > createRow
//-----------------------------------------------------------------------------------
// createRow - ウィジェット本体の次の行にデータを追加する、行は交互に替わる明暗背景をもつ
// タイトルと日付は各項目ごとに表示、
// リンクはユーザーが RSS タイトルをクリックした時使われる
//-----------------------------------------------------------------------------------
function createRow (title, link, date, even)
{
var row = document.createElement ('div');
// 各項目用div要素作成
row.setAttribute ('class', 'row ' + (even ? 'light' : 'dark'));
var title_div = document.createElement ('div');
// 子のタイトル要素作成
title_div.innerText = title;
title_div.setAttribute ('class', 'title');
if (link != null)
// リンクがあるならリンク設定
{
title_div.setAttribute ('the_link', link);
title_div.setAttribute ('onclick', 'clickOnTitle (event, this);');
}
row.appendChild (title_div);
// タイトルを行要素の子にする
if (date != null)
//日付があるなら
{
var date_div = document.createElement ('div');
date_div.setAttribute ('class', 'date');
date_div.innerText = createDateStr (date);
row.appendChild (date_div);
// 日付を行要素の子にする
}
return row;
}
構造は単純です。まず、項目用の行 div 要素を作ります。これは class が row になり、even の値によって、クラスにさらに light と dark が追加されます。つぎにタイトル要素が作成されます。適切に設定した後で、リンクがあるなら、クリックした場合のハンドラが設定されています。それからタイトル要素を行の子として追加します。さらに日付があるなら、それも処理します。createDateStr は日付オブジェクトから表示用の文字列を作るメソッドで、このファイルで実装されています。
SampleRSS.js > createDateStr
function createDateStr (date)
{
var month;
switch (date.getMonth())
{
case 0: month = 'Jan'; break;
case 1: month = 'Feb'; break;
case 2: month = 'Mar'; break;
case 3: month = 'Apr'; break;
case 4: month = 'May'; break;
case 5: month = 'Jun'; break;
case 6: month = 'Jul'; break;
case 7: month = 'Aug'; break;
case 8: month = 'Sep'; break;
case 9: month = 'Oct'; break;
case 10: month = 'Nov'; break;
case 11: month = 'Dec'; break;
}
return month + ' ' + date.getDate();
}
単純なのですぐわかると思います。月と日だけの単純な表示にするため、独自メソッドを使っています。
さて、xml_loaded に戻ります。内容の行の追加が完了すれば、あとはスクロール領域に更新を行わさせています。そして、最後に 15 分以上たっていた場合更新するために変数を設定しています。タイマーなどを使えば、15 分ごとに自動で更新するなどの機能を追加することも可能ですが、ウィジェットが表示されたままであることはあまりないので、このサンプルのようにウィジェットが表示された場合だけ再更新をチェックするほうが現実的であり、システムのリソースを無駄に使うことで他のプロセスに悪影響を及さないためにも重要です。
更新サイクルを短くする必要があり、ウィジェット表示中はつねに更新しなければならない場合は、表示時にタイマーを作成し、隠される時にタイマーを除去することで周期的な更新作業を他にあまり影響を与えない形で行えます。背後で動作している場合もつねに更新しなければならないような特殊な場合だけ、全体としてタイマーを使うことを考えたほうがいいでしょう。
また、onload ハンドラは、最初に読み込まれた時だけ呼ばれるので注意が必要です。もしシステムが再起動されず長時間使われ続けるなら、onshow ハンドラでなく onload ハンドラだけで更新されていた場合、いつまでたっても再更新されないことになってしまいます。
あとはリンクのクリックなどの場合のハンドラだけです。まず上の createRow で項目行に設定されたリンク用のハンドラを見てみます。
SampleRSS.js > clickOnTitle
//-----------------------------------------------------------------------------------
// clickOnTitle - 記事のタイトルをクリックしたとき RSS リンクに移動
//-----------------------------------------------------------------------------------
function clickOnTitle (event, div)
{
if (window.widget)
{
widget.openURL (div.the_link);
} else document.location = div.the_link;
}
単純です。単に Dashboard 内で表示されている場合、ウィジェットオブジェクトの機能を使い URL を開き、そうでなければ、通常のブラウザ機能でリンクに移動しているだけです。
openURL はウィジェットオブジェクトのメソッドで、デフォルトで指定されたブラウザで開きます。ここで URL として file: 方式を使うなら、AllowFileAccessOutsideOfWidget キーが設定されていなければならないことに注意してください。ここでは http: なので、このサンプルではこのキーは指定されていません。
最後はウィジェット全体の上の部分のタイトルに設定されていたハンドラです。上の関数がわかれば、これはすぐにわかるでしょう。
SampleRSS.js > clickOnTitle
//-----------------------------------------------------------------------------------
// clickOnFeedTitle - ウィジェットタイトルをクリックしたときフィードの主ウェブページに移動
//-----------------------------------------------------------------------------------
function clickOnFeedTitle(event)
{
if (window.widget)
{
widget.openURL (feed.url);
} else document.location = feed.url;
}
6 まとめ
このサンプルは、XMLHttpRequest オブジェクトを利用し、ネット上のリソースを取得・解析して表示します。とても簡単であることがわかったと思います。ただし、DOM 操作を行うためには、整形式の XML である必要があります。HTML など他の形式のリソースを取得して解析するには、他の手段を使うかテキスト解析するかなど、いずれにしてもさらなる工夫が必要です。
管理人:神吉 秀典 E-mail: