内容简介:根据首先看一下构造函数的代码
koa
是当下非常流行的 node
框架,相比笨重的 express
, koa
只专注于中间件模型的建立,以及请求和响应控制权的转移。本文将以 koa2
为例,深入源码分析框架的实现细节。 koa2
的源码位于 lib
目录,结构非常简单和清晰,只有四个文件,如下:
根据 package.json
中的 main
字段,可以知道入口文件是 lib/application.js
, application.js
定义了 koa
的构造函数以及实例拥有的方法,如下图:
构造函数
首先看一下构造函数的代码
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个属性,各自的含义如下:
属性 | 含义 |
---|---|
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()
use
方法很简单,接受一个函数作为参数,并加入 middleware
数组。由于 koa
最开始支持使用 generator
函数作为中间件使用,但将在 3.x
的版本中放弃这项支持,因此 koa2
中对于使用 generator
函数作为中间件的行为给与未来将被废弃的警告,但会将 generator
函数转化为 async
函数。返回 this
便于链式调用。
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()
下面是 listen
方法,可以看到内部是通过原生的 http
模块创建服务器并监听的,请求的回调函数是 callback
函数的返回值。
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } 复制代码
callback()
下面是 callback
的代码, compose
函数将中间件数组转换成执行链函数 fn
, compose
的实现是重点,下文会分析。 koa
继承自 Emitter
,因此可以通过 listenerCount
属性判断监听了多少个 error
事件, 如果外部没有进行监听,框架将自动监听一个 error
事件。 callback
函数返回一个 handleRequest
函数,因此真正的请求处理回调函数是 handleRequest
。在 handleRequest
函数内部,通过 createContext
创建了上下文 ctx
,并交给 koa
实例的 handleRequest
方法去处理回调逻辑。
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()
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
可以让中间件共享状态。可以用一张图描述这个函数中定义的关系,如下:
接下来我们分析细节, this.context
、 this.request
、 this.response
分别通过 context
、 request
、 response
三个对象的原型创建, 我们先看一下 request
的定义,它位于 request.js
文件中。
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
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
context.js
定义了 ctx
的原型对象的原型对象, 因此这个对象中所有属性都可以通过 ctx
访问到。 context.js
中除了定义 [util.inspect.custom]
这个不是很重要的属性外,只直接定义了一个属性 cookies
,也定义了几个方法,这里分别进行介绍:
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()
throw(...args) { throw createError(...args); }, 复制代码
当我们调用 ctx.throw
抛出一个错误时,内部是抛出了一个有状态码和信息的错误, createError
的实现在 http-errors
这个 npm
包中。
onerror()
下面是 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
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
这个包中,我们来看一下:
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
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); } 复制代码
以上所述就是小编给大家介绍的《深入koa2源码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深入理解 FilterChainProxy【源码篇】
- 深入理解 WebSecurityConfigurerAdapter【源码篇】
- 深入理解channel:设计+源码
- 深入浅出Semaphore源码解析
- 深入剖析Vue源码 - 组件基础
- 深入剖析Vue源码 - 组件进阶
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。