Read this tutorial in english
|
Lee este tutorial en Español
|
이 튜토리얼을 한글로 보세요
|
阅读本书中文版
|
閱讀本書繁體中文版
|
Читать этот учебник на русском
|
Đọc bằng tiếng Việt
|
本書は、Node.jsでのアプリケーション開発を始めようとする皆さんに、 ”高度な”JavaScriptについて知るべきあらゆることを解説します。 よくある”Hello World”チュートリアルの、はるか上をいくものです。
貴方が読んでいるのは、本書のいわゆる最終版となります。 つまり本書は、間違いが見つかった場合や、 Node.jsの新バージョンにおえる変更点を反映する時のみ、改訂されます。 最終更新日は2012年2月12日です。
本書内のコードのサンプルは、Node.jsのバージョン0.6.10でテストしています。
本書は、Ruby、Python、PHP、Javaのような、少なくともひとつのオブジェクト指向言語を理解しており、 JavaScriptについてはあまり経験がなく、Node.jsについては全く経験がないという、 著者と同じようなバックグラウンドをもつ読者にとって最もフィットする内容となっています。
何かしらのプログラミング言語を経験しているデベロッパー向けですので、 本書はデータ型や変数、制御構造などの本当に基本的な事柄はカバーしないということになります。 本書を理解するためには、そのような基本的な事柄は読み進める時点で理解している必要があります。
ただし、JavaScriptの関数やオブジェクトは他の言語のものと異なるので、より詳細に解説していきます。
本書を読み終わる頃には、貴方はひとつのWebアプリケーションを完成させることになります。 そのWebアプリケーションでは、ユーザがWebページを閲覧し、ファイルをアップロードすることができます。
もちろん、世界を変えるようなものができるわけではないですが、 これらの機能を実現するために”必要最低限”のコードを書くだけではありません。 さらに一歩先へ進み、アプリケーションの持ついくつかの相違点をきれいに分離した、 シンプルだけれども完全なフレームワークを作りあげることができます。 私が言わんとしていることは、すぐにわかることでしょう。
まずはNode.jsを使ったJavaScriptでの開発は、 ブラウザ内のJavaScriptの開発と何が違うのかを見てみるところから始めます。
次に、古き良き伝統として”Hello World”アプリケーションを作ります。 これは確かに”何かをする”、最も基本的なNode.jsのアプリケーションです。
そして、私たちが構築したいと考えている”現実の”アプリケーションの種類について議論します。 このアプリケーションを組み立てるために実装が必要となるパーツを分解し、 それぞれのパーツをステップバイステップで作っていきます。
約束通り、これらを通してJavaScriptのより先進的なコンセプトを知り、どう活用し、 そしてなぜ他のプログラミング言語とは異なるコンセプトが意味をなすのかを学ぶことになります。
完成したアプリケーションのソースコードは NodeBeginnerBook Githubリポジトリ からダウンロードできます。
技術的な話に入る前に、貴方とJavaScriptの関係について話をしておきましょう。 本章は、この文書をさらに読み進めることが貴方にとって意味があるのか、判断してもらいたいのです。
もし貴方が私と同じであれば、はるか昔HTMLの文書を書いてHTMLでの”開発”を始めたはずです。 JavaScriptと呼ばれるなんだか面白そうなものに出会い、 つまり貴方のWebページに相互作用を加えるような、とても基本的な使い方だけで利用していたことでしょう。
貴方はもっと”本物”が欲しくなり、複雑なWebサイトを構築する方法を学びたくてPHP、 Ruby、Javaなどのプログラミング言語を勉強し、 ”バックエンド”となるコードを書き始めたのではないでしょうか。
それにも関わらず、JavaScriptに注目してみるとjQueryやPrototypeなどが目に入ってきました。 JavaScriptの世界では物事が進化し、もはやこの言語は、window.open()では済まないのだ、 ということがわかってきたのです。
しかしそれは序章に過ぎません。jQueryを自由に扱えればWebページにスパイスを加えることができますが、 結局のところ貴方はまだ、JavaScriptのユーザでしかなく、 デベロッパーではなかったのです。
そしていま、Node.jsにたどり着きました。サーバ側でのJavaScriptって、凄いの?
そろそろ、古くて新しいJavaScriptをチェックする時期がきたことを確信しました。 でもちょっと待って、Node.jsアプリケーションを書くというのはとても厄介。 なぜそうやって書く必要があるのか、理解しないといけない、 つまりJavaScriptを理解しなければいけないのです。今度こそ、真剣に。
ここで問題があります。JavaScriptには2つ、 もしかすると3つの時代があるため(90年代半ばの小さくて可愛らしいDHTMLの助っ人、 jQueryなどのもう少し真剣なフロントエンド、そして現在のサーバサイド)、 JavaScriptを利用するのではなくてJavaScriptで開発しているのだと感じさせてくれるような、 ”正しい”使い方を示す情報を探すのは容易ではありません。
貴方は経験豊富なデベロッパーなのですから、 なんとなく時間を無駄に過ごしたり間違った使い方をしたりして新しいテクニックを身につけたい、 なんていうことはないでしょう。しっかりと正しい角度でアプローチしていきたいはずです。
もちろん素晴らしい文書はそこら中にあります。しかし一つの文書では十分でないこともあります。 必要なのはガイダンスなのです。
私の目的は、貴方にガイドを提供することなのです。
世の中には本当に素晴らしいJavaScriptエンジニアがいます。でも私は違います。
本当に、私は前段で述べたような男に過ぎません。 バックエンドのWebアプリケーションを1つや2つ知っているだけで、 まだまだ”本当の”JavaScriptは学び始めたばかりですし、Node.jsについても同じです。 JavaScriptのより先進的な側面を学んだのは最近で、経験も豊富ではありません。
ですので、本書は”初心者から上級者へ”なるためのではなくて、 どちらかというと”初心者から初級者へ”というものです。
私がNode.jsを学び始めた時にあったらよかった、そんな文書になっているはずです。
最初にJavaScriptが日の目を見たのは、ブラウザ上でした。 しかしこれは単なるコンテキストに過ぎません。 コンテキストによってその言語でできることは決まってきますが、 それはその言語自体ができることとイコールというわけではありません。 JavaScriptは”完全な”言語であり、様々なコンテキストで使えます。 他の言語でやっていることは、すべてJavaScriptでもできます。
Node.jsもまた、ひとつのコンテキストに過ぎません。 Node.jsによって、JavaScriptはバックエンド、ブラウザの外で動作できるのです。
バックエンドでJavaScriptが動作するには、インタープリターで変換され、 そして実行されなければなりません。これをNode.jsが行います。 内部ではGoogleのV8 VMが利用されています。 V8 VMはGoogle Chromeが使用しているJavaScriptの実行環境そのものと同じです。
それに加えて、Node.jsにはたくさんの便利なモジュールが同梱されています。 全てを1から作る必要はないのです。例えばコンソールに文字列を出力するモジュールなどがあります。
つまりNode.jsは、実行環境とライブラリの2つから成っているのです。
さて、これを使うためには、Node.jsをインストールする必要があります。 公式サイトのインストール手順を訪れて、またここに戻ってきて下さい。
OK、では冷水に飛び込んで、 はじめてのNode.jsアプリケーション”Hello World”を書いてみるとしましょう。
好きなエディタを選んだら、helloworld.jsというファイル名で保存して下さい。 このアプリケーションでは”Hello World”を標準出力に書き出します。コードはこの通りです:
console.log("Hello World");
ファイルを保存して、Node.jsから動かしてみます:
node helloworld.js
これで貴方の端末で、Hello Worldと出力されたはずです。
いやあ、退屈ですね。次はもっと本物っぽいものを書いてみましょう。
シンプルに、しかし現実的に考えてみましょう:
結構です。Googleで検索して、何か組み合わせていけばできてしまうでしょう。 でもここでやりたいのはそういうことではないのです。
目的を達成するためだけの最も基本的なコードだけを書く、それをやりたいだけではないのです。 綺麗で正確なコードを書きたいのです。 ここからは、もっと複雑なNode.jsアプリケーションを構築していく時の感覚を得るために、 わざと必要以上に抽象的にしていきます。
それではアプリケーションを分解していきましょう。 やりたいことを満たすためには、一体どんなものを実装したら良いのでしょうか。
ちょっと考えてみて下さい。PHPでこのスタックを作り上げるとしたら、
どうやって実現するでしょうか。何も隠すことはないので言ってしまうと、
だいたいApache HTTPサーバとmod_php5をインストールするのでしょう。
そう考えると、”Webページを提供して、HTTPリクエストに応答する必要がある”
という部分については、PHPの中で起きているわけではないことに気づきます。
さて、nodeの場合、状況が少し異なります。Node.jsでは、 アプリケーションのみを実装するのではなく、HTTPサーバ全体を実装することになります。 我々がこれから作ろうとしているWebアプリケーションとWebサーバは基本的に同一のものなのです。
もしかすると大変そうな印象を持たれてしまったかもしれませんが、 Node.jsでは、そうでもないことがすぐにわかるはずです。
それではスタックの最初の部分である、HTTPサーバから始めてみましょう。
はじめて私が”本当の”Node.js”アプリケーションを作り始めようとした時は、
どうやってコードを書くのかわかりませんでしたし、コードをどう構成したら良いかもわかりませんでした。
1つのファイルに全てを書くので良いのでしょうか。Webで見つかるチュートリアルではほとんどの場合で、
Node.jsの基本的なHTTPサーバが、全てのロジックを1カ所で持っているようなものでした。
次の機能を実装しようとした時、コードの可読性を維持することは果たしてできるのでしょうか。
モジュールを使えば、やりたいこと別にコードを分離することは、 比較的簡単だということがわかりました。
そうすることで、Node.jsで実行するメイン部分のファイルを綺麗な状態に保つことができます。 また、メインファイルから使ったりモジュール同士で相互利用したりするモジュールも、 すっきりさせることができます。
アプリケーションを起動するためのメインファイルと、 HTTPサーバのコードから利用されるモジュールファイルを作りましょう。
私の印象では、メインファイルにはindex.jsという名前にするのが標準的です。 そしてサーバモジュールにはserver.jsというファイル名を付けます。
では、サーバモジュールから始めます。このプロジェクトのルートディレクトリにserver.js という名前のファイルを作成します。ファイルの内容は以下の通りにします:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
これだけです!これで実際に動くHTTPサーバが書けました。 実行して、ちゃんと動くかテストしてみましょう。 まず、このスクリプトをNode.jsから実行します:
node server.js
それでは、ブラウザを開いて http://localhost:8888/ にアクセスしてみましょう。 ”Hello World”と表示しているWebページが表示されるはずです。
これは、とても気になるのではないでしょうか。我々のプロジェクトをどう構成していくのか、 というテーマを少し離れて、この部分について深堀してみませんか。すぐに本題に戻ることを約束します。
ではでは、何が起こっていたのか、分析していきます。
最初の行で、httpモジュールをrequire(要求)しています。 これにより、Node.jsに同梱されているhttpモジュールへのアクセスが、 変数httpを通して可能になります。
次に、httpモジュールが提供する関数のひとつであるcreateServerを呼出しています。 この関数はオブジェクトを返しますが、このオブジェクトがlistenというメソッドを持っています。 listenメソッドはポート番号を数値の引数として受け取り、HTTPサーバがそのポート番号で待ち受けします。
今のところはhttp.createServerの括弧内にある関数定義の箇所は無視しておいて下さい。
もし8888番ポートで待ち受けするサーバを開始したければ、こんなコードでも良かったはずです:
var http = require("http");
var server = http.createServer();
server.listen(8888);
これでHTTPサーバが8888番ポートで待ち受けをして、その他、何もすることはありません。 (たとえリクエストがきても応答さえしません)
(もしPHPのような、より保守的な言語の経験者であれば特に)面白いのは、 createServer()の呼出しの第1引数に関数定義があることではないでしょうか。
この関数の定義は、createServer()を呼び出す際に渡す最初の (そして最後の)引数なのです。 JavaScriptでは、関数をその他の値と同様、あちこちに渡すことができるのです。
例えばこんなことができてしまいます:
function say(word) {
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
よく読んでみて下さい!ここでは、関数sayをexecute関数の第1引数として渡しています。 sayの戻り値ではなく、say自体を渡しているのです!
ここではsayが、関数executeの中にあるローカル変数 someFunctionになっています。 そしてexecuteの中では(この変数に括弧をつけて)someFunction() と書くことができるのです。
もちろんsayは引数を1つとるので、 executeからsomeFunctionを呼び出す時には、 さらに引数を渡すことができます。
このように、関数は、他の関数への引数として名前で渡すことができますが、 何も関数の定義を先にしたうえで、それを渡す、といった手順を踏まなくても良いのです。 関数は、関数を引数として渡す際、その場で定義しても良いのです:
function execute(someFunction, value) {
someFunction(value);
}
execute(function(word){ console.log(word) }, "Hello");
まさにexecuteが第1引数を期待している場所で、execute に渡したい関数を定義しています。
ここで見たやり方をすると、関数に名前をつけてやる必要がありません。 このような関数を匿名関数(anonymous function)と呼びます。
これは”上級の”JavaScriptと呼んでいるものの最たる例ですが、 ステップバイステップでやっていきましょう。 今のところ、JavaScriptでは関数を呼び出す時の引数として関数を渡すことができる、 ということを受け入れて下さい。 あらかじめ定義した関数を変数として割り当ててから渡すこともできますし、 その場で関数を定義して渡すこともできるのです。
今知った事を活用すれば、最低限のHTTPサーバをこんな風に書く事ができます:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
もうここで何をしているのかは明確なはずです: createServer関数に匿名関数として渡しているのです。
先のコードをリファクタリングすると、こうすることができます:
var http = require("http");
function onRequest(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
さて、次はこんな疑問が湧いてくるのではないでしょうか: なぜこんなことをしているの?
先ほどの質問に対する答えは、a) (少なくとも私にとっては)それほど簡単な話ではなく、 b) Node.jsがどのように動作するかの原理に従っているものだから、ということになります。 それはつまり、イベント駆動型のことであり、高速に動作する理由でもあります。
もしこの背景について知りたければ、 Felix Geisendörferの素晴らしいエントリ ”Understanding node.js” は一読の価値があります。
つまるところ、Node.jsはイベント駆動で動作するということです。 えー、はい、私もです。何を言っているのかさっぱりわかんない。 しかし頑張って説明してみようと思います。 なぜWebベースのアプリケーションをNode.jsで書くことに意味があるのかを。
http.createServerメソッドを呼び出す時、 当然ながらサーバの待ち受けは同じポートでしないようにしたいですし、 サーバにHTTPリクエストが届いた時は、他にも何かしたいわけです。
問題は、これが非同期で起こるということです: いかなる時でも起こりますし、 サーバ内では動かせるのは単一のプロセスしかないのです。
PHPのアプリケーションを書く時は、このようなことに困る事はありません: HTTPリクエストがあった時はいつでも、 (だいたいはApacheなどの)Webサーバがこのリクエストのために新しいプロセスをフォークし、 PHPスクリプトが最初から最後まで実行されることになります。
ですので、制御フローについていうとポート8888番に新しいリクエストが届いた時には、 Node.jsプログラムの中に入ってきており、 おかしなことにならないようそれをうまく取り扱わないといけません。
これに精通するためにはいくつか新しい概念を学ぶ必要がありますが、 まさにここが、Node.js/JavaScriptのイベント駆動設計が役に立つ部分なのです。 新しい概念をどのようにサーバのコードに適用するのかを見ていきましょう。
サーバは先ほど作りました。サーバを作る時には関数をメソッドに渡しました。 リクエストを受け付けた時は必ず、渡した関数が呼ばれます。
実際起こるかどうかはわかりませんが、入ってくるリクエストを取り扱うための場所を作りました。 そう、その渡した関数のことです。最初に定義したとか、匿名で渡したとかは置いておきましょう。
この概念をコールバックと呼びます。メソッドに関数を渡した時、 メソッドに紐づいたイベントが発生していた場合に、この関数をコールバックするのです。
少なくとも私にとっては、これを理解するのは大変でした。まだ理解が足りないと感じるのであれば、 是非Felixのブログエントリを読んでみて下さい。
それではこの新しい概念を使って遊んでみましょう。サーバを作成してから、 HTTPリクエストが発生せず、渡したコールバック関数が呼ばれていなくても、 我々のコードが実行を続けることを証明できるでしょうか。やってみましょう:
var http = require("http");
function onRequest(request, response) {
console.log("Request received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
onRequest関数(コールバック関数)が呼ばれた時、 テキストをconsole.logで出力していることに注目して下さい。 その他のテキストはHTTPサーバ開始直後に出力しています。
これを起動した時(先ほどと同様、node server.js)、 すぐに”Server has started.”とコマンドライン上で出力されると思います。 (http://localhost:8888を開いて) サーバにリクエストを送った時、 ”Request received.”というメッセージがコマンドライン上に出力されます。
イベント駆動の非同期サーバサイドJavaScriptでのコールバックはうまく動くことがわかりましたね :-)
(おそらくサーバは標準出力に、”Request received.”を2回出力したのではないでしょうか。 これはあなたがhttp://localhost:8888/ を開いた時、 ブラウザがfaviconをロードしようとしているためです。)
OK、サーバのコードの残り部分をサクッと解析してしまいましょう。 残りはコールバック関数onRequest()の本体です。
コールバックによってonRequest()が呼ばれた時、2つの引数、 requestとresponseが渡されます。
これらはオブジェクトです。このオブジェクトの持つメソッドを使うと、 発生したHTTPリクエストや、そのリクエストへの応答の詳細を取り扱うことができます (例えば、サーバにリクエストを行ったブラウザに対して実際に何かを送り返す、というようなこと)。
我々のコードがやっているのはこれだけです: リクエストを受信し、response.writeHead()関数を使って、 HTTPステータスコード200とcontent-typeをHTTPレスポンスヘッダとして、 さらにresponse.write()関数を使って、 テキスト”Hello World”をHTTPレスポンスのボディーとして送ります。
さいごにresponse.end()を呼び出してレスポンスを完了しています。
この時点ではrequestオブジェクトは全く使っていないので、 リクエストの詳細についてはまだ触れません。
OK、それでは約束通り、アプリケーションの構成の話に戻りましょう。 先ほどはserver.jsというファイルにとても基本的なHTTPサーバのコードを書きました。 そしてアプリケーションのブートストラップや他のアプリケーションモジュールを使用して アプリケーションを起動するindex.jsというメインファイルを置くことが一般的だという話をしました。 (server.jsにある他のHTTPサーバモジュールと同じです)。
それでは、server.jsをまだ書いていないindex.jsメインファイルから使えるよう、 本当のNode.jsモジュールにする方法について話していきましょう。
みなさんお気づきのように、コードの中で既に書いています:
var http = require("http");
...
http.createServer(...);
Node.jsのどこかに”http”と呼ばれるモジュールがあるので、 コード内でrequireを行い、その結果をローカル変数に割り当てることでそれを使うことができます。
これによりローカル変数にはオブジェクトが割り当てられ、 httpモジュールが提供する全てのパブリックメソッドを使うことができます。
ローカル変数名にモジュール名をつけるのが慣習ですが、 このように好きな名前を付けることもできます:
var foo = require("http");
...
foo.createServer(...);
Node.js内部のモジュールの使い方はわかりました。では自分でモジュールを作り、 それを使うためにはどのようにしたら良いのでしょうか。
server.jsスクリプトをモジュールとして作り直してみましょう。
そんなに変更する必要はありません。コードをモジュール化するためには、 それを必要とするモジュールのスクリプトに提供するように、 パーツが持っている機能をexportする必要があります。
今のところ、exportしたいHTTPサーバの機能は単純なものです: 我々のサーバモジュールは単にサーバを起動するだけのスクリプトです。
これを実現するには、サーバのコードをstartという関数の中に放り込み、 この関数をexportします:
var http = require("http");
function start() {
function onRequest(request, response) {
console.log("Request received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
このようにすれば、サーバのコードがserver.jsファイルの中にある状態で、 メインファイルindex.jsを作り、そこでHTTPサーバを起動することができます。
以下の内容のindex.jsファイルを作成して下さい:
var server = require("./server");
server.start();
すぐおわかりだと思いますが、サーバモジュールを他の内部モジュールと同じように使うことができます: そのファイルをrequireし、変数に割り当てれば、exportされた関数を使うことができるのです。
これだけです。では我々のアプリをメインスクリプトから起動してみましょう。 この部分は今までと同じです:
node index.js
素晴らしいですね。モジュールを作ることで、アプリケーションの特定の部分を別のファイルに移し、 再度つなぎ合わせることができました。
アプリケーションの最初の部分、HTTPリクエストを受け付ける、という部分しか残っていません。 ただ、もう少しだけやりたいことがあります – ブラウザからリクエストのあったURLによって応答のしかたを変えたいですね。
単純なアプリケーションとしては、onRequest() コールバック関数の中で直接やることもできます。 しかし先ほど述べた通り、もう少しだけ抽象レイヤーを追加して、 サンプルアプリケーションを面白くしてみましょう。
異なるHTTPリクエストに応じて行き先となるコードを変えることを”ルーティング”と呼びます – では、routerと名付けてモジュールを作っていきましょう。
リクエストURLに加えて、GETやPOSTパラメータをルータに渡してやる必要があり、 ルータはそれに基づいて実行するコードを決定します(この”実行するコード”とは、 我々のアプリケーションの第3の部分となるものです: つまり、リクエストを受信した時に実際に働くリクエストハンドラ群です)。
HTTPリクエストの中を見て、リクエストURLやGET、POSTパラメータを抽出しなければいけません。 ルータの一部としてやるべきか、 サーバの中でやるべきか(もしくはモジュール自身か)という点は議論の余地があるかもしれませんが、 とりあえず現時点ではHTTPサーバの一部分としてやってみましょう。
必要な情報はすべて、コールバック関数onRequest()の第一引数として渡されるrequest オブジェクトから得ることができます。 ただ、内容を解釈するためには、Node.jsの追加のモジュール、 つまりurlとquerystringが必要となります。
urlモジュールが提供するのは、 URLの各部分(例えば、リクエストパスやクエリ文字列)を抽出するメソッドです。 抽出した後、querystringモジュールでリクエストパラメータのクエリ文字列をパースすることができます:
url.parse(string).query | url.parse(string).pathname | | | | | ------ ------------------- http://localhost:8888/start?foo=bar&hello=world --- ----- | | | | querystring(string)["foo"] | | querystring(string)["hello"]
もちろん、querystringを使って POSTリクエストのパラメータをパースすることもできるのですが、 それは後で見てみることにします。
ではonRequest()関数に、 ブラウザがリクエストしてきたURLパスを見つけるためのロジックを追加してみましょう:
var http = require("http");
var url = require("url");
function start() {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
はい。これで我々のアプリケーションはリクエストされたURLパスによって、 そのリクエストを区別することができます – これでリクエストを、(これから書くことになる)ルータを使ってURLパスに基づく リクエストハンドラにマッピングすることができます。
アプリケーションのコンテキストにおいて、 /startというURLに対するリクエストと、 /uploadというURLに対するリクエストは、 それぞれ別のコードに渡すことができるわけです。
OK、実際のルータのコードを書いてみましょう。 router.jsというファイルを作成し、 中身を以下の通り書いて下さい:
function route(pathname) {
console.log("About to route a request for " + pathname);
}
exports.route = route;
もちろん、このコード自体は何もしませんが、 今のところそれでOKです。色々なロジックをルータの中に書いていく前に、 どうやってこのルータをサーバと連携させるのかを見ていきましょう。
我々のHTTPサーバは、ルータのことを知り、利用する必要があります。 依存関係をサーバから密に結合させることもできますが、 様々なプログラミング言語での苦い経験からもわかるとおり、 サーバとルータの相互依存性としては粗に結合させるべきでしょう (Dependency Injection: 依存性の注入。 背景については Martin Fowlerによる素晴らしいエントリ を読むと良いでしょう)
最初にサーバのstart()関数を拡張して、 ルーティングのための関数を引数で指定して使えるようにしましょう:
var http = require("http");
var url = require("url");
function start(route) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
そしてindex.jsも拡張しましょう。ルーティングのための関数をサーバに組み込みます:
var server = require("./server");
var router = require("./router");
server.start(router.route);
また出てきました。関数を渡していますね。これはもう目新しいことではないはずです。
ここで(node indes.jsとして)アプリケーションを起動してURLへリクエストを送ったら、 HTTPサーバがルータを使用している様子、それからリクエストされたパス名が渡されている様子が、 アプリケーションの出力内容からわかるはずです:
bash$ node index.js Request for /foo received. About to route a request for /foo
(紛らわしいので/favicon.icoへのリクエストは省略しています)
今一度、少しの間だけ関数型プログラミングについての話をしても良いでしょうか。
関数を渡すのは、技術的な思慮によるものだけではありません。ソフトウェアデザインに関係していて、 哲学的とさえいえます。考えてみて下さい: indexファイルは、routerオブジェクトをサーバに渡します。 そしてサーバはこのオブジェクトのroute関数を呼びます。
このように、モノを渡して、サーバが何かをスルためにこれを使うのです。 ねえルータさん、これをルーティングしてくれない?
しかしサーバは何かする必要はありません。何かをやらせて、それをやらせるために、 最終的に何か必要なものがあるわけではなく、 とにかくアクションを必要とします。必要なのは名詞ではなく、動詞なのです。
この考え方の核心となる根本的なマインドシフトが理解できてから、 私は関数型プログラミングをとても良く理解することができました。
私は、Steve Yeggeによる名作 Execution in the Kingdom of Nouns を読んだ時にこの考え方を理解することができたのです。 是非今すぐアクセスしてこれを読んで下さい。 私が今まで読んだソフトウェアに関する書き物の中でも、出会えて嬉しいと思える、最高のものです。
さて、仕事に戻るとしましょう。HTTPサーバとリクエストルータは、我々の意図した通り、 お互いに会話ができる仲良しの友達となりました。
もちろん、それだけでは不十分です。”ルーティング”とは、個々のリクエストを別々に取り扱いたい、 ということを意味しています。私たちが欲しいのは、 /uploadへのリクエストや/startへのリクエストをそれぞれ取り扱う”ビジネスロジック”なのです。
今、ルーティングはルータの内部で”終了”しています。 これはアプリケーションがもっと複雑になった時にスケールできなくなるからで、 ルータはリクエストに対して実際に何かを”する”場所ではないわけです。
では、リクエストが引き渡されるリクエストハンドラを呼び出しましょう。 リクエストハンドラがないと、今までルータを使ってやろうとしていた事の意味がなくなってしまいます。
アプリケーションの新しい部分、新しいモジュールなど、 - 驚くような事はここでは一切ありません。 リクエストハンドラと呼ばれるモジュールを作って、 プレースホルダとなる関数を全てのリクエストハンドラに追加し、 モジュールのメソッドとしてエクスポートしましょう:
function start() {
console.log("Request handler 'start' was called.");
}
function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
こうして、ルータにルーティングする先のものを渡すことで、 リクエストハンドラとルータを繋ぎ合わせることができます。
この時点で私たちは決断をする必要があります: リクエストハンドラのモジュールはルータの中にハードコーディングするのか、 もしくは少しだけ依存性の注入を行うのか。 依存性の注入は、他のパターンも同様ですが、使いたいというだけで使うべきではありません。 ただしこのケースでは、 ルータとリクエストハンドラを粗に結合することでルータの再利用性を高められるという意味がしっかりあります。
つまり、リクエストハンドラはサーバからルータに渡す必要があることを意味していますが、 これはまだ間違っているような感じがします。ちゃんと説明するとしたら、 メインファイルからサーバに渡し、そこからさらにルータに渡す、ということになります。
ではどうやってそれを渡すのでしょうか。今現在、2つのハンドラがあります。 しかし実際のアプリケーションでは、この数はもっと増えたり減ったりします。 新しいURLやリクエストが追加される度に、 リクエストをハンドラにマッピングするというつまらない作業をしたくはありません。 かといってif request == x then call handler y といったようなことをするのでは美しくありません。
要素の数が変わって、文字列(おそらくリクエストURL)にマッピングする?ということは、 なんだか連想配列がうまくはまりそうですね。
ただ、悪いニュースがあります。JavaScriptでは、連想配列が提供されていないのです。 でも大丈夫。連想配列が必要なら、オブジェクトを使えば良いのです!
これについては、次のURLに良い記事があります。 http://msdn.microsoft.com/en-us/magazine/cc163419.aspx 関連する箇所を引用します:
C++やC#において、オブジェクトについて話をする時は、 クラスや構造体のインスタンスについて触れます。 オブジェクトはそれぞれ異なるプロパティやメソッドを持ちますが、 それらはインスタンス化するためのテンプレート (つまり、クラスのことです)に依存します。 しかしJavaScriptのオブジェクトは、これに当てはまりません。 JavaScriptでは、オブジェクトは単なる名前と値のペアの集合なのです。 よってJavaScriptにおけるオブジェクトと呼ぶものは、 文字列をキーにしたディクショナリであると考えて下さい。
JavaScriptのオブジェクトが単なる名前と値のペアの集合であるなら、 どうやってメソッドを持つのでしょうか?値は、文字列でも良いですし、 数字でも良い、 - そして関数でも良いのです!
OK、ではコードに戻ってみましょう。リクエストハンドラのリストをオブジェクトとして渡し、 粗結合にするためにこのオブジェクトをroute()に入れてしまいます。
まずは、オブジェクトをメインファイルであるindex.jsに配置してしまいましょう:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);
handleは、いわば”モノ”(リクエストハンドラの集合)ではありますが、 動詞を使って命名したいと思います。次に出てくるルータのところで、 より自然な表現をすることができるからです。
おわかりの通り、 異なるURLを同じリクエストハンドラにマッピングする箇所がとてもシンプルになっています: “/”とrequestHandlers.startというキーと値のペアを追加することで、 /startへのリクエストだけでなく/へのアクセスも同じstartハンドラに渡す処理が、 とても美しくあざやかに実現できていますね。
オブジェクトが定義できたら、 それを追加の引数としてサーバに渡します。 server.jsを修正して実際に使えるようにしましょう:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
引数handleをstart()関数に追加して、 ハンドルオブジェクトをroute()コールバックの第一引数として渡すコードに修正しました。
route()関数も同様に変更しましょう。router.jsファイルは以下のよう修正します:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname]();
} else {
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
ここで何をしているかというと、パス名に対応するリクエストハンドラが存在するかどうかをチェックし、 もし存在する場合は、紐付けられた関数を単に呼び出しています。 連想配列の要素にアクセスすればオブジェクト経由でリクエストハンドラ関数にアクセスできるので、 先述のように実に自然なhandle[パス名]();として、 あたかも”このパス名をhandleして下さい” と言っているように表現することができます。
OK、これでサーバ、ルータ、リクエストハンドラをすべて繋ぎ合わせることができました! アプリケーションを起動し、 http://localhost:8888/start にブラウザからリクエストを送れば、 このように正しくリクエストハンドラが呼び出されていることがわかります:
Server has started. Request for /start received. About to route a request for /start Request handler 'start' was called.
そして http://localhost:8888/ をブラウザで開いた時、このリクエストはさらに、 startリクエストハンドラによって処理されていることもわかります:
Request for / received. About to route a request for / Request handler 'start' was called.
美しい。あとは実際にリクエストハンドラが何かブラウザに送り返せば良いわけですね。
ひとつ思い出してみて下さい。 ブラウザがページをリクエストした時に表示される”Hello World”は、 server.jsファイルのonRequest()関数が出しているものです。
“ハンドリングリクエスト”とは、 ”リクエストに応答する”ということを意味していて、 このonRequest関数がしているように、 リクエストハンドラにブラウザと会話をしてもらう必要があります。
PHPやRubyの経験を持つデベロッパーにとって、 私たちがやりたいと思うような単刀直入なアプローチは間違えやすい可能性があります: 魔法のように動いて、もっともらしくて、そして突然予期せず失敗に終わってしまうのです。
“単刀直入なアプローチ”というのはつまりこういうことです: ユーザに対して表示したい内容をリクエストハンドラにreturn()させ、 このレスポンスデータをonRequest関数でユーザに戻す。
とりあえずやってみましょう。良いアイデアではないことがわかるはずです。
まずリクエストハンドラから始めましょう。 ブラウザに表示させたい内容を戻すようにします。 requestHandlers.jsを下記のように修正します:
function start() {
console.log("Request handler 'start' was called.");
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
これでよし。 同じように、ルータはリクエストハンドラによって返されたものをサーバに渡してやる必要があります。 なのでrouter.jsはこのようにします:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
return handle[pathname]();
} else {
console.log("No request handler found for " + pathname);
return "404 Not found";
}
}
exports.route = route;
これを見ておわかりのとおり、リクエストがルーティングされなかった時にもいくらかのテキストを返すようにしています。
そして最後に、ルータ経由でリクエストハンドラが渡してきた内容をブラウザに返すよう、サーバを改修します。server.jsは下記のようにしましょう:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
var content = route(handle, pathname)
response.write(content);
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
書き直したアプリケーションを起動すれば、すべては魔法のようにうまく行くことでしょう。つまり http://localhost:8888/start をリクエストすれば”Hello Start”がブラウザに表示され、 http://localhost:8888/upload をリクエストすれば”Hello Upload”と、 http://localhost:8888/foo などとすれば”404 Not found”となります。
OK、これで何か問題でも?端的に言うと、もしリクエストハンドラのどれかが、 後述のノンブロッキング操作を行いたいとなった場合に問題となってしまうのです。
それでは詳しい説明に入りたいと思います。
先述のとおり、リクエストハンドラ内でノンブロッキングの操作をしようとした時、 問題が発生してしまいます。まずはブロッキングでの操作について、 そしてノンブロッキングの操作について説明していきます。
“ブロッキング”と”ノンブロッキング”の方法を説明するかわりに、 まずはいまのリクエストハンドラにブロッキングの操作を追加したら何が起こるのかを見てみましょう。
これをやるためには、リクエストハンドラstartを改造して、 ”Hello Start”文字列を返すまで10秒間待つようにします。 sleep()の類いはJavaScriptにないので、 ちょっとしたうまい方法で工夫してやる必要があります。
では、requestHandlers.jsを下記のように修正して下さい:
function start() {
console.log("Request handler 'start' was called.");
function sleep(milliSeconds) {
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
sleep(10000);
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
何をしているのかというと: 関数start()が呼ばれたら、 Node.jsは10秒間待ち、”Hello Start”と返します。upload()を呼び出した時は、 待つことなくすぐに返します。
(start()内で10秒間スリープしている部分は、 実際にはブロッキング操作を行う部分で、 ある種もっと長い計算処理が行われるということを想像して下さい)
この変更で何が起こるのか見てみましょう。
いつも通り、サーバを再起動する必要があります。 今回は何が起こるのかを見るために、少し複雑な”手順”を追ってもらいます: まず、2つのブラウザウィンドウもしくはタブを開き、 1つ目のブラウザウィンドウのアドレスバーに http://localhost:8888/start と入力して下さい。ただし、まだこのURLを開かないで下さい!
次に2つめのブラウザウィンドウのアドレスバーに、 http://localhost:8888/upload と入力し、1つ目と同様、まだenterを打たないで下さい。
そしてこのようにします: 1つ目のウィンドウ(“/start”)でenterキーを押し、 すぐに2つめのウィンドウ (“/upload”) に切り替えてenterを打ちます。
このような現象に気づくでしょう: /startのURLの方はロードされるのに10秒間かかります。 これは期待通りの動きです。しかし、 /uploadのURLの方もロードに10秒間かかってしまっています。 こっちのリクエストハンドラではsleep()していないのに!
なぜでしょう? それは、start()がブロッキング操作をしているからなのです。 ”他のものが動くことをブロックしている”というわけです。
これは問題です。なぜなら有名な格言にもあるとおり: “nodeでは、コード以外のすべてが並列で動作する (In node, everything runs in parallel, except your code)” べきだからです。
Node.jsはたくさんの処理を同時に行えるけれども、 全てをスレッドに割り振って行うわけではないのです – 実際、Node.jsはシングルスレッドです。そうではなく、 イベントループを走らせることでデベロッパーはこれを利用できるのです – これらの理由から、ブロッキング操作は極力避け、 かわりにノンブロッキング操作を行うべきなのです。
しかしそれを実現するためには、時間のかかる処理になりそうな場合に、 関数を他の関数に渡してコールバック処理を行う必要があります (例えば、10秒待つ、データベースに問い合わせる、大変な計算をするときなどです) 。
つまりこういうことです。 “ねえprobablyExpensiveFunction()(時間がかかりそうな関数)さん、担当の仕事をして下さい。 でも私Node.jsはシングルスレッドだから、あなたの仕事が終わるのを待ってられないんだ。 あなたの次に並んでるコードを実行してしまいたいから、 このcallbackFunction()を持っていって、 その時間のかかりそうな仕事が終わったら呼び出してくれる?じゃあよろしく!”
(もしこれについてさらに詳しく読みたいなら、Mixuのポスト Understanding the node.js event loop を読んでみて下さい。)
では、なぜ我々のアプリケーションの”リクエストハンドラによるレスポンスのしかた” ではノンブロック操作がうまくできないのでしょうか。
今一度、アプリケーションを修正して問題を直接再現してみましょう。
もう一度startリクエストハンドラを使います。 下記を反映するよう、修正して下さい(requestHandlers.jsファイル):
var exec = require("child_process").exec;
function start() {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("ls -lah", function (error, stdout, stderr) {
content = stdout;
});
return content;
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
おわかりのとおり、新しいNode.jsモジュールchild_processを使っています。 これのおかげで、とてもシンプルで便利なノンブロッキング操作、exec()を活用できるのです。
exec()は、Node.jsの中からシェルコマンドを実行します。 この例においては、カレントディレクトリにあるすべてのファイルのリストを取得してみます(“ls -lah")。 ユーザがURL/startをリクエストするとこのリストがブラウザに表示されます。
このコードでやっていることは簡単なことです: 新しい変数contentを(“empty”という文字列の初期値で)作成し、 ”ls -lah"を実行してその結果を変数に格納し、返すだけです。
いつもどおり、アプリケーションを起動して、 http://localhost:8888/startにアクセスしてみましょう。
すっきりしたWebページがロードされ、文字列”empty”が表示されます。 おや、なにか間違っているのでしょうか?
ご想像の通りかもしれません。exec()はノンブロッキングなやりかたで魔法を使ったのです。 まあ良いでしょう、これでとても時間がかかるシェル操作を実行できるのですから (例えば、巨大なファイルをコピーしまくるとか)。 sleepの時のように、ブロッキングされてアプリケーションが完全に止まってしまうことはないのです。
(もしこれを証明したければ、”ls -lah"をもっと時間のかかる操作、 たとえば”find /”みたいなものと入れ替えてみて下さい)
しかし、このエレガントなノンブロッキング操作、あまり嬉しくないですよね。 ブラウザが結果を表示してくれないわけですから。そうでしょう?
じゃあ、直していきましょう。直しながら、 なぜこのアーキテクチャがうまくいかないのか解明しましょう。
問題はexec()です。ノンブロッキングで動作するよう、 コールバック関数を活用します。
先ほどの例では、 exec()関数を呼び出す時の第2引数として渡されるのは匿名関数です。
function (error, stdout, stderr) {
content = stdout;
}
そしてここに我々が直面している問題の原因があります: このコードは同期式に実行されているため、 exec()を呼び出した直後にNode.jsはreturn content;の実行へ進んでいます。 この時exec()は非同期式に動作するため、exec()に渡されたコールバック関数が呼び出されておらず、 contentはまだ”empty”のままなのです。
いまのところ”ls -lah"は(ディレクトリ内にファイルが数百万もない限り)そ れほど時間がかからず素早く実行されます。 それはコールバックが比較的早く呼び出されるからですが、それでも非同期に実行されます。
もっと時間のかかるコマンドで考えてみると明らかになります: “find /”と実行すると、私のマシンではおよそ1分程度かかります。 しかしリクエストハンドラ内の”ls -lah"を”find /”に置き換えても、 やはり/start URLを開いたとき、すぐにHTTPレスポンスが返ってきます。 つまりexec()はバックグラウンドで実行され、 Node.jsはアプリケーションの実行を続けるということがわかります。 そしてexec()に渡したコールバック関数は、 ”find /”コマンドの実行が完了した時にだけ呼ばれるであろうことが想像できます。
しかし、これでどうやって目的を達成できるのでしょうか。 ユーザに対してカレントディレクトリのファイル一覧を表示したいのです。
どうしたらできないかを学んだところで、 次はどのようにリクエストハンドラがブラウザに正しく応答させられるかを考えていきましょう。
“正しく”というフレーズを使いました。これは危険ですね。ひとつの”正しい方法”なんていうものはないですから。
しかし良い方法ならあります。Node.jsでよく見かけるように、関数を渡し合う方法です。少し調べてみましょう。
今のところ、アプリケーションは(リクエストハンドラがユーザに表示したい)コンテンツを送ってやることはできます。 リクエストハンドラからHTTPサーバへ、 アプリケーションのレイヤーをまたがって(リクエストハンドラ->ルータ->サーバと)返していくわけです。
新しいアプローチ方法はこうです: サーバにコンテンツを渡すのではなく、サーバをコンテンツに渡します。 もう少し正確にいうと、ルータを通して(サーバのコールバック関数onRequest()から受け取った)responseオブジェクトを、 リクエストハンドラに渡してしまいます。 それにより、このオブジェクトの関数を使ってリクエストハンドラが自分でリクエストに応答することができます。
解説はもういいとして、アプリケーションをステップバイステップで修正していきましょう。
server.jsからはじめます:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
route()関数からの戻り値は受け取らずに、 第3引数としてresponseオブジェクトを渡しています。 さらに、responseオブジェクトのメソッドは一切呼び出していません。 これはrouteにすべて任せようと考えているからです。
次にrouter.jsです:
function route(handle, pathname, response) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response);
} else {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
同じパターンです: リクエストハンドラからの戻り値を受け取らず、responseオブジェクトを渡します。
もしリクエストハンドラがない場合、適切に”404”ヘッダとボディーを自分で返します。
そして最後に、requestHandlers.jsを修正します:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("ls -lah", function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
リクエストハンドラ関数は、 リクエストに直接応答するためにレスポンスのパラメータを受け取って使用する必要があります。
startハンドラでは、exec()の匿名コールバック関数の中から応答しています。 またuploadハンドラは、”Hello Upload”と応答すること自体は同じですが、 responseオブジェクトを使って応答するようにしました。
同じように(node index.jsとして)アプリケーションを起動しましょう。期待通り動くはずです。
もし/startで実行される時間のかかる処理のせいで、 /uploadへのリクエストがブロックされて応答が返らない、 ということがないか確認したかったら、requestHandlers.jsを以下のようにしてみると良いでしょう:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("find /",
{ timeout: 10000, maxBuffer: 20000*1024 },
function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
これでhttp://localhost:8888/start へのHTTPリクエストは最低でも10秒間かかることになりますが、 /startがまだ終わっていなくても、 http://localhost:8888/upload へのリクエストに対する応答はすぐに返ってくるはずです。
ここまでのところ、まことに結構な感じですが、 まだお客さんにとって価値のある賞を獲得するようなものは何も作っていません。
サーバ、ルータ、リクエストハンドラはここにあります。 やっとコンテンツをサイトに追加して、ユーザがファイルを選択、アップロードし、 アップロード済みのファイルをブラウザで表示できるようにしていきましょう。 ここではシンプルにするため、画像ファイルだけアップロードおよび表示できるようにしたいと思います。
OK、ステップバイステップでいきましょう。 ここまででJavaScriptのテクニックや原則は説明してきましたので、 すこし飛ばしていきます。どうやらこの著者は、 自分が話しすぎるのを聞いているのが好きみたいですね。
さて、ステップバイステップと言いましたが、 これには2つの意味があります。まず1つ目はPOSTリクエストを取り扱う方法、 そして2つ目はアップロード処理にNode.jsの外部モジュールを利用する方法です。 このアプローチ方法を選んだのには2つの理由があります。
まず通常、POSTリクエストはNode.jsでは比較的シンプルに取り扱うことができます。
しかし練習する価値は十分にあります。
もう一つは、ファイルアップロード(マルチパートPOSTリクエスト)の取り扱いはたとえNode.jsでもそれほどシンプルではなく、
このチュートリアルのスコープ内ではないのですが、
外部モジュールを使う方法自体は初心者向けチュートリアルに入れる内容として意味があると考えたからです。
ここはひとつシンプルにいきます: textareaを用意して、ユーザが入力し、 サーバにPOSTリクエストで送信するようにします。このリクエストを受け取って取り扱い、 textareaの内容を表示してみます。
textareaフォームのHTMLは/startリクエストハンドラが受け持つ必要があるので、 早速requestHandler.jsに追記しましょう:
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
これがウェビー賞を受賞できないなら、他にどんなものが受賞できるのか皆目検討もつきません。
http://localhost:8888/start
にブラウザでアクセスすれば、シンプルなフォームが表示されます。
もしされないとしたら、アプリケーションの再起動を忘れているのでしょうね。
※訳注 ウェビー賞…国際デジタル芸術科学アカデミーにより優れているとされたウェブサイトに贈られる賞
こんな声が聞こえてきます: リクエストハンドラにビューの内容を書くなんて酷い。 しかしここでは追加の(例えばビューをコントローラロジックから分離する、 といったような)抽象レイヤーを挟むのはやめました。 それをやってもJavaScriptやNode.jsのコンテキストの理解を深められるわけではないですから。
それよりももっと面白い課題に取り組むことにしましょう。 ユーザがこのフォームを送信すると/uploadリクエストハンドラにたどり着く、 POSTリクエストを扱う部分です。
もう初心者から脱していますので、 POSTデータの扱いがノンブロッキングであることには驚くことはないでしょう。 非同期のコールバックを使えば良いわけです。
これは至極真っ当な動作ですね。 POSTリクエストは、とても大きい可能性があります – これで、ユーザが何メガバイトものサイズのテキストを入力しても、何ら問題ありません。 すべてのデータを一つの塊として扱うので、ブロッキング操作となってしまう可能性もあるのです。
プロセス全体がノンブロッキングで動作できるよう、 Node.jsは特定のイベントの時に呼び出されるコールバックではコード上でPOSTデータを細かい塊で扱えるようにしてくれます。 特定のイベントとは、data(POSTデータの中の新しい塊が届いた時)とend(すべての塊を受信した時)です。
Node.jsがこれらのイベント発生時にコールバックするために使う関数を指定してやらなければいけません。 これは、HTTPリクエストを受信するときにonRequestコールバックに渡されるrequestオブジェクトにリスナー (listeners)を追加することで実現できます。
このような感じになります:
request.addListener("data", function(chunk) {
// called when a new chunk of data was received
});
request.addListener("end", function() {
// called when all chunks of data have been received
});
このロジックを実装するとき、疑問が湧いてきます。 今のところ、サーバ内でしかrequestオブジェクトにアクセスすることができません – つまりresponseオブジェクトでやってきたように、 ルータやリクエストハンドラにrequestオブジェクトを渡していません。
私が思うに、リクエストから全てのデータをアプリケーションに渡すのはHTTPサーバの仕事なのです。 ですので、POSTデータの処理はサーバ内で行い、 最終的なデータをルータやリクエストハンドラに渡すのです。 そしてルータやリクエストハンドラがそれをどう扱うかを決めれば良いのです。
dataおよびendイベントコールバックはサーバ内に置いておき、 dataコールバック内ですべてのPOSTデータの断片を集めて、 endイベントが呼ばれたときにルータを呼出すのです。 ルータに集めたデータの塊を渡せば、そのままリクエストハンドラに渡されることになります。
それではserver.jsからいきましょう:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var postData = "";
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8");
request.addListener("data", function(postDataChunk) {
postData += postDataChunk;
console.log("Received POST data chunk '"+
postDataChunk + "'.");
});
request.addListener("end", function() {
route(handle, pathname, response, postData);
});
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
ここでは基本的には3つのことをしています: まず受信データのエンコーディングをUTF-8と定義し、 次に新しいPOSTデータの塊を受信する度にpostData変数に追記していく”data”イベントリスナーを追加、 そして全てのPOSTデータが収集できた時にだけ呼び出されるよう、 ルータ呼び出し部分をendイベントコールバック内に移動しました。 また、POSTデータは後ほどリクエストハンドラ内で必要になるため、ここでは一旦ルータに渡しています。
本番環境のコードでは、 一つデータの塊を受け取る度にコンソールでログ記録するのはよろしくありません (何メガバイトものPOSTデータですからね?)。 ただ今回に限っては、何が起きているか確認してみましょう。
ここでは、textareaにテキストをたくさん入れたり少しだけ入れたりして遊んでみると良いと思います。 多めのテキストを入れた時にはdataコールバックが何度も呼ばれることがわかるはずです。
アプリケーションをもう少しかっこ良くすることにしましょう。 /uploadページで、受け取った内容を表示します。 これを実現するためには、postDataをリクエストハンドラに渡す必要があります。 router.jsをこのようにします:
function route(handle, pathname, response, postData) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, postData);
} else {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
そしてrequestHandler.jsでは、uploadリクエストハンドラのレスポンスにそのデータを含めます:
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent: " + postData);
response.end();
}
exports.start = start;
exports.upload = upload;
これでよし、リクエストハンドラでPOSTデータを受信できるようになりました。
このトピックでひとつやり残したことがあります: ルータに、そしてリクエストハンドラに渡したのはPOSTリクエストのボディー全体です。 POSTデータの個々のフィールド、つまりこのケースにおけるtextフィールドの値を消化したいですね。
querystringモジュールについてはすでに読んだ通りです。このようになります:
var querystring = require("querystring");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
exports.start = start;
exports.upload = upload;
さて、初心者向けチュートリアルとしてのPOSTデータの扱い方はこれで全部です。
ではこのユースケースも終盤となりました。 ブラウザでユーザに画像ファイルをアップロードさせて、 アップロードされた画像を表示するという計画でした。
90年代では、これはIPOのためのビジネスモデルとして地位を確立できていましたが、 今日ではあと2つのことを満たさなければいけません: Node.js外部ライブラリをインストールする方法、 そしてそれらを私たちのコード内で利用する方法です。
これから使う外部モジュールは、Felix Geisendörferによるnode-formidableモジュールです。 このモジュールは受け取ったファイルデータの面倒な処理をうまく抽象化してくれます。 最終的にはファイルを受信する部分は単にPOSTデータを取り扱えば良いだけになります。 - 細かいところを見れば、たしかに悪魔が存在します。出来合いのソリューションを使うのはとても有用なことなのです。
Felixのコードを利用するためには、 関連するNode.jsモジュールがインストールされている必要があります。 Node.jsにはNPMというパッケージマネージャが同梱されています。 これのおかげで外部のNode.jsモジュールを非常に簡単に使うことができます。 Node.jsがインストールされた環境で、コマンドラインでこのように実行します
npm install formidable
そしてこのような出力になれば
npm info build Success: formidable@1.0.9
npm ok
準備完了です。
これで私たちのコード内でformidableモジュールが使えるようになりました – あとは、最初のほうで他のビルトインモジュールでやったようにrequireすれば良いだけです:
var formidable = require("formidable");
formidableという名前が暗に示しているように、 HTTP POSTで送られたフォームデータはNode.jsでパースすることができるようになります。 あとは送信されたフォームが抽象化された新たなIncomingFormを作成するだけです。 これでフォームを通して送信されたフィールドやファイルといったHTTPサーバのrequest オブジェクトをパースするのに使うことができます。
node-formidableプロジェクトのページに記載されたサンプルコードを見れば、それぞれのパーツを同時に扱う方法がわかります:
var formidable = require('formidable'),
http = require('http'),
sys = require('sys');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(sys.inspect({fields: fields, files: files}));
});
return;
}
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8888);
このコードを書いてnodeで実行すれば、 ファイルアップロードを含む簡単なフォームを送信することができます。 そしてform.parse呼出し時のコールバック関数に渡されるfilesオブジェクトがどのような構成になっているのかがわかります:
received upload: { fields: { title: 'Hello World' }, files: { upload: { size: 1558, path: '/tmp/1c747974a27a6292743669e91f29350b', name: 'us-flag.png', type: 'image/png', lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT, _writeStream: [Object], length: [Getter], filename: [Getter], mime: [Getter] } } }
このユースケースを完了するためには、 formidableでのフォームのパースロジックを我々のコードに組み込み、 アップロードされた(/tmpフォルダに保存される)ファイルの内容を、 リクエスト元のブラウザへ返してやる必要があります。
まずは後者のほう、いってみましょう: ローカルのハードドライブに画像ファイルがあったら、 リクエスト元のブラウザに対してどうやって返しますか?
このファイルの中身を読み込んでNode.jsサーバに渡すというのはわかりますね。 驚くなかれ、まさにそのためのモジュール、fsモジュールというものがあります。
/showというURLのためのリクエストハンドラを追加し、 ハードコーディングで/tmp/test.pngというファイルの内容を表示してみます。 もちろん、あらかじめその場所に本物のpng画像を保存しておく必要があります。
requestHandlers.jsを下記のとおり修正します:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "\n");
response.end();
} else {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
この新しいリクエストハンドラを、/showというURLにマッピングします。index.jsを下記のようにします:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;
server.start(router.route, handle);
サーバを再起動して http://localhost:8888/show をブラウザで開けば、/tmp/test.pngとして保存されている画像ファイルが表示されるはずです。
良いですね。あとやるべきことは
ステップ1は簡単です。multipart/form-dataのエンコーディングタイプをHTMLフォームに追記し、 textareaを削除、ファイルアップロードのinputフィールドを追加して、 submitボタンのテキストを”ファイルをアップロード”に変更すれば良いだけです。 requestHandlers.jsファイルを修正しましょう:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "\n");
response.end();
} else {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
これだけです。次のステップは少しだけ難しくなります。 最初の問題は: uploadリクエストハンドラの中で、ファイルアップロード処理を行いたいのですが、 そこで、requestオブジェクトをnode-formidableのform.parseに渡してやる必要があるということです。
しかしここで私たちが持っているのは、responseオブジェクトとpostData配列だけです。 なんと残念なことでしょう。requestオブジェクトをなんとかしてサーバからルータへ、 そしてリクエストハンドラへと渡してやらないといけないようです。もっと綺麗なやり方がありそうですが、 とりあえずこのアプローチ方法でなんとかなりそうなので、今回はこれでよしとしましょう。
そうこうしている間に、とっとと全てのpostDataをサーバやリクエストハンドラから削除してしまいましょう – ファイルアップロードではもう使いませんし、問題が発生する可能性さえあります: サーバ内では既にrequestオブジェクトのdataイベントを既に”消化”してしまっているため、 同様にこれらのイベントを消化する必要があるform.parseでそのデータを受信できなくなってしまうからです (Node.jsはデータをバッファリングしてくれないのです)。
それではserver.jsから始めましょう – postDataを扱う部分と (node-formidableが直接取り扱うことになる) request.setEncodingの行をを削除し、代わりにrequestをルータに渡します:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response, request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
次はrouter.jsです – postDataはもういらないので、代わりにrequestを渡します:
function route(handle, pathname, response, request) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, request);
} else {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/html"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
これで、requestオブジェクトはuploadリクエストハンドラ関数で使うことができます。 node-formidableではアップロードされたファイルを/tmpのローカルファイルとして保存する時の細かい処理を受け持ってくれるので、 このファイルがちゃんと/tmp/test.pngという名前にリネームされるところを確認する必要があります。 そう、ここでは簡単に済ますため、画像はすべてPNGであると想定しています。
リネームの際のロジックは少しだけ複雑です: Windowsにおけるnodeの実装では、既存のファイルの位置にリネームすることができないため、 エラー発生時にはファイルを削除する必要があります。
アップロードされたファイルの管理とリネーム処理を、requestHandlers.jsの中に追記しましょう:
var querystring = require("querystring"), fs = require("fs"), formidable = require("formidable"); function start(response) { console.log("Request handler 'start' was called."); var body = '<html>'+ '<head>'+ '<meta http-equiv="Content-Type" '+ 'content="text/html; charset=UTF-8" />'+ '</head>'+ '<body>'+ '<form action="/upload" enctype="multipart/form-data" '+ 'method="post">'+ '<input type="file" name="upload" multiple="multiple">'+ '<input type="submit" value="Upload file" />'+ '</form>'+ '</body>'+ '</html>'; response.writeHead(200, {"Content-Type": "text/html"}); response.write(body); response.end(); } function upload(response, request) { console.log("Request handler 'upload' was called."); var form = new formidable.IncomingForm(); console.log("about to parse"); form.parse(request, function(error, fields, files) { console.log("parsing done"); /* Possible error on Windows systems: tried to rename to an already existing file */ fs.rename(files.upload.path, "/tmp/test.png", function(err) { if (err) { fs.unlink("/tmp/test.png"); fs.rename(files.upload.path, "/tmp/test.png"); } }); response.writeHead(200, {"Content-Type": "text/html"}); response.write("received image:<br/>"); response.write("<img src='/show' />"); response.end(); }); } function show(response) { console.log("Request handler 'show' was called."); fs.readFile("/tmp/test.png", "binary", function(error, file) { if(error) { response.writeHead(500, {"Content-Type": "text/plain"}); response.write(error + "\n"); response.end(); } else { response.writeHead(200, {"Content-Type": "image/png"}); response.write(file, "binary"); response.end(); } }); } exports.start = start; exports.upload = upload; exports.show = show;
これで完了です。サーバを再起動すれば、このユースケースが実現できているはずです。 ハードドライブからローカルのPNG画像を選択、 サーバにアップロードすれば、Webページに表示されるでしょう。
おめでとうございます、ミッションはこれで完了です! シンプルだけど一人前のNode.jsのWebアプリケーションができました。サーバサイドJavaScript、 関数型プログラミング、ブロッキングとノンブロッキング、コールバック、イベント、 カスタムモジュールと内部、外部モジュール、その他諸々について学びました。
もちろん、ここで話さなかったことは沢山あります: どうやってデータベースに接続するのか、 どうやって単体テストを書くのか、 どうやって外部モジュールを作ってNPM経由でインストールできるようにするのか、 GETリクエストをどうやって扱うのか、といったような簡単なことも触れていません。
しかし、初心者向けの本としてはこれが運命なのでしょう。 すべてのことについて細かく完璧に説明することはできないのです。
良いニュースがあります。Node.jsコミュニティーはとても活気があり(良い意味で、 落ち着きのない子供を想像してみて下さい)、 たくさんのリソースがあります。あなたの質問に応えてくれるところも沢山あります。 次のステップとしては、 Node.js community wikiや the NodeCloud directoryあたりからはじめると良いでしょう。