前言
Koa
是由Express
团队打造的新的web server框架。相比于Express
,Koa
的特点如下:
- 体积更小。由于
Koa
不涉及路由以及其他中间件的捆绑,都通过额外的插件实现,使得Koa
本身的体积更小,更能体现“按需加载”的原则。 - 放弃回调。
Koa2
使用了ES6的aync
函数,可以通过await
来获取Promise
函数resolve()
的数据,不需要通过一个个回调才能进行异步处理,所以Koa2
可以用同步的写法实现异步的操作,并大大提高了错误处理的能力。 - 洋葱圈模型。与
express
按顺序执行中间件方式不同,Koa
采用洋葱圈模式处理中间件间的嵌套和执行顺序。请求会先进入到更外层的中间件,然后外层的中间件会等到内层的中间件执行完才会执行自己后续的逻辑,所以响应反而是从更内层的中间件开始发出,这种一层层嵌套的关系类似于洋葱层层包裹,所以称之为“洋葱圈模型”。
下面对Koa2
的分析,会和express
一起对比,从两者的差异点分析出Koa2
的特点和优点。
Koa2功能分析
首先通过Koa2
代码示例来分析一下Koa2
提供的功能:
Koa官网Hello world示例
const Koa = require('koa');
const app = new Koa();
// 1、日志打印
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// 2、计算响应时间
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// 3、请求response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
上述代码打印结果会是怎么样的呢?
// TODO
由上述代码和打印结果以及和Express
对比可看出,Koa2
实例主要有2个核心特点:
koa2
不涉及路由,所以和Express
不同的是,koa2
没有path和method处理的逻辑koa2
重点是中间件注册收集机制和洋葱圈next机制实现
代码实现
基于以上核心特点,下面实现一个简易的LikeKoa2
类。
1、类的基本结构
先确定这个类要实现的主要方法:
use()
:实现通用的中间件注册listen()
:实际上就是httpServer的listen()
函数,所以在这个类的listen()
函数里创建httpServer,透传server参数,监听请求,并执行回调函数(req, res) => {}
所以LikeKoa2
类基本结构如下:
class LikeKoa2 {
constructor() {
// 中间件存储的地方
this.middlewaraes = [];
}
// 注册中间件
use(fn) {
this.middlewaraes.push(fn);
return this; // 链式调用
}
callback() {
return (req, res) => {
// ...
}
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
2、洋葱圈next机制实现
koa2
的中间件函数是async
函数,它的参数为:ctx
, next
。我们先实现koa2
最核心的洋葱圈next机制。
next
参数是一个async
函数,只有调用它才可以一层又一层地优先调用下一个中间件函数,因此next
函数需要:
- 返回的必须是
Promise
函数 - 从中间件数组中取出第一个中间件,然后迭代地取出下一个中间件,直到里层的中间件一层一层从里向外执行完,从而实现洋葱圈执行的效果
// 组合中间件
function compose(middlewaraeList) {
return function (ctx) {
// 洋葱圈next机制核心方法
function dispatch(i) {
const fn = middlewaraeList[i];
try {
// 防止fn不是Promise
return Promise.resolve(
fn(ctx, dispatch.bind(null, i + 1)) // 实现next机制
);
} catch (e) {
return Promise.reject(e);
}
}
// 从第一个中间件开始从里向外执行
return dispatch(0);
}
}
3、创建上下文context
Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。 每个请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符。
createContext(req, res) {
const ctx = {
req,
res,
};
// ...more
// koa2的创建上下文的函数会更丰富一点,这里为了简化实现只封装了req和res
return ctx;
}
4、将上下文对象传入中间件函数中
上文我们已经通过compose
组合中间件函数返回了一个接收ctx
上下文参数的函数,所以只需要再建立一个handleRequest
函数,把创建好的上下文对象传入此函数即可。
// 把上下文传入中间件函数
handleRequest(ctx, fn) {
return fn(ctx);
}
因此,在callback
函数中:
callback() {
// 组合中间件
const fn = compose(this.middlewaraes);
return (req, res) => {
// 创建上下文对象
const ctx = this.createContext(req, res);
// 将上下文传入中间件
return this.handleRequest(ctx, fn);
}
}
测试
为了验证上述LikeKoa2
类是否实现了中间件注册收集以及洋葱圈next机制的功能,用一段代码验证:
const Koa = require('./like-koa2');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
console.log("第一层洋葱--开始")
await next();
const rt = ctx['X-Response-Time'];
console.log(`${ctx['method']} ${ctx['url']} - ${rt}`);
console.log("第一层洋葱--结束")
});
// x-response-time
app.use(async (ctx, next) => {
console.log("第二层洋葱--开始")
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx['X-Response-Time'] = `${ms}ms`;
console.log("第二层洋葱--结束")
});
// response
app.use(async ctx => {
console.log("第三层洋葱--开始")
ctx['body'] = 'Hello World';
console.log("第三层洋葱--结束")
});
app.listen(3010);
访问http://localhost:3010/,预期结果应该是开始是一、二、三层,结束是三、二、一层,实际结果:
可以看到,实际结果与预期结果相同,证明我们的实现是正确的。
完整代码
const http = require('http');
// 组合中间件
function compose(middlewaraeList) {
return function (ctx) {
function dispatch(i) {
const fn = middlewaraeList[i];
try {
// 防止fn不是Promise
return Promise.resolve(
fn(ctx, dispatch.bind(null, i + 1)) // 实现next机制
);
} catch (e) {
return Promise.reject(e);
}
}
return dispatch(0);
}
}
class LikeKoa2 {
constructor() {
// 中间件存储的地方
this.middlewaraes = [];
}
// 注册中间件
use(fn) {
this.middlewaraes.push(fn);
return this; // 链式调用
}
createContext(req, res) {
const ctx = {
req,
res,
};
return ctx;
}
// 把上下文传入中间件函数
handleRequest(ctx, fn) {
return fn(ctx);
}
callback() {
// 组合中间件
const fn = compose(this.middlewaraes);
return (req, res) => {
// 创建上下文对象
const ctx = this.createContext(req, res);
// 将上下文传入中间件
return this.handleRequest(ctx, fn);
}
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = LikeKoa2;
Koa2与Express对比
从上文的分析已知Koa2
和Express
的3个区别:
- 体积:
Koa2
不涉及路由以及其他中间件的捆绑,体积比Express
小 - 写法:
Koa2
使用async
函数 ,Express
使用Promise
回调 ,因此Koa2
可以避免回调,而且可以使用try catch
更方便地去处理错误异常 - 中间件机制:
Koa2
使用 洋葱圈模式 ,其核心实现思想是使用函数调用栈,先调用的后执行,直到里层函数一层一层由里向外执行完Express
核心思想是使用任务队列,先进队列的先取出执行,后面的任务进队等待,直到前面的任务都执行完后再执行 举个例子,加入我们要实现一个获取列表的接口,要求是更新一篇文章,更新成功后再发起其他动作。 使用Express
:
router.post('/update', function (req, res, next) {
const id = req.query.id;
const data = req.body;
const result = updateArticle(id, data);
return result.then(val => {
if (val) {
res.json(new SuccessModel());
} else {
res.json(ErrorModel("更新文章失败"));
}
}).catch(e => {
console.error(e);
}).then(() => { /** 其他动作 */ })
.then(() => {
//...
})
});
使用Koa2
:
router.post('/update', loginCheck, async function (ctx, next) {
const id = ctx.query.id;
const data = ctx.request.body;
try {
const result = await updateArticle(id, data);
if (result) {
ctx.body = new SuccessModel();
/** 其他动作 */
} else {
ctx.body = new ErrorModel("更新文章失败");
}
} catch (e) {
console.error(e);
}
})
可以看出:
- 对于结果获取:
Express
通过Promise
的resolve
的回调,获取resolve
得到的结果;而Koa2
通过await
一个async
函数,使用同步的写法实现异步的效果,写法更清晰 - 对于错误捕捉:
Express
对于每个callback
都要做错误捕捉,然后一层一层向外传递;而Koa2
可以使用一个try catch
就可以实现所有错误的捕捉