「AngularJS」と一致するもの

angular.jsはサーバとの通信にJSONを使うことが典型的ですが、別にテキストでもXMLでも大丈夫のようです。今回、XMLを取得してng-repeatで表示するサンプルを作ったので簡単に紹介します。

まず、コントローラ内で次のようにします。

  $scope.jQuery = jQuery;
  $scope.getXmlContent = function() {
    $http.get($scope.xmlUrl).success(function(data) {
      $scope.xmlDoc = jQuery(jQuery.parseXML(data));
    });
  };
  $scope.getXmlContent();

jQueryはロードされているものとします。getXmlContentが$scopeに入っているのは、ng-clickからも呼べるようにするためです。取得先のURLは同一ホストでないとブラウザにはじかれると思います。

次に、html側で次のようにします。

<div ng-show="xmlDoc">
  <ul>
    <li ng-repeat="itemEle in xmlDoc.find('item').get()">
      <span ng-bind="jQuery(item.Ele).find('content').text()" />
    </li>
  </ul>
</div>

ng-repeatのところでget()を使ってリストにすることがポイントです。

あまり利用シーンはないかもしれませんが、参考にどうぞ。

JavaScriptにはJavaで言うところのString.startsWithがないです。みんなどうやっているかというと、

if (str.indexOf('prefix') === 0) {
    console.log('prefix found');
}

みたいな感じのコードをよく見ます。AngularJSのソースでも見ました。でも、これって良く考えると無駄ですよね。indexOfというのは文字列を最初から最後までなめて、一致する位置を探すのです。見つかれば一発ですが、見つからなければ最後まで行って-1を返します。本来、prefixを判定するのなら最初だけ検査すればいいのです。

str.substring(0, 'prefix'.length) === 'prefix'

という書き方も見ますが、これもいまひとつです。substringは新たに文字列を作ってしまうので。結論としては、

str.lastIndexOf('prefix', 0) === 0

が一番いいと思います。lastIndexOfは文字列の後ろから検査するのですが、検査開始位置を0にしているので、判定するのは一回だけです。Javaの内部実装には詳しくないですが、String.startsWithも同じような実装でしょう。

さて、これが本当かを確かめるために、benchmark.jsでベンチマークを書こうかと思いました。いや、せっかくなのでjsperfで書いてみようと思って、書き始めたら、あれ、既に存在する。世の中同じこと考える人はいますね。

http://jsperf.com/string-startswith/3

最後の一つのテストケースは私が追加したものです。 想定通り、lastIndexOfが速いです。ん?しかし、Chromeだとcompiled regexの方が速い。いや、そんなはずは、、。compiled regexが速いのは事実ですが、lastIndexOfが遅いのです。こんな単純な関数でChromeがFirefoxより遅いのは想定外でした。

Chromeで遅いということは、v8が遅いのかと思って、node.jsでもテストしてみました。

https://gist.github.com/dai-shi/4950506

案の定、node.jsでも同様の結果です。 それでも、私はlastIndexOfを使うことをおすすめします。 v8で遅いのはv8が直るべきです。ところで、これはissueになっているのかな?

http://code.google.com/p/v8/issues/list

どうやらなっていないようです。ちょっとソースを覗いてみますか。v8はsubversionです。bleeding edgeブランチが最新のようです。

http://v8.googlecode.com/svn/branches/bleeding_edge/

checkoutしてStringLastIndexOfのソースコードを追ってみましたが、特に単純な問題があるようには見えません。久しぶりのCのソースで頭が疲れます。そりゃそうですよね、天下のGoogleが作っているものがそんな自明なバグを残しているわけないですよね。というわけで、しばらくは様子をみることにします。

ところで、jsperf.comにstring-startswithがあったのが悔しかったので、真似してstring-endswithを作ってみました。

http://jsperf.com/string-endswith

傾向はstartsWithと同様です。ここまで読んでくれたみなさん、suffixチェックするときに、間違ってもnaive indexOfのような書き方はしないように。

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:..."は動かないとか)でやっている部分があるため。

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

AngularJSのangularInitを読みました。

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

と書くことは、

angular.bootstrap(document, ["<mymodule>"]);

とたぶん同等です。 より正確には、readyで呼ばれるので、

angular.element(document).ready(function() {
  angular.bootstrap(document, ["<mymodule>"]);
});

となります。angular.elementはJQueryかJQLiteです。

分かったことは、それだけです。


追記。

よく読んだら、ドキュメントにちゃんと書いてありました。

http://docs.angularjs.org/guide/bootstrap

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 のe2eテストで少し困ってます。

AngularJSのブートストラップは通常、

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

とアノテーションをつけることによって行われるのですが、 ある理由からブートストラップを自分で呼びたいと考えています。 これ自体は比較的簡単で、

angular.bootstrap(document, ["<modulename>"]);

を呼び出せばいいだけです。より正確には、

<script>
  angular.element(document).ready(function() {
    angular.bootstrap(document, ["<modulename>"]);
  });
</script>

といったことろです。

ところが!

このブートストラップの方法だと、testacularが動いてくれないのです。困りました。browser().navigateTo(...)を呼び出すと返ってこないのです。

解決策は今のところ見つかっていません。とりあえず、テストするときは、ng-appをつけておくしかありませんね。

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が残っていたからでしょう。