koa 的 source code 微介紹
Node.js 的原生 http library
const http = require('http');
http
.createServer((req, res) => {
res.write('hello world');
res.end();
})
.listen(3000);
hello world
我們把 (req, res) => { ... } 叫做
http library 的 "handleRequest"
如果 server 要處理很多狀況
import http from 'http';
http
.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/') {
res.write('hello world');
res.end();
}
if (req.method === 'GET' && req.url === '/users') {
res.write('no users');
res.end();
}
...
})
.listen(3000);
這個 handleRequest 就會變得很長
Koa
是一個基於 Node.js 的 http library 的框架
他的核心運作方式
就是
middleware
middleware1
middleware2
middleware3
...
大的 function
compose
http 的
handleRequest
包裝一下
middleware
例如
logger
auth-checker
body-parser
...
(req, res)
response
middleware
middleware
middleware
middleware
middleware
routing
/
/home
/explore
...
(req, res)
middleware
middleware
middleware
middleware
response
koa vs express
Philosophically, Koa aims to "fix and replace node", whereas Express "augments node".
koa 的特點
1. 單純基於 middleware
2. 自己有另一套 api (ctx)
koa 有自己的 ctx API
hello world
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);
const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
ctx.response.body = 'Hello World!';
});
app.listen(3000);
express
koa
middleware stack
import Koa from 'koa';
const app = new Koa();
app.use((ctx, next) => {
ctx.response.body = 17;
return next();
});
app.use((ctx, next) => {
ctx.response.body += 2;
return next();
});
app.use((ctx) => {
ctx.response.body *= 5;
});
app.listen(3000);
middlewareA
middlewareB
middlewareC
koa 的設計
middlewareA
(req, res)
middlewareB
middlewareC
createContext
ctx
ctx
pass ctx
ctx
ctx
ctx
respond (操作 res)
http 的 handleRequest
fnMiddleware
response
pass ctx
pass ctx
pass ctx
koa 的設計
1. 事先把 middleware 組成一個 fnMiddleware
2. 當 request 來的時候
(1) 產生 ctx
(2) 把 ctx 丟到 fnMiddleware
(3) 以 ctx 的結果去操作 res,產生 response
compose
createContext
respond
產生 http 的 handleRequest
callback() {
const fnMiddleware = compose(this.middleware);
...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fnMiddleware);
};
return handleRequest;
}
(array)
koa 的 source code
import Koa from 'koa';
const app = new Koa();
app.use((ctx, next) => {
ctx.response.body = 17;
return next();
});
app.use((ctx, next) => {
ctx.response.body += 2;
return next();
});
app.use((ctx) => {
ctx.response.body *= 5;
});
app.listen(3000);
回頭看一下
Application 的 use 方法
class Application extends Emitter {
...
constructor(options) {
...
this.middleware = [];
...
}
...
use(fn) {
...
this.middleware.push(fn);
...
}
...
}
Application 的 listen 方法
class Application extends Emitter {
...
listen(...args) {
...
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
callback() {
const fnMiddleware = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fnMiddleware);
};
return handleRequest;
}
...
}
產生 http 的 handleRequest
Application 的 handleRequest 方法
class Application extends Emitter {
...
callback() {
const fnMiddleware = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fnMiddleware);
};
return handleRequest;
}
...
handleRequest(ctx, fnMiddleware) {
...
return fnMiddleware(ctx).then(() => respond(ctx)).catch(onerror);
}
...
}
基本上就是把 ctx 丟給 fnMiddleware,
fnMiddleware 層做完以後它再接手 respond
小總結
use 把我們寫的 function 加到 middleware
class Application extends Emitter {
...
use(fn) {
...
this.middleware.push(fn);
...
}
...
}
小總結
然後 listen 的時候幫我們把 middleware 組起來
再包成 http 的 handleRequest 丟給 http.createServer
class Application extends Emitter {
...
listen(...args) {
...
const fnMiddleware = compose(this.middleware);
const server = http.createServer((req, res) => {
const ctx = this.createContext(req, res);
return fnMiddleware(ctx).then(() => respond(ctx)).catch(onerror);
});
return server.listen(...args);
}
...
}
createContext
class Application extends Emitter {
...
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
...
}
respond
function respond(ctx) {
...
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
koa-compose
function compose (middleware) {
...
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
把 middleware (array of functions) 組成 fnMiddleware (function)
koa-compose
function compose (middleware) {
...
return function (context) {
function dispatch (i) {
const fn = middleware[i];
if (!fn) return;
return fn(context, dispatch(i + 1));
}
return dispatch(0);
}
}
不檢查錯誤 + 不考慮非同步 + 不 call next + 不管 this 的版本
koa-compose
function compose (middleware) {
...
return function (context, next) {
function dispatch (i) {
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return;
return fn(context, dispatch(i + 1));
}
return dispatch(0);
}
}
把 next 加回來的版本
koa-compose
function compose (middleware) {
...
return function (context, next) {
function dispatch (i) {
const fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch(i + 1)));
} catch (error) {
return Promise.reject(error);
}
}
return dispatch(0);
}
}
把 middleware (array) 組成 fnMiddleware
把 next 加回來 + async function 的版本
koa-compose
可以把幾個 middleware 組成一個更大的 middleware
compose 出來的 function 一樣是以 (ctx, next) 為參數的 function
koa-compose
其實 compose 出來的 fnMiddleware 沒有用到 next
class Application extends Emitter {
...
handleRequest(ctx, fnMiddleware) {
...
return fnMiddleware(ctx).then(() => respond(ctx)).catch(onerror);
}
...
}
但是 koa-compose 是一個獨立的套件
給別人用很方便
koa-router
是另外一個 koa 生態系的 library
他 library 裡面也是用 koa-compose 來組出一個大 middleware
var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router
.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
})
.post('/users', (ctx, next) => {
// ...
})
.put('/users/:id', (ctx, next) => {
// ...
})
.del('/users/:id', (ctx, next) => {
// ...
})
app.use(router.routes());
app.listen(3000);
middleware 也可以這樣寫
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
ctx.response.body = 17;
await next();
ctx.response.body *= 5;
});
app.use((ctx, next) => {
ctx.response.body += 2;
return next();
});
app.use((ctx, next) => {
return next();
});
app.listen(3000);
所以 koa 的 middleware 層更像是這樣
middelware
middelware
middelware
ctx
ctx
ctx
ctx
ctx
ctx
ctx
ctx
ctx
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
ctx.response.body = 17;
await next();
ctx.response.body *= 5;
});
app.use((ctx) => {
ctx.response.body += 2;
return;
});
app.use((ctx, next) => {
ctx.response.body = 999;
return next();
});
app.listen(3000);
或是像這樣
middelware
middelware
middelware
ctx
ctx
ctx
ctx
ctx
ctx
參考資料
Koa source code
By Jay Chou
Koa source code
- 363