Koa中的中间件与Express的不同,Koa使用洋葱模型。 Koa的中间件仅包含四个文件,今天我们只看主要文件-application.js
。 它已经包含了中间件如何工作的核心逻辑。
git clone [email protected]: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: 初始化中间件列表
this.middleware = [];
}
use(fn) {
// Step 1: 将中间件添加到列表中
this.middleware.push(fn);
return this;
}
listen(...args) {
debug('listen');
// Step 2: 使用 this.callback() 组成所有中间件
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// Step 3: 这是最重要的部分-compose,将所有内容分组
// 中间件到一个大 function 并返回 promise ,我们将继续看
// 关于这个函数
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 这个 promise
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
有关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 :
const promise = new Promise(function(resolve, reject) {
if (success){
resolve(value);
} else {
reject(error);
}
});
在Koa中,我们可以这样用:
let testPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test success');
}, 1000);
});
Promise.resolve(testPromise).then(function (value) {
console.log(value); // "test success"
});
因此,我们知道在compose函数中,它会返回一个promise。
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
中,我们有3个中间件,所有3个中间件将在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`);
});
我们可以看看index.js
中这三个中间件的执行顺序:
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()
,执行第三中间件中的其余代码。await next()
,执行第二个中间件中的剩余代码。next()
被解析,第一个中间件中的剩余代码被执行。如果我们在中间件中有 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);
});