koa
是当下很是流行的node
框架,相比笨重的express
,koa
只专一于中间件模型的创建,以及请求和响应控制权的转移。本文将以koa2
为例,深刻源码分析框架的实现细节。 koa2
的源码位于lib
目录,结构很是简单和清晰,只有四个文件,以下:javascript
根据package.json
中的main
字段,能够知道入口文件是lib/application.js
,application.js
定义了koa
的构造函数以及实例拥有的方法,以下图: java
首先看一下构造函数的代码node
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
复制代码
这里定义了实例的8个属性,各自的含义以下:git
属性 | 含义 |
---|---|
proxy | 表示是否开启代理,默认为false ,若是开启代理,对于获取request 请求中的host ,protocol ,ip 分别优先从Header 字段中的X-Forwarded-Host ,X-Forwarded-Proto ,X-Forwarded-For 获取。 |
middleware | 最重要的一个属性,存放全部的中间件,存放和执行的过程后文细说。 |
subdomainOffset | 子域名的偏移量,默认值为2,这个参数决定了request.subdomains 的返回结果。 |
env | node 的执行环境, 默认是development 。 |
context | 中间件第一个实参ctx 的原型, 具体在讲context.js 时会说到。 |
request | ctx.request的原型,定义在request.js 中。 |
response | ctx.response的原型,定义在response.js 中。 |
[util.inspect.custom] | util.inspect 这个方法用于将对象转换为字符串, 在node v6.6.0 及以上版本中util.inspect.custom 是一个Symbol 类型的值,经过定义对象的[util.inspect.custom] 属性为一个函数,能够覆盖util.inspect 的默认行为。 |
use
方法很简单,接受一个函数做为参数,并加入middleware
数组。因为koa
最开始支持使用generator
函数做为中间件使用,但将在3.x
的版本中放弃这项支持,所以koa2
中对于使用generator
函数做为中间件的行为给与将来将被废弃的警告,但会将generator
函数转化为async
函数。返回this
便于链式调用。github
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;
}
复制代码
下面是listen
方法,能够看到内部是经过原生的http
模块建立服务器并监听的,请求的回调函数是callback
函数的返回值。express
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
复制代码
下面是callback
的代码,compose
函数将中间件数组转换成执行链函数fn
, compose
的实现是重点,下文会分析。koa
继承自Emitter
,所以能够经过listenerCount
属性判断监听了多少个error
事件, 若是外部没有进行监听,框架将自动监听一个error
事件。callback
函数返回一个handleRequest
函数,所以真正的请求处理回调函数是handleRequest
。在handleRequest
函数内部,经过createContext
建立了上下文ctx
,并交给koa
实例的handleRequest
方法去处理回调逻辑。npm
callback() {
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;
}
复制代码
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;
}
复制代码
上面是createContext
的代码, 从这里咱们能够知道,经过ctx.req
和ctx.res
能够访问到node
原生的请求对象和响应对象, 经过修改ctx.state
可让中间件共享状态。能够用一张图描述这个函数中定义的关系,以下: json
接下来咱们分析细节,this.context
、this.request
、this.response
分别经过context
、request
、response
三个对象的原型建立, 咱们先看一下request
的定义,它位于request.js
文件中。数组
request.js
定义了ctx.request
的原型对象的原型对象,所以该对象的任意属性均可以经过ctx.request
获取。这个对象一共有20多个属性和若干方法。其中属性多数都定义了get
和set
方法,截取一小部分代码以下:缓存
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
...
}
复制代码
上面代码中定义了header
属性,根据前面的关系图可知,this.req
指向的是原生的req
,所以ctx.request.header
等于原生req
的headers
属性,修改ctx.request.header
就是修改req
的headers
。request
对象中全部的属性和方法列举以下:
属性/方法 | 含义 |
---|---|
header | 原生req 对象的headers |
headers | 原生req 对象的headers , 同上 |
url | 原生req 对象的url |
origin | protocol://host |
href | 请求的完整url |
method | 原生req 对象的method |
path | 请求url 的pathname |
query | 请求url 的query ,对象形式 |
queryString | 请求url 的query ,字符串形式 |
search | ?queryString |
hostname | hostname |
URL | 完整的URL对象 |
fresh | 判断缓存是否新鲜,只针对HEAD 和GET 方法,其他请求方法均返回false |
stale | fresh 取反 |
idempotent | 检查请求是否幂等,符合幂等性的请求有GET , HEAD , PUT , DELETE , OPTIONS , TRACE 6个方法 |
socket | 原生req 对象的套接字 |
charset | 请求字符集 |
type | 获取请求头的Content-Type 不含参数 charset 。 |
length | 请求的 Content-Length |
secure | 判断是否是https 请求 |
ips | 当 X-Forwarded-For 存在而且 app.proxy 被启用时,这些 ips 的数组被返回,从上游到下游排序。 禁用时返回一个空数组。 |
ip | 请求远程地址。 当 app.proxy 是 true 时支持 X-Forwarded-Proto |
protocol | 返回请求协议,https 或 http 。当 app.proxy 是 true 时支持 X-Forwarded-Proto |
host | 获取当前主机(hostname:port) 。当 app.proxy 是 true 时支持 X-Forwarded-Host ,不然使用Host |
subdomains | 根据app.subdomainOffset 设置的偏移量,将子域返回为数组 |
get(...args) | 获取请求头字段 |
accepts(...args) | 检查给定的 type(s) 是否能够接受,若是 true ,返回最佳匹配,不然为 false |
acceptsEncodings(...args) | 检查 encodings 是否能够接受,返回最佳匹配为 true ,不然为 false |
acceptsCharsets(...args) | 检查 charsets 是否能够接受,在 true 时返回最佳匹配,不然为 false 。 |
acceptsLanguages(...args) | 检查 langs 是否能够接受,若是为 true ,返回最佳匹配,不然为 false 。 |
[util.inspect.custom] | 自定义的util.inspect |
response.js
定义了ctx.response
的原型对象的原型对象,所以该对象的任意属性均可以经过ctx.response
获取。和request
相似,response
的属性多数也定义了get
和set
方法。response
的属性和方法以下:
属性/方法 | 含义 |
---|---|
header | 原生res 对象的headers |
headers | 原生res 对象的headers , 同上 |
status | 响应状态码, 原生res 对象的statusCode |
message | 响应的状态消息. 默认状况下, response.message 与 response.status 关联 |
socket | 套接字,原生res 对象的socket |
type | 获取响应头的 Content-Type 不含参数 charset |
body | 响应体,支持string ,buffer 、stream 、json |
lastModified | 将 Last-Modified 标头返回为 Date , 若是存在 |
etag | 响应头的ETag |
length | 数字返回响应的 Content-Length ,使用Buffer.byteLength 对body 进行计算 |
headerSent | 检查是否已经发送了一个响应头, 用于查看客户端是否可能会收到错误通知 |
vary(field) | 在 field 上变化。 |
redirect(url, alt) | 执行重定向 |
attachment(filename, options) | 将 Content-Disposition 设置为 “附件” 以指示客户端提示下载。(可选)指定下载的 filename |
get(field) | 返回指定的响应头部 |
set(field, val) | 设置响应头部 |
is(type) | 响应类型是不是所提供的类型之一 |
append(field, val) | 设置规范以外的响应头 |
remove(field) | 删除指定的响应头 |
flushHeaders() | 刷新全部响应头 |
writable() | 判断响应是否可写,原生res 对象的finished 为true ,则返回false , 不然判断原生res 对象是否创建套接字socket , 若是没有返回false , 有则返回socket.writable |
request
和response
中每一个属性get
和set
的定义以及方法的实现多数比较简单直观,若是对每一个进行单独分析会致使篇幅过长,并且这些不是理解koa
运行机制的核心所在,所以本文只罗列属性和方法的用途,这些大部分也能够在koa
的官方文档中找到。关心细节的朋友能够直接阅读request.js
和response.js
这两个文件,若是你熟悉http
协议,相信这些代码对你并无障碍。接下来咱们的重点是context.js
。
context.js
定义了ctx
的原型对象的原型对象, 所以这个对象中全部属性均可以经过ctx
访问到。context.js
中除了定义[util.inspect.custom]
这个不是很重要的属性外,只直接定义了一个属性cookies
,也定义了几个方法,这里分别进行介绍:
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;
}
复制代码
上面的代码中定义了cookies
属性的set
和get
方法。set
方法很简单,COOKIES
是一个Symbol
类型的私有变量。须要注意的是咱们通常不经过ctx.cookies
来直接设置cookies
,官方文档推荐使用ctx.cookies.set(name, value, options)
来设置,但是这里并无cookies.set
呀,其实这里稍微一看就明白,cookies
的值是this[COOKIES]
,它是Cookies
的一个实例,在Cookie
这个npm
包中是定义了实例的get
和set
方法的。
throw(...args) {
throw createError(...args);
},
复制代码
当咱们调用ctx.throw
抛出一个错误时,内部是抛出了一个有状态码和信息的错误,createError
的实如今http-errors
这个npm
包中。
下面是onerror
方法的代码,发生错误时首先会触发koa
实例上的error
事件来打印一个错误日志, headerSent
变量表示响应头是否发送,若是响应头已经发送,或者响应处于不可写状态,将没法在响应中添加错误信息,直接退出该函数,不然须要将以前写入的响应头部信息清空。
onerror(err) {
// 没有错误时什么也不作
if (null == err) return;
// err不是Error实例时,使用err建立一个Error实例
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
// 若是res不可写或者请求头已发出
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// 触发koa实例app的error事件
this.app.emit('error', err, this);
if (headerSent) {
return;
}
const { res } = this;
// 移除全部设置过的响应头
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// 设置错误头部
this.set(err.headers);
// 设置错误时的Content-Type
this.type = 'text';
// 找不到文件错误码设为404
if ('ENOENT' == err.code) err.status = 404;
// 不能被识别的错误将错误码设为500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
const code = statuses[err.status];
const msg = err.expose ? err.message : code;
// 设置错误码
this.status = err.status;
this.length = Buffer.byteLength(msg);
// 结束响应
res.end(msg);
},
复制代码
从上面代码中会有疑问, this.set
、this.type
等是哪里来的?context
并无定义这些属性。咱们知道, ctx
中实际上是代理了不少response
和resquest
的属性和方法的,this.set
、this.type
其实就是response.set
和response.type
。那么koa
中对象属性和方法的代理是如何实现的呢,答案是delegate
,context
中代码的最后就是使用delegate
来代理一些原本只存在于request
和response上
的属性。接下来咱们看一下delegete
是如何实现代理的,delegete
的实现代码在delegetes
这个npm包中。
delegate
方法本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象,下面是它的定义, Delegator
就是delegate
。能够看到,不论是否使用new
关键字,该函数老是会返回一个实例。
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
构造函数的原型上,定义了几个方法,koa
中用到了Delegator.prototype.method
、Delegator.prototype.accsess
以及Delegator.prototype.getter
,这些都是代理方法, 分别代理set
和get
方法。下面是代码,其中get
和set
方法的代理主要使用了对象的__defineGetter__
以及__defineSetter__
方法。
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;
};
复制代码
到这里,关于request
、response
和context
就聊的差很少了,接下来回到callback
继续咱们的重点,前面说到的compose
才是koa
的精华和核心所在,他的代码在koa-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) {
// 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 {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
函数接收一个middleware
数组为参数,返回一个函数,给函数传入ctx
时第一个中间件将自动执行,之后的中间件只有在手动调用next
,即dispatch
时才会执行。另外从代码中能够看出,中间件的执行是异步的,而且中间件执行完毕后返回的是一个Promise
,每一个dispatch的返回值也是一个Promise,所以咱们的中间件中能够方便地使用async
函数进行定义,内部使用await next()
调用“下游”,而后控制流回“上游”,这是更准确也更友好的中间件模型。从下面的代码能够看到,中间件顺利执行完毕后将执行respond
函数,失败后将执行ctx
的onerror
函数。onFinished(res, onerror)
这段代码是对响应处理过程当中的错误监听,即handleResponse
发生的错误或自定义的响应处理中发生的错误。
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);
}
复制代码
respond
是koa
内置的响应自动处理函数,代码以下,它主要功能是判断ctx.body
的类型,而后自动完成最后的响应。另外,若是在koa
中须要自行处理响应,能够设置ctx.respond = false
,这样内置的respond
就会被忽略。
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
复制代码