jQuery on Rhino

唐突にサーバサイドJavaScriptがやってみたくなった。Mayaaとかがやっているように、Rhinoを使って、JavaでつくったWebアプリのビュー部分でJavaScriptを使うという計画。

JavaScriptでHTMLをいじるならjQueryしかないと個人的に思っているので、まず jQuery on Rhino を試してみようと考える。ちょっと検索してみると Bringing the Browser to the Server というjQueryの作者自身の記事があった。記事は2007年と古いけど、もう少し新しいバージョンがGitHubjeresig/env-js にあるようなので、こちらで試してみる。

まずは、簡単な要素の操作。上のリポジトリにある test/jquery.js (jQuery 1.2.6) ではきちんと動く。(defaultValue が出るのはご愛敬)

C:\tmp>java -jar rhino\js.jar
Rhino 1.7 release 1 2008 03 06
js> load('env-js/src/env.js')
js> window.location = 'index.html'
index.html
js> load('env-js/test/jquery.js')
js> $('p').text()
testです。
js> $('p').text('変更しました')
[object Object]
js> document.innerHTML
<HTML defaultValue=''>
<BODY defaultValue=''>
<P defaultValue=''>変更しました</P>
</BODY>
</HTML>

でも、やっぱり最新版だろ! とjQueryを1.3.2に更新すると、動かない。

C:\tmp>java -jar rhino\js.jar
Rhino 1.7 release 1 2008 03 06
js> load('env-js/src/env.js')
js> window.location = 'index.html'
index.html
js> load('jquery.js')
js: "jquery.js", line 2167: uncaught JavaScript runtime exception: TypeError: Cannot
 find function createComment in object Document.
        at jquery.js:2167
        at jquery.js:2161
        at jquery.js:1419
        at jquery.js:12
        at <stdin>:4

ということで、とりあえず完全かどうか知らないが text() だけは動くように env.js を修正してみる。変更点は、

  • DOMDocument に createComment, createDocumentFragment がないので追加。
  • DOMDocumentFragment のプロトタイプが無かったので追加。
  • childNodes, firstChild, lastChild, appendChild, insertBefore はelementに固有ではないはずなので DOMElement から DOMNode に移動。

という感じ。

--- C:/tmp/env-js/src/env.js	Sun Oct 12 08:26:22 2008
+++ C:/tmp/env-js/src/my-env.js	Mon Feb 23 00:03:54 2009
@@ -150,6 +150,12 @@
 			return makeNode( this._dom.createTextNode(
 				text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")) );
 		},
+		createComment: function(data){
+			return makeNode( this._dom.createComment(data) );
+		},
+		createDocumentFragment: function(){
+			return makeNode( this._dom.createDocumentFragment() );
+		},
 		createElement: function(name){
 			return makeNode( this._dom.createElement(name.toLowerCase()) );
 		},
@@ -306,9 +312,59 @@
 		},
 		get outerHTML(){
 			return this.nodeValue;
+		},
+		get childNodes(){
+			return new DOMNodeList( this._dom.getChildNodes() );
+		},
+		get firstChild(){
+			return makeNode( this._dom.getFirstChild() );
+		},
+		get lastChild(){
+			return makeNode( this._dom.getLastChild() );
+		},
+		appendChild: function(node){
+			return makeNode( this._dom.appendChild( node._dom ) );
+		},
+		insertBefore: function(node,before){
+			this._dom.insertBefore( node._dom, before ? before._dom : before );
+			
+			execScripts( node );
+			
+			function execScripts( node ) {
+				if ( node.nodeName == "SCRIPT" ) {
+					if ( !node.getAttribute("src") ) {
+						with (window) {
+							eval( node.textContent );
+						}
+					}
+				} else {
+					var scripts = node.getElementsByTagName("script");
+					for ( var i = 0; i < scripts.length; i++ ) {
+						execScripts( node );
+					}
+				}
+			}
+		},
+		removeChild: function(node){
+			return makeNode( this._dom.removeChild( node._dom ) );
 		}
 	};
 
+	// DOM Document Fragment
+
+	window.DOMDocumentFragment = function(fragment){
+	  this._dom = fragment;
+	};
+
+	DOMDocumentFragment.prototype = extend(new DOMNode(), {
+		get nodeType(){
+			return 11;
+		},
+		get nodeName() {
+			return "#document-fragment";
+		},
+	});
+
 	window.DOMComment = function(node){
 		this._dom = node;
 	};
@@ -514,40 +570,6 @@
 		removeAttribute: function(name){
 			this._dom.removeAttribute(name);
 		},
-		
-		get childNodes(){
-			return new DOMNodeList( this._dom.getChildNodes() );
-		},
-		get firstChild(){
-			return makeNode( this._dom.getFirstChild() );
-		},
-		get lastChild(){
-			return makeNode( this._dom.getLastChild() );
-		},
-		appendChild: function(node){
-			this._dom.appendChild( node._dom );
-		},
-		insertBefore: function(node,before){
-			this._dom.insertBefore( node._dom, before ? before._dom : before );
-			
-			execScripts( node );
-			
-			function execScripts( node ) {
-				if ( node.nodeName == "SCRIPT" ) {
-					if ( !node.getAttribute("src") ) {
-						eval.call( window, node.textContent );
-					}
-				} else {
-					var scripts = node.getElementsByTagName("script");
-					for ( var i = 0; i < scripts.length; i++ ) {
-						execScripts( node );
-					}
-				}
-			}
-		},
-		removeChild: function(node){
-			this._dom.removeChild( node._dom );
-		},
 
 		getElementsByTagName: DOMDocument.prototype.getElementsByTagName,
 		
@@ -620,6 +642,8 @@
 			if ( !obj_nodes.containsKey( node ) )
 				obj_nodes.put( node, node.getNodeType() == 1?
 					new DOMElement( node ) :
+					node.getNodeType() == 11 ?
+					new DOMDocumentFragment( node ) :
 					node.getNodeType() == 8 ?
 					new DOMComment( node ) :
 					new DOMNode( node ) );

超手抜きの、Javaへの組み込み。本当はjsファイルはプリコンパイルしたほうがWebアプリとかでは使い回せるからいいっぽい。

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Scriptable;

(略)
  ContextFactory factory = new ContextFactory();
  Context cx = factory.enterContext();
  try {
    Scriptable scope = cx.initStandardObjects();
    cx.evaluateReader(scope, new FileReader("/tmp/env-js/src/my-env.js"), "env.js", 1, null);
    cx.evaluateString(scope, "window.location = 'file:///C:/tmp/index.html';", "<load>", 1, null);
    cx.evaluateReader(scope, new FileReader("/tmp/jquery.js"), "jquery.js", 1, null);
    cx.evaluateString(scope, "$('p').text('変更されました');", "<test>", 1, null);
    Object result = cx.evaluateString(scope, "document.innerHTML;", "<output>", 1, null);
    System.out.println(result.toString());
  } finally {
    Context.exit();
  }

そういえば、現在のRhinoでは上のように ContextFactory#enterContext() を使うのが作法らしいんだけど、公式のチュートリアル では未だに Context.enter() のまま。さらにRhino自体、2008年3月からなんと1年近くも更新されていない!!

これってRhinoもう終わりフラグですか。