「AngularJS」と一致するもの

BMEANスタックというのは、BreezeJS, MongoDB, Express, AngularJS, Node.jsを使ったソフトエアスタックのことです。

Breezeを簡単に使いたいと思って、social-cms-backendを大幅に改修しました。 APIは変わっていないのですが、breeze-mongodbを使えるように工夫しました。反面、処理が複雑になりオーバヘッドが増えています。

その新しいsocial-cms-backendを使う段階で一つ気づいたことがあるので、メモ代わりに書いておきます。詳細は省きますが、クライアントサイドのコードでpromiseを使いたくなりました。そもそも、BreezeのAPIがpromiseを返します。Angularにも$qというのがあってpromiseが使えます。

ところが、Breezeのpromiseのメソッドが、

promise.then()
promise.fail()
promise.fin()

なのに対して、Angularの方は、

promise.then()
promise.catch()
promise.finally()

になっているのです。ちなみに、catch,finallyはjsBeautifierでは予約語とみなされて不便です。

wrapすればいいだけですが、なんとなく不満でした。それだけです。

Angularは、https://github.com/kriskowal/qをもとにしているとドキュメントに書いてありますが、どうしてメソッド名を変えたのでしょう。きっと、なにか理由があるのだとは思いますが、読み取れなかったです。

タイトルにある「困った」はこの事実にしばらく気づかず、コーディングではまってしまった、ということでした。


2/16追記。

q.jsのソースコードを読んでいたら、catch,finallyも一応サポートされていました。いずれにしても常に、thenだけを使っておくほうが安心な気がします。

オフラインファーストという言葉があるようです。言葉の存在こそ知りませんでしたが、以前からオフラインアプリを推進したいと思っていました。モバイルファーストなら、HTML5アプリといってもオフラインで使えるべきでしょう、と思います。これには賛否両論あるみたいですが。

http://blog.joelambert.co.uk/2012/11/26/offline-first-a-better-html5-user-experience/

2012年にこんな記事があったのですね。

さて、このオフラインファーストの文脈で、最近注目しているライブラリがあります。Breezeです。 まさにオフラインアプリを作るためのクライアントサイドデータ処理ライブラリです。

stackoverflowでBreezeの代替はないのかという質問がありましたが、

http://stackoverflow.com/questions/15938866/alternative-to-breeze-js

今のところ、ぱっとしたものはなさそうです。今後の登場にも期待しましょう。

Breezeはだいぶ強力なようですが、それを理解するのはなかなか骨が折れます。基本的なデータベースの概念が理解できていないとつらいのかしら。ドキュメントもあるのですが、どこから読んでいいのか分かりにくいです。stackoverflowで聞いてね、って感じです。それはそれで、いいアプローチだとは思います。 ちなみに、Angularのドキュメントは読みやすいと感じます。

今日の本題。MEANスタックって知っていますか? ちらほら記事があるので、知名度はそこそこでしょうか。 MongoDB, Express, AngularJS, Node.jsの頭文字をとっているのですが、個人的には順序が気になって仕方がないです。まあ、語呂合わせなんでしょうけど。

http://mean.io/

こんなサイトがあるのですね。

http://jp.blog.mongodb.org/post/49262866911/the-mean-stack-mongodb-expressjs-angularjs-and

なるほど、MongoDBの人たちが呼び始めたのでしょうか。

さて、本当の本題。BMEANスタックというのをご存知の人は少ないのではないでしょうか。まぁ、Breezeの人が呼んでいるだけだと思いますが、最近の一押しです。

BMEANのBはもちろんBreezeです。BreezeのバックエンドとしてMongoDBを使います。AngularJSも実はオフラインアプリと相性がいいと思っています。ExpressとNode.jsはバックエンドのフレームワークですね。

BMEANを見つけたのは、Breezeのサンプルアプリです。

http://www.breezejs.com/samples/zza

これもまたドキュメントが少なくて、ソースコード読めっていうスタンスなのですが、がんばって読んでみています。そこまで完全なオフラインファーストは意識されていないようで、基本的にはオンラインで動かすことを前提に、一時的にオフラインになっても大丈夫なようにできる(そのようにコーディングすれば)というところでしょうか。

しばらくはBMEANスタックを考えてみようと思います。 そのうち、オフラインファーストに求められるライブラリの形が見えてくるかもしれません。


2/8追記。

Breezeのチュートリアルは面白いです。

http://learn.breezejs.com/

インタラクティブに実行できるので、勉強になりますね。もっと内容が増えてくれたらいいのですが。

小ネタです。

http://docs.angularjs.org/api/ng.directive:form

によるとこのディレクティブは普通に<form>とすれば使えるのですが、 ネストするときは、<ng-form>とする、と書かれています。

これを利用したシーンがあったのでメモしておきます。

フォームをバリデーションする機能があります。例えば、次のように使います。

<form name="myForm">
  <input required ng-model="text"/>
  <button ng-disabled="!myForm.$valid" ng-click="submit()"/>
</form>

このようにすると、テキストフィールドが空のときはボタンが押せないようにできます。

さて、テキストフィールドをng-repeatで生成したい場合はどうしましょう。特に、個々のテキストフィールドごとに例えばアスタリスクマークをつけて、空であることを表示したい場合です。

こういうときにネストしたformを使うとよいようです。

<form name="myForm">
  <ul>
    <li ng-repeat="item in items">
      <ng-form name="itemForm">
        <input required ng-model="item.text"/>
        <span ng-show="!itemForm.$valid">*</span>
      </ng-form>
    </li>
  </ul>
  <button ng-disabled="!myForm.$valid" ng-click="submit()"/>
</form>

個人的にはitemFormのスコープが分かりにくいのですが、そういうものみたいです。nameでinterpolationできないからだとか。

これは使いこなせば便利そうですね。requiredだけでなく、正規表現でパターンを限定したりもできます。

詳しくはこちら。

http://docs.angularjs.org/api/ng.directive:input.text

type="email", type="url", type="number"もあります。

angular.jsからmongodbへのクエリを投げるときに困ったことです。 mongodbのupdate操作ではupdate operatorsというものがあります。 例えば、$setがそれです。 ところで、angular.jsでは$で始まるものは予約語のようにみなされています。

どういうことが起こるかというと、angular.jsの$httpで、

{$set: {title: 'News'}}

のようなJSONを送ろうとすると、$setが消えてしまいます。 この現象に気づくまでにだいぶ時間がかかりました。

解決法は、 http://angularjscorner.blogspot.jp/2012/09/angularjs-and-mongodb-trick.html くらいしかなさそうです。

突然ですが、angular.jsを使うときは、jquery.jsを読み込まないことをおすすめします。

angular.jsとjquery.jsはコンフリクトはしません。ライブラリ的には共存します。しかし、簡単に言えば、angular.jsを使う場合は、jquery.jsを使う必要がないのです。jquery.jsを読み込まない場合、angular.jsはjqLiteというサブセットを使います。ちゃんと検証したわけではありませんが、コードを眺める限りではjqLiteは軽量そうです。

jquery.jsを読み込まないほうがいい理由は軽量化よりも、マインドの問題の方が大きいです。angular.jsのコーディングスタイルとDOM操作は合いません。angular.jsでは、$scopeを使ってDOMをコンパイル(という用語でいいのかな)させればいいのです。こっちの方がパワフルでシンプルです。angular.jsを使いつつDOM操作するというのは、車に乗っているのに自転車を漕いでいるような感じです。ちょっと違うか。

というわけで、jquery.jsを読み込むからDOM操作をしたくなるわけで、それなら読み込まなければいいのです。と思っていつつ、ところが、既存ライブラリの多くはjquery.jsに依存していたりするのです。angular.jsでは、そういうときは、directiveにしてwarpするようにということのようですが、そうすると、jquery.jsは読み込むことになってしまいます。それでもいいのですが、なんか負けた気がします。

jquery.jsを使わないほうがいいと思っている人は、きっと他にもいるはず、ということで検索しました。

http://joelhooks.com/blog/2013/07/27/using-angularjs-stop-using-jquery-as-a-crutch/

そうですよ、乗り換えるならすっぱりと乗り換えましょうよ。

stackoverflowへのリンクがありました。

http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-emberjsor-other-client-mvc-framework-if-i-have-a/15012542#15012542

Voteが3200を超えています。初めて見ました、そんな投稿。

Don't even use jQuery. Don't even include it. It will hold you back.

いいこと書いてあります。

同じ人のメールでの書き込み(上記ブログでも言及されている)では、

You can wire up some callbacks and $apply calls to make a jQuery plugin work but as Pawel said, rewriting something in AngularJS often takes less work. jQuery doesn't have any of the binding or scope magic. When we cut out all of the jQuery code that makes up for that, we're often left with very little code. And when we put those few lines of code in an AngularJS directive, everything will work out of the box. So in balancing levels of effort, rewriting makes sense more often than it doesn't.

と、あります。書き直しちゃったほうが結果的に簡単ということですね。同じことをやるのに大したコーディングの量はないということと、テストできるコードになるということがメリットのようです。トータルでみたら生産性があがるということでしょう。

これからは、angular.jsを読み込むときは、jquery.jsを読み込まないようにしようと思います。これまでもそうしてきたのですが、さらにがんばって、jqueryライブラリを移植するとかですね。CSSは再利用できるでしょう。

ところで、jqueryベースのライブラリ資産ってどれくらいあるんでしょうか。それと同レベルのangularライブラリをそろえるのは至難の業でしょうね。

http://ngmodules.org/

というサイトがあるようです。今後の発展に期待します。

突然ですが、angular.jsで$routeProviderを使うとき、次のように書くと思います。

angular.module('MyModule', []).config(function($routeProvider) {
  $routeProvider.when('/foo', {
    templateUrl: 'foo.html'
  }).
  when('/bar', {
    templateUrl: 'bar.html'
  });
});

ところで、2つならいいですが場合よっては多いこともあると思います。 そこで汎用的なルートを書いてファイルを置けばそのルートが使えるようにしたいと思います。もちろんファイルがないときはエラーになります。

ドキュメントを参照すると、

http://docs.angularjs.org/api/ngRoute.$routeProvider

正規表現は使えないのですが、named groupは使えます。つまり、

when('/:name', {

のように書くことができます。さてこの場合、 $routeParams.nameで実際の値にアクセスできるのですが、 $routeParamsはルートが切り替わるまで使えません。 つまり、moduleのconfigでは使えないのです。

検索したところ、これを見つけました。

http://stackoverflow.com/questions/11534710/angularjs-how-to-use-routeparams-in-generating-the-templateurl

ところで、常々思うのですが、stackoverflowを参照するときは注意する必要があります。回答を鵜呑みにしてはいけません。

今回のケースでは、現時点では1番初めの回答は、「できない」というものです。この回答が最もvoteされ、acceptもされています。 しかし、現時点での2番目の回答を見ると、angular-1.1.3以降では「できる」となっています。

まあ、このケースはそれほどひどくはなくて、おそらく質問時点では、できなかったのでしょう。

というわけで、現時点のangular-1.2では下記でできます。

angular.module('MyModule', []).config(function($routeProvider) {
  $routeProvider.when('/:name', {
    templateUrl: function(params) {
      return params.name + '.html';
    }
  });
});

ついでにcontrollerもnameを使って動的に設定できたらよいと思うのですが、それはまたの機会にでも調べることにします。

AngularJS 1.2.0がリリースされたので、 今までangular-1.0.8を使っていたプロジェクトをアップデートしてみました。

そのときに困ったこと、修正したことをメモしておきます。

まず初めに、読み込むスクリプトを変えたのですが、エラーになりました。

Error: [$injector:modulerr] http://errors.angularjs.org/undefined/$injector/modulerr?...

のようなエラーです。何かが悪いのは分かりますが、エラーの内容が分かりません。ここが、一番はまったところです。実は、minifyしたスクリプトだとこのようなエラーになるようです。angular.min.jsの代わりにangular.jsを読み込むようにしたら、エラーの原因が分かるようになりました。

一つ目のエラーはngRouteが別ファイルに分かれていることでした。これは、angular-route.jsを読み込むようにして、

angular.module('myApp', ['ngRoute']);

のようにすることで解決しました。

二つ目のエラーはng-bind-html-unsafeが使えないことでした。ng-bind-htmlに統合されて賢くなっています。angular-sanitize.jsを読み込んで同様に設定するだけで、サニタイズされるようになりました。ついでに、linkyというフィルターも入ったのでこれまで自作していたフィルターが不要になりました。詳しくは、

http://code.angularjs.org/1.2.0/docs/api/ngSanitize

のドキュメントを読んでみてください。linkyはアンカーのtarget属性も指定可能で、例えば、

<p ng-bind-html="x | linky:'_blank'" />

のように使います。

今回のケースはこの二つだけの修正で移行が完了しました。 angular-1.2.0ではその他変更点も多数あるようなので、これだけでは済まないケースもあるとは思いますが、一例として参考になればと。

今後、1.2.0の新しい機能にも挑戦していきたいと思います。


11/13追記。

もう一点修正が必要なものがありました。

Error: [$parse:isecprv] Referencing private fields in Angular expressions is disallowed!

というエラーがでていました。 オブジェクトの中のフィールドにアクセスできなくなったようですね。private fieldってなんだろう。まさか、単にアンダーバーのprefixが付いているというだけかしら。いずれにしても、オブジェクトごと関数に渡すようにして解決しました。

http://docs.angularjs.org/error/$parse:isecprv

に書いてありました。アンダーバーで始まるか終わるかするとプライベートとみなされるみたいです。

Angularの初期のチュートリアルでは、コントローラをグローバル関数として定義する例が載っています。

http://code.angularjs.org/1.0.8/docs/tutorial/step_02

function PhoneListCtrl($scope) {
  ...
}

といった感じで。これを参照してか、さまざまなところでAngularのサンプルコードが、グローバル関数のコントローラを使っています。

どこだか忘れましたが、よくドキュメントを読むと、production時はこれは好ましくないと書いてあります。ちゃんとDependency Injectionを使って依存解決するようにと。

最新のチュートリアルでは、上記コードは廃止されたようです。

http://docs.angularjs.org/tutorial/step_02

angular.module('MyApp', [])
.controller('PhoneListCtrl', function($scope) {
  ...
});

のようにします。これで名前の衝突が起こりません。 おそらく唯一の手間は、MyAppをちゃんと指定しないといけないことです。

<html ng-app="MyApp">

これは、ほとんどのケースで行われるので問題ないでしょう。

例によって、ちゃんと探せばこういう指針を書いたドキュメントもあるのかもしれませんが。ぜひとも正しい方法を広めていきたいものです。

backbone.jsのemulateHTTPというのは、PUTやDELETEをPOSTに変換するものです。なぜそんなことが必要かというと、PUTやDELETEが通らないHTTP Proxyがあるからです。

emulateHTTPでは、X-HTTP-Method-Overrideというヘッダーを使います。正確には、_methodというパラメータも追加されるのですが、それは対応しないものとします。X-HTTP-Method-Overrideはnode.jsのexpressやconnectでサポートされます。

app.use(express.methodOverride());

とすれば有効になります。

さて、こんな機能は当然angular.jsで用意されていて一行くらいの設定で使えるようになるだろうと思っていたら、そんなことはありませんでした。 検索すると、

http://manikanta.com/blog/2013/07/25/decorate-angularjs-http-service-to-convert-put/

がヒットしました。 これでできそうかと思ったのですが、実装が中途半端です。 具体的には、

$http.put({...});

のときは想定どおりに動くのですが、

$http({method: 'PUT', ...});

のような使い方ではうまく動きません。 そこで書き直してみることにします。

var App = angular.module('App');
App.config(['$provide', function($provide) {
  $provide.decorator('$http', ['$delegate', function($delegate) {
    var $http = function(config) {
      var method = config.method.toUpperCase();
      if (method === 'DELETE' || method === 'PUT') {
        config.headers || (config.headers = {});    
        config.headers['X-HTTP-Method-Override'] = method;
        config.method = 'POST';
      }
      return $delegate(config);
    };
    angular.extend($http, $delegate);
    $http.delete = function(url, config) {
      return $http(angular.extend(config || {}, {method: 'DELETE', url: url}));
    };
    $http.put = function(url, data, config) {
      return $http(angular.extend(config || {}, {method: 'PUT', url: url, data: data}));
    };
    return $http;
  }]);
}]);

こんな感じになりました。 もう少しスマートな方法はないかと、思ってしまいますね。 しかし、おかげで$provide.decoratorの使い方を覚えました。

上記コードは動作確認してないので、使用レポートをお待ちします。

以前書いた、 AngularJSを使ってRailsより簡単にTwitterクローンを作りたいと思って作ったnode.jsのライブラリ「social-cms-backend」をちょっとずつ改良しています。

今回は、通知機能です。通知機能というのは、例えば、自分のつぶやきに誰かが返信した場合にお知らせしてくれる機能です。今のところ、social-cms-backendはFacebookのみと連携しているので、通知機能もFacebook連携にしようと思いました。

Facebookアプリは何年か前にも一度作ったことがあって、そのときにapp requestsという機能で通知したのを覚えています。ところが、最近ではapp notificationsという機能もあるのですね。

http://developers.facebook.com/docs/games/notifications/

これです。このAPIの方がずっと素直で分かりやすい。難点は現状ではモバイルアプリに対応していないこと。また、通知先もCanvas対応アプリしか使えないようです。Betaとなっているので、将来的には使えることを期待してapp notificationsで行こうと思います。しかし、また数年後には使えなくなっていそう。Facebook依存はそのあたりがつらいので、social-cms-backendの通知機能としてはFacebook連携以外もサポートしていきたいところです。

APIは/<userid>/notificationsにHTTP POSTするだけです。

facebook-node-sdkを使った呼び出しサンプルコードは次のようになります。

FB.api(facebook_user_id + '/notifications', 'post', {
  href: href,
  template: template
}, function(res) { 
  //result handling
});

hrefはジャンプ先の相対URL、templateにはメッセージを入れます。これをちゃんと動かすにはapp access tokenが必要ですが、その方法は、

HerokuでFacebook APIを使う時の注意点

を参照してください。

いかがでしょうか、簡単でしょう?