koa是一个轻量级的web应用框架。其实现很是精简和优雅,核心代码仅有区区一百多行,很是值得咱们去细细品味和学习。javascript
在开始分析源码以前先上demo~java
const Koa = require('../lib/application'); const app = new Koa(); app.use(async (ctx, next) => { console.log('m1-1'); await next(); console.log('m1-2'); }); app.use(async (ctx, next) => { console.log('m2-1'); await next(); console.log('m2-2'); }); app.use(async (ctx, next) => { console.log('m3-1'); ctx.body = 'there is a koa web app'; await next(); console.log('m3-2'); }); app.listen(8001); 复制代码
上面代码最终会在控制台依次输出node
m1-1
m2-1
m3-1
m3-2
m2-2
m1-2
复制代码
当在中间件中调用next()
时,会中止当前中间件的执行,转而进行下一个中间件。当下一个中间件执行完后,才会继续执行next()
后面的逻辑。web
咱们改一下第一个中间件的代码,以下所示:json
app.use(async (ctx, next) => { console.log('m1-1'); // await next(); console.log('m1-2'); }); 复制代码
当把第一个中间件的await next()
注释后,再次执行,在控制台的输出以下:数组
m1-1
m2-1
复制代码
显然,若是不执行next()
方法,代码将只会执行到当前的中间件,不事后面还有多少个中间件,都不会执行。bash
这个next
为什么会具备这样的魔力呢,下面让咱们开始愉快地分析koa的源码,一探究竟~markdown
分析源码以前咱们先来看一下koa的目录结构,koa的实现文件只有4个,这4个文件都在lib目录中。cookie
application.js
— 定义了一个类,这个类定义了koa实例的方法和属性context.js
— 定义了一个proto对象,并对proto中的属性进行代理。中间件中使用的ctx对象,其实就是继承自protorequest.js
— 定义了一个对象,该对象基于原生的req拓展了一些属性和方法response.js
- 定义了一个对象,该对象基于原生的res拓展了一些属性和方法经过package.json文件得知,koa的入口文件是lib/application.js,咱们先来看一下这个文件作了什么。app
打开application.js
查看源码能够发现,这个文件主要就是定义了一个类,同时定义了一些方法。
module.exports = class Application extends Emitter { constructor() { super(); this.middleware = []; // 中间件数组 } listen (...args) { // 启用一个http server并监听指定端口 const server = http.createServer(this.callback()); return server.listen(...args); } use (fn) { // 把中间添加到中间件数组 this.middleware.push(fn); return this; } } 复制代码
咱们建立完一个koa对象以后,一般只会使用两个方法,一个是listen
,一个是use
。listen负责启动一个http server并监听指定端口,use用来添加咱们的中间件。
当调用listen
方法时,会建立一个http server,这个http server须要一个回调函数,当有请求过来时执行。上面代码中的this.callback()
就是用来返回这样的一个函数:这个函数会读取应用全部的中间件,使它们按照传入的顺序依次执行,最后响应请求并返回结果。
callback
方法的核心代码以下:
callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } 复制代码
callback
函数会在应用启动时执行一次,而且返回一个函数handleRequest
。每当有请求过来时,handleRequest
都会被调用。咱们将callback
拆分为三个流程去分析:
fn
,在fn
函数内部会依次执行this.middleware
中的中间件(是否所有执行,取决因而否有调用next
函数执行下一个中间件)createContext
生成一个可供中间件使用的ctx
上下文对象fn
,并执行,最后对结果做出响应const fn = compose(this.middleware); 复制代码
源码中使用了一个compose
函数,基于全部可执行的中间件生成了一个可执行函数。当该函数执行时,每个中间件将会被依次应用。compose
函数的定义以下:
function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') 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] if (i === middleware.length) fn = next //我的认为对在koa中这里的fn = next并无意义 if (!fn) return Promise.resolve() // 执行到最后resolve出来 try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } 复制代码
它会先执行第一个中间件,执行过程当中若是遇到next()
调用,就会把控制权交到下一个中间件并执行,等该中间件执行完后,再继续执行next()
以后的代码。这里的dispatch.bind(null, i + 1)
就是next
函数。到这里就能解答,为何必需要调用next
方法,才能让当前中间件后面的中间件执行。(有点拗口…)匿名函数的返回结果是一个Promise
,由于要等到中间件处理完以后,才能进行响应。
中间件执行函数生成好以后,接下来须要建立一个ctx
。这个ctx
能够在中间件里面使用。ctx
提供了访问req
和res
的接口。 建立上下文对象调用了一个createContext
函数,这个函数的定义以下:
/** * 建立一个context对象,也就是在中间件里使用的ctx,并给ctx添加request, respone属性 */ createContext(req, res) { const context = Object.create(this.context); // 继承自context.js中export出来proto const request = context.request = Object.create(this.request); // 把自定义的request做为ctx的属性 const response = context.response = Object.create(this.response);// 把自定义的response做为ctx的属性 context.app = request.app = response.app = this; // 为了在ctx, request, response中,都能使用httpServer回调函数中的req和res 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; } 复制代码
ctx
对象其实是继承自context
模块中定义的proto
对象,同时添加了request
和response
两个属性。request
和response
也是对象,分别继承自request.js
和response.js
定义的对象。这两个模块的功能是基于原生的req
和res
封装了一些getter
和setter
,原理比较简单,下面就再也不分析了。
咱们重点来看看context
模块。
const proto = module.exports = { inspect() { if (this === proto) return this; return this.toJSON(); }, toJSON() { return { request: this.request.toJSON(), response: this.response.toJSON(), app: this.app.toJSON(), originalUrl: this.originalUrl, req: '<original node req>', res: '<original node res>', socket: '<original node socket>' }; }, assert: httpAssert, throw(...args) { throw createError(...args); }, onerror(err) { if (null == err) return; if (!(err instanceof Error)) 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); 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'; // ENOENT support if ('ENOENT' == err.code) err.status = 404; // default to 500 if ('number' != typeof err.status || !statuses[err.status]) err.status = 500; // respond const code = statuses[err.status]; const msg = err.expose ? err.message : code; this.status = err.status; this.length = Buffer.byteLength(msg); this.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]; }, set cookies(_cookies) { this[COOKIES] = _cookies; } }; 复制代码
context
模块定义了一个proto
对象,该对象定义了一些方法(eg: throw
)和属性(eg: cookies
)。咱们上面经过createContext
函数建立的ctx
对象,就是继承自proto
。所以,咱们能够在中间件中直接经过ctx
访问proto
中定义的方法和属性。
值得一提的点是,做者经过代理的方式,让开发者能够直接经过ctx[propertyName]
去访问ctx.request
或ctx.response
上的属性和方法。
实现代理的关键逻辑
/** * 代理response一些属性和方法 * eg: proto.response.body => proto.body */ delegate(proto, 'response') .method('attachment') .method('redirect') .access('body') .access('length') // other properties or methods /** * 代理request的一些属性和方法 */ delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') .method('get') // other properties or methods 复制代码
实现代理的逻辑也很是简单,主要就是使用了__defineGetter__
和__defineSetter__
这两个对象方法,当set
或get
对象的某个属性时,调用指定的函数对属性值进行处理或返回。
当ctx
(上下文对象)和fn
(执行中间件的合成函数)都准备好以后,就能真正的处理请求并响应了。该步骤调用了一个handleRequest
函数。
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // 状态码默认404 const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 执行完中间件函数后,执行handleResponse处理结果 return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 复制代码
handleRequest
函数会把ctx
传入fnMiddleware
并执行,而后经过respond
方法进行响应。这里默认把状态码设为了404
,若是在执行中间件的过程当中有返回,例如对ctx.body
进行负责,koa
会自动把状态码设成200
,这一部分的逻辑是在response
对象的body
属性的setter
处理的,有兴趣的朋友能够看一下response.js
。
respond
函数会对ctx
对象上的body
或者其余属性进行分析,而后经过原生的res.end()
方法将不一样的结果输出。
到这里,koa2的核心代码大概就分析完啦。以上是我我的总结,若有错误,请见谅。欢迎一块儿交流学习!