LoginSignup
28
26

More than 5 years have passed since last update.

KoaでWeb開発: Koaの検証を通じてミドルウェアの概念を理解する

Posted at

はじめに

動機

やっすいサーバでしかしパフォーマンスは出て、しかも大量のリクエストが来ても大丈夫なサービスを立ち上げたい。。。
そんなわがまま放題な欲求を満たすためにはやはりnode.jsで動作するアプリケーションがいいんじゃないのか、と思ったのです。
しかし、非同期とか超苦手だし・・・と思っていたところに、このKoaというフレームワークが来たわけです。

これまで、何回にもわたって、ES2015の話題を出してきたのは、ひとえにこいつを使うためでした。

ミドルウェア?

しかし、Koaの説明の中で、頻繁に「ミドルウェア」というキーワードが出てきます。
自分のこれまでの認識だと、「DB」とか「Apache」とかのを表しているのかと思いましたが、どうも違いそうだと。。。
で、検索してもミドルウェアという言葉の意味がわかっている前提で述べられているものばかり。。。
こいつを自分の中で理解できないかぎり、先には進めないだろうと考えていました。

後で書きますが、ポンチ絵一つで理解できる内容でしたが。。。

Koaとは?

そもそもKoaってなんでしょう?
Koaの公式サイトにはこんなふうに書いてあります。
http://koajs.com/

informationの意訳

「KoaはExpressに貢献していたチームによって設計された新しいwebフレームワークだ。こいつの目的は、もっと小さく、もっと表現力豊かで、より堅牢なwebのアプリケーションやAPIの基礎となることだ。ジェネレータを利用することで、Koaはコールバックを排除し、エラーハンドリングを劇的に向上させることができるんだ。Koaのコアシステムには、ミドルウェアが何も含まれていないんだけれど、サーバーコードを早く楽しく書くための素晴らしいメソッド群を提供しているよ。」

なるほど、そういう謳い文句なわけですな。
ジェネレータを使ってコールバックを排除するっていうのは、前回までに散々やってきたものですが、さて、ミドルウェアとはなんぞや。
ミドルウェアについての私の認識は、「DB」とか「Apache」だったのですが、何やら違うようです。。。

applicationの意訳

「Koaのアプリケーションというのは、ジェネレータ関数で書かれたミドルウェアの配列を含んだオブジェクトだ。このミドルウェア群が、リクエストに対するスタックのような処理方式(stack-like manner)で構成・実行されるんだ。
Koaは他の多くのミドルウェアのシステムと似たようなものに見えるだろう。例えば、RubyのRackとかConnectとかを見ていると、わかるだろう。とはいえ、こいつの設計のキモは、低レベルのミドルウェア層を、高レベルで表現する仕組みを提供することだ。このおかげで、相互運用性と堅牢性を向上させ、ミドルウェアの実装をより楽しく書くことができるんだ。

Koaは次のような共通処理についてのメソッドを持っている - コンテンツ調整にキャッシュ更新、プロキシーのサポートに他へのリダイレクト処理だ。Koaは使いやすいたくさんのメソッドを提供しているのだけど、その動作自体は小さくまとまっているんだ。これは、いかなるミドルウェアもコアシステムに含まれていないからなんだ。」

だからミドルウェアってなんやねん!

とりあえずここからわかるのは、Koaのアプリケーションが、ミドルウェアの集合体のようになっているということです。
そしてミドルウェアはジェネレータで書かれるということです。
そして、このミドルウェアというものがおそらくKoaを理解する上で重要な概念となっているのでしょう。

Koaのアプリケーションの実装

Koaのアプリケーションがどのように実装され、どのように挙動するのかを詳しく検証してみましょう。

Hello World

Koaを使って、Hello Worldを出力するアプリケーションを試験的に実装してみましょう。

まず、適当なディレクトリを作り、そこで、Koaをインストールします。

$ mkdir hello
$ cd hello
$ npm i koa

ちなみに、node.jsのバージョンは5.3です。

次に、app.jsという名前のファイルを作って、以下のようにコーディングします。

app.js
'use strict'

const koa = require('koa');
const app = koa();

// レスポンス用のミドルウェア
app.use(function* (){
  this.body = 'Hello World!!';
});

// ポート8888をリッスン
app.listen(8888);

これで、node app.jsとコマンドを打つだけで、サーバのURLの8888ポートにアクセスすることで、「Hello World」を表示させることができます。

ここで、app.useというメソッドは、アプリケーションにミドルウェアを登録するメソッドとのことです。
このapp.useに登録されたミドルウェア群が、「stack-like」に処理されるというのがKoaのアプリケーションの動作概要でした。

stack-like

以降、app.useに登録されたものをとにかくミドルウェアと呼ぶことにします。呼ぶだけならただです。
stack-likeな処理とはどんなものなのか、適当にapp.useにいくつもジェネレータを入れて検証してみましょう。

middle.js
'use strict'

const koa = require('koa');
const app = koa();

app.use(function* (next){
  console.log(1);
  yield next;
  console.log(2)
  yield next;
  console.log(3)
});

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

app.use(function* (next){
  console.log(6);
  yield next;
  console.log(7);
  yield next;
  console.log(8);
});

app.listen(8888);

yieldの後にnextを入れていますが、Koaの公式を丸パクリしているだけなので、今は気にしないでください
これを書いたら、node middle.jsを打って、ブラウザからアクセスしてみましょう。
すると、コンソール上にこんな結果が出てきます。

1
4
5
2
3

まず注目すべきは、3番目に登録したミドルウェアは実行されてい無いというところでしょうか。
ここで、レスポンスを返すミドルウェアにnextが指定されていないから、次の処理が行われていないのでは、という当たりをつけ、nextを引数に入れていみましたが、挙動は変わらずでした。
今度はレスポンスを返すミドルウェアにyield nextを最後に加えてみます。

middle.js
'use strict'

const koa = require('koa');
const app = koa();

// gen1
app.use(function* gen1(next){
  console.log(1);
  yield next;
  console.log(2)
  yield next;
  console.log(3)
});

// gen_res
app.use(function* gen_res(next){
  console.log(4);
  this.body = 'Hello World'
  console.log(5);
  yield next;
});

// gen2
app.use(function* gen2(next){
  console.log(6);
  yield next;
  console.log(7);
  yield next;
  console.log(8);
});

app.listen(8888);

各ジェネレータに名前をつけました。
node middle.jsを実行し、アプリにアクセスすると次のような結果になります。

1
4
5
6
7
8
2
3

非常に不思議な結果になりました。次に、gen_resgen2の順番を変えてみましょう。同じようにアプリを起動すると、

1
6
4
5
7
8
2
3

なるほど。。。
リクエストが飛んできた時に、まずapp.useで登録した順に各ジェネレータのnextが実行されているように見えます。
しかしその後は、後ろに登録されたジェネレータから一気に実行されているようです。

以上から、以下の様にしてアプリケーションが動いているとわかります。

  1. app.useメソッドに追加された順に、ミドルウェアのはじめのyieldまで処理が行われる(= 各々一回ずつnextが呼ばれる)
  2. app.useメソッドに追加されたのとは逆順にミドルウェアの処理が行われる。現在のミドルウェアの処理が完全に終了するまで、次のミドルウェアの処理は行われない
  3. app.useに追加されたミドルウェアにyield nextが存在しない場合、その後ろに追加されたミドルウェアの処理は実行されない

たしかに、一番初めにnextメソッドが呼ばれる時以外は、後から追加したものから順に実行されるため、「stack-like」になっていることが実感できます。

ミドルウェア

この「stack-like」な挙動を通じて、Koaにおけるミドルウェアというものをようやく理解できました。
Koaのミドルウェアは「フロントとアプリケーションの実処理の中間に位置する機構」となっていたのです。
そして、このミドルウェアという考え方と実装が、Webアプリケーションでは当然のように実現されてきたものであることがわかりました。

Nginx - PHP-FPMのミドルウェア

ここで唐突に、Nginx - PHP-FPMのアプリケーションにおいてのミドルウェアを考えてみましょう。これの処理の流れは次のようになっています。

  1. ユーザーがリクエストを投げる
  2. Nginxがリクエストを受け取る
  3. NginxがリクエストをPHP-FPMに投げる
  4. PHP-FPMが処理を実行する
  5. PHP-FPMがレスポンスをNginxに返す
  6. Nginxがレスポンスをユーザーに返す

すると、アプリケーションとユーザーの間には、NginxとPHP-FPMの2つのアプリケーションが存在しています。
ということで、NginxとPHP-FPMはミドルウェア = 「フロントとアプリケーションの実処理の中間に位置する機構」として機能していることがわかります。

さて、ところでこのNginxとPHP-FPMの挙動を見ると、初めに実行されているのはNginxですが、最後に実行されているのもNginxです。
また、PHP-FPMが仕事を始めると、そのリクエストに対し、レスポンスが返ってくるまではNginxは一切仕事をしません。

こんな感じで、Webアプリケーションの処理のイメージは以下の様なものになるのではと思います。

koa_middleware.jpg

ユーザーとアプリケーションの間には層状にミドルウェアが並んでいて、ユーザーに近い側から順に起動していき、アプリケーションに近い順に処理が終了していくという感じです。

Koaのミドルウェア

Koaのミドルウェアの実装方法は、このイメージを忠実に意識して作られているように思えます。
app.useに登録される順番こそが、上記のイメージにあるミドルウェアの層構造を再現したものになっています。
リクエストに対しては、まずapp.useに登録された順にミドルウェアが起動します。
リクエストに対する実処理部分も最後に実行されるミドルウェアであると考えれば、アプリケーションが層構造のミドルウェア群で構成されていることになります。
続いてレスポンスはアプリケーションからユーザーへの方向なので、実行順序は逆になります。
また、レスポンスの流れが逆行することが基本的にはないと考えれば、現在実行しているミドルウェアを終了させてから、次のミドルウェアの処理を行うことは自然な考え方と言えるでしょう。

つまり、Koaのアプリケーションの実装は、ユーザ - ミドルウェア群 - アプリケーションの層構造を、コードにしたものであるということがわかります。

まとめ

ようやくミドルウェアを理解することができました。
Webアプリケーションをスタック構造として考えるというのは、当然といえば当然なのですが、なかなかこのイメージに辿りつけませんでした。
そういえば、LAMP「スタック」とかMEAN「スタック」とか言いますよね。
このようなアプリケーションをミドルウェアの層構造で構築する形式は、Koaにかぎらず最近の様々なフレームワークで採用されているようです。

さて、Koaの構造がわかったので、もう少し本格的な実装とかやってみようかな。。。

参考

Rack解説 - Rackの構造とRack DSL
Node : 今更ながらに、connect を読んでる。
Koa - next generation web framework for node.js

28
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
26