jQuery on Rhino
唐突にサーバサイドJavaScriptがやってみたくなった。Mayaaとかがやっているように、Rhinoを使って、JavaでつくったWebアプリのビュー部分でJavaScriptを使うという計画。
JavaScriptでHTMLをいじるならjQueryしかないと個人的に思っているので、まず jQuery on Rhino を試してみようと考える。ちょっと検索してみると Bringing the Browser to the Server というjQueryの作者自身の記事があった。記事は2007年と古いけど、もう少し新しいバージョンがGitHubの jeresig/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, "&").replace(/</g, "<").replace(/>/g, ">")) ); }, + 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年近くも更新されていない!!