Manuel Kiesslingによるnode.jsのチュートリアル
この文書の狙いはNode.jsアプリケーションの開発を始められるようにすること、それに伴なう"高度な"JavaScriptについて知る必要のある全てを教えることです。それは典型的な"Hello World"チュートリアルのはるか上を行くものです。
この本は最終版です。更新は誤りを修正したりNode.js.の新しいバージョンの変更を反映するためだけに行われます
この本のコードサンプルは、Node.jsのバージョン0.4.9で動作するようにテストされています。
このドキュメントは、おそらく筆者に似た経歴、つまりRuby・Python・PHP・Javaのように、少なくとも一つのオブジェクト指向言語語についての経験があり、JavaScriptを少しだけ使った経験があり、Node.jsについては全く知らない読者に最も適合します。
この文書は、既に他のプログラミング言語の経験がある開発者を対象としていて、データ型・変数・制御構造のような本当に基礎的な事はカバーしていません。この文書を理解するためには、これらについて予め知っておく必要があります。
しかし、JavaScriptでの関数とオブジェクトは、他のほとんどの言語での対応とは異なるので、これらについては詳細に説明します。
この文書の終りには、あなたはユーザにページの閲覧とファイルのアップロードを許す完全なWebアプリケーションを作成しているでしょう。
これはもちろん、世界を変革させるような代物ではありませんが、いくつか追加の工程を費やして、これらのユースケースを作成可能にするのに"過不足無い"コードを作成するだけではなく、フレームワーク以外の部分とキレイに分離したシンプルかつ完全なフレームワークを作成します。その意味するところはすぐに明らかになります。
最初にNode.jsのJavaScript開発はブラウザでのJavaScript開発とはどう違うのかを眺めていきます。
次に"Hello World"アプリケーションの記述という古き良き伝統にのっとり、基本的なNode.jsのアプリケーションを作ります。
それから、構築したい"本当の"アプリケーションとは何かについて議論し、このアプリケーションの構築を実装するのに必要な様々な部品を分析し、これらの部品についてステップバイステップで作業していきます。
そしてお約束したとおり、JavaScriptのより高度な概念のいくつかについて順を追って学び、それらを利用する方法、そして他のプログラミング言語で知っている概念の代わりにここれらの概念を利用することがなぜ道理にかなっているかを学びます。
最終的なアプリケーションのソースコードは、the NodeBeginnerBook Github repository. から入手できます。
目次
技術的なことについて話す前に、少々お時間を頂いて、あなたとJavaScriptの関係についてお話ししましょう。この章はあなたがこの文章を読む必要があらるかどうか見極める為にあります。
昔々、あなたは筆者のように、HTML文書を書くことによってHTML"開発"を始めました。あなたはJavaScript呼ばれる面白いモノを取り入れて、非常に基本的な方法だけを使い、時々あなたのWebページに対話性を追加しました。
しかし、あなたが本当に欲しいものは"本物"でした。複雑なWebサイトを構築する方法を知りたかった。だからPHPやRuby、Javaなどのプログラミング言語を学び、"バックエンド"のコードを書き始めました。
それにもかかわらず、あなたがJavaScriptに目を止めると、JQueryの導入でJavaScriptの中でもプロトタイプ等の、より高度なモノを目にします。JavaScript言語は実はwindow.open()以上のものだったのです。
しかし、ウェブページを活気づけたいと感じる時は何時でもJQueryを使えることは素敵でしたけれども、まだこれはすべてフロントエンドのものでしかありません。つまるところあなたはJavaScriptの利用者として最高だったけれどもJavaScriptの開発者ではなかったのです。
そしてNode.jsがやって来ました。サーバ上のJavaScriptは何がクールなのでしょうか。
あなたは、古いのにさよならする時が来たと、新しいJavaScriptだと決めつけました。しかし待ってください。Node.jsアプリケーションの記述はそれらとは別物で、書き方を理解している必要があることを意味します。つまりJavaScriptへの理解が必要です。 今がその時です。
ここで以下の問題があります。JavaScriptは実際には2つ、おそらく3つ(90年代半ばからのおかしなDHMTLヘルパーや、jQueryとその同類のようなよりまじめなフロントエンドのもの、および現在のサーバサイド)の潮流があるので、あなたがJavaScriptを"正しい"方法で学ぶためにに役立つ情報を見つけることは容易ではありません。あまりJavaScriptを使用していない人がNode.jsアプリケーションを作成するためには、実際にそれを開発してみることです。
なぜならそれが分かりやすいからです。あなたは既に経験豊かな開発者なので、試行錯誤で新しいテクニックを学びたくないでしょう。あなたは正しい角度からアプローチしていることを確信したいはずです。
もちろん優れたドキュメントはあります。しかしドキュメント単体では、しばしば十分ではありません。必要なのはガイドです。
私の目標はあなたのためのガイドを提供することです。
幾人かは本当に優秀なJavaScript関係者がいるけれども、筆者はその中の一人ではありません。
筆者はまったくもって実際に前の段落で話したとおりの者です。筆者は、バックエンドのWebアプリケーションの開発について多少知っている、しかし筆者にとって"本物"のJavaScriptは新しいものだし、Node.js.もまだ新しいものです。筆者はつい最近のJavaScriptのより高度な側面のいくつかを学びました。筆者の経験は無いに等しいです。
これが"初心者からエキスパートになれる"の本でない理由です。これはむしろ"初心者から上級者初心者になれる"に近いです。
筆者の目論見が成功したなら、これはNode.jsを始めるときに筆者自身が欲しかったかった文書になります。
JavaScriptの最初の実装はブラウザ内に行われました。しかし、これは一側面でしかありません。これは言語でできることの一部が定義されていますが、この言語自体がどこまでできるかについてはあまり触れられていません。JavaScriptは"完全な"言語です。つまりあなたは様々な状況でそれを使用して、他の"完全な"言語で実現できることですべてを達成することができます。
Node.jsがもう一つの側面です。ブラウザの外で、バックエンドでJavaScriptコードを実行することができます。
バックエンドでJavaScriptを実行するためには、それは解釈され、そして実行される必要があります。Node.jsはGoogle Chromeが使用しているJavaScriptと同じランタイム環境である、GoogleのV8 VMを使ってこれを行ないます。
加えて、多くの有用なモジュールをNode.jsが提供するので、例えばコンソールに文字列を出力するもののようなのをゼロからすべてを記述する必要はありません。
Node.jsは2つの部分から成ります。それはランタイム環境とライブラリです。
これらを利用するためにはNode.jsをインストールする必要があります。インストールについては正式なインストール手順がありますので、そちらを参照して行って下さい。以下はインストールが完了しているものとして進めさせて頂きます。
それでは清水の舞台から飛び降りて、最初のNode.jsのアプリケーションを書いてみましょう。それは"Hello World"です。
お好みのエディタを開きhelloworld.jsというファイルを作成します。"Hello World"とSTDOUTに書きこむ事を欲し、そしてここでそうするのに必要なコードは以下の通りです。
console.log("Hello World");
ファイルを保存し、Node.jsを介して実行します。
node helloworld.js
これは、あなたのターミナルでHello Worldを出力するはずです。
ああ、確かにこんなものはつまらないよね?じゃあもうちょっとリアルなものを書いてみましょう。
シンプルだけどリアルにしよう:
もちろん、今、あなたはググったり何かハッキングすることにより、この目標を達成することもできました。しかし、ここではそうしたい訳ではありません。
さらに、我々は目標を達成するためだけに酷く基本的なコードを書くのは避けたい、だからエレガントで正しいこのコードは次のようになります。我々は意図的に、より複雑なNode.jsアプリケーションを構築するための感触を掴むために必要以上の抽象化を追加します。
アプリケーションを分析してみましょう。ユースケースを満たすためにどんな部分が実装される必要があるのでしょうか。
ちょっと脇道にそれて、PHPでこの積層を構築する方法について考えてみましょう。それはまさに典型的なセットアップとなり、mod_php5がインストールされたApache HTTPサーバになることは自明です。
つまり"我々が必要とする、Webページを提供しHTTP要求を受信できるようにすること"は、PHP内部で発生していないことを意味します。
もちろん、ノードでは少々異なります。Node.jsでは、まだアプリケーションを実装していないので、全体のHTTPサーバを実装します。実際には、WebアプリケーションとそのWebサーバは、基本的には同一です。
これは多くの作業に聞こえるかもしませんが、Node.jsではそうでないことがすぐに分かります。
今から我々の積層の最初の部分、HTTPサーバの実装を始めましょう。
筆者は最初の"本当"のNode.jsアプリケーションの開始時点に到達したとき、実際のコーディング方法についてだけでなく、コードをどのように整理するかについても疑問を感じました。
全てが1つのファイルにある必要がありますか?Node.jsでどのように基本的なHTTPサーバを書くか教えるWeb上のほとんどのチュートリアルは、全てのロジックを一つ処に持っています。コードの実装が、もし見渡せる以上の長さだったら?
分離したコードをモジュールに置くことによって、それぞれ異なる問題として扱う事が比較的簡単な事が分かりました。
これはあなたが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/を指定します。Webページは"Hello World"と言う表示をするはずです。
たいして興味深い事ではありませんが、ここで何をしているのか、そしてそれから我々のプロジェクトをどのように作るのかという質問が残っています。これからそれに答えていきます。
それでは次に、実際にここで何が起こっているのか分析してみましょう。
最初の行はNode.jstoに同梱されたhttpモジュールを要求(訳注:require)し、それを変数httpを介してアクセス可能にします。
その後、httpモジュールが提供する機能の一つ、createServerを呼び出します。この関数はオブジェクトを返し、そのオブジェクトはlistenという名前のメソッドを持ち、私達のサーバがリッスン(訳注:listen)しようとしているポート番号を示す数値を取ります。
http.createServerの開き括弧に続く2番目の関数定義は無視してください。
以下のようにして、サーバを起動して8888ポートでリッスンするコードを記述することができます。
var http = require("http");
var server = http.createServer();
server.listen(8888);
それは8888ポートをリッスンするHTTPサーバを開始し、そして何もしません(全ての要求着信に対して応答しない)。
真に興味深い(そして、あなたの背景がPHPのようなより保守的な言語にあると奇妙に見える)部分は、createServer()の呼出の最初のパラメータがあると期待する部分です。
結局、この関数定義は、createServer()コールに与えている最初の(そして唯一の)パラメータです。なぜならJavaScriptでは、関数を他の任意の値と同じように渡すことができるからです。
例えば次のような事を行うことができます。
function say(word) {
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
注意深く読んでください!ここで何をしているかというと、関数sayをexecute関数への最初のパラメータとして渡します。sayの戻り値はありませんが、それ自身がしゃべります!(訳注:say itself!)。
したがって、sayはexecute内でローカル変数someFunctionとなり、executeはsomeFunction()(括弧を追加)を発行することによって、この変数で関数を呼び出すことができます。
もちろん、sayはパラメータを取れるようにでき、sameFunctionを呼ぶときにexecuteにそのパラメータを渡すことができます。
既にやったように、その名前によって別の関数へパラメータとして関数を渡すことができます。しかしまず定義してからそれを渡すという間接的な方法を取る必要はありません。 他の関数内で関数定義とその関数へのパラメータ渡しを行う事ができます。
function execute(someFunction, value) {
someFunction(value);
}
execute(function(word){ console.log(word) }, "Hello");
execute呼び出しの最初のパラメータの所でexecuteに渡す関数を定義します。
この方法では、関数名まで与える必要はありません。それは無名関数と呼ばれています。
これは筆者が"高度な"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はイベントドリブンで動作するという事実です。ええ、はい、筆者は、その正確な意味は知りません。しかし、Node.js.のWebベースアプリケーションを作成する我々にとってそれがなぜ理にかなっているか説明を試みてみます。
http.createServerメソッドを呼び出すとときは、もちろんそれはいくつかのポートでリッスンするだけのサーバーを持ちたい訳ではなく、このサーバへのHTTPリクエストがあるときに何かがやりたい訳です。
問題はこれが次のように非同期に発生することです。それは任意の時点で発生しますが、サーバはサーバが走っている単一のプロセスしか持っていません。
PHPアプリケーションを書くときには、これにには全く悩まされません。HTTP要求を受信するたびにWebサーバ(通常はApache)はこの要求に対して新しいプロセスをフォークさせ、ゼロから対応する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でイベント駆動が動きました :-)
(注意:サーバはおそらくブラウザがページを開くときにSTDOUTに2回"Request received."と書き込むでしょう。ほとんどのブラウザではhttp://localhost:8888/を開くたびに、http://localhost:8888/favicon.icoを要求してfaviconをロードしようと試みるからです)
それではまずサーバーコードの残りの部分、つまりコールバック関数のonRequest()を分析してみましょう。
コールバックが発火しonRequest()関数がトリガされると、二つのパラメータがそれに渡されます。requestとresponseです。
それらはオブジェクトであり、発生したHTTPリクエストの詳細を処理するためや、要求に応答するためにそれらのメソッドを使うことができます。 (すなわち、サーバに要求したブラウザに書き出す為に実際に何かを送信する)。
そしてそのコードは以下の事をします。。 要求が受信されるたびに、それはHTTP応答ヘッダのHTTPステータス200とcontent-typeをおくるためにresponse.writeHead()関数を使い、そしてHTTP応答ボディに"Hello World"というテキストを送るためにResponse.Write()関数を使います。
最後に、Response.End()を実際に応答を完了するために呼び出します。
この時点では、なぜ全部にrequestオブジェクトを使わないかという、要求の詳細については気にしないことにします。
上で約束したとおりアプリケーションの整理方法に戻ります。server.jsファイルに非常に基礎的なHTTPサーバのためのコードを持っていて、そしてそれがブートストラップやそのアプリケーションの他のモジュールを使用する(server.jsにあるHTTPサーバモジュールのような)アプリケーションを開始するために使われる、index.jsというメインファイルから呼ばれるのが一般的であると述べました。
index.jsメインファイルで使用できる、いまだ記述されてない実際のNode.jsモジュールserver.jsを作成する方法について説明しましょう。
すでにお気づきかもしれませんが、我々はすでに以下のようにして我々のコードでモジュールを使用しました。
var http = require("http");
...
http.createServer(...);
Node.js内のどこかに"http"と呼ばれるモジュールがあって、requireした結果をローカル変数に割り当てることによって、それを私達が書くコードで利用することができます。
これはローカル変数をhttpモジュールが提供するすべてのパブリックメソッドを実行するオブジェクトにします。
ローカル変数の名前としてモジュールの名前を選択するのが一般的ですが、任意の名前にしても構いません。
var foo = require("http");
...
foo.createServer(...);
内部Node.jsモジュールを利用できるようにする方法が明らかになりました。我々独自のモジュールはどのように作り、どのように使えばいいのでしょうか?
実際にモジュール内にserver.jsスクリプトを入れてみましょう。
ほとんど変更する必要が無いことが分かります。いくつかのコードをモジュールにするということは、それらのモジュールを必要とするスクリプトに、提供したい機能の部品をexportする必要があることを意味します。
今のところ、我々のHTTPサーバがエクスポートする必要のある機能は単純です。サーバーモジュールを必要とするスクリプトは、単純にサーバを起動する必要があります。
これを可能にするために、startという名前の関数に我々のサーバーのコードを配置し、その関数をエクスポートします。
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)し、それを変数に割り当てることによって、そのエクスポートした関数は我々に利用できるようになります。
それだけです。いまやメインのスクリプトを介してアプリを起動することができます、そして、それは以前と正確に同じ動きをします。
node index.js
すばらしい。今やアプリケーションの異なる部品を異なるファイルに置くことができ、それらをモジュールにすることによって連携させることができようになりました。
まだ我々アプリケーションの極々最初の部品しかありません。それはHTTPリクエストを受け取ることができます。 しかし、それらで何かを動作させる必要があります。そうです、我々のサーバーはブラウザから要求されたURLに応じて異なる反応する必要があるのです。
非常にシンプルなアプリケーションでは、コールバック関数のonRequest()の内部で直接これを行うことができます。しかし、言ったように、例であるアプリケーションをもうちょっと面白くするために、もうちょっと抽象化を追加してみましょう。
コードの異なる部分で作る、別のHTTP要求のポイントは、"ルーティング"(訳注:routing)と呼ばれています。よろしい、ならばrouterというモジュールを作りましょう。
ルータでは要求されたURLと追加可能なGET・POSTパラメータを供給できる必要があり、それらのルータに基づいて、実行するコードを決定できるようにする必要があります。(この"実行するコード"は我々のアプリケーションの3つ目の部品です。それは要求を受信したときに実際の作業を行う要求ハンドラのコレクションです)。
そう、HTTP要求を調べ、要求されたURLを抽出するだけでなく、GET/POSTパラメータを抽出する必要があります。これがルータの一部あるいはサーバの一部(あるいは独自のモジュール)であるかは議論の余地がありますが、今回はサーバの一部として作成することに同意したとしましょう。
必要なすべての情報は、コールバック関数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"]
もちろん、後述するように、パラメータのPOST要求の本体を解析(訳注:parse)するのにもquerystringを使う事ができます。
そして、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パスに基づいて、要求ハンドラに要求を割り当てることができます。
アプリケーションのコンテキストで、それは単純に我々が我々のコードの異なる部品で処理されるURL達、/startと/uploadいう要求をを持つことができることを意味します。我々はすべてがすぐに一緒に適合する方法を説明します。
よろしい、それでは実際にルータを書く時です。以下の内容でrouter.jsと呼ばれる新しいファイルを作成します。
function route(pathname) {
console.log("About to route a request for " + pathname);
}
exports.route = route;
もちろん、このコードは基本的には何もしませんが、今はそれで構いません。最初に、ルータに複数のロジックを入れる前に、サーバーとこのルータを連携させる方法を見てみましょう。
HTTPサーバはルータについて知っている必要があり、そしてルータを利用する必要があります。サーバ内にこの依存関係をハードコーディングすることもできますが、他のプログラミング言語の経験からそれは酷いやり方だと学んだので、疎結合するサーバと、この依存関係を注入したルータとにしています。(Martin Fowlers excellent post on Dependency Injection(訳注:Martin Flowerの素晴らしい投稿「依存性の注入」)を読むことをお勧めします)。
最初に、route関数によって使われるパラメータを渡すために、サーバの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;
そして、サーバー内にルータのroute関数を注入するためにindex.jsを拡張しましょう。
var server = require("./server");
var router = require("./router");
server.start(router.route);
再び、今やおなじみの関数を渡しています。
今、プリケーションを(いつものようにnode index.jsとして)起動して、そしてURLを要求すると、 HTTPサーバがルータを利用して、要求したパス名をアプリケーションが出力するのを確認できます。
bash$ node index.js
Request for /foo received.
About to route a request for /foo
(上記は/favicon.icoを要求するのが目的では無いので、それ関連の出力は省略しています)。
またちょっと脱線して、再び関数型プログラミングについて話させてください。
関数を渡すことだけが技術的な考慮事項ではありません。ソフトウェアの設計というのは多分に哲学的なものです。それについて考えると、indexファイルでは、サーバにルータオブジェクトを渡すこともできましたし、サーバがこのオブジェクトのroute関数を呼ぶ事もできました。
この方法では、物を渡すと、サーバが何かするというふうにしていました。やぁ、ルータを私に配送してくれる?
しかし、サーバは物を必要としません。それは何かをするために必要なだけで、何かをする事では全くモノは必要なく、必要なのはactionです。名詞は必要としない、必要とするのは動詞です 。
この考えの核となる根本的な発想の転換(訳注:mind-shift)の理解は、筆者が実際に関数型プログラミングを理解して得られたものです。
Steve Yeggeの傑作名詞の王国の実行を読んでいる時に筆者はそれを理解しました。あなたも今すぐ読むべきです。それは筆者が遭遇したもっとも素晴らしい、最高のソフトウェ関連書物の一つです。
実務に戻ります。HTTPサーバと要求ルータは、今や最高の友人であり、意図したとおりお互いに会話します。
もちろん、それだけでは十分ではありません。"ルーティング"とは、異なるURLリクエストで異なる処理をして欲しいと言うことを意味しています。/uploadへの要求よりも、別の関数内で処理される/startへの要求の為の"ビジネスロジック"を持ちたいのです。
ここで、ルータのルーティングの"末尾"は、我々アプリケーションがより複雑になるとそれをうまく拡大できなくなるため、実際にその要求の何かを"行う"ための場所ではありません。
要求ハンドラにルーティングされているこれらの関数を呼び出してみましょう。この場所にない限り、今、そのルータに何かするのはあまり意味がないため、その次を片付けましょう。
いわずもがな、新しいアプリケーションの部品、新しいモジュールです。requestHandlersと呼ばれるモジュールを作成し、そして全ての要求トハンドラのプレースホルダの処理を追加し、それらをモジュールのメソッドとしてエクスポートしてみましょう。
function start() {
console.log("Request handler 'start' was called.");
}
function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
これはルータ内の要求トハンドラとの連携を許し、ルータに何かrouteを与えます。
この時点で意思決定を行う必要があります。ルータ内でrequestHandlersモジュールをハードコーディングするか、あるいはもう少し依存性注入をしたいかです。依存性の注入が、他のすべてのパターンと同様に、それを使用するのためにのみ使用すべきではないこのケースでは、疎結合ルータとその要求ハンドラが理にかなっているため、ルータを実際に再利用可能にします。
これはルータにサーバからの要求ハンドラを渡す必要があるということを意味しますが、それはさらに間違っているように感じます。なぜメインファイルからサーバに全部渡し、そしてそこからルータへ渡すのか。
どのようにそれらを渡すつもりなのか?今は2つのハンドラを持っていますが、実際のアプリケーションではこの数は増加し変化していきます。そして新しいURL/リクエストハンドラが追加されるたびにルータ内でハンドラへの要求の割り当てをいじくりまわしたくは無いです。そしてルータで幾つものif request == x then call handler y を持つのは酷く醜くなってゆきます。
膨大な数のアイテムを、それぞれの文字列(リクエストされたURL)に割り当てられますか?うん、それには連想配列が完璧に当てはまるように思える。
この考えは、JavaScriptは全く連想配列を提供しないという事実に失望します。果たしてそうでしょうか?結論から言うと、連想配列が必要な場合オブジェクトを使用できます。
で、これについての素晴らしい紹介がこれhttp://msdn.microsoft.com/en-us/magazine/cc163419.aspxで 、以下に関連する部分を引用させてください。
C++やC#では、オブジェクトについて話しているときには、クラスや構造体のインスタンスを参照しています。オブジェクトは、インスタンス化されるそれらのテンプレート(つまり、クラス)に応じて、さまざまなプロパティとメソッドを持ちます。JavaScriptのオブジェクの場合そうではない。JavaScriptでは、オブジェクトは名前/値のペアの単なるコレクションです。つまり文字列のキーを持つ辞書としてJavaScriptオブジェクトを考えてください。
JavaScriptオブジェクトは、名前/値のペアのコレクションである場合、どのようにメソッドを持つことができるのでしょうか?その値は文字列や数値等にできますし、そして関数にもできるのです!
我々はやっとにコードに戻ってきました。route()内にこのオブジェクトを注入するする疎結合を実現するために、オブジェクトとしてrequestHandlerのリストを渡します。
メインのファイルの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;
start()関数へのhandleパラメータを追加し、その最初のパラメータとして、 ハンドルのオブジェクトをroute()コールバックへ渡しました。
router.jsファイルで、それに応じてroute()関数を変更しましょう。
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[pathname]();という表現により以前話した通り、連想配列の要素にアクセスできたのと同様にオブジェクトから要求トハンドラ関数にアクセスできます。 "このpathnameをhandleしてください。"(訳注:handle=処理)
ええ、これがサーバとルータおよび要求ハンドラを連携させるために必要なすべてです!アプリケーションを起動し、ブラウザで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"を生成します。
これがなぜ問題なのでしょう?一言でいうと、要求ハンドラのいずれかが非ブロック操作を使用したら問題に直面するからです。
以下、詳しい解説です。
すでに述べたとおり、問題は要求ハンドラに非ブロック操作を含めたときに発生します。しかし、ここでは非ブロック操作については後でお話することとし、まずは操作をブロックすることについてお話ししましょう。
"ブロック"と"非ブロック"の意味の説明を試みる代わりに、要求ハンドラへブロック操作を追加するとどうなるか実証してみましょう。
これを行うには、"Hello Start"という文字列を返す前に10秒待機させるようにstart要求ハンドラを変更します。JavaScriptにはsleep()ようなものが存在しないので、そのための巧妙なハックを使用します。
次のように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()を呼び出すときに、以前と同様に、すぐに返ります。
(もちろん、10秒間スリープではなくて、start()で、長時間の計算を行ういくつかの並べ替えのような、リアルなブロックが発生すると想定してください。)
この変更が何をするか見てみましょう。
いつものようにサーバを再起動する必要があります。今回は、何が起こるか見るためにちょっと複雑な"手順"に従ってください。 最初に、2つのブラウザウィンドウやタブを開きます。 最初のブラウザウィンドウで、http://localhost:8888/startをアドレスバーに入力してください。でもまだこのURLを開かないように!
2番目のブラウザウィンドウのアドレスバーに、http://localhost:8888/uploadを入力、そして再び、ここでもまだエンターを押下しないでください。
そして次の通り操作してください。最初のウィンドウ("/start")でエンターキーを押し、その後すぐに2番目のウィンドウ("/upload")に変更してエンターキーを押します。
以下の事に気付くことでしょう。/startのURLは、我々が期待するように、ロードに10秒かかります。しかし/uploadのURLもまたロードに10秒掛かります。対応するリクエストハンドラにはsleep()が無いのに!
なぜ?なぜならstart()はブロック操作を含んでいるからです。"それは作業している以外のすべてをブロックしている"ようです。
そしてこれが問題です。なぜなら"nodeではあなたのコード以外は全て並列に走る"と言われる通りだからです。
どういう意味かというと、Node.jsは大量の並列処理ができますが、それは全てをスレッドに分割することによって行うのではありません。事実、Node.jsはシングルスレッドです。その代わりに、それはイベントループを走らせることで行われ、開発者はこれを使うことができる。よって可能な限りブロック操作を回避し、代わりに非ブロック操作を使用すべきです。
しかし、そのような何か時間がかかるような(例えば、10秒間スリープしたり、データベースを照会したり、いくつかの高価な計算を行うような)ことをするには、別の関数を関数に渡すコールバックを利用する必要があります。
その方法はこう言われています。やぁ。probablyExpensiveFunction()さん、自分のを実行してください。 しかし私こと、単一のNode.jsスレッドはprobablyExpensiveFunction()さんが終了するまでここにとどまるつもりはありません。probablyExpensiveFunction()さん以降のコードの実行を続けます。 だからprobablyExpensiveFunction()さんはここではcallbackFunction()を使って、probablyExpensiveFunction()さんの高価なのの実行を終了した時に呼び出します。ではごきげんよう!
(詳細は、 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")ためにそれを使用して、/startのURLを要求しているユーザーのブラウザにこのリストを表示できるようにするつもりです。
そのコードがすることは簡単です。 新しい変数content("空"の初期値を持つ)を作成し、"ls -lah"を実行し、その結果を変数に入れて、それを返します。
いつものように、アプリケーションを起動し、そしてhttp://localhost:8888/startを訪れます。
これは空っぽのWebページを表示します。一体何が間違っているのだろうか?
ええ、あなたがすでに推測しているであろうとおり、exec()は非ブロックな方法でその魔法を行います。それは良いことです。sleepのやらかしたブロック操作のようにアプリケーションを完全停止させる事無く、非常に高価なシェル操作(例えば巨大なファイル類や類似のもののコピーのような)を実行できます。
(このことを証明したい場合は、"ls -lah"を"find/"のようなより高価な操作で置き換えてください)。
しかしブラウザがその結果を表示しないのでは、まったくもって非ブロック操作には満足しようもないですよね?
それじゃ、これから、それを修正しましょう。そして、上記のようにしている間は現行のアーキテクチャが機能しない理由を理解しようと試みてみましょう。
問題はexec()内にあり、非ブロック操作を動作させるにはコールバック関数を利用しなければなりません。
この例では、それはexec()関数呼び出しで2番目のパラメータとして渡される無名関数です。
function (error, stdout, stderr) {
content = stdout;
}
そしてここに問題の根があります。 コードは同時に実行されます。その意味はexec()の呼び出し直後に実行されるということで、Node.jsは続けて行return contentを実行します。この時点では、contentはまだ"空"で、実際にはexec()に渡すコールバック関数はまだ呼ばれていません。なぜならexec()操作は非同期だからです。
"ls -lah"は(ディレクトリ内のファイルが何百万もなければ)非常に安価で高速な操作です。ここではそのコールバックは比較的迅速処理されます。けれどもそれは非同期に発生します。
より高価なコマンドについて考察することで、これはより明白になります。 "find /"は筆者のマシンで1分程度かかりますが、要求ハンドラで、"ls -lah"を"find /"と置き換えた場合、/start URLを開くと、いまだすぐにHTTPレスポンスを受け取ります。 Node.js自身がアプリケーションを続行する間exec()がバックグラウンドで何かをすることは明らかで、"find /"の実行が終了した時だけexec()に渡されるコールバック関数が呼び出されると仮定してよい。
しかし、どのようにすればユーザーに現在のディレクトリ内のファイルの一覧を表示するという目標というを達成することができるのでしょうか?
それでは、それをしない方法を学習した後、どのようにして要求ハンドラをブラウザの要求に正しい方法で応答するさせるようにするか説明しましょう。
筆者は"正しい方法"というフレーズを使用しました。それは危険な言葉です。しばしば、"正しい方法"は一つではありません。
しかし、このためのしばしば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オブジェクトを渡します。更に加えて、今やその世話をrouteに期待しているので、onRequest()ハンドラから全てのresponseメソッドの呼び出しを削除した。
お次は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;
同一のパターンです。要求ハンドラからの戻り値の代わりに、respondオブジェクトを渡します。
要求ハンドラが使用できない場合は、適切な"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{/9}の要求をブロックしない事を証明したいなら、requestHandler.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への要求は直ちに応答があります。
今やっているのは大変結構な事ですが、受賞歴のあるウェブサイトの顧客のための価値は何も作り出していません。
サーバとルータと要求ハンドラは同じ場所にあります。したがってブラウザ内で、ユーザ対話やファイル選択やファイルアップロードやアップロードしたファイルの閲覧のユースケースを辿る事を許すサイトに、コンテンツを追加し始める事ができます。単純化のために、イメージファイルのみのアプリケーションを通じてアップロードし表示しようとしていると仮定します。
ではステップバイステップでやりましょう。しかしJavaScriptの技法と原則のほとんどは既に説明済ですので、同じ事の繰り返しについては少々短縮します。著者と言うのは語り過ぎるきらいがありますので。
ここでのステップバイステップとは、大まかに2つのステップを意味する。最初のPOSTリクエストの着信(しかしファイルアップロードではない)を処理する方法を見て、そして第二のステップでファイルアップロード処理のための外部Node.jsのモジュールを利用します。 二つの理由からこのアプローチを選択しました。
最初に、基本的なPOSTリクエストを処理するNode.jsは比較的簡単で、それを行使するのに十分か教えてくれます。
次に、Node.jsでファイルのアップロード(すなわち、マルチパートPOSTリクエスト)を処理するのは単純ではないためこのチュートリアルの範囲を超えていますが、外部モジュールを使用すること自体は初心者向けチュートリアルに含まれ、理にかなったレッスンになっています。
この平凡なシンプルさを保ちましょう。ユーザによって、入力ができPOSTリクエストでサーバーに送信できる、テキストエリアを表示します。要求の受信と処理時にテキストエリアの内容を表示します。
このテキストエリアのフォームのHTMLは、/start要求ハンドラによって提供される必要があるので、ここではファイルのrequestHandlers.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の文脈で知っておく価値のある何かを教えて無いと思ったので、抽象化の余分なレベル(例えば、ビューとコントロールロジックの分離)を含めないことを決定した。
ユーザはこのフォームを送信するとき、POST要求の処理で我々の/upload要求ハンドラに触れる。
今や我々は初歩の専門家になっているので、もはや処理のPOSTデータが非同期コールバックを使用して、非ブロック方式で行われるという事実に驚かされていません。
POST要求は潜在的に非常に大きくなる可能性があるので、これは、理にかなっている - 数メガバイトサイズのテキストを入力でもユーザーを待たせません。一括でデータの全体の大半を処理すると、ブロック操作になるでしょう。
全体のプロセスを非ブロックで行うために、Node.jsはPOSTデータをチャンク(訳注:小さなかたまり)にして特定のイベントで呼び出せるコールバックを提供しています。これらのイベントは、 (POSTデータの新しいチャンクが到着したら)dataと(すべてのチャンクを受け取り済なら)endです。
これらのイベントが発生したときにコールバックするための関数群をNode.jsに教える必要があります。これは、HTTPリクエストが受信されるたびに、onRequestコールバックに渡されるequestオブジェクトにリスナーを追加することによって行われます。
これは基本的に次のようになります。
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オブジェクトでやったように、ルータおよび要求ハンドラにそれを渡すことはありません。
筆者の意見では、その仕事をする必要のある要求からのデータ全てをアプリケーションにに与える事はHTTPサーバの仕事です。したがって、我々はサーバでPOSTデータ処理し、 それをどうするかを決めることができるルータおよび要求ハンドラへの最終的なデータを、渡すことを示唆している。
このように、 そのアイディアはそのサーバにdataとendイベントのコールバックを置き、 dataのコールバックで全てのPOSTのデータのかたまりを収集し、 ルータへで収集されたデータのチャンクを渡しながら、このように、アイデアはこれで、 データのコールバックですべてのPOSTデータのチャンクを収集、および終了イベントを受信したときにルータをコールし、サーバー内のデータとエンドのイベントコールバックを置くことですターンは、要求ハンドラに渡します。
ここでは、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データの新しいチャンクが到着するたびに新しいPOSTデータの変数を埋めるステップバイステップで"data"イベントのイベントリスナーを追加し、ルータへのコールはすべてのPOSTデータが収集されるときにのみ呼び出されたかどうかを確認するendイベントのコールバックに移動。 要求ハンドラでそれを必要としているので、ルータにPOSTデータを渡す。
受信される全てのチャンクをコンソールログに出力することは多分製品コードでは良くない考えですが(POSTデータがメガバイトあることを思い出してください)、何が起こっているかは分かります。
私はこれとちょっと付き合う事を提案します。大量のテキストと同様に少量のテキストをtextareaに入れると、 textareaのテキストと同様のロットに少量のテキストを入れて、そして、より大きなテキストのために、 dataのコールバックは実際に複数回呼び出されていることがわかります。
アプリにもっとすごい追加してみましょう。/uploadページで受信したコンテンツを表示します。これを可能にするために、router.jsで要求ハンドラへpostDataを渡す必要があります。
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;
そしてrequestHandlers.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年代に戻ればこれは株式公開できるビジネスモデルとしての資格があるだろうが、今日それに十分である為には以下の二つのことを学ぶ必要があります。 外部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.2
npm ok
上記ならば、うまく行っています。
formidableモジュールは今や使用可能です。コードで行う必要があるのは、以前に使用した最初から組み込みのモジュールののようにそれをrequireすることだけです。
var formidable = require("formidable");
formidableの使用はNode.jsでHTTP POST経由で送信されるフォームを解析可能にします。やらなければいけないことは新しい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で実行する場合、 ファイルのアップロードを含む簡単なフォームを送信することができます。そしてfileオブジェクトがどのようにform.parse呼び出しで定義されたコールバックに渡されるかを見ます。以下のように構造化されています。
アップロード受信は以下です。
{ 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と呼ばれています。
/tmp/test.pngファイルの内容の表示をハードコーディングして、URL /showのための別の要求ハンドラを追加してみましょう。それはもちろん、最初にこの場所へ実際の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;
また、index.jsファイルでURL/showに新しい要求ハンドラを割り当てる必要があります。
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は簡単です。HTMLフォームへmultipart/form-dataタイプのエンコーディングを追加し、textareaを削除し、ファイルアップロード入力フィールドを追加し、submitボタンのテキストを"Upload file"に変更する必要がある。今ここでファイル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から始めましょう- POSTデータのハンドリングと(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画像がアップロードされることを前提としています。
今のところfs.renameSync(path1, path2)がその仕事を行います。気をつけて下さいね!名前が示すように、それは同期動作する。したがって名前の変更操作が高価で時間がかかるならば、それはブロック操作につながります。我々は全員大人であり、自分やっていることがわかっていると同意しているものとします。
ファイル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");
fs.renameSync(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要求の処理方法のような単純ないくつかの事。
しかし、それは初心者向けのすべての本のs宿命で、ひとつひとつの細部のあらゆる局面について話すことはできません。
良いニュースは、Node.jsのコミュニティはめちゃめちゃ活気に満ちて(いい意味で、カフェインを服用した注意欠陥・多動性障害児達のようだと思う)(訳注:ADHD kids)いて、質問に答えてもらうためにそこに多くのリソース、および多くの場所があることを意味する。Node.js community wikiと the NodeCloud directoryは、おそらくより多くの情報のための最高の出発点です(訳注:もちろん英語)。
完璧な導入に加え、つのバンドルでは完璧なリファレンス!
LeanBundleは、現在の最終版を提供しています
ノードの初心者ブック
プラスペドロテイシェイラの優れた
ハンズオンNode.jsのための唯一の
765 円
(正規価格 10.98ドル )
合計で226ページ
PDF、EPUB&MOBI
直接ダウンロード
無料のアップデート
あなたが唯一のノード初級の本を購入したい場合、それはでたったの$ 4.99利用可能ですhttp://Leanpub.com/NodeBeginner 。
"これは、ノードへの素晴らしい入門書です。"
ライアンダール、Node.jsの生みの親
"私はnodebeginner.orgを愛して - 、簡潔なポイントに直接と読むことさえ楽しい。"
Gojko Adzic、 例によって仕様の作成 者とのコミュニケーションのギャップを埋める
"これは私が読んだ最高のチュートリアルの一つです。前者のJavaプログラマとして、私はいつも黒魔術になるにはJavaScriptを見つけたが、あなたは本当にこのチュートリアルで物事を単純化している。"
コメントからアースキン、
"これは私がそれを通るので、それが書かれているどれだけのすべての方法で作られたいくつかの初心者の記事のひとつです。"
コメントのPaul Gibler、
"かけがえのない。"
Twitterで@ lecolibrilibre、
"私はちょうどあなたのノードにそのような優れた入門書を書いてくれてありがとうと言ってノートを落としていました。あなたの本の説明は素晴らしいです、そしてあなたがそれを終了するのを私は待つことができない!"
電子メールによるセスマクラフリン、
The Node Beginner Book by Manuel Kiessling is licensed under a
Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
Permissions beyond the scope of this license may be available at manuel@kiessling.net.