Hatena::ブログ(Diary)

Yet Another Hackadelic

2008-12-07

Ex DOM Storage の話を Yokohama.pm でしてきました

すっかり忘れかけてたけど資料の方、SlideShare にて公開します。

まとめエントリとかは Yokohama.pm テクニカルトーク #3 に参加中 - 北海道苫小牧市出身の初老PGが書くブログ が秀逸なのでそちらをご覧下さい。

id:Yappo も言ってたけど Shibuya.pm とはまた違う空気感があるし、newvie な人の発表も聞けて中々楽しい日でした。

今後も続けて行きたいなぁと思います。

改めてリーダーの id:clouder さんに感謝。

またいつもの運営メンバー(!?)の、id:tomyhero さんや id:ikasam_a さん、そして id:daiba さんに感謝します。

スピーカーの皆さんや参加して下さった皆さんもまた是非ご参加下さい。

2008-12-05

クロスブラウザな onhashchange イベント (作りかけ)

今更 IE8 の話なんて遅れてる感満載な訳ですが、IE8 からは onhashchange イベントと言う location.hash の値が書き変わったら発火するイベントが出来ました。onhashchange イベントについては下記参照。*1

で、これなんですが location.hash にアプリケーションの状態を保持したいなんて言う、"えいじゃっくす" 的なコンテンツの場合、かなり使えるじゃないすかと。*2

クロスブラウザで行けるかもなーと思って試しに書いてみました。まだ作りかけ。

ソースコードと解説

(function() {
  var createEvent = function() {
	var evt;

	if (document.createEvent) {
	  evt = document.createEvent("Event");
	  evt.initEvent("hashchange", true, true);
	}
	else {
	  // IE6, 7
	}

	return evt;
  };

  if (Object.prototype.watch && location.watch) { // for Mozilla
	location.watch("hash", function(prop, oldVal, newVal) {
	  var evt = createEvent();

	  if (document.body.dispatchEvent(evt)) {
		return newVal;
	  }
	  else {
		return oldVal;
	  }
	});
  }
  else if (location.__defineSetter__ && location.__lookupSetter__) { // for Opera
	var nativeSetter = location.__lookupSetter__("hash");
	try {
	  location.__defineSetter__("hash", function(val) {
		var evt = createEvent();

		if (document.body.dispatchEvent(evt)) {
		  nativeSetter.call(location, val);
		}
	  });
	}
	catch (e) { }
  }
  else {
	// for IE6, 7 and Safari
  }
})();

って訳で、当初は __lookupSetter__ して native code な setter を取得して、__defineSetter__ で wrap すると言う一本で考えてたんだけど、Firefox では無理でした。try-catch があるのは名残。

なので Firefox では watch() してみました。人生初の watch() です。

あと IE6,7 と Safari は setInterval しか無いのかなーと言う感じ。面倒だからそこは書かなかった。

さらに言うと Firefox でも Opera でもオレオレカスタムイベントを定義出来るし dispatchEvent も問題無く出来るんですね。これも初めてでした。

と言う訳で、DOM で構築された各オブジェクトに hook 的な何かを書く際に、こうした手法が使えるかもねって話でした。

問題点

  • Safari, IE6, 7 はどうするか
    • setInterval() くらいしか思いつかない
  • a[@href="#hoge"] では発火しない
    • document.body とかで待ち受ける?
  • IE6, 7 ってオレオレイベントって作れても発火出来ない記憶
    • 無理?

改訂版ソースコード (2008-12-05T18:43:25+09:00 ,id:amachang に色々教えて貰った)

(function() {
  var createEvent = function() {
	var evt;

	if (document.createEvent) {
	  evt = document.createEvent("Event");
	  evt.initEvent("hashchange", true, true);
	}
	else {
	  // IE6, 7
	}

	return evt;
  };

  if (Object.prototype.watch && location.watch) { // for Mozilla
	location.watch("hash", function(prop, oldVal, newVal) {
	  var evt = createEvent();

	  if (document.body.dispatchEvent(evt)) {
		return newVal;
	  }
	  else {
		return oldVal;
	  }
	});
  }
  else if (location.__defineSetter__ && location.__lookupSetter__) { // for Opera
	var nativeSetter = location.__lookupSetter__("hash");
	try {
	  location.__defineSetter__("hash", function(val) {
		var evt = createEvent();

		if (document.body.dispatchEvent(evt)) {
		  nativeSetter.call(location, val);
		}
	  });
	}
	catch (e) { }
  }
  else if (window.__defineGetter__) { // Safari 3
	var nativeLocation = location;
	window.__defineGetter__("location", function() {
	  return {
		get href() {
		  return nativeLocation.href;
		},
		assign: function(url) {
		  nativeLocation.assign(url);
		},
		replace: function(url) {
		  nativeLocation.replace(url);
		},
		reload: function() {
		  nativeLocation.reload();
		},
		set protocol(val) {
		  nativeLocation.protocol = val;
		  return val;
		},
		get protocol() {
		  return nativeLocation.protocol;
		},
		set host(val) {
		  nativeLocation.host = this.host = val;
		  return val;
		},
		get host() {
		  return nativeLocation.host;
		},
		set hostname(val) {
		  nativeLocation.hostname = val;
		  return val;
		},
		get hostname() {
		  return nativeLocation.hostname;
		},
		set port(val) {
		  nativeLocation.port = val;
		  return val;
		},
		get port() {
		  return nativeLocation.port;
		},
		set pathname(val) {
		  nativeLocation.pathname = val;
		  return val;
		},
		get pathname() {
		  return nativeLocation.pathname;
		},
		set search(val) {
		  nativeLocation.search = val;
		  return val;
		},
		get search() {
		  return nativeLocation.search;
		},
		set hash(val) {
		  var evt = createEvent();
		  if (document.body.dispatchEvent(evt)) {
			nativeLocation.hash = val;
		  }
		  return nativeLocation.hash;
		},
		get hash() {
		  return nativeLocation.hash;
		},
		toString: function() {
		  return nativeLocation.toString();
		}
	  };
	});
  }
  else { // for IE6, 7
  }

  window.addEventListener("hashchange", function() {
	alert("DEKITA");
  }, false);
})();

しかしそもそも戻るボタンのハンドリングがって話だったのにどれも満たせてない事にあとから気づいた二人 orz...

SEE ALSO

*1:そもそも hashchange イベントって HTML5 だったのかー!

*2:自分は余りそういうアプリケーションを作りたいとは思わないけど

2008-09-24

StorageEvent の非互換性メモ

Fx3 で確認したけど、StorageEvent のプロパティに差異があるようで、sessionStorage 由来で onstorage イベントが fire した場合、IE8 だと

event.uri; // #session

で取得出来るのに対して、Fx3 だと、

event.domain; // #session

で取れるみたい。ただ HTML5 の spec 的には event.url が正解のようです。

Inside Ex DOM Storage

Ex DOM Storage の中の実装ですが、結構苦労したので折角だから解説しちゃうぞ的なエントリです。

なので興味のある人以外にはだいぶニッチですw

Ex DOM Storage の技術的概観

簡潔に書いてしまうと以下の二点につきます。

  • データの格納先は userData behavior
  • Storage オブジェクトの実態は script 要素に適用した element behavior

userData behavior とは?

userData Behavior とは、IE が標準で用意しているクライアントサイドストレージです。

使い方は非常に簡単で、

element.addBehavior("#default#userData");
element.load("myStorage");
element.setAttribute("myData", "blah blah");
element.save("myStorage");

のようにすると、element の myData と言う属性に対してデータを永続的に保存出来ます。

userData behavior もドメイン単位での保存なので、保存先はこれを使っています。

Storage オブジェクトプロパティに対する代入と onstorage イベント

プロパティへの代入の検出

そもそも Storage オブジェクトには getItem(key), setItem(key, newValue) と言うメソッドがあり、こちらを律儀に叩いてくれるなら問題は無かったのですが、

sessionStorage.hoge = "fuga";

と言うような、ただの代入に対しても onstorage イベントが fire するのですが、素の JS ではこれを実現する手だてが無いです。

Object#watch() がかなり近いですが、IE の JScript には無いし、watch はそもそも指定したプロパティへの代入を監視なので、任意のプロパティに対してはどうにもなりません。

そこで思いついたのが onpropertychange イベント。つまり Storage オブジェクトの実態を HTMLElement にしてしまえば補足出来るよねと。

具体的には、

element.attachEvent("onpropertychange", function(evt) {
  alert("key: " + evt.key);
  alert("newValue: " + element[key]);
});

のように取得出来る感じです。

Behavior で作ったカスタムイベントの制約

onstorage イベントですが、当初は Behavior でカスタムイベントを使ってました。つまり、htcで

<public:event id="storegeEvent" name="onstorage" />

のようにしておいて、

var evt = document.createEventObject();
// evt のプロパティとか設定
storageEvent.fire(evt);

な感じでやってたのですが、カスタムイベントってイベントバブリングしないし、この要素のonstorage属性に直接ハンドラを定義しないと捕捉出来ないと言う制約があります。*1

さらに追い打ちをかける話で、document.createEventObject() で作ったイベントの type プロパティの値が IE にとって未知の値 (例えば storage と言う値) だとやはり attachEvent 出来ないと言う罠。

これは悩んだ末に attachEvent を hack すると言う、多少 evil な手法を取ってます。この割り切りは互換性重視のためです。

HTC にもレンダリングモードがある

document.compatMode の値ですね。でこれが互換モードの場合だと onpropertychange イベントを扱えないので、htc の冒頭に DOCTYPE 宣言が付いてます。これ、地味だけど凄い重要。

sessionStorage の実装

ブラウザを閉じると消える と言う挙動を実現する為に、cookie を使ってます。これは userData ではどうにも出来ない事が判明したので苦肉の策です。

length, remainingSpace プロパティの実装

これは HTML Component の getter 設定を使ってます。つまり、htc で

<public:property name="length" get="getLength" />
<public:property name="remainingSpace" get="getRemainingSpace" />

と設定してあるように、プロパティの取得の時に指定したメソッドが走るようになってます。実態は length だったら、

function getLength() {
  var length = 0;
  for (var p in storage)
    length++;
  return length;
}

みたいになってます。

まとめ

ここまで読んでくれたあなたは、だいぶニッチです。

Ex DOM Storage をリリースしました

結構前に作っていたんだけど、IE6, IE7 でも動作する DOM Storage を作ったので、きちんと告知します。

追記

  • ちなみにサーバーにファイル置くだけで動きます。ユーザーに何かインストールさせる必要はありません。(2008-09-24T11:45:56+09:00)
  • CodeReposにソースを移動しました。(2008-09-24T12:37:24+09:00)

Ex DOM Storage

dist
http://svn.coderepos.org/share/lang/javascript/exdomstorage/tags/0.01/
source
http://svn.coderepos.org/share/lang/javascript/exdomstorage
sample
http://svn.coderepos.org/share/lang/javascript/exdomstorage/trunk/sample/index.html (Fx2, 3 でも動くようにしました。まだちょっとサンプルにバグあるっぽぃ)

ソースはいずれ CodeRepos に移動しようかなと思います。

DOM Storage ってなんだよ

HTML5 で仕様化されているクライアントサイドストレージに関する仕様が DOM Storage です。

具体的には、

  • localStorage
  • sessionStorage

と言う二つのストレージが存在して、共にドメイン単位でクライアントサイドにデータを保存出来ますが、

localStorage
永続的に保存
sessionStorage
ブラウザを閉じると消える

と言う違いがあります。

使い方は簡単で、

localStorage.foo = "zigorou";

のように代入しておくと、そのドメイン単位で foo に代入した "test" と言う値は保存され、ドメイン単位で共有され、以降は明示的に消さない限りは、localStorage.foo と言う参照で "test" と言う値を取得する事が出来ます。

現在これを実装しているブラウザは IE8 と、部分的に Fx2, Fx3 *2 となっています。

それと Safari の trunk でも使えるそうです。(id:amachang 談)

使い方

サンプル を見て頂くのが一番早いのですが、詳しく解説しておきます。

Content-Type を設定する

Ex DOM Storage では IE 独自機能である DHTML Behavior を使っています。これについては別のエントリで解説しようと思いますが、配布ファイルにある exdomstorage.htc に正しい Content-Type を設定する必要があるので、例えば Apache であれば、httpd.conf などで、

AddType text/x-component .htc

と設定して置く必要が(おそらく)あります。

ファイルを置く

という二つのファイルを必ず同じディレクトリに配置して下さい。

Ex DOM Storage を読み込む

IE6, 7 だけに読ませたいので、条件付きコメント を用いてロードします。

<!--[if lt IE 8]>
<script type="text/javascript" src="/path/to/exdomstorage.js"></script>
<![endif]-->

これで使えるようになるはずです。

特徴と制約に関して

onstorage イベント

document オブジェクトに対して fire されます。つまり、

localStorage.setItem("name", "ZIGOROu");

などをした際に、

document.attachEvent("onstorage", function(evt) {
  alert(evt.key + " was changed to " + evt.newValue + " from " + evt.oldValue);
});

としておく事で、onstorage イベントを捕捉する事が出来ます。

但し、これは制約ですが本来の DOM Storage は同一ドメインのページを開くウインドウが複数存在している場合、そのうちの一つで onstorage イベントが fire されると他のウインドウの document オブジェクトに対しても onstorage イベントが fire されますが、Ex DOM Storage ではされません。*3

remainingSpace プロパティ

no title にありますが、IE8 には Storage オブジェクトに対して remainingSpace プロパティが存在します。

これは後どれくらい Storage に保存出来るかと言う目安(byte単位)になるのですが、Ex DOM Storage でも一応実装してます。

Ex DOM Storage のバックエンドは userData behavior なので、おおよそ 64Kbyte 保存出来るのに対して、IE8 での nativeDOM Storage は 5,000,000 byte (約5MB) 保存出来ます。

実は userData behavior ベースでももっと容量を増やせる事は分かっているんですが、速度重視にしたので現在の制約のままにしています。

swf ベースのクライアントサイドストレージとの違い

これは id:amachang に教えて貰ったんだけど、onload イベント後じゃないと swf にアクセス出来ないのに対して、Ex DOM Storage はこのライブラリを読み込んだ直後から使えるようになります。

余計な要素が増えます>< (追記:2008-09-24T11:45:56+09:00)

実は Storage オブジェクトの実態は script 要素になってるので、head要素内に二つ余計な script 要素が増えてしまいます。

と言う訳で

手軽に使えるようになってるんで、是非お試し下さい。またバグ報告等ございましたら、お近くの id:ZIGOROu までお声掛け下さい。

SEE ALSO

*1:つまりattachEventが使えない

*2http://developer.mozilla.org/Ja/DOM/Storage を参照の事。簡単に言えば古い仕様に則っていて、localStorage が実装されてない代わりに globalStorage が実装されている。globalStorage で localStorage は代替可能

*3:と言うか HTC と JS だけじゃ不可能

2008-05-19 ブーメラン

typeof 演算子と RegExp オブジェクト

Fx3 の MozReplsearch メソッドが動かないなーと思っていて調べた内容です。

ちなみに仕様的なものを引っ張って来ていますが、JSは門外漢なので自己責任でお願いします(ぇ

repl.search() の実装

function search(criteria, context) {
  context = context || this._workContext;var matcher;
  if (typeof criteria == "function") {
    matcher = criteria;
  } 
  else {
    matcher = function (name) { return name == criteria; };
  }

  for (var name in context) {
    if (matcher(name)) {
      this.print(name);
    }
  }
}

ってな具合で、repl.search(regex, obj)やらrepl.search(func, obj)みたいに使えるハズ。

typeof regex

で、結論から言うとFx3では、

typeof (/^foo/)

は "object" と返ってくる。従って最初の if-elseelse 句が実行されると言うオチ。

typeof regexの挙動

11.4.3 typeof 演算子 (The typeof Operator) によれば、

Object (native and doesn't implement [[Call]]) "object"
Object (native and implements [[Call]]) "function"
Object (host) Implementation-dependent
11.4.3 typeof 演算子 (The typeof Operator)

とある。ここで言う[[Call]]とは、8.6.2 内部プロパティとメソッド にある内部プロパティの事で、

プロパティ パラメータ 説明
[[Call]] 呼出側が提供する引数値のリスト オブジェクトに関連するコードを実行する。 関数呼出の式 (expression) を経由して呼出される。 この内部メソッドを実装するオブジェクトは関数 (function) と呼ばれる。
8.6.2 内部プロパティとメソッド

まぁ [[Call]] 内部プロパティを持てば関数呼び出し可能だよと言う事で良さそう。

ところで、

必須の内部プロパティに加えて、各ファンクション実体は [[Call]] プロパティ、 [[Construct]] プロパティ、 [[Scope]] プロパティ (8.6.2 及び 13.2 を参照) を持つ。[[Class]] プロパティの値は "Function" である。 15.3.5 Function インスタンスのプロパティ

とあるように Function インスタンスはもちろん呼び出し可能なのだが、15.10.7 RegExp インスタンスのプロパティ (Properties of RegExp Instances) には [[Call]] プロパティは存在しない。つまり呼び出し可能では無いのが ECMAScript 3rd 的には正しいと思われるが、実際には Firefox や Safari では呼び出せる模様。

ここまでまとめると、RegExpオブジェクトは、

  1. typeofの結果は"object"
  2. 関数呼び出し出来ない

が本来正しいと思われます。でFx2, Fx3での結果をそれぞれ並べると、

browser typeof call
Fx2 function 出来る
Fx3 object 出来る

となっていて、まぁ何とも言えない状況みたい。

まとめ

id:amachangがきっと補足してくれます。

それはともかくtypeofでregexを期待する変数のチェックはダメです。Fxオンリーでいいなら、

typeof regex == "function" || regex.__proto__ == RegExp.prototype

などで判別すると良さそう。

かしこ。

追記

判定は普通にinstanceof RegExpでももちろんおkです。(thanks id:ku0522)

2008-01-09 LDRの購読者数を増やしたい

IEでのJavaScriptのデバッグ方法 - Microsoft Script Editor編 -

IEでJSのエラーになったときに立ち上がるデバッガって大体の人が使いこなせてないのではないでしょうか?

ちなみに僕もそうだったのですが、ふと思い立って調べてみました。

追記 (2008-01-10T14:24:30+09:00)

はてブのコメントで、

MS Office持ってない人はVisual Web Developer(VWD)で。/ちょっと前にこんなの書いたので、VWD 2008使う人は参考にしてもらえると幸い。 http://d.hatena.ne.jp/terurou/20071218/119796573 terurouのブックマーク / 2008年01月10日

とありました。

これはid:kaorunさんにも聞いていたので後で試そう〜と思ってたので助かります。

合わせてご覧になって下さい。

Microsoft Script Editorとは?

f:id:ZIGOROu:20080109151755p:image

のようにOffice付属のツールだったりします。僕の環境では、

C:\Program Files\Microsoft Office\OFFICE11\MSE7.EXE

にあります。

Microsoft Officeがインストールされていない環境では残念ながら使えません

使い方

起動方法(1)

f:id:ZIGOROu:20080109152244p:image

のようにIE6の場合は、[表示]->[スクリプトデバッガ]->[次のステートメントで中断]で使用する事が出来ます。あるいは[表示]->[スクリプトデバッガ]->[開く]でも構いません。

f:id:ZIGOROu:20080109152627p:image

のような画面が立ち上がるので、「新しいインスタンスMicrosoft Script Editor」を選択し「はい」を押して下さい。

f:id:ZIGOROu:20080109152841p:image

のようになるのでOKを押します。これで起動と準備は完了です。

起動方法(2)

スタンドアローンで起動している場合は、プロセスを指定して起動する事が出来ます。

Script Editorの[デバッグ]->[プロセス]を選択します。

f:id:ZIGOROu:20080109153628p:image

次にそのプロセス画面でIEのプロセスを探します。iexplorer.exeがIEです。

f:id:ZIGOROu:20080109153627p:image

見つけたらアタッチします。

f:id:ZIGOROu:20080109153626p:image

ここではOKを押します。

ウインドウの設定

まずはアタッチ後は対象となるIEの画面をリロードしましょう。

[デバッグ]->[ウインドウ]から、

  • 実行中のドキュメント
  • イミディエイト(コマンドウインドウ)
  • ウォッチ

を開いておきましょう。

「実行中のドキュメント」から適当なファイルを選択してダブルクリックします。

f:id:ZIGOROu:20080109154455p:image

こんな感じになると思います。(多分)

ブレークポイントを指定する

指定したい行でF9を押すとキャプチャーのように赤の丸がその行につきます。

f:id:ZIGOROu:20080109154853p:image

この状態で再度ブラウザをリロードすると指定した行からデバッグを開始できます。

f:id:ZIGOROu:20080109155026p:image

のようにイミディエイトウインドウにてPrototypeと打ってエンターを押すと、変数の中身を垣間見る事が出来ます。

他にも当然その行のコンテキストで実行できるJavaScriptの式ならば何でも実行出来ます。

後は[デバッグ]メニューにあるステップ関連の機能でいったりきたりします。(ぉ

ウォッチ機能の使い方

f:id:ZIGOROu:20080109155445p:image

のように[デバッグ]->[クイックウォッチ]を指定して、クイックウォッチ機能を立ち上げます。

f:id:ZIGOROu:20080109155714p:image

のように、Function.prototype.bindを指定して再計算します。

この段階では何も定義されていないのでundefinedになっています。これをウォッチ式の追加を押して監視対象の変数とします。

bindの定義が完了する辺りまでカーソルを持ってきてから[デバッグ]->[カーソル行の前まで実行]を行うと、ウォッチウインドウでbindがundefinedでは無くなっています。

イミディエイトウインドウで、Function.prototype.bind.toString()などと実行すれば、

Function.prototype.bind.toString()
"function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}"

こんな風に出てくるはずです。

まぁ後は各自色々触って弄って見て下さい。*1

後はドキュメントアウトラインを使うとDOM Inspector的な画面が出てくるので、それも割りとお勧め。

問題点

Debugオブジェクトが使えない

実はIEのJScriptにはDebugオブジェクトと言う物があります。

このオブジェクトは他と趣が違い、インスタンス化せずに使います。*2

Debug.write("foo");
Debug.writeln("bar");

みたいな使い方をします。(JScript - Debug Object (JScript 5.6) )

この出力先は実はMicrosoft Script Debuggerのイミディエイトウインドウで、Script Editorをデバッガとして使っても何も起こりません。凄い残念><

まぁそれ以外は特に無いかな。

まとめ

画像をふんだんに使うエントリを書くと凄い疲れる事が分かりました。

参考リンク

*1:めんどくさくなってきたので><

*2:と言うかインスタンス化できません