let middlewares = [m1, m2, m3]; let len = middlewares.length; // 最后一个中间件的next设置为一个立即resolve的promise函数 let next = async function () { return Promise.resolve(); } for (let i = len - 1; i >= 0; i--) { next = createNext(middlewares[i], next); } next(); // 输出m1, m2, m3
至此,我们也有了koa中间件机制实现的思路,新的application.js如下:
/**
* @file simpleKoa application对象
*/
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('.//response');
class Application {
/**
* 构造函数
*/
constructor() {
this.middlewares = [];
this.context = context;
this.request = request;
this.response = response;
}
// ...省略中间
/**
* 中间件挂载
* @param {Function} middleware 中间件函数
*/
use(middleware) {
this.middlewares.push(middleware);
}
/**
* 中间件合并方法,将中间件数组合并为一个中间件
* @return {Function}
*/
compose() {
// 将middlewares合并为一个函数,该函数接收一个ctx对象
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}
/**
* 获取http server所需的callback函数
* @return {Function} fn
*/
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let fn = this.compose();
return fn(ctx).then(respond);
};
}
// ...省略后面
}
module.exports = Application;
可以看到,首先对app.use进行改造了,每次调用app.use,就向this.middlewares中push一个回调函数。然后增加了一个compose()方法,利用我们前文分析的原理,对middlewares数组中的函数进行组装,返回一个最终的函数。最后,在callback()方法中,调用compose()得到最终回调函数,并执行。
改写example.js验证一下中间件机制:
let simpleKoa = require('./application'); let app = new simpleKoa(); let responseData = {}; app.use(async (ctx, next) => { responseData.name = 'tom'; await next(); ctx.body = responseData; }); app.use(async (ctx, next) => { responseData.age = 16; await next(); }); app.use(async ctx => { responseData.sex = 'male'; }); app.listen(3000, () => { console.log('listening on 3000'); }); // 返回{ name: "tom", age: 16, sex: "male"}
例子中一共三个中间件,分别对responseData增加了name, age, sex属性,最后返回该数据。
至此,一个koa框架基本已经浮出水面了,不过我们还需要进行最后一个主线的分析:错误处理。
主线四:错误处理
一个健壮的框架,必须保证在发生错误的时候,能够捕获错误并有降级方案返回给客户端。但显然现在我们的框架还做不到这一点,假设我们修改一下例子,我们的中间件中,有一个发生错误抛出了异常:
let simpleKoa = require('./application'); let app = new simpleKoa(); let responseData = {}; app.use(async (ctx, next) => { responseData.name = 'tom'; await next(); ctx.body = responseData; }); app.use(async (ctx, next) => { responseData.age = 16; await next(); }); app.use(async ctx => { responseData.sex = 'male'; // 这里发生了错误,抛出了异常 throw new Error('oooops'); }); app.listen(3000, () => { console.log('listening on 3000'); });
这个时候访问浏览器,是得不到任何响应的,这是因为异常并没有被我们的框架捕获并进行降级处理。回顾我们application.js中的中间件执行代码:
// application.js // ... callback() { return (req, res) => { let ctx = this.createContext(req, res); let respond = () => this.responseBody(ctx); let fn = this.compose(); return fn(ctx).then(respond); }; } // ...
其中我们知道,fn是一个async函数,执行后返回一个promise,回想promise的错误处理是怎样的?没错,我们只需要定义一个onerror函数,里面进行错误发生时候的降级处理,然后在promise的catch方法中引用这个函数即可。
于此同时,回顾koa框架,我们知道在错误发生的时候,app对象可以通过app.on('error', callback)订阅错误事件,这有助于我们几种处理错误,比如打印日志之类的操作。为此,我们也要对Application对象进行改造,让其继承nodejs中的events对象,然后在onerror方法中emit错误事件。改造后的application.js如下:
/** * @file simpleKoa application对象 */ let EventEmitter = require('events'); let http = require('http'); let context = require('./context'); let request = require('./request'); let response = require('./response'); class Application extends EventEmitter { /** * 构造函数 */ constructor() { super(); this.middlewares = []; this.context = context; this.request = request; this.response = response; } // ... /** * 获取http server所需的callback函数 * @return {Function} fn */ callback() { return (req, res) => { let ctx = this.createContext(req, res); let respond = () => this.responseBody(ctx); let onerror = (err) => this.onerror(err, ctx); let fn = this.compose(); // 在这里catch异常,调用onerror方法处理异常 return fn(ctx).then(respond).catch(onerror); }; } // ... /** * 错误处理 * @param {Object} err Error对象 * @param {Object} ctx ctx实例 */ onerror(err, ctx) { if (err.code === 'ENOENT') { ctx.status = 404; } else { ctx.status = 500; } let msg = err.message || 'Internal error'; ctx.res.end(msg); // 触发error事件 this.emit('error', err); } } module.exports = Application;