Open tomoya06 opened 4 years ago
本文所用的框架是koa2,它跟koa1不同,koa1使用的是generator+co.js的执行方式,而koa2中使用了async/await,因此本文的代码和demo需要运行在node 8版本及其以上。
源码GitHub链接在这,代码量不大,主要包括application.js/context.js/request.js/response.js
四个文件。
实现koa的服务器应用和端口监听,其实就是基于node的原生代码进行了封装:
class Application extends Emitter {
// ....
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// ....
}
request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象;context的作用就是将request、response对象挂载到ctx的上面,让koa实例和代码能方便的使用到request、response对象中的方法。
当前版本的Koa2使用的是delegates模块完成request/response中属性的读取设置。
然后在application.js中使用createContext(req, res)
方法把req/res挂载到ctx。
首先是中间件的语法:
let Koa = require('../src/application');
let app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
app.listen(3000, () => {
console.log('listenning on 3000');
});
首先是use的实现,在application.js中可以看到,use方法就只是把所有的中间件方法丢到middleware队列中而已.
定义Koa类的时候在http.createServer(this.callback())
中调用了callback
方法。定义如下:
const compose = require('koa-compose');
//...
class Application extends Emitter {
callback() {
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);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
看到compose方法对middleware队列做了一些处理。参考koa/compose库的定义:
function compose (middleware) {
// ...检查middleware是否为队列,以及是否每一项都是function
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函数了,它将遍历整个middleware,然后将context
和dispatch(i + 1)
传给middleware
中的方法:
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
这段代码就很巧妙的实现了两点:
context
一路传下去给中间件middleware
中的下一个中间件fn
作为未来next
的返回值因此在中间件定义中,await next()
这一方法就会触发下一个中间件的调用,递归执行完之后再返回本中间件。
Node.js主流框架比较
一、 Express vs Koa
因为Egg是基于Koa开发的(Egg 1.x 基于Koa 1.x,Egg 2.x 基于Koa 2.x),所以Egg会继承Koa2对比Express的所有优点。
1. 架构设计
Express是一个集合式的框架,自身集成了router,static,views,bodyparse等等常用中间件。让开发者能非常快速的搭建一个node后端服务,但是使用Express就必须引入全部的中间件和功能。
Koa2的设计思想就是小而美,轻量,插件化设计。只提供最基础的框架,所有功能都通过中间件引入。Koa将Express集成的中间件进行拆分,开发者按需引入即可使用,也可以选择社区或第三方开发的中间件,甚至自己写一个都可以。
2. ECMAScript版本
Express是基于ES5开发的,受限制于当时的JavaScript标准,很多功能的实现方式不是很好。比如异步执行只能采用回调函数的方式。
Koa2是基于ES6(ES2015)开发的,原生支持异步函数 async/await,使用Promise作为异步处理的标准。Promise的异步操作使用async/await的写法更简单整洁。
3. 中间件执行机制
中间件的执行机制可以说是Koa相较Express最大的改进。
Express的中间件时线性执行的,每一个中间件处理完后只有两个选择:交给下一个中间件或者返回Response。只要请求离开了,中间件就无法再次获得这个请求,更不能再对它进行处理。
Koa的中间件机制是洋葱模型,中间件像一层层的洋葱。请求要穿过洋葱,每个中间件也会被穿过两次。
以统计一个请求耗时时间的需求为例,koa的洋葱模型实现起来就非常方便,express需要开发者在每一个中间件上都添加回调函数,非常复杂。
4. 异常处理
由于中间件执行机制的差异,导致两者异常处理的方式有很大不同。
Express的异常处理是有一个error处理中间件完成的,由于Express的中间件时线性流程,所以要处理错误信息就必须把error中间件放到最后。并且只要有错误,就需要手动调用error中间件。
Koa的异步处理是基于Promise的,而且Koa总是将next()包装在Promise中,所以我们不用关心是同步错误还是异步错误。另外最重要的是:Koa的错误处理中间件是在最顶端的。
5. context封装
express没有context封装,每层中间件都以传参方式接收 req 和 res。
Koa对context(上下文)进行了封装,使用 ctx.request 和 ctx.response 来代理 req 和 res。一个ctx同时封装了 req 和 res,在中间件中也能随时操作request 和 response 。
二、Egg VS Koa2
Egg是基于Koa开发的,底层基本上都是相同的。但是Egg对Koa做了大量的改进以使用企业级应用。
1. 设计理念
Koa是简洁轻量的框架,功能都是通过中间件引入。Egg没有遵循Koa的设计,内置了很多常用模块,比如路由,Config,Logger,定时任务等。
2. 约定
对文件目录结构有约定。比如中间件要放到middleware目录下,配置都放到config下等。
实际开发时发现,Egg会对middleware/service/controller等文件自动生成typescript的d.ts声明文件,在vscode中有自动补全提示,所以对文件路径有约定要求。
3. 新概念和改动
Egg引入了Controller,Service 概念,很明显借鉴了Java的设计思想。
另外Egg还对Koa做了很多小改动。比如获取运行环境要从app.config.env获取;使用egg-bin启动本地测试环境,使用 egg-scripts 启动线上环境。
4. 内置功能
安全
内置对XXS,SSRF,CSRF等攻击的防御。提供IP白名单机制,钓鱼攻击的防御方案。
其他
内置单元测试、日志、多进程、异常处理等中间件。也提供了可选的mysql、session、redis中间件。
总结
2. 生态稳定,第三方库较多
2. 事件处理和中间件机制都基于回调,开发不方便
2. 按需引用,自由选择中间件
2. 内置常用插件,满足安全、日志、多进程、多配置等要求
3. 引入了controller/service等Java开发概念,配合TypeScript可以实现类Java的开发体验,提高项目稳定性
2. 内置模块较多,更适用于企业级项目
3. 需要理解强类型语言的设计思想