Node.js
nodejs
Koa
co
Node.jsDay 9

Koa(1.x)のmiddlewareの仕組みを調べた

More than 1 year has passed since last update.

この記事は Node.js Advent Calendar 2015 9日目の記事です。(大遅刻)

注意

この記事で扱うのはKoaのv1.1.2です。なお、今後のバージョンアップで書き方やAPI、そしてコードが大きく変わる可能性があるのでご注意ください。現時点でv2.xはアルファ版ですが、coがサポートされないようになり手動で追加する必要がある、などの違いがあります。

Koaとは

https://github.com/koajs/koa

Expressive middleware for node.js using generators

WebアプリのサーバーサイドがES6 Generatorを使っていい感じに書けるという話ですね。

特徴

リポジトリのREADMEよりサンプルコードを引用

簡潔に書ける

簡潔に書けます。

app.js
var koa = require('koa');
var app = koa();

// logger

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

非同期処理が簡単に書ける

app.js
var fs = require('co-fs');

app.use(function *(){
  var paths = yield fs.readdir('docs');

  var files = yield paths.map(function(path){
    return fs.readFile('docs/' + path, 'utf8');
  });

  this.type = 'markdown';
  this.body = files.join('');
});

Koaの内部ではcoが使われています。
https://github.com/tj/co
middlewareの中でPromiseやPromiseの配列などをyieldすることで非同期処理が楽に書けます。

詳しく知りたい人はいろんな記事があるので調べてもらったら幸いです。

Koaがどうやって動いているのか興味があったので調べてみました。

前提知識

  • ES6 Generator
  • ES6 Promise

coについて

https://github.com/tj/co

The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)

主に非同期処理がうまく書けるようになるライブラリです

sample.js
const co = require("co");
co(function*() {
  var result = yield Promise.resolve(true);
  return result;
}).then(function(result) {
  console.log(result);
}).catch(function(error) {
  console.error(error.stack);
});

co自体はPromiseを返します。

coの引数のgenerator function(generatorでもいいようです)内でPromiseをyieldするとPromiseがresolveされたときに処理を再開するため同期処理のように書けます。他にもPromiseの配列をyieldするとPromise.allのように並列処理ができたりgeneratorやgenerator functionをyieldすると内部で再帰的にcoが呼ばれたりします。

sample.js
co(function*() {
    yield function*() {
        var result = yield Promise.resolve(true);
        console.log("yield generator in co");
    };
});

この場合、内部でyieldしたgenerator functionを引数にcoが呼び出されるためyield generator in coと出力されます。

co.wrapについて

呼び出すとcoが実行されるような関数を返します。

sample.js
var fn = co.wrap(function* (val) {
  return yield Promise.resolve(val);
});

fn(true).then(function (val) {
  // do stuff
});

co.wrapの返り値であるfnが実行されると内部でco.wrapの引数として渡された関数やfnの引数(複数可)を引数にcoが実行されます。
参考:co.wrapの実装(Koa v1.1.2が依存するco v4.2.0より)
https://github.com/tj/co/blob/4.2.0/index.js

index.js
co.wrap = function (fn) {
  return function () {
    return co.call(this, fn.apply(this, arguments));
  };
};

Koaを読む

lib/application.js
app.use = function(fn){
  if (!this.experimental) {
  // 省略
  this.middleware.push(fn);
  return this;
};

app.useではthis.middlewareに引数のmiddlewareを追加しています。
そこでmiddlewareを呼び出す処理の部分を見てみます。(必要な場所だけ抜粋)

application.js
app.callback = function() {
  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
  // 省略
  fn.call(ctx); // return Promise
};

middlewareをcompose関数で処理したものをco.wrapしてcall(ctx)しています。(ctxはmiddlewareの処理が行われるときのthis)
そこで、compose関数(koa-composeパッケージ)を見に行きます。

index.js
function compose(middleware){
  return function *(next){
    var i = middleware.length;
    var prev = next || noop();
    var curr;

    // 1
    while (i--) {
      curr = middleware[i];
      prev = curr.call(this, prev);
    }

    yield *prev;
  }
}

function *noop(){}

1の部分でmiddlewareを後ろから処理しています。
最後のmiddlewareでは引数のnext(このコードの中ではprev)は空のgeneratorで、それ以外では次のgeneratorです。最終的にはmiddlewareはcoの引数として実行されるのでmiddlewareでgeneratorであるnextをyieldするとcoは先にyieldされたgeneratorを処理し、終わると処理が戻ってきます。

app.js
app.use(function*(next) {
  // next is generator
  // do something
  yield next; // nextを処理する(後ろのmiddlewareを先に処理する)
  // do something
});

これをふまえて最小構成のコードを書くとこうなります。

koa-middleware.js
"use strict";
var co = require("co");

function *noop(){}

class App {
    constructor() {
        this.middleware = [];
    }
    use(middleware) {
        this.middleware.push(middleware);
    }
    /**
     * middlewareを呼び出す
     * @param  {any} context middlewareの内部でthisとして扱われる
     * @return {Promise}
     */
    call(context) {
        var self = this;
        var composed = function *(next) {
            var i = self.middleware.length;
            var prev = next || noop();
            var curr;

            while (i--) {
                curr = self.middleware[i];
                prev = curr.call(this, prev);
            }

            yield *prev;
        };
        return co.call(context, composed);
    }
}

var app = new App();
app.use(function*(next) {
    this.push(1);
    yield next;
    this.push(2);
});
app.use(function*(next) {
    this.push(3);
    yield next;
    this.push(4);
});
var array = [];
app.call(array).then(()=>{
    console.log(array); // [ 1, 3, 4, 2 ]
});

最後に

遅れてすみませんでした。