「AngularJS」と一致するもの

以前の記事で紹介したsocial-cms-backendについてです。

念のため、リンクを載せておきます。

さて、social-cms-backendはバックエンドにMongoDBを使っていて、MongoDBのAPIをほぼそのままRESTに乗っけたものになっています。より正確には、認証やアクセス制限などの機構が入っています。

ところで、MongoDBのAPIでは複数のクエリを一回の呼び出しで投げることができません。これがどういう問題を引き起こしたかというと、twitter-clone-sampleではLikeボタンがありますが、Like数の取得はAPIの一回分がかかるのです。MongoDBのcount関数を使っているからです。

social-cms-backendはなるべく学習コストがかからないように、MongoDBのAPIをいじらないのが基本方針なのですが、現状だとLike数を表示する分だけ、ブラウザからREST呼び出しがかかります。例えば、25件の投稿が表示されそれぞれにLike数を表示するとREST呼び出しは26回になってしまいます。いくらデータ転送量が少なくても、26回のXHRはなかなかの時間がかかります。

そこで、25件のクエリを一回のXHRで行えるように改良しました。APIはとてもシンプルで、MongoDBのクエリを配列にして渡すだけです。当然、同一のREST endpointにしか使えません。今回のケースではこれでよしです。

ちなみに、サーバ側のコードではasync.jsを使いました。配列かどうかをチェックして分岐するだけなので、数行の追加ですみました。コードのリファクタリングをした後のカウントですが。

twitter-clone-sampleの方のコードはまだいじってません。スクリーンキャストのコードからあまりいじらないほうがいいかなと思いまして。スクリーンキャストをもう一度取り直すのはちょっと手間がかかってしまうので、当面保留です。

昨日、 NodeとAngularを使ってTwitterクローンを15分で作るスクリーンキャスト を書いたのですが、ライブラリの紹介をし損ねたので紹介します。

そもそもの動機は、Twitterクローンを簡単に作りたいと思ったことが発端です。 そこで、なにかいいライブラリはないかと色々調べたら、Railsもどきのフルスタックフレームワークがいくつか見つかりました。

他にもいっぱいあるのですが、気になったのがこのあたりです。

ところが、なんと言うかAngularJSを使おうとするとこれらのライブラリはオーバースペックな感じがするのです。Ruby on Railsのようにデファクトが一つある状態であればいいですが、もどきがいっぱいある状態ではコンベンションも統一感がなさそうです。

Angularを前提とするのであれば、フレームワークではなくもっとシンプルなライブラリがあればいいのではないかと思いました。それでもって、面倒なことはそのライブラリが引き受けてくれるような。

そこで、TwitterやFacebookのようなSNSのサイトを作ることに限定したライブラリを作りました。node.jsではexpress.jsがほぼデファクトなのでexpressのmiddlewareとして作りました。middlewareにすることで、単一のフレームワークに依存することなく、他のアプリにアドオンする形で導入できます。

面倒な、認証や権限管理の機能はライブラリが面倒みてくれます。 現状では、Facebook認証と、標準的な権限管理(ownership based)の機能が用意されているだけですが、プラグインとして他の認証や権限管理の機能を追加できるようになっています。 また、通知(メールアラートなど)の機能も今後追加してきたいと考えています。

このライブラリを使うと、フロントエンドを作るだけですぐサービスできます。Railsのようにデフォルトがないので作らないとなにもできないのですが、Angularに慣れていればコンベンションを意識してコーディングするよりもずっと楽かもしれません。

まずは、 こちら からスクリーンキャストを見てみると雰囲気が分かるかもしれません。 もしかしたら、分からないかもしれません。

ソースコードは、

https://github.com/dai-shi/social-cms-backend

にあります。


8/7追記。

最近知ったのですが、http://sproute.io/というフレームワークもあるようですね。 こちらは比較的SNSに特化したようなフレームワークであるようです。 ほとんどコーディングなしでTwitterクローンが作れるようです。 でも、$99って。

みなさん、AngularJS使ってますか?

Angularを使うと、お手軽にWebアプリ(と言っていいのかな)が作れます。

そのお手軽さをさらに助けるのが、Node.jsとそのライブラリ達です。(いや、Nodeに限った話でもないですが。) 細かい説明は他にお任せしますが、Connect/Expressやその様々なmiddlewareを使うと機能を色々追加できます。 今回、そのmiddlewareの一つとして、SNSのバックエンドライブラリを作りました。 これを使うと、フロントエンドを作るだけで簡単にSNSのサイトができあがります。

試しに、Twitterクローンを作ってみました。今回はフォロー機能などなしの単機能版です。ちなみに、フォローやグループの機能はライブラリには入ってます。

Ruby on Railsのまねをして15分でコーディングするスクリーンキャストを作りました。NodeやAngularの知識を前提としているので、見るだけそれらが理解できるようになるわけではありませんが、どうぞご覧ください。音もテロップもなし、それどころかマウスポインタもなしです。シンプルに。

スクリーンキャストを別ウインドウで開く

コーディングした結果のソースコードはこちらに置いてあります。

https://github.com/dai-shi/twitter-clone-sample/tree/20130804_recorded

実は一文字だけ打ち間違いがあってあとから修正しました。

せっかくなので作ったTwitterクローンを動くようにアップしました。

http://twitterclonesample-nodeangularapp.rhcloud.com/

もしよろしければ試してみてください。

AngularJSがnode.jsと通信するときは、RESTでJSONを返すのが便利です。標準的なやり方にしておくと、コードを書く量が減らせます。

node.jsとexpress.jsでRESTを書くときは、

res.json({foo: 'hoge', bar: 123});

のようにします。

これまで、真偽値を返したい場合は、

res.json(true);

としていました。node.jsはこれで正しいです。しかし、これだとAngular的にうまくありません。

Angularはプリミティブな値が返ってくることは想定していないようで、オブジェクトかオブジェクトの配列の形で返してあげる必要があります。文字列の配列もダメです。

今回の例では、

res.json({result: true});

とすることで解決できました。

実は、この話は半年くらい前にも気づいて調べたことがあったのですが、すっかり忘れていました。今日も全く同じことをして1時間ほどはまりました。忘れないように、ということでメモでした。

AngularJSにはフィルタという便利な機能があります。

チュートリアルの3で解説されています。

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

動作するデモもあります。

http://angular.github.io/angular-phonecat/step-3/app

試してみたことない人はぜひどうぞ。

さて、このフィルタですが、およそ次のように使います。

<input ng-model="query">
<ul>
  <li ng-repeat="item in items | filter:query">
    {{item.title}}
  </li>
</ul>

この場合、queryが空の場合は全件が出力され、queryを入力し始めると件数が絞られていきます。 これを、queryが空の場合は全件ではなく例えば、10件にしたいと思いました。

AngularJSにはlimitToというフィルタもありますので、それを使うことでできました。

<li ng-repeat="item in items | filter:query | limitTo:limit">

のように変更します。 さて、このlimit変数はどうしましょう。 あまりうまい方法は思いつかなかったのですが、JSで次のようにしました。

$scope.updateLimit = function() {
  if(query.length) {
    limit = 99999;
  } else {
    limit = 10;
  }
};
$scope.updateLimit();

さらに、HTML側を次のようにします。

<input ng-model="query" ng-change="updateLimit()">

これでqueryが空の場合はlimitが10になります。そうでない場合は99999にすることで全件表示しようとしてますが、当然この数字より大きい配列がきたら切れてしまいます。nullとかNaNとか指定できないのかと思いましたが、angular.jsのソースを読んでもそんな例外扱いはありませんでした。 フィルタを条件付にできないのかとも思いましたが、ドキュメントを読む限りでは分かりません。

とりあえず、これでよしとします。トップ10件にする場合は、limitを呼び出す前に並び替えればよいでしょう。

<li ng-repeat="item in items | filter:query | orderBy:orderFunc | limitTo:limit">

それで、同じようにorderFuncを条件に応じて変更するということです。まあ、ここまでするくらいならカスタムフィルタを書いたほうが早いかもしれませんね。

おまけ。

filterやorderByやlimitToなどのことを総称して「フィルタ」と呼ぶのですが、filterも「フィルタ」です。正確には「フィルタのフィルタ」でしょうか。ややこしい。

最近、ページ内リンクというのはあまり使われないと思いますが、使いたいケースがあったので、調べました。備忘メモです。

ページ内リンクというのは、リンク先に

<a name="id001">

とか

<div id="id001">

とか書いておいて、リンク元を

<a href="#id001">

とすることで、このリンクをクリックするとリンク先にスクロールしてくれるものです。

ところが、AngularJSではこれはルートとみなされ、ご丁寧に#/id001に変換して、$routeProviderの処理がされてしまいます。当然、そのようなルートは用意していないので、予期せぬ動作になります。例えば、otherwiseにredirectToを設定していたりすると、トップページに戻ります。

これは困ったということで、いろいろ調べました。はじめに見つけたのがこれ。

Angular JS - Scrolling To An Element By Id

AngularJSには$anchorScroll()というのが用意されていてその使い方が説明されています。具体的には、$rootScopeの$routeChangeSuccessイベントをフックするのですが、詳しくは、上記サイトをご覧ください。

さて、これを試しましたが、うまくありませんでした。 新しいページを表示してスクロールする分にはいいのですが、同一ページでもリフレッシュされてしまいます。つまり、$routeChangeSccessイベントはルートが新しくなってから呼ばれるものなのです。AngularJSのドキュメントを読むと、$routeChangeStartというイベントもあるのですが、結局、そのイベント処理のあとに、ルートを書き換えてしまいました。

さらに調べると、次のIssueを見つけました。

https://github.com/angular/angular.js/issues/1699

$locationのhashやpathを変更しても、ルートを書き換えない方法を用意してほしいという要望です。この中に書いてある方法では解決しませんでしたが、最後にPRを見つけました。

https://github.com/angular/angular.js/pull/2555

蛇足ですが、このPRは分かりやすく書かれてますね、Basic Usageとか。 でも、結局このPRもリジェクトされてました。 これを許すと、URLの状態とルートの状態が整合しないことができてしまうので避けたいそうです。

悩んだあげく、次の方法で解決しました。

まず、$routeProviderを次のようにします。

$routeProvider.when('/home', {
  templateUrl: 'partials/home.html',
  controller: HomeCtrl,
  reloadOnSearch: false
}).
otherwise({
  redirectTo: '/home'
});

ポイントは、reloadOnSearchです。OnSearchとありますが、hashの変更にも有効のようでした。

その上で、

<a href="#id001">

の代わりに、

<a href="#/home#id001">

とします。まあなんとも普通の方法ですが、これで目的は達成できました。 /homeがハードコードされているのが気になる方は、$locationが使える状況なら、次のようにしましょう。

'<a href="#' + $location.path() + '#id001">'

いかがでしょうか。AngularJSを使いつつ、レガシーなことをやろうとすると大変だ、という例でした。

ちなみに、hrefを使わなくて良いのなら、

Angular JS - Scrolling To An Element By Id

の2つ目の方法がよさそうです。試していませんが。

最近、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数が増えてきました。それはそれで不思議。

RSS Pipesは、JavaScriptでフィルターを書く、RSSアグリゲーターです。

RSSの登録数が増えてきたら検索機能を作ろうと思っていたのですが、 先に作ってしまいました。検索と言っても、表示されているものを絞り込むだけです。

AngularJSには標準でフィルターというものが組み込まれているので非常に簡単にできました。ソースコードに検索ボックス配置の1行と、フィルター追記の1行で済んでしまいました。Bootstrapのレイアウトでもう数行いじりましたが、トータルで5行の改変のみです。

興味ある方は、diffを見てみてください。

https://github.com/dai-shi/rss-pipes/commit/c250c8a7d9b03476503c78fda5a2f0ab5cef5894#views/partials/home.jade

お手軽です。詳しくは、AngularJSのチュートリアルを参照しましょう。

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

RSS PipesはJavaScriptでフィルターを書くRSSアグリゲーターです。

既に存在するアイテムをコピーして新しく登録する「複製」機能が欲しいと思って実装方法を考えていたのですが、思いのほか簡単にできました。

備忘メモしておきます。

$routeProviderのコードは、

$routeProvider.when('/home', {...});
$routeProvider.when('/edit', {...});

のようになっています。/homeに複製ボタンがあり、それを押すと/editに遷移します。つまり、/homeから/editに複製元のデータを渡したいということです。

$rootScopeを使いました。

$rootScope.saved = {};

としておいて、/homeのコントローラで、

$scope.saved.data = data;

として保存し、/editのコントローラで、

$scope.data = $scope.saved.data;

として復元します。場合によりますが、今回は消したかったので、さらに、

delete $scope.saved.data;

としました。

もっとスマートな方法があるのかもしれませんが、今回はこれでよしとします。

JQueryやAngularJSはGoogle Hosted Librariesが配信してくれて、 BootstrapはBootstrapCDNというのもあって 便利です。

しかし、マイナーなライブラリになるとどうしたものかと思っていました。手を入れないライブラリは自分でホスティングする理由もないので、CDNがないかなと思って探したところ、

を見つけました。cdnjsはGitHubのPull Requestsでライブラリの登録を依頼できるというのが面白いです。

しかし、どちらもcssファイルは登録されていない様子なので、結局すべてを外部CDNに頼ることはできないと。探せば他にもありそうですね。