最近空闲时间读了一下Koa2的源码;在阅读Koa2(version 2.2.0)的源码的过程当中,个人感觉是代码简洁、思路清晰(不得不佩服大神的水平)。
下面是我读完以后的一些感觉。node
Koa 是一个轻量级的、极富表现力的 http 框架。
一个web request会经过 Koa 的中间件栈,来动态完成 response 的处理。
Koa2 采用了 async 和 await 的语法来加强中间件的表现力。
Koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库。git
Koa源码很是精简,只有四个文件:es6
// 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建立 } // ... }
application.js 是 koa 的入口主要文件,暴露应用的 class, 这个 class 继承自 EventEmitter ,这里能够看出跟 koa1.x 的不一样,koa1.x 是用的是构造函数的方式,koa2 大量使用 es6 的语法。调用的时候就跟 koa1.x 有区别github
var koa = require('koa'); // koa 1.x var app = koa(); // koa 2.x // 使用class必须使用new来调用 var app = new koa();
application.js除了上面的的构造函数外,还暴露了一些公用的api,好比两个常见的,一个是listen
,一个是use
。web
// application.js 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; }
use
函数作的事很简单:注册一个中间件fn
,其实就是将fn
放入middleware
数组。json
// application.js listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
listen
方法首先会经过this.callback
方法来返回一个函数做为http.createServer
的回调函数,而后进行监听。咱们已经知道,http.createServer
的回调函数接收两个参数:req
和res
,下面来看this.callback
的实现:api
// application.js callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest; }
首先,callback
方法把全部middleware
进行了组合,使用了koa-compose
,咱们来看一下koa-compose
的代码:数组
// 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) { 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] //下面两行代码是处理最后一个中间件还有next的状况的,其实就是直接resolve出来 if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { // 这里就是传入next执行中间件代码了 return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
能够看到koa-compose
基本就是个dispatch
函数的递归调用。其中最重要的就是下面这段代码:cookie
return Promise.resolve(fn(context, function next () { return dispatch(i + 1) }))
这段代码等价于:app
fn(context, function next () { return dispatch(i + 1) }) return Promise.resolve()
这里middlewareFunction
的第二个参数(也就是next)是动态传递进去的信使,它会调取dispatch(index)
执行下一个的middleware
。最后会返回一个Resolved
(已完成)状态的Promise对象。这个对象的做用咱们稍后再说。
咱们先暂时回到callback
方法里面,前面说了它先对middleware
进行了组合,生成了一个函数fn
。
而后,callback
方法返回http.createServer
所须要的回调函数handleRequest
。
handleRequest
函数,先把http code默认设置为404,接着利用createContext
函数把node返回的req和res进行了封装建立出context
,
而后经过onFinished(res, onerror)
监听http response
,当请求结束时执行回调。这里传入的回调是context.onerror(err)
,即当错误发生时才执行。
最后返回 fn(ctx).then(handleResponse).catch(onerror)
的执行结果,这里的fn
函数就是就是组合全部middleware
后生成的函数,调用它执行全部middleware
后会返回前面提到的Resolved
(已完成)状态的Promise对象,以后执行响应处理函数respond(ctx)
(respond
函数里面也主要是一些收尾工做,例如判断http code为空如何输出啦,http method是head如何输出啦,body返回是流或json时如何输出;代码就不贴了,感兴趣的小伙伴本身能够去看一下),当抛出异常时一样使用 context.onerror(err)
处理。
咱们能够看看createContext
函数
// 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; 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
的时候,还会同时建立request和response,经过下图能够比较直观地看到全部这些对象之间的关系。
图中:
经过上面的分析,咱们已经能够大概得知Koa处理请求的过程:当请求到来的时候,会经过 req 和 res 来建立一个 context (ctx) ,而后执行中间件。
content.js 主要的功能提供了对request
和response
对象的方法与属性便捷访问能力。
其中使用了node-delegates(有兴趣的能够看一下源码),将context.request
与context.response
上的方法与属性代理到context
上。
在源码中,咱们能够看到:
// context.js delegate(proto, 'response') .method('attachment') // ... .access('status') // ... .getter('writable'); delegate(proto, 'request') .method('acceptsLanguages') // ... .access('querystring') // ... .getter('ip');
request.js 封装了请求相关的属性以及方法。经过 application.js 中的createContext
方法,代理对应的 request 对象。
const request = context.request = Object.create(this.request); // ... context.req = request.req = response.req = req; // ... request.response = response;
request.req
为原生的请求对象,在 request.js 中属性的获取都是经过 ths.req
来获取的(即 request.req
)。
response.js 封装了响应相关的属性以及方法。与 request 相同,经过createContext
方法代理对应的 response 对象。
const response = context.response = Object.create(this.response); // ... context.res = request.res = response.res = res; // ... response.request = request;
关于Koa2的源码就先分析到这,但愿对你们有所帮助。若有不一样的见解,欢迎交流!