Koa ミドルウェアの作り方。
前回、Koaのミドルウェアは積み重ねる事ができる関数だと言うことを説明しました。
今回は具体的にミドルウェアを作ってみます。
ちょっと面倒くさくなったので、Typescriptで書く事にしました。
前回の記事
https://qiita.com/kei-nakoshi/items/c513bbf52c66250c6d2a
環境構築
必要なもののインストール
//適当なディレクトリを作成に移動
$ mkdir koa-sample
$ cd koa-sample
$ npm init -y
//koa to インストール
$ npm i koa
// typescipt typescript
// ts-node テスト用
// @types/koa koa用の型定義
$ npm i -D typescript ts-node @types/koa
// .tsconfig を作成
$ npx tsc -init
tsconfig.json の編集
allowSyntheticDefaultImports のコメントアウトを外しておいてください。
(Nodeのバージョンによっては不要かもしれない。
{
-//allowSyntheticDefaultImports : true
+allowSyntheticDefaultImports : true
}
とりあえず サーバーを作成して、ts-nodeで起動できることを確認します。
import Koa from "koa"
type Context = {}
type State = {}
const app = new Koa<Context,State>()
app.use( async(ctx,next) => {
ctx.body = "koa app."
} )
app.listen(3000)
//サーバの起動
$ npx ts-node app.ts
ブラウザで http://localhost:3000 にアクセスした際に koa app. と表示されれば成功です。
確認できたらCTL+B等でサーバを終了してください。
ここから本題 ミドルウェアを作ってみる
ルーティング仕様
今回は単純なルーティングを作りながら説明します。今回の仕様はこんな感じでGETのみでURLによって出力する内容を変更するAPIとします。
メソッド | パス | レスポンス |
---|---|---|
GET | /name | {name:"headphone"} |
GET | /price | {price:13000} |
GET | /brand | {brand:"M.I.C"} |
※ 実際にはルーティングkoa-route/koa-router等のライブラリを使うことが多いので実用的なものでは無いです。
ミドルウェア作成考え方
今回のミドルウェア は 以下の事を実現すれば完成するはずです。
- リクエストされたURL(パス)を取得する
- リクエストされたHTTP Method を取得する
- 上2つが一致する場合レスポンスを返す。
ctx には リクエスト/レスポンスがうまくラップされており、パス及びメソッドは簡単に取得する事ができます。よく使われるものは ctxのプロパティとして取得できるようになっていることが多いです。
method や url についてはドンズバなプロパティが用意されています。
また ctx.bodyに値をセットすることでクライアントへのレスポンスとなります。
Context API
https://github.com/koajs/koa/blob/master/docs/api/context.md
ifを使って愚直に書いてみます。
import Koa from "koa"
const app = new Koa()
app.use( async (ctx,next) => {
const url = ctx.url
const method = ctx.method
if(method === "GET" && url === "/name"){
ctx.body = {name:"headphone"}
return
}
if(method === "GET" && url === "/price"){
ctx.body = {price:13000}
return
}
if(method === "GET" && url === "/brand"){
ctx.body = {brand:"M.I.C"}
return
}
} )
app.listen(3000)
再度サーバを起動して作成したエンドポイント3つを開いてみましょう。
//サーバの起動
$ npx ts-node app.ts
うまく表示されたでしょうか?
表示された場合、特に指定をしていないにも関わらず Content-Type が自動で設定されていることに
気づくかもしれません。ctx.bodyにオブジェクトを指定した場合は自動的にJSONと判断されます。便利ですね。
これでKoaのミドルウェアデビューを果たす事が出来ました。(パチパチ
実際これで何の問題も無いのですが、もう少しKoaのミドルウェアらしく書いてみたいと思います。
例えば今回の場合もう一つの引数nextを用いていませんよね、これを利用して各レスポンスを独立したミドルウェアとして書くことができます。
今回のようなルーティングの場合『自分が処理出来ないリクエストだったら、次のミドルウェアに任せる』イメージで書き換える事ができます。
import Koa from "koa"
const app = new Koa()
app.use(
async (ctx,next) => {
const url = ctx.url
const method = ctx.method
if(method === "GET" && url === "/name"){
ctx.body = {name:"headphone"}
return
}
next();
})
app.use(
async(ctx,next) => {
const url = ctx.url
const method = ctx.method
if(method === "GET" && url === "/brand"){
ctx.body = {brand:"M.I.C"}
return
}
next()
})
app.use(
async(ctx,next) => {
const url = ctx.url
const method = ctx.method
if(method === "GET" && url === "/price"){
ctx.body = {price:13000}
return
}
next()
})
app.listen(3000)
起動して確認してみます、if文を使ったミドルウェアと変わらないレスポンスが得られることがわかると思います。
//サーバの起動
$ npx ts-node app.ts
このように各ミドルウェアを薄く書いていくと独立性が高くKoaのミドルウェアぽい書き方になります。
とはいえ、単純にコードが増えてしまったのでこういった場合は高階関数やクラスの出番です。
今回の例では method と path で レスポンスが変わる点が共通していますので、ミドルウェアを作成する高階関数(createRoute)を作成してみます。
import Koa , {Middleware} from "koa"
const app = new Koa()
const createRoute = (targetMethod : string,targetPath : string, response:any) : Middleware => async (ctx,next) => {
const { url ,method } = ctx
if(targetMethod === method && targetPath === method){
ctx.body = response
}
next()
}
app.use(createRoute("GET","/name",{name:"headphone"}))
app.use(createRoute("GET","/price",{price:13000}))
app.use(createRoute("GET","/brand",{brand:"M.I.C"}))
app.listen(3000)
大分スッキリ書くことができるようになりました。高階関数で今回は書きましたが、ミドルウェアのルールは単純なので classを使うことも出来ます。
まだ 少し app.use の部分が冗長に感じますよね、そういった場合はkoa-composeライブラリを利用して複数のミドルウェアから、新しいミドルウェアを作る事が出来ます。
compose に ついては 前回記事で詳しく触れています。
https://qiita.com/kei-nakoshi/items/c513bbf52c66250c6d2a
import Koa , {Middleware} from "koa"
import compose from "koa-compose"
const app = new Koa()
const createRoute = (targetMethod : string,targetPath : string, response:any) : Middleware => async (ctx,next) => {
const { url ,method } = ctx
if(targetMethod === method && targetPath === url){
ctx.body = response
}
next()
}
const route = compose([
createRoute("GET","/name",{name:"headphone"}),
createRoute("GET","/price",{price:13000}),
createRoute("GET","/brand",{brand:"M.I.C"})
])
app.use(route)
app.listen(3000)
Classでも書いてみる
import Koa , {Middleware} from "koa"
import compose from "koa-compose"
const app = new Koa()
class Route {
routes : Middleware[] = []
get(targetPath:string,response:any){
this.routes.push(async (ctx,next) => {
const url = ctx.url
if(targetPath === url){
ctx.body = response
}
next()
})
}
use(){
return compose(this.routes)
}
}
const router = new Route()
router.get("/name",{name:"headphone"})
router.get("/price",{price:13000})
router.get("/brand",{brand:"M.I.C"})
app.use(router.use())
app.listen(3000)
レスポンスにJSONしか書き出せないのがナンセンスですが、よく見るルーターっぽい書き味に仕上がりました。koa-composeはミドルウェアを別ファイルでまとめたい場合等にすごく重宝します。
まとめ
手続き型っぽい書き方から、少しずつつkoaらしい書き方へ変化していく感じをなんとなく翻訳調でお送りしました。
- ミドルウェアを手続き型っぽく書くこともできる
- next を使うと 各ミドルウェアを薄くできる事がある
- koa-compose で ミドルウェアをまとめられる
- 高階関数が便利