最近读了一下Koa2的源码;在阅读Koa2 (2.3.0) 的源码的过程当中,个人感觉是整个代码设计精巧,思路清晰,是一个小而精的 nodejs web服务框架。html
做为web服务框架,都是要围绕核心服务而展开的。那什么是核心服务呢?其实就是接收客户端的一个http的请求,对于这个请求,除了接收之外,还有解析这个请求。因此说会有node
HPPT:接收 -> 解析 -> 响应web
在响应客户端的时候,也有不少种方式,好比返回一个html页面,或者json文本。在解析请求和响应请求的中间,会有一些第三方的中间件,好比 日志、表单解析等等来加强 koa 的服务能力,因此 koa 至少要提供 "请求解析"、"响应数据"、"中间件处理" 这三种核心能力的封装,同时还须要有一个串联他们执行环境的上下文(context)json
上下文能够理解为是http的请求周期内的做用域环境来托管请求响应和中间件,方便他们之间互相访问。后端
以上分析是站在单个http请求的角度来看一个web服务能力。那么站在整个网站,站在整个后端服务的角度来看的话,可以提供 "请求"、"响应"、"解析"、"中间件"、"http流程全链路" 这些服务能力的综合体,能够看作是一个应用服务对象。若是把这些全放到 koa 里的话,那么对应的就是:api
首先看下koa的目录结构数组
// application.js
const isGeneratorFunction = require('is-generator-function'); // 判断当前传入的function是不是标准的generator function
const debug = require('debug')('koa:application'); // js调试工具
const onFinished = require('on-finished'); // 事件监听,当http请求关闭,完成或者出错的时候调用注册好的回调
const response = require('./response'); // 响应请求
const compose = require('koa-compose'); // 中间件的函数数组
const isJSON = require('koa-is-json'); // 判断是否为json数据
const context = require('./context'); // 运行服务上下文
const request = require('./request'); // 客户端的请求
const statuses = require('statuses'); // 请求状态码
const Cookies = require('cookies');
const accepts = require('accepts'); // 约定可被服务端接收的数据,主要是协议和资源的控制
const Emitter = require('events'); // 事件循环
const assert = require('assert'); // 断言
const Stream = require('stream');
const http = require('http');
const only = require('only'); // 白名单选择
const convert = require('koa-convert'); // 兼容旧版本koa中间件
const deprecate = require('depd')('koa'); // 判断当前在运行koa的某些接口或者方法是否过时,若是过时,会给出一个升级的提示
复制代码
以上是koa入口文件的依赖分析。接下来咱们进行源码分析,首先咱们利用删减法来筛出代码的核心实现便可,不用上来就盯细节! 咱们只保留constructorbash
// application.js
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false; // 是否信任 proxy header 参数,默认为 false
this.middleware = []; //保存经过app.use(middleware)注册的中间件
this.subdomainOffset = 2; // 子域默认偏移量,默认为 2
this.env = process.env.NODE_ENV || 'development'; // 环境参数,默认为 NODE_ENV 或 ‘development’
this.context = Object.create(context); //context模块,经过context.js建立
this.request = Object.create(request); //request模块,经过request.js建立
this.response = Object.create(response); //response模块,经过response.js建立
}
// ...
}
复制代码
咱们能够看到,这段代码暴露出一个类,构造函数内预先声明了一些属性,该类继承了Emitter,也就是说这个类能够直接为自定义事件注册回调函数和触发事件,同时能够捕捉到其余地方触发的事件。cookie
除了这些基本属性以外,还有一些公用的api,最重要的两个一个是==listen==,一个是==use==。koa的每一个实例上都会有这些属性和方法。app
// application.js
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen() {
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
use(fn) {
this.middleware.push(fn);
return this;
}
}
复制代码
listen 方法内部经过 http.createServer 建立了一个http服务的实例,经过这个实例去 listen 要监听的端口号,http.createServer 的参数传入了 this.callback 回调
// application.js
module.exports = class Application extends Emitter {
...
callback() {
const fn = compose(this.middleware); // 把全部middleware进行了组合,使用了koa-compose
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); // 返回了自己的回调函数
};
return handleRequest;
}
}
复制代码
能够看到,handleRequest 返回了自己的回调,接下来看 handleRequest 。
handleRequest 方法直接做为监听成功的调用方法。已经拿到了 包含 req res 的 ctx 和能够执行全部中间件函数的 fn。 首先一进来默认设置状态码为==404== . 而后分别声明了 成功函数执行完成之后的成功 失败回调方法。这两个方法实际上就是再将 ctx 分化成 req res。 分别调这两个对象去客户端执行内容返回。 ==context.js request.js response.js== 分别是封装了一些对 ctx req res 操做相关的属性,咱们之后再说。
// application.js
module.exports = class Application extends Emitter {
...
handleRequest(ctx, fnMiddleware) {
const res = ctx.res; // 拿到context.res
res.statusCode = 404; // 设置默认状态吗404
const onerror = err => ctx.onerror(err); // 设置onerror触发事件
const handleResponse = () => respond(ctx); // 向客户端返回数据
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
复制代码
失败执行的回调
onerror(err) {
assert(err instanceof Error, `non-error thrown: ${err}`);
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
复制代码
成功执行的回调
function respond(ctx) {
...
}
复制代码
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
咱们拆分理解,首先 return fnMiddleware(ctx) 返回了一个中间件数组处理链路,then(handleResponse) 等到整个中间件数组所有完成以后把返回结果经过 then 传递到 handleResponse。
// application.js
module.exports = class Application extends Emitter {
...
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
}
复制代码
这里咱们不用去太深刻去抠代码,理解原理就行。createContext 建立 context 的时候,还会将 req 和 res 分别挂载到context 对象上,并对req 上一些关键的属性进行处理和简化 挂载到该对象自己,简化了对这些属性的调用。咱们经过一张图来直观地看到全部这些对象之间的关系。
createContext 简单理解就是挂载上面的对象,方便整个上下游http能及时访问到进出请求及特定的行为。
// application.js
module.exports = class Application extends Emitter {
...
}
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status; // 赋值服务状态码
if ('HEAD' == ctx.method) { // 请求头方法判断
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
body = ctx.message || String(code);
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// 经过判断body类型来调用,这里的res.end就是最终向客户端返回数据的动做
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// 返回为json数据
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
复制代码
respond 函数是 handleRequest 成功处理的回调,内部作了合理性校验,诸如状态码,内容的类型判断,最后向客户端返回数据。
以上就是咱们对application.js文件的分析,经过上面的分析,咱们已经能够大概得知Koa处理请求的过程:当请求到来的时候,会经过 req 和 res 来建立一个 context (ctx) ,而后执行中间件。