「jsdom」と一致するもの

jsdomへのpull request: XHRサポート

  • 投稿日:
  • by

connect-prerendererをちゃんと動かすためにやったjsdomの修正その2をpull requestにしました。

https://github.com/tmpvar/jsdom/pull/654

実はXHR自体は、 node-XMLHttpRequest をそのまま使うだけでほとんど苦労はありませんでした。

大変だったのは、クッキーを引き継ぐところでした。 そもそもXHRを使わない場合も、jsdomはクッキーの引継ぎをサポートしていませんでした。XHRを使わない場合というのは、JavaScriptやCSSや画像ファイルなどを読み込む場合です。

まず、JavaScriptのロード時にもクッキーを引き継ぐようにするコーディングをしてから、XHRでもクッキーを引き継ぐようにしました。XHRはnode-XMLHttpRequestの内部のコードまで理解しなくてはならず、苦労しました。ちょっと強引に実装したため、もしかしたら将来のバージョンのnode-XMLHttpRequestでは動かないかもしれません。

pull requestは無事マージされて、jsdom v0.8.1がリリースされました。興味がある方はお試しください。

https://npmjs.org/package/jsdom

connect-prerenderer もこのバージョンを使うように修正しました。

https://npmjs.org/package/connect-prerenderer

今回は、npm周りのTIPSです。特に新しい知見ではないのですが、メモのために書いておきます。

node.jsのパッケージマネージャであるnpmは依存パッケージの解決をしてくれます。依存パッケージは、package.jsonに書いておきます。

例えば、expressを使う場合、

"dependencies": {
  "express": "*"
}

のように書きます。バージョン番号を指定しておきたい場合もあります。

"dependencies": {
  "express": "3.1.0"
}

のように書きます。他にも不等号やチルダを使ってバージョン番号の範囲を書くこともできます。今回はそのあたりは詳しく書きませんが、package.jsonにはバージョン番号の範囲を書いておくことがおすすめです。node.js関連のパッケージはまだ発展途上で仕様が変わることがよくあるからです。そうでなくても、">=3.0.0"などと書いておけば、"3.0.0"では動いたのだということが分かってよいかもしれません。

さて、本題です。依存パッケージがnpmに登録されていない場合はどうすればよいでしょう? 一つの答えは、登録してしまえばよいのです。が、登録したくない場合もあります。今回そのようなケースがありました。それは、GitHubでforkした場合です。将来的にはPull Requestして取り込んでもらうつもりなら、npmに独自バージョンを登録するのはうまくありません。そこで、npmに登録されていないけれど、依存パッケージとして使いたいときにどうするかという話です。

GitHubを使うとしても方法は2通りあります。一つは、tarballで取得する方法、もう一つは、gitプロトコルで取得する方法です。

前者は、

"dependencies": {
  "jsdom": "https://github.com/dai-shi/jsdom/tarball/3bb5b24c5e"
}

のように書き、後者は、

"dependencies": {
  "jsdom": "git://github.com/dai-shi/jsdom.git#3bb5b24c5e"
}

のように書きます。

どちらがいいのでしょう? よく分かりません。 試しに一回だけ、npm installの時間を計測してみました。 結果、前者が11秒、後者が14秒でした。 gitプロトコルの方がオーバヘッドがあるのかもしれません。 httpプロキシしか使えない場合は、前者に決まりですね。 GitHubがtarballのURLを廃止したら(ZIPのURLはリンクがありますが、tarballってどこまでオフィシャルなのでしょう?)、後者に決まりですね。

今のところはどちらでもよいのかもしれません。


10/20追記。

"https://github.com/dai-shi/jsdom/tarball/3bb5b24c5e"

の代わりに、

"https://github.com/dai-shi/jsdom/archive/3bb5b24c5e.tar.gz"

と書く方法もあるようです。こっちの方がオフィシャルな感じがしますね。

connect-prerendererをちゃんと動かすためにやった 修正をjsdomのpull requestにして欲しいと依頼されました。

unit testを書いて欲しいということなので、書いてみたのですが動かなくて苦労しました。 __defineGetter__と__defineSetter__を初めて使いました。 ECMAScriptではこれらは入っていない(definePropertyを使う)そうですが、nodeでは__defineGetter__と__defineSetter__は使えるようです。

結局、当初やっていた修正では全くダメ(なぜconnect-prerendererが動いたのか不明)で、window.locationまわりのコードを全部書き直しました。 おかげで、全体的により仕様に合った動きをするようになり、立派なpull requestができあがりました。

ちゃんとマージされたようで、めでたしめでたし。

https://github.com/tmpvar/jsdom/pull/650

connect-prerendererのIssue #3を解決

  • 投稿日:
  • by

DailyJSに取り上げてもらって、 一気にStar数が増えたconnect-prerendererですが、 Issueが一つ残ってました。

https://github.com/dai-shi/connect-prerenderer/issues/3

jsdomがらみなので、難しいなぁと思っていたのですが、 こっちの方 でも使ってくれそうな様子なので、ちょっとがんばってみました。

ヒントは、Issue #3で報告してくれた、

http://stackoverflow.com/questions/10054071/jsdom-hashchange-event

と、jsdomの古いIssueの

https://github.com/tmpvar/jsdom/issues/433

でした。

そもそも、hashchangeイベントの発火が実装されていないとのことでした。 みようみまねでjsdomに手を加えて、コードを追加しました。

結果、動くようになりました。まだ簡単なe2eテストが動いただけですが、 不具合があれば誰か報告してくれることでしょう。それまで、待つ。

最近、connect-prerendererのIssueを報告してもらって、少しずつ不具合が見つかっては修正しています。

うまく解決したのは、

の二つ。前者は、

<div>
  count={{count}}
  <span>hoge</span>
</div>

のように、TextNodeとHTMLElementが並んでいるときに、 ng-bind-templateで上書きしてしまう問題でした。

<div>
  <span>count={{count}}</span>
  <span>hoge</span>
</div>

とコンパイル時になるように、Angular.jsをいじりました。 ただ、CSSがそれを想定しない書き方だと、表示に不具合がでるかもしれません。

後者は、doctypeがjsdomで消えている問題でした。こっちはすぐに直りました。

https://github.com/dai-shi/connect-prerenderer/issues/3 がなかなか手ごわく、まだ解決の糸口が見つかりません。 以前書いた、jsdomからAngularJSの不具合二つ目の問題です。

なんにしても、興味を持ってくれる人が増えるのはうれしいことです。 connect-prerendererはほとんど宣伝していないのに、Star数が増えてきました。それはそれで不思議。

connect-prerendererが一応完成

  • 投稿日:
  • by

AngularJSのマイルストーンにserver-side prerenderingというのがあるのですが、当分できそうにないので自分で作ってしまっています。

https://github.com/dai-shi/connect-prerenderer

express.jsのmiddlewareで使うことを想定していますが、express.jsには依存しないようにしたので、connect-prerendererです。expressとconnectの違いは、他のブログなどでも説明されていることでしょう。

何をやっているかを一言で言うと、サーバ側でjsdomを使ってJavaScriptを走らせて、レンダリング後のHTMLページをクライアントに返すというものです。

基本的なところはしばらく前にできていて、簡単なテストケースは動いていたのですが、AngularJSを動かすのがなかなか大変でした。結局、angular.jsのソースに手を入れることになりました。

AngularJSを使った簡単なテストケースも動いて、満足です。 もってきてすぐ使えるようなライブラリではないですが、たぶん世界初の取り組みでしょう。

あとははAngulaJS側でもっとうまく取り込んでもらわないとつらいところです。今は決めうち(ng-repeat="..."はOKだが、class="ng-repeat:..."は動かないとか)でやっている部分があるため。

あまり説明できずに意味不明かと思いますが、これにて。

jsdomからAngularJSの不具合二つ目

  • 投稿日:
  • by

jsdomからAngularJSで作ったページにアクセスしたら、こんなエラーがでました。

Error: 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [["fn: $locationWatch; newVal:18; oldVal: 17"],["fn: $locationWatch; newVal: 19; oldVal:18"],["fn: $locationWatch; newVal: 20; oldVal: 19"],["fn:$locationWatch; newVal: 21; oldVal: 20"],["fn: $locationWatch;newVal: 22; oldVal: 21"]]

このエラーは"familiar"らしいです。一番該当しそうなのが、

angular.js Issue #1417

これです。現時点ではまだ解決されていない様子です。

果たして、$routeProviderでredirectToを使うのを止めてみたらエラーが出なくなりました。んー、不便です。

https://github.com/dai-shi/connect-prerendererを開発中に出会った事象について書き留めておきます。

jsdomでAngularJSを使ったテストページにアクセスすることになったのですが、 angularが初期化されなくて困ってました。ぐぐったところ、

AngularJSがIE8で動かないときは

を見つけたのですが、この方法では解決せず。結局、次のようにしたら動きました。

<html ng-app class="ng-app">

id="ng-app"はあってもなくても変わらず。モジュールがある場合は、class="ng-app:<modulename>"でもうまくいくのかもしれません。

angularのソースコードを少し眺めると、

function angularInit(element, bootstrap) {
  var elements = [element],
      appElement,
      module,
      names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
      NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
//-----snipped-----
  forEach(names, function(name) {
    names[name] = true;
    append(document.getElementById(name));
    name = name.replace(':', '\\:');
    if (element.querySelectorAll) {
      forEach(element.querySelectorAll('.' + name), append);
      forEach(element.querySelectorAll('.' + name + '\\:'), append);
      forEach(element.querySelectorAll('[' + name + ']'), append);
    }
  });

ってな感じで、どんな風に書いても動きそうです。なぜ、id="ng-app"でうまくいかなかったのか疑問が残りますが、追求しないことにします。


追記。

ng-appだけではダメだった理由は、ng-app="ng-app"に展開されてしまうからのようなので、ng-app="<modulename>"にすればうまくいくのかもしれません。

さらに、追記。

結局、ng-app=""で動いたので、ng-appだけだとjsdomでうまくいかないというのは正しそうです。class="ng-app:<modulename>"もうまくいきそう。id="ng-app"で動かなかった理由はおそらく、ng-appが残っていたからでしょう。