読者です 読者をやめる 読者になる 読者になる

koa入門

node.js koa

f:id:yosuke_furukawa:20131226000731p:plain

さて、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

f:id:yosuke_furukawa:20131225235729p:plain

ルーティング1:http://localhost:3000/hello

f:id:yosuke_furukawa:20131226000029p:plain

ルーティング2 : http://localhost:3000/hello/yosuke

f:id:yosuke_furukawa:20131226000324p:plain

静的ファイル配信:http://localhost:3000/

f:id:yosuke_furukawa:20131226000613p:plain

※ブラウザのconsoleを見ると、testという文字が出てるはず。

Koa Examples

下記を読むと大体の例は書いてあります。なにか作りたいのであれば一旦ここを読むといいかと。

koajs/examples · GitHub

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はこんな感じにしています。

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に関しては次のエントリで詳しく説明します。