koa入門
さて、2013年12月19日にkoaというフレームワークの0.1.0がリリースされ、Hackers Newsに乗り、それが話題になっています。
これまでNode.jsのWeb Application Frameworkとして最もメジャーなのはExpressだと思いますが、Expressの作者であるTJを筆頭にExpressチームがKoaを積極的にエンハンスし始めているため、今後のNode.jsのフレームワーク勢力図が変わる可能性があります。
作者のメッセージを引用すると
Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. Through leveraging generators Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within core, and provides an elegant suite of methods that make writing servers fast and enjoyable.
Koa は Expressチームによって設計されている新しいweb frameworkである。
web application/APIのための、軽量であり、表現豊富、かつ、ロバストな基盤を実現することを目標としている。
generatorを通して、Koaはcallbackを回避し、強力なエラー処理を提供する。
またKoaはミドルウェアをコア部分に組み込まないため、サーバーを素早くそして楽しく書くための方法を提供している。
ということです。かなり長くなりますが、説明します。
Koa特徴
- generator/yieldを使ってmiddlewareを書くことができる
- Koa自身はsubstack wayというか、最小限のことをやるmodule群の集まりになっており、Routingや静的ファイル配信など、かなりのモジュールが別モジュールになっている
- http requestやresponse等をwrapしたコンテキストという概念を持っており、responseやrequestを横断で利用できる。
KoaでHello World
前提として、Koaはgeneratorを利用するため、v0.11.9以上のnode.jsでしか利用できません。また、nodeを起動する際には —harmonyオプションを付ける必要があります。
# make node.js latest! # if you use nodebrew, # nodebrew install-binary latest # nodebrew use latest $ npm install koa --save
その後で、以下のように記述して、app.jsとか名前を付けて保存します。
本当に数行。
var koa = require('koa'); var app = koa(); // ここが特徴的、function * でgeneratorを利用している。 app.use(function *(){ this.body = 'Hello World'; }); app.listen(3000);
で、実際に実行する時はnode --harmonyオプションを付けて実行します。
$ node --harmony app.js
ブラウザで http://localhost:3000/ にアクセスすれば Hello Worldと記述されているのがわかると思います。
毎回harmonyオプションを付け忘れそうな人はnpm startに記述しておくか、nodeのエイリアスを切って、node —harmonyにしておくことをオススメします。
"scripts": { "start": "node --harmony app.js" }
$ alias node='node --harmony'
node v0.10.xからどうしても動作させたい場合や--harmonyオプションをどうしても付けたくない場合は、gnodeを使うとできます、ただしchild_processを使うため、パフォーマンスが落ちると記述されています。
Koaのmiddlewareを使ってみる
さて、Koaは先ほど特徴を示したとおり、最小限のことをやるモジュール群で構成されています、そのため、Routingや静的ファイル配信、テンプレートエンジン等のモジュールを利用することでWAFの機能が追加されるという形式になっております。これらのモジュールはmiddlewareとして公開されており、それらを利用することで実現できます。
先ほどのHello Worldに対してRouting、静的ファイル配信、テンプレートエンジンを追加してみましょう。
モジュールをインストール
$ npm install koa-route koa-static co-views jade --save
Routingはkoa-route、静的ファイル配信はkoa-static、テンプレートはjade、koaのレンダリングエンジンは今のところ動作を確認できない怪しげなモジュールしか無かったので、TJ作成のco-viewsを利用。
app.jsをmiddlewareを使うように変更
app.jsを以下のように変更します。
var koa = require('koa'); var route = require('koa-route'); var serve = require('koa-static'); var views = require('co-views'); var app = koa(); // jadeをテンプレートエンジンとして設定。 var render = views(__dirname + '/views', { map : {html : 'jade'}}); // GET /views => render template engine app.use(route.get('/views', function *(next) { // bodyに対してindex.jadeの変更を実施。 this.body = yield render('index.jade', {name: "koa"}); })); // GET /hello => 'Hello!' app.use(route.get('/hello', function *() { this.body = 'Hello!!'; })); // GET /hello/:name => 'Hello :name' app.use(route.get('/hello/:name', function *(name) { this.body = 'Hello ' + name; })); // static file serve app.use(serve(__dirname + '/public')); app.listen(3000);
jadeのテンプレートとして、views/index.jadeを追加。
doctype html html(lang="en") head title koa-render style. body { font-family: Helvetica; text-align: center; } h1 { padding-top: 300px; font-weight: 200; color: #333; font-size: 6em; } body h1 koa render #{name}
静的ファイルとして、public/index.htmlとpublic/js/client.jsを追加。
index.html
<html> <head> <script src="/js/client.js"></script> </head> <body> Static content, for KOA </body> </html>
js/client.js
console.log('test');
アクセスしてみる
テンプレート:http://localhost:3000/views
ルーティング1:http://localhost:3000/hello
ルーティング2 : http://localhost:3000/hello/yosuke
静的ファイル配信:http://localhost:3000/
※ブラウザのconsoleを見ると、testという文字が出てるはず。
Koa vs Express
KoaのMLを読むと早速Expressとの比較質問が出てて、TJの回答に利点が集約されてるなーと思ったので翻訳しておきます。
質問
新しいweb frameworkを見たけど、Expressと比較して利点がよくわからない。
今までのExpress/Connectで解決されているんじゃない?
ベンチマークとかあったけど、高速化もされているの?
TJからの回答
Express/Connectで既に解決されてるよ、でもKoaは違った解決法を採用しているんだ。
generatorを使うことで、普通にtry/catchを使うことができるし、coreにmiddlewareを組み込んでいないからConnectよりも複雑さを抑えているんだ、そうすることでリリースサイクルをモジュールごとで管理できる。これらの事で、他のnodeのフレームワークよりもフォールトトレランスに優れていると言えると思う。また、cascadingにはConnectにはできない他の機能がある。Connectでのupstreamは意味がない、それも利点といえるだろう。
まとめると、
1. middlewareが別モジュールになっていて各モジュールでリリースサイクルが分離できる。
2. generatorを使っていて、エラー処理がしやすい
3. cascadingの機能がある。upstreamの概念がある。
1に関しては、これまで説明したとおりですが、ここで2つめの利点としてエラーハンドリングについて、3つめの利点として、Cascading/upstreamという言葉が出てきます。
エラーハンドリング
try-catchでエラーハンドリングができるようになります。
Expressでのエラーハンドリング:
これまでは、(err, req, res, next) の4つの変数を受けてエラーハンドリングをしていました。
// error thrower app.use(function(req, res, next) { if (req.url === "/_err") { next(new Error("abc")); } }); // handle error app.use(function(err, req, res, next){ if (err) { console.error(err.stack); res.send(500, err.message); } });
Koaでのエラーハンドリング:
Koaではyieldを利用して、通常のtry-catchでもエラーが受け取れるようになります。
// handle error app.use(function *(next) { try { yield next; } catch (err) { console.error(err.stack); this.status = 500; this.body = err.message; } }); // error thrower app.use(function *(next) { if (this.url === "/_err") { throw new Error("Send Error"); } yield next; });
next関数にerrを渡す必要がなく、try-catchでハンドリングできる事が分かります。
また、koaのエラーハンドリングのもう一つの機能として、appに対してerrorイベントをemitする方法も提供されています。app.on('error')でエラーイベントを中央集権的に集約することで共通のエラー処理を記述することができます。
// handle error app.use(function *(next) { try { yield next; } catch (err) { this.status = 500; this.body = err.message; // errorイベントを発行 this.app.emit("error", err, this); } }); // error thrower app.use(function *(next) { if (this.url === "/_err") { throw new Error("Send Error"); } yield next; }); // ここで中央集権的にエラーを管理できる。 app.on('error', function(err){ console.error(err.stack); });
Cascading
Cascadingというのは処理を繋いで各ミドルウェアに委譲できる仕組みを指します。downstreamとupstreamについては実例を含めて説明します。
Expressの時:
// 1. response time の記録モジュール app.use(function(req, res, next){ // startの値を取っておく。 var start = new Date; // resのheaderイベントを受け取り、startから現在時刻を引いた値を設定する。 res.on('header', function(){ var duration = new Date - start; res.setHeader('X-Response-Time', duration + 'ms'); }); // 次のモジュールに委譲 console.log("1 response time"); next(); console.log("1 response time"); }); // 2. loggerモジュール app.use(function(req, res, next){ // startの値を取っておく。 var start = new Date; res.on('header', function(){ var duration = new Date - start; console.log('%s %s - %s', req.method, req.url, duration); }); // 次のモジュールに委譲 console.log("2 logger"); next(); console.log("2 logger"); }); // 3. response app.use(function(req, res, next){ console.log("3 response"); res.send(200, "Hello World"); console.log("3 response"); });
これで
ログには
1 response time
2 logger
3 response
3 response
2 logger
1 response time
と出力されます、最初の
1 response time
2 logger
3 response
が上から下へのdownstreamのcascadingですね。
で、次の
3 response
2 logger
1 response time
が下から上へのupstreamのcascadingですね。
で、response#sendは非同期なので、upstreamのcascadingの時にはまだHTTPのレスポンスが返されていません。そのため、Connectにおけるupstreamのcascadingは意味が無いと言われています。
そのため、これまでのExpress/Connectでは、レスポンスを返したことを表す 'header' イベントを受信することでレスポンスタイムやログを実現していました。
res.on('header', function(){ var duration = new Date - start; console.log('%s %s - %s', req.method, req.url, duration); });
これがKoaの場合には異なります。
Koaの時:
// x-response-time app.use(function *(next){ var start = new Date; console.log("1 response time"); yield next; console.log("1 response time"); var ms = new Date - start; this.set('X-Response-Time', ms + 'ms'); }); // logger app.use(function *(next){ var start = new Date; console.log("2 logger"); yield next; console.log("2 logger"); var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // response app.use(function *(){ console.log("3 response"); this.body = 'Hello World'; console.log("3 response"); });
先程と同様にdownstream, upstreamのcascadingが行われますが、Express/Connectと異なり、headerイベントを受信していません。
yieldオペレータを使ってnext関数を呼び出した場合、非同期の処理があってもnextが終了するまで待つため、upstreamのcascadingの時にはnextの中の処理が終わっています。
yieldを使うことでheaderイベントを使わなくても直感的に書くことができるようになります。
TJの言う楽しくミドルウェアが書ける(enjoyable)というのが分かる気がします。
※Cascadingに関しては下記のサイトを読むと詳しく書かれています。
[guide.md](https://github.com/koajs/koa/blob/master/docs/guide.md)
まとめ
Koaについて、特徴、Hello World、ミドルウェアの使い方、Expressとの利点について説明しました。
Node.jsのv0.12から正式サポートとなるyieldを使ったWeb Application Frameworkであり、これまでのNode.jsプログラミングに対して新しい手法が提示された形になります。
※ただまだまだ絶賛開発中ということもあって、情報の鮮度が落ちるのが早いと思います。多分自分が書いたこの記事も数週間後には変わっている可能性もあります。
今回のサンプルは以下のリポジトリにアップしました。
yosuke-furukawa/ubuntu-koa-hello · GitHub
おまけ:koaのサンプルはdockerでも動作するようにしました。
Koaを利用しつつ、これまでのNode v0.10で自分のライブラリをエンハンスするのが存外面倒だったので、仮想化環境でやろうと思い、これを機にDockerにも入門してみました*1。
Dockerfileはこんな感じにしています。
Dockerイメージ作成
Dockerが入っていれば、こんな感じで動作させることができます。
Dockerのインストールはインストールドキュメントを参考にして下さい
基本的には、Running a Node.js app on CentOS - Docker Documentationを参考にしました。
$ git clone https://github.com/yosuke-furukawa/ubuntu-koa-hello.git $ cd ubuntu-koa-hello $ docker build -t yosuke/ubuntu-koa-hello .
イメージをpublicに公開したので、多分これでもイメージ作成が可能かと。
$ docker pull yosuke/ubuntu-koa-hello
Docker 起動
$ docker run -p 49160:3000 -d yosuke/ubuntu-koa-hello
これで、仮想環境のホストマシンの49160ポートからゲストの3000ポートに接続することが可能です。
$ curl -i http://localhost:49160/hello HTTP/1.1 200 OK X-Powered-By: koa Content-Type: text/plain; charset=utf-8 Content-Length: 7 Date: Thu, 26 Dec 2013 03:36:36 GMT Connection: keep-alive Hello!!
X-Powered-Byがkoaになっていることが分かりますね。
次回はミドルウェアの作り方とかを中心に紹介します!!
*1:※というか、最初Dockerの方を中心に使ってKoaはおまけ程度で考えていたのですが、思いの外面白く、記事のボリュームが完全に逆転しました。Dockerに関しては次のエントリで詳しく説明します。