洋葱模型
compose函数实现了洋葱模型。主要的思想是递归的调用的dispatch函数dispatch函数在内部是返回一个promise对象,这个promise对象resolve出去一整个‘洋葱’,中间件函数串起来的promise对象。于是,如果在中间件中调用next的时候不使用异步函数async/await处理的话,在内部就会把调用的下一个中间件当作是异步函数处理,就会先去执行后面的代码,导致结果出现问题。
function compose (middleware) {
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 {
//fn = (ctx,next) => {ctx.body = "Hello, world" next()}
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
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);
}
错误处理
context.onerror
onerror(err) {
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
// When dealing with cross-globals a normal `instanceof` check doesn't work properly.
// See https://github.com/koajs/koa/issues/1466
// We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
return;
}
const { res } = this;
// first unset all headers
/* istanbul ignore else */
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// then set those specified
this.set(err.headers);
// force text/plain
this.type = 'text';
let statusCode = err.status || err.statusCode;
// ENOENT support
if ('ENOENT' === err.code) statusCode = 404;
// default to 500
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
这一块做了两件事:处理了发生error时ctx的情况,和把err emit给application进行处理。
错误处理这一块是调用ctx里的onerror函数,这个函数接受一个err参数,然后通过this.app.emit('error', err, this);把err发射给app里 。当然,这部分是在fnMiddleware里返回的promise对象里的catch完成的
ctx总结
callback() {
//fn是一个promise包裹的对象
//fne接受两个参数(context, next)
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必须要有node原生的req和res以及自己封装的request和response,这样他才能去里面做逻辑做中间件里的业务。首先看看context,request,response里面做了哪些事
createContext(req, res) {
const context = Object.create(this.context);
//把request.js和response.js里的方法导出来放到copntext里,好在context里把ctx代理到response和request上
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.createContextapp = response.app = this;
//把原生的req和res给到koa封装的request和response对象里
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.js里可以看出来,导出了一些delegate的方法。、导出去了一个proto,这个proto就是createContext里的context。proto主要是下面两个代理,代理了response和request。这样一来,我们对上下文 ctx 的操作,如 ctx.type 和 ctx.length 就会由 response 对象执行,ctx.path 和 ctx.method 就会由 request 对象执行。 response 和 request 是 Koa 自己的请求和响应。
//context.js
const delegate = ("delegates");
module.exports = {
inspect() {},
toJSON() {},
throw() {},
onerror() {},
};
const proto = module.exports;
delegate(proto, "response")
.method("set")
.method("append")
.access("message")
.access("body");
delegate(proto, "request")
.method("acceptsLanguages")
.method("accepts")
.access("querystring")
.access("socket");
真正将请求与响应的操作落实到位的不是上下文 ctx ,而是来自 request.js 的 request 对象和来自 response.js 的response 对象。我们看一下这两个对象的实现。
/* request.js */
module.exports = {
/* ... */
/**
* Get request URL.
*
* @return {String}
* @api public
*/
get url() {
return this.req.url;
},
/* ... */
}
/* response.js */
module.exports = {
/* ... */
/**
* Check if a header has been written to the socket.
*
* @return {Boolean}
* @api public
*/
get headerSent() {
return this.res.headersSent;
},
/* ... */
}
是靠 Koa 请求/响应去操作 Node 请求/响应来实现的!整个流程串起来就是,上下文 ctx 委托给 Koa 请求/响应,Koa 请求/响应操作 Node 请求/响应,从而实现了完整的请求/响应处理流程。