はじめまして!こんにちワン!
スマートフォン版Amebaプラットフォームでフロントエンドの開発を担当している2012年入社の鳥山と申します。
スマートフォン版Amebaプラットフォームのフロントエンドの開発チームは、HTML、CSS、JavaScriptといったクライアントサイドからNode.jsを使用したサーバサイドの開発もおこなっています。
そこで今回は、Node.jsを始める上で知っておくと便利な知識を現場での開発例を交えて紹介させていただきたいと思います。
想定している読者は、
・普段HTML、CSS、JavaScriptなどのクライアントサイドの開発をしている方
・Node.jsに興味があり、始めてみたいと思っている方
・サーバサイドプログラミングと聞くと身震いしてしまう方
です。
※今回は、記事にも制限があるので、Node.jsについての説明や環境構築については省略させていただきます。ご容赦下さい。
その辺りについて知りたい方は、Node.js 日本ユーザグループに日本語ドキュメントで掲載されていますので、そちらの方をご覧下さい。
また、まだNode.jsをインストールしていない方は、Node.jsの公式サイトからダウンロードできるインストーラーを実行すれば簡単にインストールできます。
なお、本記事でのサンプルコードの実行はWindowsならコマンドプロンプト、Mac/Linuxはターミナルでおこなっています。
Node.jsとは?
Node.jsは、簡潔に言うと「Javascriptでサーバープログラミングをするためのプラットフォーム」です。
特徴
JavaScriptと同じ記述方法、同じ仕様で動作するので、JavaScriptを使用したことのある人であれば簡単にサーバサイドプログラムを作成することができます。
また、Node.jsは基本的なネットワークサーバとしての機能をもっているため、PHPやPerlのような別途ApacheのようなHTTPサーバを導入しなくてもすぐにHTTPサーバやソケットサーバとして動作します。
簡単に言えば、「Node.jsだけインストールしてしまえばオッケー」ということです。
なぜ今注目されているのか?
・非同期処理を簡単に扱える
現在、インターネットがよりリアルタイムな情報を扱うようになったため、常時接続や一定期間待機して応答を待つことが不可欠となりました。
Node.jsは非同期I/Oを扱うことで、1つのスレッドで大量の接続を処理でき、1台のサーバで大量の接続を取り扱うことができます。非同期I/Oを扱うには、高度な知識と非同期と同期処理を使い分ける経験が必要なのですが、Node.jsは非同期前提なので、特に高度な知識や経験がなくてもプログラミングすることができます。
実際、私も現在の業務に就くまでは、Node.jsをまったく触れたことがなかったのですが、HTTP通信をしてくれる関数も用意されていますし、処理を書くときもJavaScriptの構文で書けるので、サーバサイドプログラミングだからといって毛嫌いしていたのですが、思っていた以上にすんなりと取りかかることができました。
・JavaScriptの台頭
昨今のスマートフォンの急速な普及と非Flash対応デバイスなどの出現により、JavaScriptの需要性が増してきており、JavaScirptでサーバサイドプログラムを実行できるNode.jsは今非常に注目されています。
以上のようなことからスマートフォン版AmebaもNode.jsを採用しています。
モジュールの使い方
requireとexports
ブラウザ上で動くJavaScirptは、複数あるjsファイルが全て統合され、違うファイルにある関数も自由に呼び出せるのに対して、Node.jsにはモジュールという単位で機能を分割しています。
モジュールが1つのjsファイルとなり、閉じた状態になっています。他のファイルを参照したい場合は、参照したいファイルをexportsオブジェクトで渡す用意をし、それをrequire関数で受け取ることできます。
sample.js(使われるファイル)
exports.print = function() {
console.log('sample.jsからきたよ');
};
main.js(使うファイル)
var sample = require('./sample.js');
sample.print();
これを以下のように実行すると
$ node main.js
コンソールに「sample.jsからきたよ」と出力されます。
Node.jsはこうしてモジュール間で変数のやりとりをします
HTTPサーバを動かしてみよう
Node.jsサイトのトップページにあるコードを少しローカル用に変えた以下のコードで動きを見てみます。
http_sample.js
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('Hello World\n');
res.end();
}).listen(8080);
console.log('server started on 8080');
まず、Node.jsに標準搭載されているhttpモジュールを呼び出し、その中のcreateServerメソッドでHTTPリクエストを処理してレスポンスを返しています。このサンプルでは無名関数としてリクエストの内容にかかわらず、ブラウザに送るレスポンスとして、writeHead関数を使って送るデータの各種情報を定義します。今回は、ステータスコード「200」とし、常に成功の状態を渡し、Content-typeにtext/htmlを返します。そしてwrite関数を使ってデータの中身を定義します。今回は、「Hello World」を書き出しています。そしてlistenメソッドで8080番ポートにバインドしてサーバを開始しています。
そしてこれを実行し、ブラウザで「http://localhost:8080/」と入力すると以下のように表示されます。
このようにNode.jsでは簡単にHTTPサーバ通信をおこなうことができます。
非同期処理プログラミングをしてみよう
非同期処理プログラミングをする上で、必要なものとその使い方を紹介します。
npmを活用しよう
Node.jsで開発する際にnpm(Node Package Manager)というツールを使うと便利です。Node.jsではすでに多くのライブラリパッケージが公開されており、それらのパッケージをnpmというパッケージ管理ツールを使って簡単に導入することができます。npmを使うとインストールしようとしているパッケージが依存している他のライブラリも自動でインストールしてくれます。(npmに登録されているライブラリ一覧)
$ npm install hoge
↑実行したディレクトリ下にnode_modulesディレクトリを作成してモジュールを展開する
$ npm install -g hoge
↑どのディレクトリからでも使える
非同期処理における注意点
さあ、いよいよここから非同期処理のプログラミングにはいっていくわけですが、注意しなければいけないことがあります。それは非同期処理は「イベント駆動」だということです。
イベント駆動とはその名の通り、イベントが発生した時に動き出すものです。つまり非同期処理プログラミングをおこなう時は、いつ発生するかわからないイベントに対して処理を登録していけなければならなく、同期処理をしているJavaScriptのようにコードを上から下に処理してくわけではないのです。
例外処理
上記のようなことからNode.jsでは例外処理の扱いに注意しなければなりません。
JavaScriptの例外処理といえば、try/catchをよく用いるが、これをNode.jsで使用すると、
miss.js
var fs = require('fs');
try { //このブロックの中の処理をまずおこなう
fs.readFile('hoge.text', function(err, data) {
console.log(data);
});
} catch(e) { //try内でエラーおきたらこっち
console.log('failed this called');
}
$ node miss.js
undefined
hoge.textというファイルが存在しない状態で、このmiss.jsを実行してみると「undefined」が出力されます。
try-catch文はそもそもtryブロックから処理をし、その中でエラーおきた場合にcatchブロックの中の処理をおこないます。
今回の場合は、非同期処理がtryブロックを抜けた後に実行されるため、tryブロックにいる時は、まだエラーかどうかも判断できなく、catchブロックにいかなくdataの中身を出力してしまっています。今回の場合は、readFileの処理が走ってないので出力するdataは「undefined」となっています。
では、どう対処するのが良いのか。
正しくは、登録した処理すべての各々のコールバックで例外処理をおこなう必要があります。
correct.js
var fs = require('fs');
fs.readFile('hoge.text', function(err, data) {
if (err) { //エラーのとき
cosole.log(err.message);
} else { //成功のとき
console.log(data);
}
});
Node.jsではコールバックの第一引数にエラーが返ってくるため、上記のように処理すればエラーメッセージがちゃんと出力されます。
$ node correct.js
ENOENT, open 'hoge.text'
async.js
例外処理の仕方もわかったところで非同期プログラミングの中身にはいっていきます。今回は、非同期プログラミングで欠かせないasync.jsについて紹介します。
async.jsはnpmでインストールできます。
$ npm async
前述のとおり、非同期処理はコールバックで定義されるので、大規模な処理をおこなおうとするとコールバックがどんどんネストしてしまい、見た目がとても複雑になってしまいます。
ネスト例
var fs = require('fs'); fs.mkdir('./hoge', function (err) { if (err) { // エラー処理 return; } fs.mkdir('./hoge/piyo', function (err) { if (err) { // エラー処理 return; } fs.writeFile('./hoge/piyo/neko', 'Hello Node.js', function (err) { if (err) { // エラー処理 return; } console.log('success'); }); }); });
上記のように、大規模になればなるほどどんどん入れ子になっていき、とても見づらくなってしまいます。
そういった問題を解決してくれるモジュールの一つがasync.jsです。
async.jsには非同期処理を管理する関数がいくつか用意されていますが、今回は代表的なseriesとparallelを紹介します。
・async.series
seriesは、それぞれの関数を実行し、実行が完了したら次に定義されている関数を順次実行していく関数です。
async_series.js
var async = require('async');
console.log('start');
async.series([
function (callback) { //処理1
console.log('call function1');
setTimeout(function() {
console.log('done function1');
callback(null, 'result1');
}, 3000);
},
function (callback) { //処理2
console.log('call function2');
setTimeout(function() {
console.log('done function2');
callback(null, 'result2');
}, 2000);
},
function (callback) { //処理3
console.log('call function3');
setTimeout(function() {
console.log('done function3');
callback(null, 'result3');
}, 1000);
}],
function (err, results) { //ここでまとめて処理
console.log('true end');
console.log(results);
});
console.log('end? ... NO. while processing.');
実行すると...
$ node async_series.js
start
end? ... NO. while processing.
call function1
done function1
call function2
done function2
call function3
done function3
true end
[ 'result1', 'result2', 'result3' ]
となり、配列に定義された関数を順次実行しているのがわかります。
順次実行された関数はまとめて最後に処理します。(今回の場合は、resulltsにそれぞれの関数の処理がオブジェクトで格納される)
・async.parallel
parallelは、それぞれの関数を一度に実行し、全ての関数の結果が得られると完了用のコールバックを実行する関数です。
例は、先ほどのasync_series.jsをasync_parallel.jsに書き換え、ファイル内の3行目のseriesをparallelに書き換えたものを実行します。
$ node async_parallel.js
start
call function1
call function2
call function3
end? ... NO. while processing.
done function3
done function2
done function1
true end
[ 'result1', 'result2', 'result3' ]
実行の順序は違いますが、結果は同じになります。
以上のように、非同期処理は、実行の順番に気をつけながらプログラミングしていくことが重要となります。
asyncにはまだまだ役に立つ関数が用意されていますので是非見てみて下さい。(async.js)
現場での使用例
まとめとして、実際に現場で開発しているものを例として紹介します。(※一部改変あり)
開発例
game.js
この例は、なるべく本記事で紹介したものを使用しているところを選んで掲載してみました。//必要なモジュールをrequireする var async = require('async'); var api = require('../api');
/*---------------------1------------------------*/ var graph = api.graph; // api/graph.jsをreqiureできる var preregister = api.preregister; var operation = api.operation;
//exportsオブジェクトで外部とやりとりできるようにする exports.register = function(app) { app.get('/api/game/top/?', function (req, res) { async.parallel({ func1: function (callback) { //func1の処理 var opts = { query: {category : 'game'} }; //graph.jsで定義されているhttpサーバーにアクセス graph.get('hoge/', req, opts, function (err, obj) { /*---------------------2------------------------*/ if (obj && obj.summary) { callback(null, obj); } else { var noObj = {auth: 0, obj: 0}
}; callback(null, noObj); } }); }, func2: function(callback) { //func2の処理 //operation.jsで定義されているhttpサーバーにアクセス operation.get('me/events', req, function (err, obj) { /*---------------------3------------------------*/ if (err) { callback(err, null); } else { callback(null, obj); } }); }, func3: function(callback) { //func3の処理 //preregist.jsで定義されているhttpサーバーにアクセス preregister.get('me/app', req, opts, function (err, obj) { if (err) { callback(null, {error: err}); } else { callback(null, obj); } }); } }, function (err, results) { if (err) { res.json({ //JSON形式で返す func1: 0, func2: undefined, func3: results.func3 }); } else { res.json({ //JSON形式で返す func1: results.func1, func2: results.func2, func3: results.func3 }); } }); }); };
ポイントを説明すると、まずコメントアウトで1と書いてあるところです。
1の上の行で「var api = require('../api');」と定義しています。このように定義すると「var graph = api.graph;」のようにapi/配下にあるものを定義できます。
次にコメントアウト2と3です。これは非同期処理の注意点のところでお話したエラー処理の部分です。
2のところでは、コールバックにエラーが返ってきた時に「callback(null,noObj);」と書き、エラー内容を返してません。
それに対して3では、「callback(err,null);」と返しています。これは、async.parallelを使用する際に気をつけなければなりません。async.parallelでは、複数の処理を同時におこなっているのですが、どれかの処理がエラーを返してしまうと処理全体がエラーの扱いになってしまいます。なので処理によってエラーを返すか(3)、またはエラー用のオブジェクトを用意してそれを返すか(2)を考え開発しなければなりません。これは私もNode.jsをさわり始めた頃ハマったところでした。
現場で感じたこと
以上のように、実際の現場でクライアントサイドのJavaScriptとサーバサイドのNode.jsの両方の開発をおこなっていて感じたこととしましては、「非同期への危機管理」が非常に高まったということです。
この業務に就く前もクライアントサイドのJavaScriptで非同期を扱うことがありましたが、基本的にはコードを上から下に処理していくという手続き形式でプログラムをしていました。しかし非同期が強制されるNode.jsに触れてからは、手続き形式のプログラムをしているとバグだらけのプログラムになってしまうので自動的にスケーラブルなプログラムを書くように心がけるクセをつけることができました。これが結果的にクライアントサイドのプログラムにも活きており、フロントエンドエンジニアとしての幅を広げることができています。
最後に
今回、入門といった形で、あくまでフロントエンドエンジニアという立場で「Node.js」を紹介させていただきました。
ですのでサーバ構築やデータベース連携などの深い部分のお話は端折らせていただきました。中には物足りないと思った方もいらっしゃると思いますが、HTML、CSS、ブラウザ上のJavaScriptにしか関わってこなかった方達にとって少しでもNode.jsを敷居の低いものとして感じて興味をもっていただけたならば幸いです。
最後まで読んでいただきありがとうございました!
何卒スマートフォン版Ameba(スマホで見てください)をよろしくお願いいたします。