Node.js 界大名鼎鼎的 koa ,不需要多废话了,用了无数次,今天来拜读一下它的源码。
Koa 并不是 Node.js 的拓展,它只是在 Node.js 的基础上实现了以下内容:
中间件式的 HTTP 服务框架 (与 Express 一致)
洋葱模型 (与 Express 不同)
一统天下级别的框架,只包含了约 500 行源代码。极致强大,极致简单。大概这就是码农与码神的区别,真正的代码的艺术吧。
源码结构如下:
lib ├── application.js ├── context.js ├── request.js └── response.js
一共就这四个文件(当然,还包含了发布在 npm 上面的其它 package,后面会说到),一目了然。
application.js
是应用的入口,也就是 require('koa')
所得到的东西。它是一个继承自 events 的 Class
context.js
就是对应每一个 req / res 的 ctx
request.js
/ response.js
就不用说了
下面从最基础的看起。
request.js request.js
大概的样子如下:
module .exports = { get header () { return this .req .headers ; }, set header (val ) { this .req .headers = val; }, get headers () { return this .req .headers ; }, set headers (val ) { this .req .headers = val; }, }
这里的 this.req
实际上是 http.IncomingMessage ,创建的时候传入的,后面会提到。
这个文件绝大多数是 helper 方法,把本来已经存在在 http.IncomingMessage
中的属性通过更方便的方式(getter
/ setter
)来存取,以达到通过同一个属性来读写的目的。比如想要获取一个 request 的 header 时,通过 ctx.request.heder
,而想写入 header 时,可以通过 ctx.request.heder = xxx
来实现。这也是 koa 的友好特性之一。
其中有一个特殊的是 ip
:
const IP = Symbol ('context#ip' );module .exports = { get ip () { if (!this [IP ]) { this [IP ] = this .ips [0 ] || this .socket .remoteAddress || '' ; } return this [IP ]; }, set ip (_ip ) { this [IP ] = _ip; }, }
Symbol('context#ip')
是 request
对象唯一一个来自自身的 key,我猜测它的目的是:
允许开发者对真实请求 ip 进行改写
同时利用 Symbol 不等于任何值的特性,使它成为私有属性,对外不可见,只可通过 getter 获取
response.js response.js
与 request.js
类似,不同之处在于,response.js
重点更多在 setter
上面,很好理解,因为 response 的重点是一个服务器向用户返回内容的过程。
koa 的一大特性是在于,只需要向 ctx.response.body
赋值就能完成一次请求响应。代码:
module .exports = { get body () { return this ._body ; }, set body (val ) { const original = this ._body ; this ._body = val; if (null == val) { if (!statuses.empty [this .status ]) this .status = 204 ; this .remove ('Content-Type' ); this .remove ('Content-Length' ); this .remove ('Transfer-Encoding' ); return ; } if (!this ._explicitStatus ) this .status = 200 ; const setType = !this .header ['content-type' ]; if ('string' == typeof val) { if (setType) this .type = /^\s*</ .test (val) ? 'html' : 'text' ; this .length = Buffer .byteLength (val); return ; } if (Buffer .isBuffer (val)) { if (setType) this .type = 'bin' ; this .length = val.length ; return ; } if ('function' == typeof val.pipe ) { onFinish (this .res , destroy.bind (null , val)); ensureErrorHandler (val, err => this .ctx .onerror (err)); if (null != original && original != val) this .remove ('Content-Length' ); if (setType) this .type = 'bin' ; return ; } this .remove ('Content-Length' ); this .type = 'json' ; }, }
可以看到,在 body 的 setter 里面,分别对传入的值为 null / string / buffer / stream / json 的情况进行了处理,并完成了向客户端返回的其它逻辑(设置各种响应头以及状态码),以达到上述目的。
为了达到「至简」目的,koa 对外暴露的 API 基本都是通过 getter / setter 的方式实现的,值得借鉴。
context.js Context 「上下文」(通常简写为 ctx)是 koa 的核心之一,它代表了一次用户请求,每个请求都对应着一个独立的 context,实际上它就是 request
与 response
的结合体,通过「委托模式」实现。它的作用是,开发者对于每一个请求,只需要拿到它的 ctx,就能获取到所有请求的相关信息,亦能做出任何形式的响应。
它的核心代码如下:
'use strict' ;const delegate = require ('delegates' );const Cookies = require ('cookies' );const COOKIES = Symbol ('context#cookies' );const proto = module .exports = { 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; } }; 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, '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' );
可以看到里面的主要内容有:
实现 Cookies 的 getter / setter(因为 koa 把 req 和 res 的 cookies 结合在一起了,所以它须要在 ctx 内实现)
将 request / response 的逻辑代理到 ctx 上面
关于这个「委托模式」的具体实现,TJ 把它放到了一个独立的 NPM Package delegates 中。它的功能是:将一个类的子类中的方法与属性,暴露到父类中去,而暴露在父类上的方法可以看做真实方法的「代理」。koa 使用了其中的三种模式,分别是:
method
代理方法
access
代理 getter 与 setter
getter
仅代理 getter
其主要源码:
module .exports = Delegator ;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 ; }; Delegator .prototype .access = function (name ){ return this .getter (name).setter (name); }; 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 ; }; 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 ; };
依然非常简洁。method 代理使用 Function.apply
实现,getter / setter 代理使用 object.__defineGetter__
与 object.__defineSetter__
实现。
application.js 去除兼容、校验、实用方法等逻辑,精简过后,该文件的主要内容如下:
'use strict' ;const onFinished = require ('on-finished' );const response = require ('./response' );const compose = require ('koa-compose' );const context = require ('./context' );const request = require ('./request' );const Emitter = require ('events' );const util = require ('util' );module .exports = class Application extends Emitter { constructor ( ) { super (); this .middleware = []; this .context = Object .create (context); this .request = Object .create (request); this .response = Object .create (response); } listen (...args ) { const server = http.createServer (this .callback ()); return server.listen (...args); } use (fn ) { this .middleware .push (fn); return this ; } 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 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); } 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; } onerror (err ) { if (!(err instanceof Error )) throw new TypeError (util.format ('non-error thrown: %j' , err)); if (404 == err.status || err.expose ) return ; if (this .silent ) return ; const msg = err.stack || err.toString (); console .error (); console .error (msg.replace (/^/gm , ' ' )); console .error (); } };
不到一百行,Koa 主要功能已经全在里面了。
现在可以梳理一下当我们创建一个 koa 服务器的时候,实际上都干了些什么吧:
调用 constructor
,初始化 ctx / req / res,以及最重要的 middleware
数组(不过不理解的是,为什么命名没有加 s 呢?)
对于各种业务场景,调用 app.use
,这一步只是一个简单的向 middleware
数组 push 的过程
调用 app.listen
,启动 HTTP 服务器
对于每一个进来的请求,调用 callback
方法,这个方法做了三件事:
通过 koa-compose
将中间件数组组合为一个「洋葱」模型
调用 createContext
方法,为请求创建 ctx 上下文,同时挂载 req / res
调用 handleRequest
方法,按洋葱模型的顺序执行中间件,并最终返回或报错
这里面最重要的一步就是「洋葱」模型的构建。实际上这个过程也非常简单,以下是 koa-compose
的源码(为了精简,已去除校验等逻辑):
function compose (middleware) { return function (context, next ) { let index = -1 return dispatch (0 ) function dispatch (i) { 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) } } } }
它是一个递归:
首先约定,每一个 middleware 都是一个 async 函数(即 Promise),接受两个参数 ctx
与 next
当 middleware 内部调用 next 函数时,实际上是递归调用了 dispatch.bind(null, i + 1)
函数,也就是,将 index + 1
的中间件取出来并执行了。因为中间件都是 Promise,所以能够被 await
递归执行步骤 2,直到调用到最后一个 middleware 时,最后被调用的 middleware 会最先结束,然后到上一个,再到上上一个,如此往复就形成了「洋葱」模型
最终所有 middleware 都执行完毕,compose 函数返回 Promise.resolve()
,即退出递归
「洋葱」模型构建完毕后,compose
函数返回一个 Promise,所有 middleware 都已经被有序串联,只需要直接执行该 promise 实例即可。
让人不禁感叹:大道至简 。
end 至此,koa 的最主要的功能实现都已过了一遍了。
总结一下它做了的事情:
通过 getter
/ setter
方法简化 Node.js HTTP 的使用方式
通过 ctx
简化开发者访问 req / res 的方式
通过「洋葱」模型简化 HTTP 请求的处理流程
大概就这样。