

Koa源码解析
source link: https://sobird.me/koa-source-code-note.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

koa
的源码位于lib
目录,结构非常简单和清晰,只有四个文件:
application.js
context.js
request.js
response.js
Application类
Application
类定义继承自Emitter.prototype
,这样在实例化Koa后,可以很方便的在实例对象中调用Emitter.prototype
的原型方法。
/**
* Expose `Application` class.
* Inherits from `Emitter.prototype`.
*/
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
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;
}
}
}
上面的构造函数中,定义了Application实例的11个属性:
属性含义proxy表示是否开启代理,默认为false。如果开启代理,对于获取request请求中的host,protocol,ip分别优先从Header字段中的X-Forwarded-Host
,X-Forwarded-Proto
,X-Forwarded-For
获取。subdomainOffset子域名的偏移量,默认值为2,这个参数决定了request.subdomains的返回结果。proxyIpHeader代理的 ip 头字段,默认值为X-Forwarded-For
。maxIpsCount最大的ips数,默认值为0,如果设置为大于零的值,ips获取的值将会返回截取后面指定数的元素。envkoa的运行环境, 默认是development。keys设置签名cookie密钥,在进行cookie签名时,只有设置 signed 为 true 的时候,才会使用密钥进行加密。middleware存放中间件的数组。context中间件第一个实参ctx的原型,定义在context.j
s中。requestctx.request的原型,定义在request.js
中。responsectx.response的原型,定义在response.js
中。[util.inspect.custom]util.inspect
这个方法用于将对象转换为字符串, 在node v6.6.0及以上版本中util.inspect.custom
是一个Symbol类型的值,通过定义对象的[util.inspect.custom]属性为一个函数,可以覆盖util.inspect
的默认行为。
use(fn)
use(fn)
接受一个函数作为参数,并加入到middleware
数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用generator
函数作为中间件的行为给与未来将被废弃的警告,但会将generator
函数转化为async
函数。返回this
便于链式调用。
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
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(…args)
可以看到内部是通过原生的http
模块创建服务器并监听的,请求的回调函数是callback
函数的返回值。
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
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方法去处理回调逻辑。
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
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;
}
handleRequest(ctx, fnMiddleware)
该方法最终将中间件执行链的结果传递给respond函数,经过respond函数的处理,最终将数据渲染到浏览器端。
/**
* Handle request in callback.
*
* @api private
*/
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(ctx)
respond是koa服务响应的最终处理函数,它主要功能是判断ctx.body的类型,完成最后的响应。另外,如果在koa中需要自行处理响应,可以设置ctx.respond = false,这样内置的respond就会被忽略。
/**
* Response helper.
*/
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
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 && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
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);
}
request.js
request.js
定义了ctx.request
的原型对象的原型对象,因此该对象的任意属性都可以通过ctx.request获取。这个对象一共有30+个属性和若干方法。其中属性多数都定义了get和set方法:
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
...
}
request对象中所有的属性和方法列举如下:
属性含义属性含义header原生req对象的headersheadersheader别名url原生req
对象的url
originprotocol://hosthref请求的完整urlmethod原生req
对象的method
path请求url
的pathname
query请求url的query,对象形式querystring请求url
的query
,字符串形式search?queryStringhosthosthostnamehostnameURLGet WHATWG parsed URLfresh判断缓存是否新鲜,只针对HEAD
和GET
方法,其余请求方法均返回falsestalefresh取反idempotent检查请求是否幂等,符合幂等性的请求有GET
,HEAD
,PUT
,DELETE
,OPTIONS
,TRACE
6个方法socket原生req对象的socketcharset请求字符集length请求的 Content-Length
protocol返回请求协议,https 或 http。当 app.proxy 是 true 时支持 X-Forwarded-Protosecure判断是否https请求ips当 X-Forwarded-For 存在并且 app.proxy 被启用时,这些 ips的数组被返回,从上游到下游排序。 禁用时返回一个空数组。ip请求远程地址。 当 app.proxy 是 true 时支持 X-Forwarded-Proto
subdomains根据app.subdomainOffset设置的偏移量,将子域返回为数组acceptGet/Set accept objectaccepts检查给定的 type(s) 是否可以接受,如果 true,返回最佳匹配,否则为 falseacceptsEncodings(…args)检查 encodings 是否可以接受,返回最佳匹配为 true,否则为 falseacceptsCharsets(…args)检查 charsets 是否可以接受,在 true 时返回最佳匹配,否则为 false。acceptsLanguages(…args)检查 langs 是否可以接受,如果为 true,返回最佳匹配,否则为 false。is(type, …types)type()get(field)Return request header[util.inspect.custom]
response.js
response.js
定义了ctx.response
的原型对象的原型对象,因此该对象的任意属性都可以通过ctx.response获取。和request类似,response的属性多数也定义了get和set方法。response的属性和方法如下:
属性含义属性含义socket原生res对象的socketheader原生res对象的headersheadersheader别名status响应状态码, 原生res对象的statusCodemessage响应的状态消息, 默认情况下,response.message 与 response.status 关联body响应体,支持string、buffer、stream、jsonlengthSet Content-Length field to `n`./Return parsed response Content-Length when present.headerSent检查是否已经发送了一个响应头, 用于查看客户端是否可能会收到错误通知varyVary on field
redirect(url, alt)执行重定向attachment(filename, options)将 Content-Disposition 设置为 “附件” 以指示客户端提示下载。(可选)指定下载的 filenametypeSet Content-Type response header with type
through mime.lookup()
when it does not contain a charset.lastModifiedSet/Get the Last-Modified date using a string or a Date.etagSet/Get the ETag of a response.is(type, …types)get(field)has(field)set(field, val)append(field, val)Append additional header field
with value val
.remove(field)Remove header field
.writableChecks if the request is writable.
context.js
context.js
定义了ctx的原型对象的原型对象, 因此这个对象中所有属性都可以通过ctx访问到。context的属性和方法如下:
- cookies 服务端cookies设置/获取操作
- throw() 抛出包含 .status 属性的错误,默认为 500。该方法可以让 Koa 准确的响应处理状态。
- delegate 用来将
ctx.request
和ctx.response
两个对象上指定属性代理到ctx对象下面。这样可以直接通过ctx.xxx
来访问ctx.request和ctx.response 对象下的属性或方法。
compose
compose来自koa-compose这个npm包,核心代码如下:
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()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型(洋葱模型)。

洋葱模型是一种中间件流程控制方式,koa2中的中间件调用就是采用了这一模型来实现的,简单代码示例如下:
const m1 = (ctx, next) => {
ctx.req.user = null;
console.log('中间件1 进入', ctx.req);
next()
console.log('中间件1 退出', ctx.req);
}
const m2 = (ctx, next) => {
ctx.req.user = { id: 1 };
console.log('中间件2 进入');
next()
console.log('中间件2 退出');
}
const m3 = (ctx, next) => {
console.log('中间件3');
}
const middlewares = [m1, m2, m3];
const context = { req: {}, res: {} };
function dispatch(i) {
if (i === middlewares.length) return;
return middlewares[i](context, () => dispatch(i + 1));
}
dispatch(0);
Recommend
-
62
-
58
Koa 在众多NodeJs框架中,以短小精悍而著称,核心代码只有大约570行,非常适合源码阅读。 实际上核心来说,Koa主要是两块 ctx 本文就核心阅读中间件的源码。 Koa使用 中间件可以理...
-
43
从今天开始阅读学习一下 Koa 源码, Koa 对前端来说肯定不陌生,使用 node 做后台大部分会选择 Koa 来做, Koa 源码的代码量其实很少,接下来让我们一层层剥离,分析...
-
21
用 Node.js 写一个 web服务器 ,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个 web服务器 ,主要是熟悉 Node.js 原生API的使用:
-
12
koa-router源码解读腾讯 高级前端工程师个人订阅号,知乎和微信同步推文,希望大家关注一波!微信订阅号:小前端看世界,id:fe_watch_world
-
11
Node学习笔记 - Koa源码阅读腾讯 高级前端工程师个人订阅号,知乎和微信同步推文,希望大家关注一波!微信订阅号:小前端看世界,id:fe_watch_world
-
12
深入源码:手写一个koa夏日Enjoy what you are doing!前言:了解koa
-
11
koa是一个非常轻量的web框架,里面除了ctx和middleware之外什么都没有,甚至连最基本的router功能都需要通过安装其他中间件来实现。不过虽然简单,但是它却非常强大,仅仅依靠中间件机制就可以构建完整的web服务。而koa的源码同样很简洁,基础代码只有不到2000...
-
10
koa router实现原理 本文两个目的 1. 了解path-to-regexp使用 2. koa-router源码解析 path-to-regexp path-to-regexp用法简介。 如何使用...
-
9
发表评论 取消回复电子邮件地址不会被公开。 必填项已用*标注评论 姓名 * 电子邮件 * 站点 在此浏览器中保存我的名字、电邮和网站。
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK