本文围绕koa服务从启动,处处理请求再到回复响应这个过程对源码进行简单的解析node
在koa中ctx是贯穿整个请求过程的,它是此次请求原信息的承载体,能够从ctx上获取到request、response、cookie等,方便咱们进行后续的计算处理。 ctx在实现上本来就是一个空对象,在koa服务起来时,往上挂载了不少对象和方法。固然开发者也能够自定义挂载的方法。 在context.js
文件中对ctx初始化了一些内置的对象和属性,包括错误处理,设置cookie。git
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
复制代码
相对于这种写法,还有另一种较为优雅的挂载方法。github
// ./context.js
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
复制代码
delegate方法的做用是将其余对象的方法或者属性挂载在指定对象上,在这里就是proto,也就是最初的ctx对象,属性提供方就是第二个参数"response"。 method是代理方法,getter代理get,access代理set和get. 看delegate如何实现:json
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
复制代码
delegate中的method的实现是在调用原属性上指定方法时,转而调用提供方的方法。这里能够发现提供方也被收在了this上,这里的不直接传入一个对象而是将该对象赋值在原对象上的缘由,我想应该是存放一个副本在原对象上,这样能够经过原对象直接访问到提供属性的对象。数组
./context.js
中使用delegates为ctx赋值的过程并不完整,由于这里的属性提供方虽然是request和response, 可是是从./application.js
createContext方法中传入,这样delegates才算完成了工做promise
到这里咱们就能够看下平时用koa时常走的流程。服务器
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
复制代码
基本上就是分为三步,实例化Koa,注册中间件再监听端口, 这里正常能让koa服务或者说一个http服务起的来的操做实际上是在app.listen(...args)里,是否是和想象中的有点差距, 看下源码实现。cookie
// ./application.js
...
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
复制代码
在listen方法里使用了http模块的createServer方法来启动http服务,这里至关因而声明了一个http.Server实例,该实例也继承于EventEmitter,是一个事件类型的服务器,并监听了该实例的request事件,意为当客户端有请求发过来的时候,这个事件将会触发,等价于以下代码app
var http = require("http");
var server = new http.Server();
server.on("request", function(req, res){
// handle request
});
server.listen(3000);
复制代码
这个事件有两个参数req和res,也就是此次事件的请求和响应信息。有点扯远了,回到koa源码, 处理req和res参数的任务就交给了this.callback()的返回值来作,继续看callback里作了什么koa
// 去除了一些不影响主流程的处理代码
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
callback返回一个函数由他来处理req和res,这个函数内部作了两件事, 这两件事分别在koa服务的初始化和响应时期完成,上述代码中compose中间件就是在服务初始化完成, 而当request事件触发时,该事件会由callback返回的handleRequest方法处理,这个方法保持了对fn,也就是初始化事后中间件的应用, handleRequest先会初始化贯穿整个事件的ctx对象,这个时候就能够将ctx以此走入到各个中间件中处理了。
能够说koa到这里主流程已经走一大半了,让咱们理一理通过简单分析过的源码能够作到哪一个地步(忽略错误处理)
如上咱们已经能够作到将响应进入readly状态,但尚未返回响应的能力,后续会说道。在前三个过程当中有两个点须要注意,ctx和middleware,下面咱们依次深刻学习下这两个关键点。
ctx是贯穿整个request事件的对象,它上面挂载了如req和res这种描述该次事件信息的属性,开发者也能够根据本身喜爱,经过前置中间件挂载一些属性上去。 ctx在koa实例createContext方法上建立并被完善,再由callback返回的handleRequest也就是响应request的处理函数消费。看下createContext源码
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.state = {};
return context;
}
复制代码
前三行依次声明了context、request和response,分别继承于koa实例的三个静态属性,这三个静态属性由koa本身定义,在上面有一些快捷操做方法,好比在Request静态类上能够获取经过query获取查询参数,经过URL解析url等,能够理解为request的工具库,Response和Context同理。res和rep是node的原生对象,还记得吗,这两个参数是由http.Server()实例触发request事件带来的入参。 res是http.incomingMessage的实例而rep继承于http.ServerResponse, 贴一张图。
箭头指向说明了从属关系,有五个箭头指向ctx表面ctx上有五个这样的的属性,能够很清楚看到ctx上各个属性之间的关系。
接下来咱们再来看看koa中的中间件,在koa中使用use方法能够注册中间件.
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
复制代码
两件事,统一中间件格式,再将中间件推入中间件数组中。 在koa2.0之后middleware都是使用async/await语法,使用generator function也是能够的,2.0之后版本内置了koa-convert,它能够根据 fn.constructor.name == 'GeneratorFunction'
check here.来判断是legacyMiddleware仍是modernMiddleware,并根据结果来作相应的转换。 koa-convert的核心使用是co这个库,它提供了一个自动的执行器,而且返回的是promise,generator function有了这两个特性也就能够直接和async函数一块儿使用了。
回到koa源码来,callback中是这样处理中间件数组的
const fn = compose(this.middleware);
复制代码
这里的compose也就是koa-compose模块,它负责将全部的中间件串联起来,并保证执行顺序。经典的洋葱圈图:
koa-compose模块的介绍只有简单的一句话
Compose the given middleware and return middleware.
言简意赅,就是组合中间件。贴上源码
function compose (middleware) {
return function (context, next) {
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)
}
}
}
}
复制代码
compose先存入中间件数组,从第一个开始执行依次resolve到最后一个,中间件函数签名为(ctx,next)=>{}
,在内部调用next就会间接唤起下一个中间件,也就是执行dispatch.bind(null, i + 1)
,中间件执行顺序以下(网上扒下来的图)。
图上是遇到yield,和执行next同理。 也不是全部的中间件都须要next,在最后一个middleware执行完毕后能够不调用next,由于这个时候已经走完了全部中间件的前置逻辑。固然这里调用next也是能够的,是为了在全部前置逻辑执行完后有一个回调。咱们单独使用koa-compose:
const compose = require("koa-compose");
let _ctx = {
name: "ctx"
};
const mw_a = async function (ctx, next) {
console.log("this is step 1");
ctx.body = "lewis";
await next();
console.log("this is step 4");
}
const mw_b = async function (ctx, next) {
console.log("this is step 2");
await next();
console.log("this is step 3");
}
const fn = compose([mw_a, mw_b]);
fn(_ctx, async function (ctx) {
console.log("Done", ctx)
});
// =>
//
// this is 1
// this is 2
// Done {name: "ctx", body: "lewis"}
// this is 3
// this is 4
复制代码
compose返回的函数接受的参数不光是ctx,还能够接受一个函数做为走完全部中间件前置逻辑后的回调。有特殊需求的开发者能够关注一下。 固然整个中间件执行完后会返回一个resolve状态的promise,在这个回调中koa用来告诉客户端“响应已经处理完毕,请查收”,这个时候客户端才结束等待状态,这个过程的源码:
// handleRequest 的返回值
// 当中间件已经处理完毕后,交由handleResponse也就是respond方法来最后处理ctx
const handleResponse = () => respond(ctx);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
/** * 交付response */
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;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
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);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
复制代码
以上代码对各类款式的status和ctx.body作了相应的处理,最关键的仍是这一句res.end(body)
,它调用了node原生response的end方法,来告诉服务器本次的请求以回传body来结束,也就是告诉服务器此响应的全部报文头及报文体已经发出,服务器在此调用后认为这条信息已经发送完毕,而且这个方法必须对每一个响应调用一次。
至此,koa整个流程已经走通,能够看到koa的关键点集中在ctx对象和中间件的运用上。 经过delegate将原生res和req的方法属性代理至ctx上,再挂载koa内置的Request和Reponse,提供koa风格操做底层res和req的实现途径和获取请求信息的工具方法。 中间件则是使用koa-compose库将中间件串联起来执行,并具备能够逆回执行的能力。