Koa中的中间件不同于Express,Koa使用了洋葱模型。神奇的Koa框架仅仅只包含了4个文件。今天我们只看一下主文件—application.js,它包含了中间件如何工作的核心逻辑。
准备
git clone git@github.com:koajs/koa.git
npm install
然后我们在项目根目录添加一个index.js文件,作测试用途。
// index.js
// Include the entry file of koa
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug')('koa');
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// time logger here
app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next();
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'Hello World';
await next();
console.log(4);
});
app.listen(3000);
运行服务器:
node index.js
访问http://localhost:3000,你将看到1, 2, 3, 4, 5, 6的输出,这叫做洋葱模型(中间件)
洋葱模型如何工作
让我们来看看Koa的核心代码,了解一下中间件的工作原理。在index.js文件中,我们可以这样使用中间件:
const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);
然后再来看看application.js,下面的代码是和中间件有关的,我在代码中加了一下备注。
const compose = require('koa-compose');
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false;
// Step 0: init a middleware list
this.middleware = [];
}
use(fn) {
// Step 1: adding the middleware to the list
this.middleware.push(fn);
return this;
}
listen(...args) {
debug('listen');
// Step 2: using this.callback() to compose all middleware
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// Step 3: This is the most important part - compose, it group all
// middleware to one big function and return a promise, we will talk more
// about this function
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// Step 4: Resolve the promise
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
关于Compose函数
关于compose函数的更多信息,我们来看看koa-compose包
module.exports = compose
function compose (middleware) {
// skipped type checking code here
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)
}
}
}
}
所有的中间件都传递给compose函数,它将返回dispatch(0),dispatch函数将立即执行并返回一个promise。在我们理解dispatch函数的内容前,我们必须先了解promise的语法。
关于Promise
通常我们是这样使用promise的:
const promise = new Promise(function(resolve, reject) {
if (success){
resolve(value);
} else {
reject(error);
}
});
在Koa中,promise是这样使用的:
let testPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test success');
}, 1000);
});
Promise.resolve(testPromise).then(function (value) {
console.log(value); // "test success"
});
所以,我们知道,在compose函数中,它返回一个promise
回到Koa - compose中间件
module.exports = compose
function compose (middleware) {
// skipped type checking code here
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)
}
}
}
}
dispatch是一个递归函数,它将遍历所有的中间件。在我们的index.js文件中,我们有三个中间件,这三个中间件将在await next()前执行代码
app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next(); // <- stop here and wait for the next middleware complete
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
我们可以看看三个中间件的执行顺序:
- 当执行
dispatch(0), Promise.resolve(fn(context, dispatch.bind(null,0+1)))
被执行 - 第一个中间件运行至
await next()
- 第二个中间件是,
next() = dispatch.bind(null, 0+1)
- 第二个中间件运行至
await next()
- 第三个中间件是,
next() = dispatch.bind(null, 1+1)
- 第三个中间件运行至
await next()
-
next() = dispatch.bind(null, 2+1)
, 没有第四个中间件,立即返回if(!fn) return Promise.resolve()
, 在第三个中间件中的await next()
被 resolved, 并执行第三个中间件剩下的代码 - 在第二个中间件中的
await next()
被resolve,并执行第二个中间件剩下的代码 - 在第一个中间件中的
await next()
被resolve,并执行第一个中间件剩下的代码
为什么使用洋葱模型?
如果在中间件中有async/await,编码会变得更加的简单。当我们想写一个针对api请求的时间记录器,将会是一件非常简单的事:
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // your API logic
const ms = Date.now() - start;
console.log('API response time:' + ms);
});
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。