虽然express.js有着精妙的中间件设计,但是以当前js标准来说,这种精妙的设计在现在可以说是太复杂。里面的层层回调和递归,不花一定的时间还真的很难读懂。
而koa2的代码呢?简直可以用四个字评论:精简彪悍!仅仅几个文件,用上最新的js标准,就很好实现了中间件,代码读起来一目了然。
1.express用法和koa用法简单展示
如果你使用express.js启动一个简单的服务器,那么基本写法应该是这样:
const express = require('express');
const app = express();
const router = express.Router();
app.use(async (req, res, next) => {
console.log('I am the first middleware');
next();
console.log('first middleware end calling');
})
app.use(async (req, res, next) => {
console.log('I am the second middleware');
next();
console.log('second middleware end calling');
})
router.get('/api/test1', async (req, res, next) => {
console.log('I am the router middleware => /api/test1');
res.status(200).send('hello');
})
app.use('/', router);
app.listen(3000);
console.log('server listening at port 3000')
换算成等价的koa2,那么用法是这样的:
const koa = require('koa');
const Router = require('koa-router');
const app = new koa();
const router = Router();
app.use(async (ctx, next) => {
console.log('I am the first middleware')
await next()
console.log('first middleware end calling')
})
app.use(async (ctx, next) => {
console.log('I am the second middleware')
await next()
console.log('second middleware end calling')
})
router.get('/api/test1', async(ctx, next) => {
console.log('I am the router middleware => /api/test1')
ctx.body = 'hello'
})
app.use(router.routes());
app.listen(3000);
console.log('server listening at port 3000')
二者的使用区别通过表格展示如下:
上表展示了二者的使用区别,从初始化就看出koa语法都是用的新标准。在挂载路由中间件上也有一定的差异性,这是因为二者内部实现机制的不同。其他都是大同小异的了。
在理念上,Koa 旨在 “修复和替换节点”,而 Express 旨在 “增加节点”。 Koa 使用Promise(JavaScript一种异步手段)和异步功能来摆脱回调地狱的应用程序,并简化错误处理。 它暴露了自己的 ctx.request 和 ctx.response 对象,而不是 node 的 req 和 res 对象。
另一方面,Express 通过附加的属性和方法增加了 node 的 req 和 res 对象,并且包含许多其他 “框架” 功能,如路由和模板,而 Koa 则没有。
因此,Koa 可被视为 node.js 的 http 模块的抽象,其中 Express 是 node.js 的应用程序框架。
因此,如果您想要更接近 node.js 和传统的 node.js 样式编码,那么您可能希望坚持使用Connect/Express 或类似的框架。 如果你想摆脱回调,请使用 Koa。
由于这种不同的理念,其结果是传统的 node.js “中间件”(即“(req,res,next)”的函数)与Koa不兼容。 你的应用基本上要重新改写了。
Koa 与 Connect/Express 有哪些不同?
基于 Promises 的控制流程
没有回调地狱。
通过 try/catch 更好的处理错误。
无需域。
Koa 非常精简
不同于 Connect 和 Express, Koa 不含任何中间件.
不同于 Express, 不提供路由.
不同于 Express, 不提供许多便捷设施。 例如,发送文件.
Koa 更加模块化.
Koa 对中间件的依赖较少
例如, 不使用 “body parsing” 中间件,而是使用 body 解析函数。
Koa 抽象 node 的 request/response
减少攻击。
更好的用户体验。
恰当的流处理。
Koa 路由(第三方库支持)
由于 Express 带有自己的路由,而 Koa 没有任何内置路由,但是有 koa-router 和 koa-route 第三方库可用。同样的, 就像我们在 Express 中有 helmet 保证安全, 对于 koa 我们有 koa-helmet 和一些列的第三方库可用
2.koa2中间件
看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。
2.1阅读koa-compose源码
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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];
// next的值为undefined,当没有中间件的时候直接结束
// 其实这里可以去掉next参数,直接在下面fn = void 0,和之前的代码效果一样
// if (i === middleware.length) fn = void 0;
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
// fn为当前执行的中间件函数
// 当前中间件函数执行时传入的next参数为下一个中间件
// 当所有的中间件都执行完毕时, 当前中间件传入的next参数是请求处理的回调
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
上面的代码等价于
// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
也就是说koa-compose返回的是一个Promise,Promise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。
第一个next函数里也是返回的是一个Promise,Promise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。
第二个next函数里也是返回的是一个Promise,Promise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。
第三个…
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。
这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。
3.koa2 和 koa1 的简单对比
koa1中主要是generator函数。koa2中会自动转换generator函数。
app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
koa-convert源码挺多,核心代码其实是这样的。
function convert(){
return function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
function * createGenerator (next) {
return yield next()
}
}
最后还是通过co来转换的。所以接下来看co的源码。
// 写一个请求简版请求
function request(ms= 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({name: '若川'});
}, ms);
});
}
// 获取generator的值
function* generatorFunc(){
const res = yield request();
console.log(res, 'generatorFunc-res');
}
generatorFunc(); // 报告,我不会输出你想要的结果的
简单来说co,就是把generator自动执行,再返回一个promise。
generator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步。
所以有了async、await函数。
// await 函数 自动执行
async function asyncFunc(){
const res = await request();
console.log(res, 'asyncFunc-res await 函数 自动执行');
}
asyncFunc(); // 输出结果
也就是说co需要做的事情,是让generator向async、await函数一样自动执行。
最终来看下co源码
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// 把参数传递给gen函数并执行
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 如果不是函数 直接返回
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
// 反复执行调用自己
function next(ret) {
// 检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
// 确保返回值是promise对象。
var value = toPromise.call(ctx, ret.value);
// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
总结
koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise。
koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。
co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。
koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。
koa洋葱模型怎么实现的。
app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数ctx是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise。
如果中间件中的next()方法报错了怎么办。
ctx.onerror = function {
this.app.emit('error', err, this);
};
listen(){
const fnMiddleware = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
onerror(err) {
// 代码省略
// ...
}
中间件链错误会由ctx.onerror捕获,该函数中会调用this.app.emit(‘error’, err, this)(因为koa继承自events模块,所以有’emit’和on等方法),可以使用app.on(‘error’, (err) => {}),或者app.onerror = (err) => {}进行捕获。
co的原理是怎样的。
co的原理是通过不断调用generator函数的next方法来达到自动执行generator函数的,类似async、await函数自动执行。