![]() Read this tutorial in english
|
![]() Lee este tutorial en Español
|
![]() 이 튜토리얼을 한글로 보세요
|
![]() 阅读本书中文版
|
![]() 閱讀本書繁體中文版
|
![]() Читать этот учебник на русском
|
![]() Đọc bằng tiếng Việt
|
console.log("Hello World");ファイルを保存して、Node.jsから動かしてみます:
node helloworld.jsこれで貴方の端末で、Hello Worldと出力されたはずです。 いやあ、退屈ですね。次はもっと本物っぽいものを書いてみましょう。
var http = require("http");これだけです!これで実際に動くHTTPサーバが書けました。 実行して、ちゃんと動くかテストしてみましょう。 まず、このスクリプトをNode.jsから実行します:
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
node server.jsそれでは、ブラウザを開いて http://localhost:8888/ にアクセスしてみましょう。 ”Hello World”と表示しているWebページが表示されるはずです。 これは、とても気になるのではないでしょうか。我々のプロジェクトをどう構成していくのか、 というテーマを少し離れて、この部分について深堀してみませんか。すぐに本題に戻ることを約束します。
var http = require("http");これでHTTPサーバが8888番ポートで待ち受けをして、その他、何もすることはありません。 (たとえリクエストがきても応答さえしません) (もしPHPのような、より保守的な言語の経験者であれば特に)面白いのは、 createServer()の呼出しの第1引数に関数定義があることではないでしょうか。 この関数の定義は、createServer()を呼び出す際に渡す最初の (そして最後の)引数なのです。 JavaScriptでは、関数をその他の値と同様、あちこちに渡すことができるのです。
var server = http.createServer();
server.listen(8888);
function say(word) {よく読んでみて下さい!ここでは、関数sayをexecute関数の第1引数として渡しています。 sayの戻り値ではなく、say自体を渡しているのです! ここではsayが、関数executeの中にあるローカル変数 someFunctionになっています。 そしてexecuteの中では(この変数に括弧をつけて)someFunction() と書くことができるのです。 もちろんsayは引数を1つとるので、 executeからsomeFunctionを呼び出す時には、 さらに引数を渡すことができます。 このように、関数は、他の関数への引数として名前で渡すことができますが、 何も関数の定義を先にしたうえで、それを渡す、といった手順を踏まなくても良いのです。 関数は、関数を引数として渡す際、その場で定義しても良いのです:
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
function execute(someFunction, value) {まさにexecuteが第1引数を期待している場所で、execute に渡したい関数を定義しています。 ここで見たやり方をすると、関数に名前をつけてやる必要がありません。 このような関数を匿名関数(anonymous function)と呼びます。 これは”上級の”JavaScriptと呼んでいるものの最たる例ですが、 ステップバイステップでやっていきましょう。 今のところ、JavaScriptでは関数を呼び出す時の引数として関数を渡すことができる、 ということを受け入れて下さい。 あらかじめ定義した関数を変数として割り当ててから渡すこともできますし、 その場で関数を定義して渡すこともできるのです。
someFunction(value);
}
execute(function(word){ console.log(word) }, "Hello");
var http = require("http");もうここで何をしているのかは明確なはずです: createServer関数に匿名関数として渡しているのです。 先のコードをリファクタリングすると、こうすることができます:
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
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);
var http = require("http");onRequest関数(コールバック関数)が呼ばれた時、 テキストをconsole.logで出力していることに注目して下さい。 その他のテキストはHTTPサーバ開始直後に出力しています。 これを起動した時(先ほどと同様、node server.js)、 すぐに”Server has started.”とコマンドライン上で出力されると思います。 (http://localhost:8888を開いて) サーバにリクエストを送った時、 ”Request received.”というメッセージがコマンドライン上に出力されます。 イベント駆動の非同期サーバサイドJavaScriptでのコールバックはうまく動くことがわかりましたね :-) (おそらくサーバは標準出力に、”Request received.”を2回出力したのではないでしょうか。 これはあなたがhttp://localhost:8888/ を開いた時、 ブラウザがfaviconをロードしようとしているためです。)
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.");
var http = require("http");Node.jsのどこかに”http”と呼ばれるモジュールがあるので、 コード内でrequireを行い、その結果をローカル変数に割り当てることでそれを使うことができます。 これによりローカル変数にはオブジェクトが割り当てられ、 httpモジュールが提供する全てのパブリックメソッドを使うことができます。 ローカル変数名にモジュール名をつけるのが慣習ですが、 このように好きな名前を付けることもできます:
...
http.createServer(...);
var foo = require("http");Node.js内部のモジュールの使い方はわかりました。では自分でモジュールを作り、 それを使うためにはどのようにしたら良いのでしょうか。 server.jsスクリプトをモジュールとして作り直してみましょう。 そんなに変更する必要はありません。コードをモジュール化するためには、 それを必要とするモジュールのスクリプトに提供するように、 パーツが持っている機能をexportする必要があります。 今のところ、exportしたいHTTPサーバの機能は単純なものです: 我々のサーバモジュールは単にサーバを起動するだけのスクリプトです。 これを実現するには、サーバのコードをstartという関数の中に放り込み、 この関数をexportします:
...
foo.createServer(...);
var http = require("http");このようにすれば、サーバのコードがserver.jsファイルの中にある状態で、 メインファイルindex.jsを作り、そこでHTTPサーバを起動することができます。 以下の内容のindex.jsファイルを作成して下さい:
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;
var server = require("./server");すぐおわかりだと思いますが、サーバモジュールを他の内部モジュールと同じように使うことができます: そのファイルをrequireし、変数に割り当てれば、exportされた関数を使うことができるのです。 これだけです。では我々のアプリをメインスクリプトから起動してみましょう。 この部分は今までと同じです:
server.start();
node index.js素晴らしいですね。モジュールを作ることで、アプリケーションの特定の部分を別のファイルに移し、 再度つなぎ合わせることができました。 アプリケーションの最初の部分、HTTPリクエストを受け付ける、という部分しか残っていません。 ただ、もう少しだけやりたいことがあります – ブラウザからリクエストのあったURLによって応答のしかたを変えたいですね。 単純なアプリケーションとしては、onRequest() コールバック関数の中で直接やることもできます。 しかし先ほど述べた通り、もう少しだけ抽象レイヤーを追加して、 サンプルアプリケーションを面白くしてみましょう。 異なるHTTPリクエストに応じて行き先となるコードを変えることを”ルーティング”と呼びます – では、routerと名付けてモジュールを作っていきましょう。
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");はい。これで我々のアプリケーションはリクエストされたURLパスによって、 そのリクエストを区別することができます – これでリクエストを、(これから書くことになる)ルータを使ってURLパスに基づく リクエストハンドラにマッピングすることができます。 アプリケーションのコンテキストにおいて、 /startというURLに対するリクエストと、 /uploadというURLに対するリクエストは、 それぞれ別のコードに渡すことができるわけです。 OK、実際のルータのコードを書いてみましょう。 router.jsというファイルを作成し、 中身を以下の通り書いて下さい:
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;
function route(pathname) {もちろん、このコード自体は何もしませんが、 今のところそれでOKです。色々なロジックをルータの中に書いていく前に、 どうやってこのルータをサーバと連携させるのかを見ていきましょう。 我々のHTTPサーバは、ルータのことを知り、利用する必要があります。 依存関係をサーバから密に結合させることもできますが、 様々なプログラミング言語での苦い経験からもわかるとおり、 サーバとルータの相互依存性としては粗に結合させるべきでしょう (Dependency Injection: 依存性の注入。 背景については Martin Fowlerによる素晴らしいエントリ を読むと良いでしょう) 最初にサーバのstart()関数を拡張して、 ルーティングのための関数を引数で指定して使えるようにしましょう:
console.log("About to route a request for " + pathname);
}
exports.route = route;
var http = require("http");そしてindex.jsも拡張しましょう。ルーティングのための関数をサーバに組み込みます:
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;
var server = require("./server");また出てきました。関数を渡していますね。これはもう目新しいことではないはずです。 ここで(node indes.jsとして)アプリケーションを起動してURLへリクエストを送ったら、 HTTPサーバがルータを使用している様子、それからリクエストされたパス名が渡されている様子が、 アプリケーションの出力内容からわかるはずです:
var router = require("./router");
server.start(router.route);
bash$ node index.js Request for /foo received. About to route a request for /foo(紛らわしいので/favicon.icoへのリクエストは省略しています)
function start() {こうして、ルータにルーティングする先のものを渡すことで、 リクエストハンドラとルータを繋ぎ合わせることができます。 この時点で私たちは決断をする必要があります: リクエストハンドラのモジュールはルータの中にハードコーディングするのか、 もしくは少しだけ依存性の注入を行うのか。 依存性の注入は、他のパターンも同様ですが、使いたいというだけで使うべきではありません。 ただしこのケースでは、 ルータとリクエストハンドラを粗に結合することでルータの再利用性を高められるという意味がしっかりあります。 つまり、リクエストハンドラはサーバからルータに渡す必要があることを意味していますが、 これはまだ間違っているような感じがします。ちゃんと説明するとしたら、 メインファイルからサーバに渡し、そこからさらにルータに渡す、ということになります。 ではどうやってそれを渡すのでしょうか。今現在、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に配置してしまいましょう:
console.log("Request handler 'start' was called.");
}
function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
var server = require("./server");handleは、いわば”モノ”(リクエストハンドラの集合)ではありますが、 動詞を使って命名したいと思います。次に出てくるルータのところで、 より自然な表現をすることができるからです。 おわかりの通り、 異なるURLを同じリクエストハンドラにマッピングする箇所がとてもシンプルになっています: “/”とrequestHandlers.startというキーと値のペアを追加することで、 /startへのリクエストだけでなく/へのアクセスも同じstartハンドラに渡す処理が、 とても美しくあざやかに実現できていますね。 オブジェクトが定義できたら、 それを追加の引数としてサーバに渡します。 server.jsを修正して実際に使えるようにしましょう:
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);
var http = require("http");引数handleをstart()関数に追加して、 ハンドルオブジェクトをroute()コールバックの第一引数として渡すコードに修正しました。 route()関数も同様に変更しましょう。router.jsファイルは以下のよう修正します‥
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;
function route(handle, pathname) {ここで何をしているかというと、パス名に対応するリクエストハンドラが存在するかどうかをチェックし、 もし存在する場合は、紐付けられた関数を単に呼び出しています。 連想配列の要素にアクセスすればオブジェクト経由でリクエストハンドラ関数にアクセスできるので、 先述のように実に自然なhandle[パス名]();として、 あたかも”このパス名をhandleして下さい” と言っているように表現することができます。 OK、これでサーバ、ルータ、リクエストハンドラをすべて繋ぎ合わせることができました! アプリケーションを起動し、 http://localhost:8888/start にブラウザからリクエストを送れば、 このように正しくリクエストハンドラが呼び出されていることがわかります:
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;
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.
function start() {これでよし。 同じように、ルータはリクエストハンドラによって返されたものをサーバに渡してやる必要があります。 なのでrouter.jsはこのようにします:
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;
function route(handle, pathname) {これを見ておわかりのとおり、リクエストがルーティングされなかった時にもいくらかのテキストを返すようにしています。 そして最後に、ルータ経由でリクエストハンドラが渡してきた内容をブラウザに返すよう、サーバを改修します。server.jsは下記のようにしましょう:
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;
var http = require("http");書き直したアプリケーションを起動すれば、すべては魔法のようにうまく行くことでしょう。つまり http://localhost:8888/start をリクエストすれば”Hello Start”がブラウザに表示され、 http://localhost:8888/upload をリクエストすれば”Hello Upload”と、 http://localhost:8888/foo などとすれば”404 Not found”となります。 OK、これで何か問題でも?端的に言うと、もしリクエストハンドラのどれかが、 後述のノンブロッキング操作を行いたいとなった場合に問題となってしまうのです。 それでは詳しい説明に入りたいと思います。
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;
function start() {何をしているのかというと: 関数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ファイル):
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;
var exec = require("child_process").exec;おわかりのとおり、新しい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 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;
function (error, stdout, stderr) {そしてここに我々が直面している問題の原因があります: このコードは同期式に実行されているため、 exec()を呼び出した直後にNode.jsはreturn content;の実行へ進んでいます。 この時exec()は非同期式に動作するため、exec()に渡されたコールバック関数が呼び出されておらず、 contentはまだ”empty”のままなのです。 いまのところ”ls -lah"は(ディレクトリ内にファイルが数百万もない限り)そ れほど時間がかからず素早く実行されます。 それはコールバックが比較的早く呼び出されるからですが、それでも非同期に実行されます。 もっと時間のかかるコマンドで考えてみると明らかになります: “find /”と実行すると、私のマシンではおよそ1分程度かかります。 しかしリクエストハンドラ内の”ls -lah"を”find /”に置き換えても、 やはり/start URLを開いたとき、すぐにHTTPレスポンスが返ってきます。 つまりexec()はバックグラウンドで実行され、 Node.jsはアプリケーションの実行を続けるということがわかります。 そしてexec()に渡したコールバック関数は、 ”find /”コマンドの実行が完了した時にだけ呼ばれるであろうことが想像できます。 しかし、これでどうやって目的を達成できるのでしょうか。 ユーザに対してカレントディレクトリのファイル一覧を表示したいのです。 どうしたらできないかを学んだところで、 次はどのようにリクエストハンドラがブラウザに正しく応答させられるかを考えていきましょう。
content = stdout;
}
var http = require("http");route()関数からの戻り値は受け取らずに、 第3引数としてresponseオブジェクトを渡しています。 さらに、responseオブジェクトのメソッドは一切呼び出していません。 これはrouteにすべて任せようと考えているからです。 次にrouter.jsです:
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;
function route(handle, pathname, response) {同じパターンです: リクエストハンドラからの戻り値を受け取らず、responseオブジェクトを渡します。 もしリクエストハンドラがない場合、適切に”404”ヘッダとボディーを自分で返します。 そして最後に、requestHandlers.jsを修正します:
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;
var exec = require("child_process").exec;リクエストハンドラ関数は、 リクエストに直接応答するためにレスポンスのパラメータを受け取って使用する必要があります。 startハンドラでは、exec()の匿名コールバック関数の中から応答しています。 またuploadハンドラは、”Hello Upload”と応答すること自体は同じですが、 responseオブジェクトを使って応答するようにしました。 同じように(node index.jsとして)アプリケーションを起動しましょう。期待通り動くはずです。 もし/startで実行される時間のかかる処理のせいで、 /uploadへのリクエストがブロックされて応答が返らない、 ということがないか確認したかったら、requestHandlers.jsを以下のようにしてみると良いでしょう:
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;
var exec = require("child_process").exec;これでhttp://localhost:8888/start へのHTTPリクエストは最低でも10秒間かかることになりますが、 /startがまだ終わっていなくても、 http://localhost:8888/upload へのリクエストに対する応答はすぐに返ってくるはずです。
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;
function start(response) {これがウェビー賞を受賞できないなら、他にどんなものが受賞できるのか皆目検討もつきません。 http://localhost:8888/start にブラウザでアクセスすれば、シンプルなフォームが表示されます。 もしされないとしたら、アプリケーションの再起動を忘れているのでしょうね。 ※訳注 ウェビー賞…国際デジタル芸術科学アカデミーにより優れているとされたウェブサイトに贈られる賞 こんな声が聞こえてきます: リクエストハンドラにビューの内容を書くなんて酷い。 しかしここでは追加の(例えばビューをコントローラロジックから分離する、 といったような)抽象レイヤーを挟むのはやめました。 それをやってもJavaScriptやNode.jsのコンテキストの理解を深められるわけではないですから。 それよりももっと面白い課題に取り組むことにしましょう。 ユーザがこのフォームを送信すると/uploadリクエストハンドラにたどり着く、 POSTリクエストを扱う部分です。 もう初心者から脱していますので、 POSTデータの扱いがノンブロッキングであることには驚くことはないでしょう。 非同期のコールバックを使えば良いわけです。 これは至極真っ当な動作ですね。 POSTリクエストは、とても大きい可能性があります – これで、ユーザが何メガバイトものサイズのテキストを入力しても、何ら問題ありません。 すべてのデータを一つの塊として扱うので、ブロッキング操作となってしまう可能性もあるのです。 プロセス全体がノンブロッキングで動作できるよう、 Node.jsは特定のイベントの時に呼び出されるコールバックではコード上でPOSTデータを細かい塊で扱えるようにしてくれます。 特定のイベントとは、data(POSTデータの中の新しい塊が届いた時)とend(すべての塊を受信した時)です。 Node.jsがこれらのイベント発生時にコールバックするために使う関数を指定してやらなければいけません。 これは、HTTPリクエストを受信するときにonRequestコールバックに渡されるrequestオブジェクトにリスナー (listeners)を追加することで実現できます。 このような感じになります:
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;
request.addListener("data", function(chunk) {このロジックを実装するとき、疑問が湧いてきます。 今のところ、サーバ内でしかrequestオブジェクトにアクセスすることができません – つまりresponseオブジェクトでやってきたように、 ルータやリクエストハンドラにrequestオブジェクトを渡していません。 私が思うに、リクエストから全てのデータをアプリケーションに渡すのはHTTPサーバの仕事なのです。 ですので、POSTデータの処理はサーバ内で行い、 最終的なデータをルータやリクエストハンドラに渡すのです。 そしてルータやリクエストハンドラがそれをどう扱うかを決めれば良いのです。 dataおよびendイベントコールバックはサーバ内に置いておき、 dataコールバック内ですべてのPOSTデータの断片を集めて、 endイベントが呼ばれたときにルータを呼出すのです。 ルータに集めたデータの塊を渡せば、そのままリクエストハンドラに渡されることになります。 それではserver.jsからいきましょう:
// called when a new chunk of data was received
});
request.addListener("end", function() {
// called when all chunks of data have been received
});
var http = require("http");ここでは基本的には3つのことをしています: まず受信データのエンコーディングをUTF-8と定義し、 次に新しいPOSTデータの塊を受信する度にpostData変数に追記していく”data”イベントリスナーを追加、 そして全てのPOSTデータが収集できた時にだけ呼び出されるよう、 ルータ呼び出し部分をendイベントコールバック内に移動しました。 また、POSTデータは後ほどリクエストハンドラ内で必要になるため、ここでは一旦ルータに渡しています。 本番環境のコードでは、 一つデータの塊を受け取る度にコンソールでログ記録するのはよろしくありません (何メガバイトものPOSTデータですからね?)。 ただ今回に限っては、何が起きているか確認してみましょう。 ここでは、textareaにテキストをたくさん入れたり少しだけ入れたりして遊んでみると良いと思います。 多めのテキストを入れた時にはdataコールバックが何度も呼ばれることがわかるはずです。 アプリケーションをもう少しかっこ良くすることにしましょう。 /uploadページで、受け取った内容を表示します。 これを実現するためには、postDataをリクエストハンドラに渡す必要があります。 router.jsをこのようにします‥
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;
function route(handle, pathname, response, postData) {そしてrequestHandler.jsでは、uploadリクエストハンドラのレスポンスにそのデータを含めます:
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;
function start(response, postData) {これでよし、リクエストハンドラでPOSTデータを受信できるようになりました。 このトピックでひとつやり残したことがあります: ルータに、そしてリクエストハンドラに渡したのはPOSTリクエストのボディー全体です。 POSTデータの個々のフィールド、つまりこのケースにおけるtextフィールドの値を消化したいですね。 querystringモジュールについてはすでに読んだ通りです。このようになります:
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;
var querystring = require("querystring");さて、初心者向けチュートリアルとしてのPOSTデータの扱い方はこれで全部です。
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;
npm install formidable
そしてこのような出力になれば
npm info build Success: formidable@1.0.9準備完了です。 これで私たちのコード内でformidableモジュールが使えるようになりました – あとは、最初のほうで他のビルトインモジュールでやったようにrequireすれば良いだけです:
npm ok
var formidable = require("formidable");formidableという名前が暗に示しているように、 HTTP POSTで送られたフォームデータはNode.jsでパースすることができるようになります。 あとは送信されたフォームが抽象化された新たなIncomingFormを作成するだけです。 これでフォームを通して送信されたフィールドやファイルといったHTTPサーバのrequest オブジェクトをパースするのに使うことができます。 node-formidableプロジェクトのページに記載されたサンプルコードを見れば、それぞれのパーツを同時に扱う方法がわかります:
var formidable = require('formidable'),このコードを書いてnodeで実行すれば、 ファイルアップロードを含む簡単なフォームを送信することができます。 そしてform.parse呼出し時のコールバック関数に渡されるfilesオブジェクトがどのような構成になっているのかがわかります:
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);
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"),この新しいリクエストハンドラを、/showというURLにマッピングします。index.jsを下記のようにします:
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;
var server = require("./server");サーバを再起動して http://localhost:8888/show をブラウザで開けば、/tmp/test.pngとして保存されている画像ファイルが表示されるはずです。 良いですね。あとやるべきことは ● /startで、ファイルアップロードのHTML要素をフォームに追加する ● /tmp/test.pngにアップロードされたファイルを保存するように、 node-formidableをuploadリクエストハンドラに組み込む ● アップロードされた画像を/uploadURLのHTML出力結果として組み込む ステップ1は簡単です。multipart/form-dataのエンコーディングタイプをHTMLフォームに追記し、 textareaを削除、ファイルアップロードのinputフィールドを追加して、 submitボタンのテキストを”ファイルをアップロード”に変更すれば良いだけです。 requestHandlers.jsファイルを修正しましょう:
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);
var querystring = require("querystring"),これだけです。次のステップは少しだけ難しくなります。 最初の問題は‥ uploadリクエストハンドラの中で、ファイルアップロード処理を行いたいのですが、 そこで、requestオブジェクトをnode-formidableのform.parseに渡してやる必要があるということです。 しかしここで私たちが持っているのは、responseオブジェクトとpostData配列だけです。 なんと残念なことでしょう。requestオブジェクトをなんとかしてサーバからルータへ、 そしてリクエストハンドラへと渡してやらないといけないようです。もっと綺麗なやり方がありそうですが、 とりあえずこのアプローチ方法でなんとかなりそうなので、今回はこれでよしとしましょう。 そうこうしている間に、とっとと全てのpostDataをサーバやリクエストハンドラから削除してしまいましょう – ファイルアップロードではもう使いませんし、問題が発生する可能性さえあります: サーバ内では既にrequestオブジェクトのdataイベントを既に”消化”してしまっているため、 同様にこれらのイベントを消化する必要があるform.parseでそのデータを受信できなくなってしまうからです (Node.jsはデータをバッファリングしてくれないのです)。 それではserver.jsから始めましょう – postDataを扱う部分と (node-formidableが直接取り扱うことになる) request.setEncodingの行をを削除し、代わりにrequestをルータに渡します:
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;
var http = require("http");次はrouter.jsです – postDataはもういらないので、代わりにrequestを渡します:
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;
function route(handle, pathname, response, request) {これで、requestオブジェクトはuploadリクエストハンドラ関数で使うことができます。 node-formidableではアップロードされたファイルを/tmpのローカルファイルとして保存する時の細かい処理を受け持ってくれるので、 このファイルがちゃんと/tmp/test.pngという名前にリネームされるところを確認する必要があります。 そう、ここでは簡単に済ますため、画像はすべてPNGであると想定しています。 リネームの際のロジックは少しだけ複雑です: Windowsにおけるnodeの実装では、既存のファイルの位置にリネームすることができないため、 エラー発生時にはファイルを削除する必要があります。 アップロードされたファイルの管理とリネーム処理を、requestHandlers.jsの中に追記しましょう‥
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;
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ページに表示されるでしょう。