お気楽SE Node.js 奮闘日記

  • 41
    いいね
  • 5
    コメント

1. 前置き

ここには、Node.js どころか jQuery をちょこっと使ったことくらいしかなかった実力の私が、Node.js の開発を始めて苦労した記録を記します。
(記録と言いつつも、過去形ではなくまだ作ってる途中なので現在進行形ですが)

先に結論から申し上げておきますと、 Node.jsを書く時はNode.jsらしく書きましょう!
それが一番です。

1.1 既存言語との違い

非同期である!・・・以上(と、厚切りジェイソンばりに叫びたい)。
Objective-Cの改行だらけのロングコードにも耐え、Swiftで非同期プログラミング(クロージャ)の片鱗に触れ、それなりに耐性があるつもりでした。

Node.js は、基本的にコールバック関数(クロージャ?)を多用するプログラミングになります。他にも色々な特徴があるものの、プログラムは上から下に向かって実行されるという原則が必ずしも通用しない ことが、従来のプログラムとの大きな違いではないでしょうか?

2. コールバック地獄との戦い

例えば以下のようなケースです。
※ 中身は重要じゃないので、アプリケーションロジックは全て割愛してあります

実装イメージ
io.on('connection', function(socket) {
    // 登録情報を元にソケットの認証処理を行う
    redis.get(key, function(error, value) {
        // Redisから登録情報を引けなかったらDBから引く
        db.getConnection( function(error, dbConnection) {
            dbConnection.query(sql, params, function (error, results) {
            });
        });
    });
});

Node.jsは、I/Oなどの時間のかかる処理を非同期実行させる特性を持ちます。そのため、非同期処理完了の通知をコールバック関数で受け取り、本線のプログラムは下まで実行しきったとしても、後からコールバック関数の内部が実行されるという仕組みになっています(若干分かりづらい説明ですみません)。

上記のケースでは、ソケット接続時にRedisから登録情報を引いて認証、引けなければプールしているDBコネクションを取得して・・・な感じの処理です。Redisを待ってからDbアクセスを走らせたいため、コールバック関数がネストしています。これに処理が増えればさらにネストは増えるでしょう。

2.1 Promise アプローチ

Node.js(Javascript) には、非同期を直線的に書くための Promise という仕組みがあります。
参考: https://html5experts.jp/takazudo/17107/

Promis で直線的に書くと、例えば以下のようなイメージになります。

実装イメージ
var promise = new Promise(function(resolve, reject){
    // Redis から取得を試みる
    redis.get(key, function(error, value) {
        // 処理
    });
});
promise.then({ 
    // DBからコネクションを取得する
    db.getConnection( function(error, dbConnection) {
        // 処理
    });
}).then({
    // SQLを実行
    dbConnection.query(sql, params, function (error, results) {
         // 処理
    });
});

だいぶ直線的になりました。ただ、何となく不自然にコードが分断されてしまっている感じがします。
※ 僕の Promise の使い方が悪いだけなのかもしれません。。。

ちなみに、同じ違和感をiOSでBoltsを使用した時も感じました。
Bolts-iOS: http://qiita.com/morizotter/items/8fcd8017b938927f6c4a

コールバック地獄対策は先駆者も苦労されていて、色々な方法を駆使して実装しておられました。
参考: http://qiita.com/LightSpeedC/items/7980a6e790d6cb2d6dad

2.2 関数(メソッド)で切り出すアプローチ

色々なコールバック地獄を回避するためのハックを見ましたが、どうにも自分的にしっくり来る実装に出会えませんでした。

紆余曲折あった挙句、自分が至った結論は、 ネストが深くなりそうな箇所は処理や機能単位で関数として切り出せば良いんじゃないか という結論です。

実装イメージ
io.on('connection', function(socket) {
    // 関数(メソッド)で分離する
    socketLogic.authSocket(socket, function (error, hoge) {
       // 認証終わった後の処理
    })
});
socketLogicの実装イメージ
function authSocket(socket, finishHandler) {
     redis.get(key, function(error, value) {
         // DBコネクション取得からクエリ実行までの処理を関数として分離する
         dbUtil.executeQuery(sql, params, function(error, value) {
             finishHandler(null, hoge); // 最終的に、authSocket で受け取った完了ハンドラを実行する
        });
    });
});

無理やり全てを詰め込もうとするからネストが深くなるわけで、ある程度の処理や機能を関数で切り出すことで、ネストが深くなることを抑制する効果があります。ここで注意しなければならないことは、 切り出した関数はコールバック関数を受け取って処理完了後にコールバック関数を実行する 事にあります。
(普通に実行すると、非同期で流れてしまうため)

ここが Node.js 流といいますか、非同期ならではといった感じでしょうか。

2.3 Generator アプローチ

私はまだ試せていなのですが、Generator によるコールバック撲滅アプローチもあります。
以下の Yahoo Japan 様の記事に記載されておりますので、皆様もぜひ試してみては如何で
しょうか?
※ 私も今度試してみます!

Generator化
http://techblog.yahoo.co.jp/javascript/nodejs/Node-es6/

3. 例外(エラー)処理問題

私が PHPでプログラムを書く時は、 try/catch を使用します。 勿論、Javascript にも try/catch がありました。意気揚々と try/catch を書いていたのですが、幻想は打ち砕かれました。

実装イメージ
try {
  // Redis から取得を試みる
  redis.get(key, function(error, value) {
      if (value.isExpired) { 
          throw Error('This value was expired.');
      }
} catch(e) {
    // 例外を補足
}

非同期であるが故に、エラーが投げられる頃にはプログラムは下に流れてしまっており、既に try/catch ブロックを抜けてしまっていたのです。
参考: http://mk.hatenablog.com/entry/2013/09/18/003944

3.1 Domainによるエラーの補足

Domainを使用すると、非同期処理中のエラー処理をうまく補足することが出来ます。
参考: http://dev.classmethod.jp/server-side/domain/

おお、これは行ける!・・・と思っていたのですが。
だが、・・・しかし!

さらに調査を進めた結果。以下の事実に出会いました。

Stability: 0 - Deprecated
https://nodejs.org/api/domain.html

現時点で 非推奨!という不穏な文字が。

3.2 他のモジュールの挙動に合わせる

Domainへの夢を打ち砕かれた私は、諦めて他のモジュールに実装を合わせてみることにしました。つまり、 エラーを投げるのではなく、エラーを返す ということです。

実装イメージ
'use strict';

const CustomError = require('app/custom-error');

function authSocket(socket, finishHandler) {
     redis.get(key, function(error, value) {
         if (error) {
             finishHandler(error, null);
             return;
         }

        if (value.isExpired) {
            // 独自エラーを使って投げると、受け取る側でエラー種別を判断しやすい
            // if (error instanceof CustomError.ValueExpireError) {} 的な。
            finishHandler(new CustomError.ValueExpireError('Value was expired.'), null);
            return;
        }
        // 略         
    });
});
custom-error.js実装イメージ
'use strict';

const customErrorGenerator = require('custom-error-generator');

module.exports = {
    // クラス名は、エラーの種類によって適切な名前をつけよう!
    ValueExpireError: customrErrorGenerator('ValueExpireError')
}

前項(2.)で、関数に分割して完了ハンドラを呼び出すと述べました。
ここで、通常であればエラーをスローしたいところですが、エラーを投げるのではなく、完了ハンドラにエラーを返します。これがおそらく Node.js(Javascript流?) なのではないかと。

思い返せば、自分が使用している node-mysql も、例外はスローされずコールバックの引数で返って来ました。 Node.js では第一引数を常にエラーとし、コールバック関数内で第一引数のエラーを元にエラー判定をすれば良いのだ と、そういう結論に至りました。

参考: http://d.hatena.ne.jp/kazuhooku/20120420/1334891656

4. var を駆逐せよ

元々 swift を書いていた私が Node.js を書いていて、違和感を感じていたのが var の存在です。
どのサンプルやコードを見ても、変数定義は var ばかりでした。

PHP みたいに、変数のスコープがゆるい問題を引きずるのかなぁとか、Swift みたいに let で書き換えできない変数は書けないのかなぁとか、そう思っていました。

4.1 ES6に巡りあう

Node.js は var でしか変数を定義出来ないのかと途方にくれていたところに、朗報を見つけました。

ES6時代のNode.js : Yahoo!デベロッパーネットワーク
http://techblog.yahoo.co.jp/javascript/nodejs/Node-es6/

const と let が使える!!!
この記事を見つけた時の私のテンションは、異常でした。ニヤニヤしながら薄ら笑いを浮かべる、そんな感じです。

感動をありがとうございます。

4.2 const で再代入を防ぎ、変数の中身を保証する

Yahoo Japan 様の記事を読んでいて分かったことは、Node.js の const は Swift の let とほぼ同義であるということです。

書き換えを想定しない変数は、 let で定義し変数が想定外のケースで書き換えられる事を予防するのが Swiftです。Node.js でも同じく、 const で真似ることにしました。

実装イメージ
'use strict';  // 宣言しないと使えない

const socketIo = require('socket.io');

func start(server) {
    const io = socketIo(server);
    io.transports = ['websocket'];
}

大体のケースにおいて const で書けました。そして、 Swift の let と同じくただの再代入禁止なので、 オブジェクトの操作には支障をきたさないということです。これは使わない手はありません。
※ Node.js の let は Swift だと var に相当するのでご注意を。Swift エンジニアが混同しやすいところです。

5. クラスがない

当初私は、以下のようなコードでクラスを作る事をイメージしていました。

実装イメージ
class Hoge {
    var bar = treu;
    init () {
        // イニシャライザ
    }
}

ただ、調査した結果 Javascript にはクラスがない事がわかりました。

他のメジャーなオブジェクト指向プログラミング言語と異なりJavaScriptには「クラス」が存在しません
http://www.yunabe.jp/docs/javascript_class_in_google.html

5.1 クラスはないがモジュールはある

未だに完璧に理解していないのですが、Node.js(Javascript) にはモジュールという機能が存在します。

実装イメージ
// (引用 tkd55 ブログ様: http://www.tkd55.net/blog/?p=559)
var RoomInfo = function(name) {
     this.roomName = name;
};

RoomInfo.prototype.getRoomName = function() {
  return this.roomName;
};

module.exports = RoomInfo;
モジュール利用側
// 独自モジュールの読み込み
var RoomInfo = require('./public/javascripts/roomInfo');
// 生成 
var roomInfo = new RoomInfo("room1");
// ルーム名の取得
var roomNum = roomInfo.getRoomNum();

モジュール化すると、クラスのように扱う事が出来ます。ただ、perl や php のようなプログラミング言語のクラスの作成方法とはだいぶ異なるので、慣れるのに時間がかかりました。

5.2 と思ったらクラスがあった

てっきり、Node.js ではクラスは書けないものだと思ってました。ただ、この記事書いてる最中に参考文献を探していたら見つけました。。。

Node.jsでES6について勉強したときのメモ
オブジェクト指向のプログラミングで使えるクラスが対応
http://qiita.com/toshihirock/items/ca49650188c7e6f71c74#class%E3%81%AE%E5%88%A9%E7%94%A8

ずっとモジュールで書いてましたよ、ぐすん。。。
クラスの方が書きやすそうではないですか。明日にでも早速試してみますよ!

6. サーバ周り千夜一夜

Node.jsで疑問だったこと、それはサーバの起動についてです。

node sampleapp.js

上記のようにして起動すると、最初に読んだ勉強サイトには書いてありました。
つまり、Apache + mode_php ではない、 Node.js はWebサーバも兼ねてるんだ!と。

でも、じゃあ実運用で、ただ & つけてバックグラウンド化して運用するのか・・・?

node sampleapp.js &  

そんな事はない、ちゃんとありました。
(ここは社内の人に教えてもらいました!)

6.1 forever によるデーモン化運用

php の場合、Apache はデーモン化して動いているわけで、やはり Node.js でもそれなりな構成?
(適切な言葉が思いつかない。。)がないと、実運用で不安なわけです。

そこで出てくるのが foreverです。

node.jsスクリプトをデーモン化するツール forever
http://onlineconsultant.jp/pukiwiki/?node.js%20node.js%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92forever%E3%81%A7%E3%83%87%E3%83%BC%E3%83%A2%E3%83%B3%E5%8C%96%E3%81%99%E3%82%8B

私が感動した点は、forever には色々なオプションが備わっているため、自由自在にログの出力ディレクトリが設定できたりと、かなり便利なところです。

これは使うっきゃない!

6.2 再起動しないとソースコードが反映されない

もう一つの注意点としてあげられるのは、ソースコードを書き換えただけでは反映されないというところです。すなわち、 一度 node を終了させて再度起動しないと修正したソースコードは反映されません

これは陥りやすい罠というか、なかなかに気づかない。あれーと思ってたら、実は修正したソースコードが反映されてなかっただけ・・・というのは往々にして遭遇する事態です。・・・というか遭遇しましたよ :expressionless:

6.3 Node.js を自動で再起動する

やはり、同様の悩みを抱えている人は世の中にいるわけで、それなりな解決手段がありました。

nodemon
http://qiita.com/twipg/items/cb969b335d66c4aee690

そして、我らが forever にも、監視して変更を自動検知するるオプションがありました。
(X Japan: Forever Loveが聞こえるくらい感動しましたよ!)

-w, --watch Watch for file changes
--watchDirectory Top-level directory to watch from
https://github.com/foreverjs/forever

開発段階では自動再起動が望ましいと思います。
ただ、実運用では自動再起動が逆に不都合な時もあると思いますので、生半可な状態では始めず、しっかり検証してベストな状態リリースする事が望ましいと思われます。

6.4 node は単一プロセスで動いている(クラスタリングしようぜ!)

Node.js でソースコードを変更するには、node を再起動する必要があると述べました。でも、当たり前ですがそれだと困るわけです。

今後本番稼動した場合、リリースのたびに再起動するのかと。。。憂鬱になりますよね。憂鬱です。

なので、Node.js は最低限複数立ち上げて、一つずつ時間をずらして順番に再起動する事で、アクセス断を防ぐ必要があるわけです。これはまあ、Nginx のロードバランサなりでダウンを検知し、適切にアクセスを各 node に分散させつつやれば良いわけですが。

でも、もう一つの側面があって、node のプロセスを眺めて感じる事は、Node.js が単一プロセスで動いているという事実です。

Node.jsのClusterをセットアップして、処理を並列化・高速化する
http://postd.cc/setting-up-a-node-js-cluster/

Node.jsが多数のイベントの非同期な処理に長けていることはよく知られていますが、それが単一のスレッドで行われていることを多くの人は知りません。Node.jsは実際にはマルチスレッドではないので、リクエストは全て単一スレッドのイベントループで処理されているだけなのです。
Node.jsクラスタを使って、クワッドコアプロセッサの能力を最大限に引き出しましょう。

この章を要約すると、Node.js は Webサーバそのものであり、コーディングだけでなく Webサーバとしての Node.jsという存在を意識して、適切に運用する必要があるわけです。自分も肝に銘じる事にします。

7. 進化が激しい Node.js

ウェブサーフィンをしていて見かけるコードの多くは var 変数で定義されていたり、 socke.io のコードも検索でヒットする多くが socket.io 0.9 以前だったりと、情報が古い割合が多いです。

他の言語でも往々にしてあることではありますが。Node.js のそれは、今まで私が出会ってきた中で一番早いような気がします。

LTS schedule(サイクルが早い!)
https://github.com/nodejs/LTS

ネットに記載されているコードは古い可能性も多く、特に進化の早い Node.js では動かない可能性も高いので、安易に鵜呑みにしない方が良いと思われます。

そして、ES6 における拡張に後々になって気づいた自分はだいぶ損したなと。
※ もっと早くこの事実に気づいてたら、開発がスムーズに進んでいたなと後悔してます。。。

なので、Node.js(Javascript) のリリースやバージョンアップによる変更点などは、定期的にチェックした方が良いと思います!
(まずは、QiitaのNode.jsタグをフォローすることから始めてみようかなと。フォローするタグに関しては、Qiita登録時に設定したっきりで放置状態でした。ちゃんとメンテしないとダメですな。。)

8. まとめ

まだまだ慣れていないところもありますが、 Node.js(というかJavascript?)にはNode.js流の書き方があって、それに慣れてくると楽しくなってきます。

私が主戦場としてきたのは、perl のようないわゆる普通の同期的言語だったわけですが。
過去の言語の流儀にはとらわれず、npm でインストールしたモジュールのインターフェースを参考にしてみたりとかして、色々工夫してみるとスマートなプログラムになるのかなと。

Node.js に慣れるとプログラミングの幅が広がる。そんな気がします。
まだまだ色々と遭遇した苦労はありますが、書き疲れましたので今回はこの辺にて。