「node.js」と一致するもの

以前の記事で紹介した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の方のコードはまだいじってません。スクリーンキャストのコードからあまりいじらないほうがいいかなと思いまして。スクリーンキャストをもう一度取り直すのはちょっと手間がかかってしまうので、当面保留です。

MongoDBでちゃんと$andがあった件

  • 投稿日:
  • by

以前の記事でMongoDBで$orが複数使えないという件を書きましたが、最近になって$andがちゃんと用意されていることを知りました。

node.jsのドライバを使っていたので、

http://mongodb.github.io/node-mongodb-native/

のドキュメントを読んでいたのですが、これが敗因でした。 MongoDB本体のドキュメントである

http://www.mongodb.org/

にはちゃんと書いてありました。こちら。

http://docs.mongodb.org/manual/reference/operator/and/

なぜ、前回は見つけられなかったのでしょう。

これを使えば、 「(AまたはB)かつ(CまたはD)」のようなクエリを書くことができます。

{"$and": [{$or: [{"name": "orange"}, {"name": "apple"}]},
          {$or: [{"color": "red"}, {"color": "green"}]}]}

といったところ。ちなみに、この例は$inでも書けてしまいますが。

MongoDBには他にも色々機能がありそうなので、また調べてみようと思います。

node.jsで作ったサンプルアプリをOpenShiftで動かそうとして、苦労した話を以前紹介したのですが、その後プロセスが落ちてしまうことが2度ありました。クラウドなのに。

一度目は、

~/nodejs/configuration/node.env

の環境変数の設定が書き戻ってしまう問題でした。 おそらく、OpenShift側のシステム更新のタイミングかなにかで、~/nodejsが丸ごと置き換わっているのだろ思われます。 このときは、症状は分かったもののそんなに頻繁に起こらないだろうと思って、またそのファイルを手動でいじって、プロセスを再起動して動かしました。

ところが、二度目が来たのです。 しかも今回はnode.envが書き換わるだけでなく、他にも問題がありました。 色々調べた(結局、起動スクリプトを読んだ)結果、以前はpackage.jsonに

"script": { "start": "node app.js"}

と書いておけばよかったのですが、今回の更新(?)でその設定は読まれなくなり、代わりに、

"main": "app.js"

を書かなければいけなくなりました。 これで一件落着。

しかし、また次の更新タイミングで、node.envが書き換わってしまうことは目に見えています。そこで、別の方法を探しました。ドキュメントを読んでみると、どうやら、クライアント側のコマンドツールであるrhcを使うとrhc set-envのようにすることで、環境変数が設定できるらしいのです。でも、rhcを入れて試すのは面倒なので、なんとかsshでファイルを編集するだけでできないか調べました。

そして、見つけました。前回調べた時は見つからなかったのですが、ありました。

~/.env/user_vars/

です。ここに環境変数ごとにファイルを作成しておくと、ちゃんと読み込まれるようです。~/.env/直下のファイルを参照すると分かります。直下のファイルはroot権限でしか変更できないのですが、user_varsの下は書き込み権限があるのです。

ぜひ、お試しください。

ところで、起動スクリプトを読んでいるときに、node010という文字列を見つけました。どうやら、OPENSHIFT_NODEJS_VERSIONという環境変数が0.10のときはnode010になるようで、もしかしたらnode-v0.10.*が動くのか、と期待します。試してみる?どうする?またの機会にしましょう。

しかし、こういう話ってどこかのドキュメントをちゃんと読めば載っているのでしょうか。

久しぶりの記事です。

social-cms-backendのテストコードをMochaで書いていますが、Facebookのログイン周りのテストをするのに、Facebookのテストユーザを使いました。

Facebookのテストユーザについては、

http://developers.facebook.com/docs/test_users/

にドキュメントがあります。

手順は書いてある通りで、(1)アプリのアクセストークンを取得、(2)テストユーザを作成、(3)専用ログインURLを開いてログインする、だけです。

requestを使ってこれらのステップを実行していたのですが、たったこれだけのことをやるのに何時間もはまりました。

1と2は何の問題もなかったのですが、3がうまく行かず、Facebookのログインページにリダイレクトされてしまうという問題でした。

解決方法は、User-Agentを設定する、です。

ドキュメントには書かれていません。もし同様の問題を抱えている人に参考になればと思います。参考になるか分かりませんが、その時点でのテストコードは、

https://github.com/dai-shi/social-cms-backend/blob/c678043a2ad0ce45bc51b576d77e53c7eac658e5/test/80_facebook_login_test.js

です。

初めに断っておきますが、今回の調査は、node v0.11.5 (Linux)で試したもので、他の環境では異なるかもしれません。さらに、今後のバージョンアップによっては全く異なる結果となることも十分ありえます。ご注意を。

ES6にgeneratorsが入るとのことで、JavaScriptでcontinuationが使えないかなぁと思っていた自分としては、興味を持ちました。

ちなみに、generator, coroutine, continuationの順に記述力が上がるそうです。こちらが参考になるかもしれません。

さて、generatorsの説明は他に任せるとして、いきなりコードです。

function* es6_generator() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  return 5;
}

これを複数回実行して時間を計測するコードはこちら。

console.time('es6_generator');
for (i = 0; i < 1000000; i++) {
  iterator = es6_generator();
  obj = {};
  while (!obj.done) {
    obj = iterator.next();
  }
}
console.timeEnd('es6_generator');

実行結果は、

es6_generator: 360ms

でした。うちのマシンは非力なので、最近のマシンならもっと速いかもしれません。

これと同等のコードをgeneratorsを使わずに書くとどうなるでしょうか。おそらく、

function hand_generator1() {
  var iterator = {};
  iterator.next = function() {
    iterator.next = function() {
      iterator.next = function() {
        iterator.next = function() {
          iterator.next = function() {
            return {value: 5, done: true};
          };
          return {value: 4, done: false};
        };
        return {value: 3, done: false};
      };
      return {value: 2, done: false};
    };
    return {value: 1, done: false};
  };
  return iterator;
}

こんな感じになるのでしょう。 これを同じように実行したら、

hand_generator1: 4529ms

となりました。これが等価なコードなのかは問題ですが、 少なくともhand_generator1のように書くよりは、 es6_generatorの方が速いとうことですね。

しかし、この単純な例だとfunctionをイテレーション毎に生成する必要はなくて、もっと効率的に書けます。

function hand_generator2() {
  var array = [1, 2, 3, 4, 5];
  var index = 0;
  var iterator = {
    next: function() {
      return {value: array[index++], done: array.length <= index};
    }
  };
  return iterator;
}

この場合の実行結果は、

hand_generator2: 265ms

となりました。es6_generatorと比べてどちらが読みやすいと思うかは人次第でしょうか。できることが違うので比較の対象にならないですが。

さて、少し話がややこしくなりますが、もともとは末尾再帰をやりたかったのです。末尾再帰の説明は他に任せるとして、factorialをgeneratorsを使って書いてみました。

function fact_tail_loop1(x) {
  var fact_tail_sub = function* (x, r) {
    if (x === 0) {
      return r;
    } else {
      yield fact_tail_sub(x - 1, x * r);
    }
  };
  var c = fact_tail_sub(x, 1);
  var d = {};
  while (!d.done) {
    d = c.next();
    c = d.value;
  }
  return d.value;
}

これを見て、ふーん、とか、おー、とか思わない場合は、この先を読んでもつまらないかもしれません。

実行コードはこちら。

console.time('fact_tail_loop1');
console.log(fact_tail_loop1(3));
console.log(fact_tail_loop1(5));
console.log(fact_tail_loop1(10));
console.log(fact_tail_loop1(100));
console.log(fact_tail_loop1(1000));
console.log(fact_tail_loop1(10000));
console.log(fact_tail_loop1(100000));
console.log(fact_tail_loop1(1000000));
console.timeEnd('fact_tail_loop1');

実行すると次のようになりました。

6
120
3628800
9.332621544394418e+157
Infinity
Infinity
Infinity
Infinity
fact_tail_loop1: 217ms

Infinityになってますが、ちゃんと計算はしているようです。ちなみに普通の再帰やproperでない末尾再帰の場合は、最後の方は計算できずにエラーになります。

さて、generatorsを使わないで書き直してみました。

function fact_tail_loop2(x) {
  var fact_tail_sub = function(x, r) {
    return {
      next: function() {
        if (x === 0) {
          return {
            value: r,
            done: true
          };
        } else {
          return {
            value: fact_tail_sub(x - 1, x * r),
            done: false
          };
        }
      }
    };
  };
  var c = fact_tail_sub(x, 1);
  var d = {};
  while (!d.done) {
    d = c.next();
    c = d.value;
  }
  return d.value;
}

同じように実行すると、

6
120
3628800
9.332621544394418e+157
Infinity
Infinity
Infinity
Infinity
fact_tail_loop2: 796ms

になりました。しかし、このコードはオブジェクトの生成が多すぎます。改良版はこちら。

var LoopCont = function(f) {
  this.f = f;
};

function fact_tail_loop3(x) {
  var fact_tail_sub = function(x, r) {
    if (x === 0) {
      return r;
    } else {
      return new LoopCont(function() {
        return fact_tail_sub(x - 1, x * r);
      });
    }
  };
  var c = fact_tail_sub(x, 1);
  while (c instanceof LoopCont) {
    c = c.f();
  }
  return c;
}

これを実行すると、

6
120
3628800
9.332621544394418e+157
Infinity
Infinity
Infinity
Infinity
fact_tail_loop3: 63ms

となりました。いかがでしょう。

私見ですが、generatorsのパフォーマンスは状況次第といったところでしょうか。v8の最適化が進めば、様々なケースで改善されるかと思われます。

しかし、generatorsを使って末尾再帰やcontinuationを実装できないかなぁ、と期待したのでちょっと残念でした。やっぱり言語レベルでサポートしてほしいですね。

個人的には、yield expressionは構造が分かりにくく感じ、好きになれそうにないので、直接使うことはないかと思います。うまく隠蔽してくれるライブラリがあればいいのかもしれません。

ここまで読んでくれた方は、 https://github.com/dai-shi/continuation.js にも興味を持っていただける可能性がありますので、 よろしければご覧ください。

これまでnode.jsのアプリのデプロイにはHerokuを使っていたのですが、 無料アカウントでは上限があるらしいのと、そもそも一つのサービスに依存するのもよろしくないので、別の候補を探しました。

いくつかあるようなのですが、昔は無料でも今は終わっていたり、 今も無料だけどベータサービスなので期限があったりと、 あまりぱっとしません。ビジネスモデルとしてフリーミアムがちゃんと まわってないと持続性がないでしょう。

一番、有望かなと思ったのがRedHatの OpenShift です。

正式サービスしてからはまだ間もないですが、無料のアカウントがサポートなしという形で残っているので、今後も継続することが期待できます。 ただ、無料でホストできるアプリは2つだけのようで、厳しいですが。

さて、 NodeとAngularを使ってTwitterクローンを15分で作るスクリーンキャスト で作ったTwitterクローンをOpenShiftの上にのせることにしました。

まず、OpenShiftのアカウント登録。簡単でした。次に、sshの公開鍵の登録。これもコピペするだけ、簡単。

続いて、アプリの作成。 domainが空いてないと登録できないので、何度もトライしました。 動的にチェックしてくれればいいのに。このあたり、まだまだな感じですね。

続いて、ソースのアップロード。gitのURLが表示されるので、それを、リモートリポジトリとして登録します。

$ git remote add openshift <URL>

で、PUSHします。

$ git push openshift master

ここまで大きな問題なし。このあと大変でした。

まず、今回のアプリはFacebookと連携するのでその情報を入れないといけないのですが、それはリポジトリにはいれるものではありません。 Herokuでは環境変数を使っていました。 openshiftでも環境変数を使えるようで、

$ vi ~/app-root/data/.bash_profile

で追記すれば環境変数を設定できます。 しかし、このままだとnodeを起動するときにその設定は読み込まれないようです。

openshiftにはpre_startというスクリプトがあって、それを使って、 source .bash_profileするようにしたのですが、うまくいきませんでした。

調べているうちに別の方法を見つけました。nodeでしか使えないようなのですが、

$ vi ~/nodejs/configuration/node.env

で環境変数の設定をすることができます。中身は、.bash_profileでの環境変数の設定と同じです。 これは、うまく行きました。

次の問題は、EACCESエラーです。なぜか、webサーバの起動でエラーになるのです。これは、どんぴしゃの記事がありました。

https://www.openshift.com/forums/openshift/other-threads-didnt-help-503-service-unavailable

OpenShiftではINADDR_ANYでlistenできないようです。 app.jsを次のように変えて対処しました。

app.set('port', process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 3000);
app.set('ipaddr', process.env.OPENSHIFT_NODEJS_IP);
....
http.createServer(app).listen(app.get('port'), app.get('ipaddr'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

これで、アプリが正常に動作しました。

慣れてしまえば、OpenShiftでもHerokuと同様の使用感でnode.jsのアプリをデプロイできそうです。ただ、困りそうなことが一つだけあります。OpenShiftで用意されているのは、node v0.6なのです。現在の安定版がv0.10なので2世代も前です。Herokuではv0.10もv0.8も使えます。開発スピードが速いnode.jsエコシステムでは致命的かもしれません。npmとかパッケージによっては動かないケースがでてきそうです。

今回は試しませんでしたが、node.jsカートリッジとは別に、DIYカートリッジというのがあるらしく、それを使えば好きなバージョンを入れることができるそうです。そもそも、Herokuではできないsshができるのでいろいろ自由度は高そうですね。

もう一つ、Herokuと比べて不便を感じたのは、デプロイの遅さです。pushしてから待つこと数分かかったような気がします。Herokuってすごいんだなぁ、と思った瞬間。

実際に今回デプロイしたアプリはこちらです。

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

なんにしてもPaaSの選択肢が増えることはよいことです。 興味ある方は試してみると良いでしょう。


9/10追記。 ~/nodejs/configuration/node.envのファイルですが、いつのまにか勝手に戻っていて、プロセスが8月後半から動いていなかったことが判明しました。とりあえず、再度書いておきましたが、また、勝手に上書きされてしまうかもしれません。もっといい方法があるのでしょうか。

昨日、 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/

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

今回は、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"

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

ちょっと新しいことに挑戦しようと思って、MongoDBを勉強中です。 node.jsから使うモジュールとして、 native driver と、 mongoose をみました。

Railsに慣れている人はODMのmongooseのが受け入れやすいかもしれませんが、 自分は生のJSONをいじりたかったので、native driverを使うことにしました。

http://christkv.github.com/node-mongodb-native/

を読み始めましたが、初めはとってもとっつきにくかったです。 どこから読んでいいか分からなかったです。

チュートリアルの一つ目から読むのがおすすめです。

全体像を理解してからは、Manualが読みやすかったです。 http://mongodb.github.io/node-mongodb-native/api-generated/collection.html が一例ですが、URLにapi-generatedと有るので、ソースコードから生成しているのでしょうか。ちなみに、ページの最初にあるUsageも読めなくはないですが、内容も古く中途半端な感じがしました。

MongoDBのAPIには満足です。JSONをそのまま保存でき、クエリで検索できます。Collectionを事前に生成しておく必要もありません、オンディマンドで作ってくれます。このあたりを便利に使うと、ODMには戻れないかも。まあ、自由度が高すぎるのでそれを嫌うケースもあるとは思います。

ところで、自分がやろうとしたことでできないことがこれまでに一つありました。

クエリもJSONで書くのですが、例えば、

{"product_name":"apple", "color":"red"}

のように書くと、product_nameがappleで、かつ、colorがredのデータを検索することになります。「または」の指定も可能で、

{"product_name":"apple", "$or":{"color":"red", "name":"ringo"}}

のように書けます。しかし、この書き方だと、 「(AまたはB)かつ(CまたはD)」のようなクエリが書けないのです。 うーん、惜しい。

Issueになっているのかと調べようと思いましたが、Issue Trackerの使い方がよく分かりません。nodeのドライバーの問題ではなく、本体の問題だろうとは思うので本体のIssueを探そうとは試みました。

https://jira.mongodb.org/secure/IssueNavigator.jspa

これで合ってますよね。"or"という検索キーワードが論理式と認識されているのですかね。とりあえず、あきらめることにします。

そのうち実装されるといいですね。