l'essentiel est invisible pour les yeux

Saturday, November 04, 2006

[javascript] オフライン/オンライン切り替え時に呼び出すイベントハンドラを定義する

少し前にうみがめで自社用の社内ブログシステムを作ったのですが、最近naotakeさんにAny devices, Any Networkどうよ!とアドバイスをもらった。

これはつまり、どんなデバイスからでもアプリケーション利用することができ、オンライン/オフライン関係無しにサービスを利用できるというものだ。クロスデバイスの話はUIEngineでさくっとてことでひとまず置いておいて、ここではオンライン/オフラインの話。

Youtubeによるデモムービーが話題になっている、Scrybeもオフラインでも利用できるカレンダーアプリケーションです。詳しく見ていませんが、Google Docsもローカルで動作することを感じさせるコードが埋め込まれています。

http://docs.google.com/javascript/Utilities.js?v=cnz
のGetMashedURLあたり。

オフラインでもアプリケーションを利用でき、オンラインになったらサーバと同期するスタイルは、WebベースのOfficeアプリケーションには必須となりそうです。

となると、オンライン/オフライン切り替え時のイベントハンドラを定義したくなる。
オフライン時は、データストア先をローカルのストレージに切り替え、オンラインになったらサーバと同期を取る。そこで、イベントハンドラをアタッチできるかどうか調べてみた。

動作デモ

まず始めに、ブラウザがオフラインモードかどうか調べるには、


navigator.onLine // #=> true or false

で調べることが出来る。
(注意) 正常にネットワークにつながっているかどうかを検出できるわけではない。

windowエレメントかHTMLエレメントにそれらしきイベントハンドラをアタッチできないだろうか?という疑問がわく。

window.addEventListener('online', function(){alert('オンラインになりました。')}, false);
window.addEventListener('offline', function(){alert('オフラインになりました。')}, false);

残念ながらonline/offlineイベントに対するイベントリスナを定義することは現在の実装ではできない。

Web Applications 1.0 Working Draft — 4 November 2006に提案されているように、近いうちに実装されるかもしれない。

そこで、Object#watchを使用してnavigator.onLineを監視しようと考えた。
Firefox 1.5, 2.0で動作を確認。(IE未対応)


window.addNetoworkEventListener = function(type, listener, useCapture){
if(type == 'online') {
navigator.watch('onLine', function(id, oldval, newval){
if(newval) listener();
return newval;
});
} else if(type == 'offline') {
navigator.watch('onLine', function(id, oldval, newval){
if(!newval) listener();
return newval;
});
}
}
window.addNetworkEventListener('online', function(){alert('オンラインになりました。')}, false);
window.addNetworkEventListener('offline', function(){alert('オフラインになりました。')}, false);

これは正しく動作しなかった。
navigator.onLineの値はファイル→オフライン作業を選択することでtrue or falseに切り替わる。ブラウザ内部からプロパティが変更された場合は、watch式は動作しないようだ。

そこで、navigator.onLineの値を定期的に監視し変更があった場合は別に用意したオブジェクトのプロパティを変更する。そのプロパティをObject#watchによって監視するというアプローチを取ってみた。

Firefox 1.5, 2.0 限定 (IE未対応)
デモ

var Status = {
_onLine: navigator.onLine,
observers: {online: [], offline: []},
_update: function() {
if(Status._onLine != navigator.onLine) {
Status._onLine = navigator.onLine;
}
},
isOnline: function() { // wrapper of navigator.onLine
return navigator.onLine;
},
observe: function(type, listener, useCapture){
type == 'online'? Status.observers.online.push(listener) : Status.observers.offline.push(listener);
Status.watch('_onLine', function(id, oldval, newval) {
if(newval) for(var i=0,l=Status.observers.online.length;i<l;++i) Status.observers.online[i]();
else for(var i=0,l=Status.observers.offline.length;i<l;++i) Status.observers.offline[i]();
return newval;
});
},
checkInterval: function(delay/*=1000*/) {
setInterval(Status._update, delay || 1000);
}
}
Status.checkInterval(1000);
Status.observe('online', function() {alert('オンラインになりました');});
Status.observe('offline', function() {alert('オフラインになりました');});

これは正しく動作する。
厳密には、オフライン/オンラインイベントハンドラをアタッチしていることにはならないが、"それっぽく"動作する。早くonline/offlineイベントを実装してほしい。

参考
Core JavaScript 1.5 Reference:Global Objects:Object:watch
5.2.1. [SCS] Offline Web applications