ブログをはじめてみる2007年04月24日 03時13分06秒

なんとなくはじめてみました。
だらだらとJavaScriptのこととか書いてみよう。

といっても、Ajax系のこととかはよいブログが
すでにたくさんあるので、スキマを狙って
WSHとかHTAとかにからめていく予定。

WSHで外部スクリプトのロード2007年04月24日 03時16分33秒

方針

すなおに.wsfにすればあまり苦労はないんだけど、XML中にスクリプトを書くのはなんとなく収まりが悪いので、.jsファイルのみの方向で。

基本的な考え方

とはいってもJScript自体にも、WScriptにもファイルをインクルードする機構はないから、eval()するしかない。

しかし、ライブラリロードでグローバル変数を消費するのはなんだかヤなので、withブロックで使い捨てスコープを形成することにする。

問題点

eval()はeval自身を呼び出した実行コンテキストをそのまま引き継ぐので(あってるのか?)ライブラリソースをevalする関数を作成すると、いつまでたってもグローバル空間にスクリプトはロードされない。

仕方ないので、ソースのロードのみ関数にして、あとはグローバルコンテキスト中でeval()することに。

で、実装

骨組みは以下の通り。

with( {
	// ライブラリのパスリスト
	libs : [
		/* ここにライブラリのパスを列挙する */
	],
	// libs[]のインデックス
	index : 0,
	// ソース取得メソッド
	getSource : function(path) {
		var stream, fso = new ActiveXObject("Scripting.FileSystemObject");
		try {
			stream = fso.OpenTextFile( path, 1 );
			return stream.ReadAll();
		} finally {
			stream.Close();
		}
	}
} ) {
	while( index < libs.length ) {
		try {
			eval( getSource( libs[ index++ ] ) );
		} catch(e) {
			WScript.Echo( e.description || e.message || "error" );
		}
	}
}

たとえば、

function test(a, b) {
	return a + b;
}
なんてソースを「lib.js」とした場合、
with( {
	// ライブラリのパスリスト
	libs : [
		"lib.js"
	],
	// libs[]のインデックス
	index : 0,
	// ソース取得メソッド
	getSource : function(path) {
		var stream, fso = new ActiveXObject("Scripting.FileSystemObject");
		try {
			stream = fso.OpenTextFile( path, 1 );
			return stream.ReadAll();
		} finally {
			stream.Close();
		}
	}
} ) {
	while( index < libs.length ) {
		try {
			eval( getSource( libs[ index++ ] ) );
		} catch(e) {
			WScript.Echo( e.description || e.message || "error" );
		}
	}
}
WScript.Echo( test( "A", "B" ) ); // 'AB'
となる。 このままだとShift_JISでローカルディスクにあるライブラリしかロードできないが、あとでXHRとADODB.Streamを組み合わせてgetSourceメソッドを書き換えればWebサーバにあるライブラリのロードもできるようになる。

prototype.jsを使えるように2007年04月24日 03時46分36秒

きっかけ

prototype.jsが大好きだ。組み込みオブジェクトのprototypeを汚染拡張しているところが大好きだ。

なので、WSHでも使ってみたい。

※:以下の記述は、prototyp.js1.4.0ベースです。1.5系はどうかわかりません

困ったことに

prototype.jsを(Windowsで、ね。)ダブルクリックしてみると、さっそく「documentは宣言されていません」とか怒られるんですよ。

これ、当然といえば当然で、prototype.jsはブラウザホストで稼動させることが前提なので、windowやらdocumentやらlocationやらを参照してるからなのだけど、この辺のオブジェクトをダミー宣言してやれば、ClassだのEnumerableだのTryだのは使用できるんですよ。

ダミースクリプト

前のエントリの感じで以下のスクリプトを、prototype.jsよりも前にロードしておくことにする。仮に「dummy.js」とでもしておこう。

// prototype.jsのためのダミー宣言
var window = { Element : null }
var document = {
	getElementById : function() {},
	getElementsByName : function() {},
	getElementsByTagName : function() {},
	createElement : function() {},
	documentElement : null,
	body : null
}
var navigator = { userAgent : null }

んで、prototype.jsもロード

dummy.jsよりも後なら、prototype.jsのロードができるんですよ。以下のように。

with( {
	// ライブラリのパスリスト
	libs : [
		"dummy.js",
		"prototype.js"
	],
	// libs[]のインデックス
	index : 0,
	// ソース取得メソッド
	getSource : function(path) {
		var stream, fso = new ActiveXObject("Scripting.FileSystemObject");
		try {
			stream = fso.OpenTextFile( path, 1 );
			return stream.ReadAll();
		} finally {
			stream.Close();
		}
	}
} ) {
	while( index < libs.length ) {
		try {
			eval( getSource( libs[ index++ ] ) );
		} catch(e) {
			WScript.Echo( e.description || e.message || "error" );
		}
	}
}
WScript.Echo(
	$R(1,3).map( function(i) {
		return "番号:" + i;
	} ).join(", ")
); // 番号:1, 番号:2, 番号:3

コンソールベースのWSHだとテキスト処理かファイル処理が主になると思うけど、こういうときにEnumerableインターフェイスが使えるのって、個人的にはかなり便利で手放せない感じ。

特に、FileSystemObjectのFilesとかSubFoldersとかを取り回すEnumeratorにEnumerableなラッパーかぶせたりすると、100単位のファイルがあるディレクトリを取りまわしたりするときもメモリを消費しないですむ(処理は遅いけど)ので、お勧めです。需要はなさそうだけど。

http経由でライブラリ読み込み2007年04月24日 21時04分28秒

XHRとADODB.Streamを組み合わせる

http経由でソースを取得するのはXHRで問題ないが、日本語のコメントが入ったeucエンコードのソースなんかだとちょっとアレなので、ADODB.Streamと組み合わせてみたり。まあ、定番テクニックですか。

基本的には以下の流れになる。

  1. XHR(Microsoft.XMLHTTP)でソースを取得
  2. バイナリモードで開いたADODB.StreamにXHR.responseBodyを書き込む
  3. ADODB.Streamを巻き戻し、テキストモードに変更する
  4. Charsetプロパティに"_autodetect"を指定して自動検出にする
  5. ReadText()でテキスト読み出しをする
具体的なコードは以下のようになる。(前のエントリのgetSource()のみ変更)
getSource : function(url) {
	var stream = new ActiveXObject("ADODB.Stream");
	var xhr = new ActiveXObject("Microsoft.XMLHTTP");
	try {
		xhr.open( "GET", url, false );
		xhr.send();
		
		stream.Open();
		stream.Type = 1; // バイナリモード
		stream.Write( xhr.responseBody ); // バイナリ書き込み
		
		stream.Position = 0; // 読み取り位置を巻き戻す
		stream.Type = 2; // テキストモードに変更
		stream.Charset = "_autodetect"; // 文字コード自動検出
		
		return stream.ReadText();
	} finally {
		stream.Close();
	}
}
ちょっと注意点があって、
  • 先にTypeを変更してからPositionを変更する
  • Typeを変更する前にCharsetを割り当てる
といった操作を行うと、「このコンテキストで操作は許可されていません」と怒られるので、かならずPosition = 0 → Type = 2 → Charset = charsetの順序で実行する必要がある。

コードサンプル

ローカルでwebサーバが動いていて、ルート直下にdummy.jsとprototype.jsがある場合

with( {
	// ライブラリのパスリスト
	libs : [
		"http://localhost/dummy.js",
		"http://localhost/prototype.js"
	],
	// libs[]のインデックス
	index : 0,
	// ソース取得メソッド
	getSource : function(url) {
		var stream = new ActiveXObject("ADODB.Stream");
		var xhr = new ActiveXObject("Microsoft.XMLHTTP");
		try {
			xhr.open( "GET", url, false );
			xhr.send();
			
			stream.Open();
			stream.Type = 1; // バイナリモード
			stream.Write( xhr.responseBody ); // バイナリ書き込み
			
			// ↓ 最初のPOST時に順序が間違ってたので修正(07.04.25 02:07)
			stream.Position = 0; // 読み取り位置を巻き戻す
			stream.Type = 2; // テキストモードに変更
			stream.Charset = "_autodetect"; // 文字コード自動検出
			
			return stream.ReadText();
		} finally {
			stream.Close();
		}
	}
} ) {
	while( index < libs.length ) {
		try {
			eval( getSource( libs[ index++ ] ) );
		} catch(e) {
			WScript.Echo( e.description || e.message || "error" );
		}
	}
}

WScript.Echo( $R(1, 10).inject( 0, function(total, value) {
	return ( total + value );
} ) ); // → 55
ちなみにlibsの各要素で「file://ファイルへの絶対パス」を使用するとローカルファイル読み出しができる。getSource()内でパラメータを検査して判断すればURLでもファイルパスでも対応できるようになる。

蛇足

せっかくADODB.Streamでエンコードの判別を行ってるけど、_autodetect時はutf-8をうまく判別してくれないのよね...

コマンドライン引数の展開2007年04月25日 02時56分17秒

WshArgumentsは使いづらい

もともとWSH実行環境下では、WScript.Argumentsでコマンドライン引数にアクセスできるが、ちょっと使いづらい(特に名前付き引数)。

なので、感覚的に使いやすいJavaScriptオブジェクトに展開してみる。(要:prototype.js)

名前付き引数と名前なし引数

WScript.Argumentsプロパティ(=WshArgumentsオブジェクト)には、Namedプロパティ(WshNamedオブジェクト)とUnnamedプロパティ(WshUnnamedオブジェクト)があり、それぞれ名前付き引数と名前なし引数へのアクセスを提供している。

名前付き引数は、たとえば「/f:ファイルパス」のようにスラッシュから始まるスイッチと引数の値をコロンで区切った形式で提供される。

名前なし引数はそのまんまで、スイッチを伴わず値のみで提供される。エクスプローラ上で.jsファイルにファイル/フォルダをドラッグドロップすると、そのパスが名前なし引数として渡される。

コレクション??

WshNamed/WshUnnamedオブジェクトはコレクションオブジェクトで、たとえばlengthプロパティはあるがfor/for...inでアクセスできないなど、結構使いづらい。

このようにActiveXで提供されるコレクションを操作する場合は、JScriptの独自拡張であるEnumeratorオブジェクトを使用する。Enumeratorの使い方は以下のような感じ。

// コレクションオブジェクトを引数にしてEnumeratorをnewする
var enumerator = new Enumerator( WScript.Arguments.Unnamed );
// atEnd()がtrueを返すまで、moveNext()して列挙を繰り返す
for(; ! enumerator.atEnd(); enumerator.moveNext()) {
	// 要素へアクセスするにはitem()メソッドを使う
	WScript.Echo( enumerator.item() );
}
このままじゃ使いづらいし、せっかくprototype.jsを使うんだからEnumerableとして使えるラッパーのファクトリを考えてみる。

$E()じゃひねりなさすぎだろ

ま、別に$Eじゃなくてもいいんだけど、仮ということで。

function $E(collection) {
	var en = new Enumerator( collection );
	return Object.extend( {
		_each : function(iterator) {
			// ここでリセットしておかないと繰り返し使えない
			en.moveFirst();
			for(; ! en.atEnd(); en.moveNext()) {
				iterator( en.item() );
			}
		}
	}, Enumerable );
}
_eachさえ実装できればEnumerableになるのはありがたい。

やっとこ引数の展開

ようやっと準備が整ったので、引数を展開する関数を作る。

function parseArguments() {
	// 引数が展開されたオブジェクト
	var result = {
		named : {},
		// Unnamedは単純なコレクションなので一発で展開
		unnamed : $E( WScript.Arguments.Unnamed ).toArray()
	}
	
	// NamedはEnumerateしてもキーしか列挙されないのでベタにまわして展開
	$E( WScript.Arguments.Named ).each( function(key) {
		result.named[ key ] = WScript.Arguments.Named.item( key );
	} );
	return result;
}
使い方は単純で、
var args = parseArguments();
と呼び出すだけ。 コマンドラインから
cscript test.js /f:C:\test.txt
と呼ばれた場合、
args.named["f"] // → C:\test.txt
のようにアクセスできる。