koa源码分析
一、Koa介绍
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助开发者快速而愉快地编写服务端应用程序。
相对使用koa本身,koa更重要的是成为了一个基石。由于koa对开发者的束缚更小,让开发者可以基于koa去完成功能更丰富的框架,比如gulu和egg就是基于koa的二次封装。
二、Koa源码分析
Koa主要包含4个js文件,包括application.js,context.js,request.js, response.js。
application:koa应用实例。context:一次请求的上下文环境。request:基于node原生req对象封装后的koa的request对象。response:基于node原生res对象封装后的koa的response对象。
流程:
1. application.js
构造函数,暴露了Application类,继承了Emitter模块。构造函数做了以下几件事。
默认不设置代理初始化中间件数组middleware子域名偏移量默认为2,也就是默认忽略数量为2
假设域名是"tobi.ferrets.example.com"。如果app.subdomainOffset没有设置,也就是说默认要忽略的偏移量是2,那么ctx.subdomains是[“ferrets”, “tobi”]。 环境变量的处理挂载context、request、response重写util.inpsect方法util.inspect是一个将任意对象转换为字符串的方法,通常用于调试和错误输出。在node 6+版本中,util.inspect.custom返回一个Symbol类型。对象用util.inspect.custom作为key,值为函数的话,在对对象使用util.inspect()时候,util.inpsect会被该函数覆盖。
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
;
}
}
only方法返回对象白名单属性,即返回只想对外暴露的属性。此处表明只对外暴露‘subdomaimOffset’、‘proxy’以及‘env’三个属性。
toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
}
这时候再利用util.inspect将app实例转换为字符串的话,只能看到经过only处理后暴露的属性。
inspect() {
return this.toJSON();
}
服务器的启动,调用Node.js的http模块来创建一个服务器,具体的handler是使用callback方法的返回值。通过(…args)将listen的参数转发给创建好的server。
listen(...args
) {
debug('listen');
const server
= http
.createServer(this.callback());
return server
.listen(...args
);
}
}
this.callback()返回一个handleRequest函数处理请求
callback函数首先利用compose组合已注册的中间件,形成函数fn利用了闭包的性质,将fn持久化,不用每次接收到请求后都重新对中间件进行组合此时每次到达一个新的请求,都会执行一次handleRequest
该函数首先会结合node的生的req和res对象创建一个请求上下文ctx然后将ctx和fn一起传入this.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
;
}
该函数进行了以下操作,这些操作的目的是为了让开发者更加自由的去访问某一个对象
首先创建了一个context对象然后基于node原生的req和res创建koa实例的request和response对象,并挂载到context上将koa实例分别挂载到context、request、response上将node原生req分别挂载到context、request、response上将node原生res分别挂载到context、request、response上将context挂载到request、response上将resquest和response分别互相挂载将req.url挂载到context和request上
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
;
}
handleMiddleware,处理请求,并执行compose后的中间件
首先获取res对象,将其statusCode设置为默认值404然后利用onFinished函数侦听res,如果响应过程出错,回调用ctx.onerror on-finished
onFinished(res, listener)添加一个监听器去监听响应的完成情况当响应顺利完成时,监听器会在响应完成后被调用一次如果响应已完成,监听器将会被执行如果响应完成但是过程中出错了,监听器的第一个参数会是对应的error用来处理响应结束后的相关资源释放工作&&异常处理 最后利用fnMiddleware处理ctx,处理完成后,将处理后的ctx传递给handleResponse,也就是respond函数,完成对客户端的响应如果过程中发生错误,则调用ctx.onerror进行错误处理
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
);
}
通过use方法,将中间件注册到app实例上,主要是加入到middleware数组里。
首先进行了类型判断,如果参数不是function则抛出错误。然后判断注册的中间件函数是不是generator函数,如果是的话,将其转化为async/await形式。同时提示开发者,generator函数在v3版本不再支持。这和koa v1有关,因为koa v1的中间件函数默认使用的就是generator函数,而koa v2引入了async/await。将该函数加入middleware数组。
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;
}
用于处理响应。为了绕过 Koa 的内置 response 处理,你可以显式设置 ctx.respond = false;。 如果您想要写入原始的 res 对象而不是让 Koa 处理你的 response,请使用此参数。但是需要注意,Koa 不 支持使用此功能。这可能会破坏 Koa 中间件和 Koa 本身的预期功能。使用这个属性被认为是一个 hack,只是便于那些希望在 Koa 中使用传统的 fn(req, res) 功能和中间件的人。
function respond(ctx
) {
if (false === ctx
.respond
) return;
if (!ctx
.writable
) return;
}
onerror见错误处理
2. context.js
context中有两部分,一部分是自身的属性,用作框架内使用。(onerror见错误处理)
toJSON、inspect同上cookies的getter和setter
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>'
};
},
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
;
}
另一部分是Request和Response委托的操作方法,主要为提供给我们更方便从Request获取想要的参数和设置Response内容,它用到的是delegates三方库,把request,esponse 对象上的属性和方法代理到context 对象上。比如经常使用的ctx.body便是将response.body的body属性委托到了ctx上,这样可以让代码更加简洁。此时 ctx.body === response.bodyctx.body = {}
delegate(proto
, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.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');
3. request.js && response.js
对原生的 http 模块的 request对象进行封装,提供请求相关的数据与操作,使用es6的get和set方法,重新定义并暴露api。request:包含了一些操作Node原生请求对象的方法,如获取query数据,获取请求url等。
源码连接:https://github.com/koajs/koa/blob/master/lib/request.js各属性与方法作用,参考https://koa.bootcss.com/#requestsetter:
this.req.url,header, headers, url, method, path, query, querystring, search, getter:
header, headers, url, origin, href, method, path, query, querystring, search, host, hostname, URL, fresh, stale, idempotent, socket, charset, length, protocol, secure, ips, ip, subdomains, accept, type method:
accepts、acceptsEncodings、acceptsLanguages、get、is、inspect、toJSON response:包含了一些用于设置状态码,主体数据,header等一些用于操作响应请求的方法。
源码链接:https://github.com/koajs/koa/blob/master/lib/response.js各属性与方法作用,参考https://koa.bootcss.com/#responsesetter:
status, message, body, length, type, lastModified, etag getter:
socket, header, headers, status, message, body, length, headerSent, lastModified, etag, type method:
attachment、redirect、remove、vary、has、set、append、flushHeaders、inspect、toJSON
4. 中间件
简介
Koa最大的特色和最优的设计就是中间件,就是在匹配路由之前和匹配路由之后执行函数。使用app.use()加载中间件。每个中间件接收两个参数,ctx对象和next函数,通过调用next将执行权交给下一个中间件。 分类
应用级中间件(app.use)
任何路由都会先经过应用级中间件,当执行完成next后再去匹配相应的路由 路由级中间件(router.use)
路由匹配过程中,对于相同路由会从上往下依次执行中间件,直到最后一个没有next参数的中间件为止 错误处理中间件
路由在匹配成功并执行完相应的操作后还会再次进入应用级中间件执行 next 之后的逻辑。所以对于404、500等错误可以在最外层的(第一个)应用级中间件的next之后做相应的处理。如果只有一个应用级中间件的话,顺序就无所谓所有路由中间件之前和之后了。 第三方中间件
类似于koa-router、koa-bodyparser等就是第三方中间件。 内置中间件(比如express的内置router) 洋葱圈模型
洋葱模型是中间件的一种串行机制,并且是支持异步,第一个中间件函数中如果执行了next(),则下一个中间件会被执行,运行原理是基于以上提到的compose,下方有详细说明。基于洋葱圈模型,每个中间件都会执行两次。ctx.throw,抛出错误,停止洋葱圈。
|[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igz8wObb-1599143236417)(https://bytedance.feishu.cn/space/api/box/stream/download/asynccode/?code=19a83fab5c21d35079f046c3208d76a1_8f118824ce50c961_boxcnAUrlGA4PzPUdTLDYp3MVHg_YrhhpxL7bGrRjjGSLNz4HZPoseGVbzSN)]
5. 错误处理
application.js中的onerror:
绑定在 koa 实例对象上的,它监听的是整个对象的 error 事件,用来处理出错函数的堆栈打印, 方便我们进行问题定位。
onerror(err
) {
const isNativeError
=
Object
.prototype
.toString
.call(err
) === '[object Error]' ||
err
instanceof Error;
if (!isNativeError
) 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(`\n${msg.replace(/^/gm, ' ')}\n`);
}
};
context.js中的onerror:
在中间函数数组生成的 Promise 的 catch 中与 res 对象的 onFinished 函数的回调应用到,为了处理请求或响应中出现的 error 事件
在该函数中,首先判断错误的类型,以格式化对应的错误提示信息。之后通过headerSent头检测是否已经发送过一个响应头,如果已经发送过响应,则跳过后续部分。再之后通过this.app.emit(‘error’, err, this);触发了application的onerror函数,将出错函数的堆栈打印出来。如果之前没有发送过响应头,即headerSent为false并且响应是可写的。就进行响应数据的准备并返回给客户端,包括
设置响应头为err.headers设置相应的Content-Type为text/plain设置响应状态码与响应数据
onerror(err
) {
if (null == err
) return;
const isNativeError
=
Object
.prototype
.toString
.call(err
) === '[object Error]' ||
err
instanceof Error;
if (!isNativeError
) err
= new Error(util
.format('non-error thrown: %j', err
));
let headerSent
= false;
if (this.headerSent
|| !this.writable
) {
headerSent
= err
.headerSent
= true;
}
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
= {};
}
this.set(err
.headers
);
this.type
= 'text';
let statusCode
= err
.status
|| err
.statusCode
;
if ('ENOENT' === err
.code
) statusCode
= 404;
if ('number' !== typeof statusCode
|| !statuses
[statusCode
]) statusCode
= 500;
const code
= statuses
[statusCode
];
const msg
= err
.expose
? err
.message
: code
;
this.status
= err
.status
= statusCode
;
this.length
= Buffer
.byteLength(msg
);
res
.end(msg
);
},
**异步异常捕获。基于洋葱圈中间件模型,**可以把 try { next() } 当成第一个中间件,如下,这样就可以集中处理各个中间件抛出的所有异步异常。
app
.use(async function(ctx
, next
) {
try {
next();
} catch (err) {
this.status
= err
.status
|| 500;
this.body
= err
.message
;
this.app
.emit('error', err
, this);
}
});
app
.use(async function(ctx
, next
) {
throw new Error('some error');
})
三、 几个需要注意的点
1. compose——来自koa-compose包
首先进行了参数类型判断
要求参数middleware为数组,且middleware中所有元素类型都要是function 然后以闭包的形式返回处理函数,延迟执行时机,以便于多次利用,不用每一次都重新组合中间件作为返回结果的函数中,中间件运行机制如下
dispatch(i)代表调用中间件数组middleware中第i个中间件函数return dispatch(0)表示从第一个中间件开始调用利用index来避免一个中间件中出现两次next()调用,每一次调用都会更新index的值来配合这个检测每个中间件都在Promise.resolve()中执行,执行结果被resolve到上一层但由于context是引用类型,所有对context的修改都是同步更新的,这里的resolve只是配合await 的等待
async function(context, next){await next()}每次调用next方法,都会运行dispatch(i + 1)递归调用下一个中间,待执行结束,再执行本中间件的后续部分如果运行到最后一个中间件,则通过Promise.resolve()去resolve一个空值进行返回
'use strict'
module
.exports
= 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
if (!fn
) return Promise
.resolve()
try {
return Promise
.resolve(fn(context
, function next () {
return dispatch(i
+ 1)
}))
} catch (err) {
return Promise
.reject(err
)
}
}
}
}
2. delegate——来自delegates包
method,将目标对象的对应方法委托给指定对象getter,将目标对象的对应属性的getter委托给指定对象setter,将目标对象的对应属性的setter委托给指定对象access,将目标对象的对应属性的getter和setter委托给指定对象fluent,将目标对象的对应属性委托给指定对象,并提供了另外一种使用方法,可以通过「name()」获取该属性值,通过「name(val)」为该属性赋值。
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;
};
Delegator
.prototype
.fluent = function (name
) {
var proto
= this.proto
;
var target
= this.target
;
this.fluents
.push(name
);
proto
[name
] = function(val
){
if ('undefined' != typeof val
) {
this[target
][name
] = val
;
return this;
} else {
return this[target
][name
];
}
};
return this;
};