我们从以下几个方面来学习Koa
- 创建应用程序函数
- 扩展res和req
- 中间件实现原理
创建应用程序函数
Koa 是依赖 node 原生的 http 模块来实现 http server 的能力,原生 http 模块可以通过几行代码就启动一个监听在 8080 端口的http服务,createServer 的第一个参数是一个回调函数,这个回调函数有两个参数,一个是请求对象,一个是响应对象,可以根据请求对象的内容来决定响应数据的内容;
const http = require("http");
const server = http.createServer((req, res) => {
// 每一次请求处理的方法
console.log(req.url);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello NodeJS");
});
server.listen(8080);
在 Koa 中,createServer 回调函数中的 req 和 res 会被保存到 ctx 对象上,伴随整个处理请求的生命周期,Koa 源码中的 request.js 和 response.js 就是对这两个对象添加了大量便捷获取数据和设置数据的方法,如获取请求的方法、请求的路径、设置返回数据体、设置返回状态码等操作。
而Koa在封装创建应用程序的方法中主要执行了以下流程:
- 组织中间件(监听请求之前)
- 生成context上下文对象
- 执行中间件
- 执行默认响应方法或者异常处理方法
// application.js
// 这个方法是封装了http模块提供的http.createServer和listen方法
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
//组织中间件,在监听请求之前完成的
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
//创建context上下文对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默认状态码为404
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 执行中间件
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// 创建context上下文对象
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;
}
扩展res和req
NodeJS中原生的res和req是http.IncomingMessage和http.ServerResponse的实例,Koa中则是自定义request和response对象,保持对原生的res和req引用,然后通过getter和setter方法实现扩展。
// application.js
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; // 保存原生 req 对象
context.res = request.res = response.res = res; // 保存原生 res 对象
request.ctx = response.ctx = context;
request.response = response; // response 拓展
response.request = request; // request 拓展
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 最终返回完整的context上下文对象
return context;
}
在Koa中要区别这两组对象:
- request、response: Koa扩展的对象
- res、req: NodeJS原生对象
// request.js
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
}
此时已经可以采用这样的方式访问header属性:
ctx.request.header
delegates 属性代理
koa对response和request使用了属性代理,使我们可以直接在context中使用request和response的方法,其中method方法是委托方法,getter方法用来委托getter,access方法委托getter+setter
// context.js
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
delegates 实现
对于 setter 和 getter方法,是通过调用对象上的 __defineSetter__ 和 __defineGetter__ 来实现的
// delegates/index.js
// getter
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
// setter
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
// access
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
// method
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;
};
中间件实现原理
在Koa中,通过app.use() 来注册中间件,Koa支持三种不同类型的中间件:普通函数,async 函数,Generator函数,如果是Generator函数,那就用 convert 把函数包起来,然后在push到 this.middleware
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
convert作用
convert是用于将koa中以前基于generator写法的中间件转为基于promise写法。convert()的源码实现逻辑如下:
- convert 方法首先判断传入的中间件是否是一个函数,如果不是就抛出异常;
- 接着判断是否是一个 generator 函数,如果不是就直接返回,不做处理;
利用co将 generator 函数形式的中间件转成 promise 形式的中间件。
function convert (mw) { if (typeof mw !== 'function') { throw new TypeError('middleware must be a function') } // assume it's Promise-based middleware if ( mw.constructor.name !== 'GeneratorFunction' && mw.constructor.name !== 'AsyncGeneratorFunction' ) { return mw } const converted = function (ctx, next) { return co.call( ctx, mw.call( ctx, (function * (next) { return yield next() })(next) )) } converted._name = mw._name || mw.name return converted }
中间件的执行
Koa中间件的执行流程主要通过koa-compose中的compose函数完成,基于洋葱圈模型:
koa-compose 的代码很短,一共才不到50行,主要执行顺序如下:// koa-compose function compose (middleware) { //传入middleware数组 // 不是数组抛出异常 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') // 判断每个middleware中的每一项是否为函数 // 不是函数抛出异常 for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } // 返回一个函数 return function (context, next) { //index计数 let index = -1 return dispatch(0) //调用dispatch,从第一个中间件开始 function dispatch (i) { // i小于index,证明在中间件内调用了不止一次的next(),抛出错误 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 更新index的值 let fn = middleware[i] //middleware中的函数,从第i个开始 if (i === middleware.length) fn = next //如果i走到最后一个的后面,就让fn为next,此时fn为undefined if (!fn) return Promise.resolve()// 那么这时候就直接resolve try { // 把下一个中间件作为当前中间件的next传入 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }