接上次挖的坑,对koa2.x
相干的源码举行剖析 第一篇。
不得不说,koa
是一个很轻量、很文雅的http框架,尤其是在2.x今后移除了co
的引入,使其代码变得更加清楚。
express
和koa
同为一批人举行开辟,与express
比拟,koa
显得非常的迷你。
由于express
是一个大而全的http
框架,内置了相似router
之类的中间件举行处置惩罚。
而在koa
中,则将相似功用的中间件悉数摘了出来,初期koa
里边是内置了koa-compose
的,而如今也是将其分了出来。 koa
只保存一个简朴的中间件的整合,http
请求的处置惩罚,作为一个功用性的中间件框架来存在,自身唯一少许的逻辑。 koa-compose
则是作为整合中间件最为症结的一个东西、洋葱模子的详细完成,所以要将二者放在一同来看。
koa基础组织
.
├── application.js
├── request.js
├── response.js
└── context.js
关于koa
全部框架的完成,也只是简朴的拆分为了四个文件。
就象在上一篇笔记中模仿的那样,建立了一个对象用来注册中间件,监听http
效劳,这个就是application.js
在做的事变。
而框架的意义呢,就是在框架内,我们要依据框架的礼貌来做事变,一样的,框架也会供应给我们一些更易用的体式格局来让我们完成需求。
针对http.createServer
回调的两个参数request
和response
举行的一次封装,简化一些经常运用的操纵。
比方我们对Header
的一些操纵,在原生http
模块中可以要如许写:
// 猎取Content-Type
request.getHeader('Content-Type')
// 设置Content-Type
response.setHeader('Content-Type', 'application/json')
response.setHeader('Content-Length', '18')
// 或许,疏忽前边的statusCode,设置多个Header
response.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': '18'
})
而在koa
中可以如许处置惩罚:
// 猎取Content-Type
context.request.get('Content-Type')
// 设置Content-Type
context.response.set({
'Content-Type': 'application/json',
'Content-Length': '18'
})
简化了一些针对request
与response
的操纵,将这些封装在了request.js
和response.js
文件中。
但同时这会带来一个运用上的搅扰,如许封装今后实在猎取或许设置header
变得层级更深,须要经由历程context
找到request
、response
,然后才举行操纵。
所以,koa
运用了node-delegates来进一步简化这些步骤,将request.get
、response.set
统统代办到context
上。
也就是说,代办后的操纵是如许子的:
context.get('Content-Type')
// 设置Content-Type
context.set({
'Content-Type': 'application/json',
'Content-Length': '18'
})
如许就变得很清楚了,猎取Header
,设置Header
,再也不会忧郁写成request.setHeader
了,一挥而就,经由历程context.js
来整合request.js
与response.js
的行动。
同时context.js
也会供应一些其他的东西函数,比方Cookie
之类的操纵。
由application
引入context
,context
中又整合了request
和response
的功用,四个文件的作用已很清楚了:
file | desc |
---|---|
applicaiton | 中间件的治理、http.createServer 的回调处置惩罚,天生Context 作为本次请求的参数,并挪用中间件 |
request | 针对http.createServer -> request 功用上的封装 |
response | 针对http.createServer -> response 功用上的封装 |
context | 整合request 与response 的部份功用,并供应一些分外的功用 |
而在代码组织上,只要application
对外的koa
是采纳的Class
的体式格局,其他三个文件均是抛出一个一般的Object
。
拿一个完整的流程来诠释
建立效劳
起首,我们须要建立一个http
效劳,在koa2.x
中建立效劳与koa1.x
轻微有些区分,请求运用实例化的体式格局来举行建立:
const app = new Koa()
而在实例化的历程当中,实在koa
只做了有限的事变,建立了几个实例属性。
将引入的context
、request
以及response
经由历程Object.create
拷贝的体式格局放到实例中。
this.middleware = [] // 最症结的一个实例属性
// 用于在收到请求后建立上下文运用
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
在实例化完成后,我们就要举行注册中间件来完成我们的营业逻辑了,上边也提到了,koa
仅用作一个中间件的整合以及请求的监听。
所以不会像express
那样供应router.get
、router.post
之类的操纵,仅仅存在一个比较靠近http.createServer
的use()
。
接下来的步骤就是注册中间件并监听一个端口号启动效劳:
const port = 8000
app.use(async (ctx, next) => {
console.time('request')
await next()
console.timeEnd('request')
})
app.use(async (ctx, next) => {
await next()
ctx.body = ctx.body.toUpperCase()
})
app.use(ctx => {
ctx.body = 'Hello World'
})
app.use(ctx => {
console.log('never output')
})
app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))
在翻看application.js
的源码时,可以看到,暴露给外部的要领,经常运用的基础上就是use
和listen
。
一个用来加载中间件,另一个用来监听端口并启动效劳。
而这两个函数现实上并没有过量的逻辑,在use
中仅仅是推断了传入的参数是不是为一个function
,以及在2.x版本针对Generator
函数的一些迥殊处置惩罚,将其转换为了Promise
情势的函数,并将其push
到组织函数中建立的middleware
数组中。
这个是从1.x
过渡到2.x
的一个东西,在3.x
版本将直接移除Generator
的支撑。
实在在koa-convert
内部也是援用了co
和koa-compose
来举行转化,所以也就不再赘述。
而在listen
中做的事变就更简朴了,只是简朴的挪用http.createServer
来建立效劳,并监听对应的端口之类的操纵。
有一个细节在于,createServer
中传入的是koa
实例的另一个要领挪用后的返回值callback
,这个要领才是真正的回调处置惩罚,listen
只是http
模块的一个快捷体式格局。
这个是为了一些用socket.io
、https
或许一些其他的http
模块来举行运用的。
也就意味着,只假如可以供应与http
模块一致的行动,koa
都可以很轻易的接入。
listen(...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
运用koa-compose兼并中间件
所以我们就来看看callback
的完成:
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
}
在函数内部的第一步,就是要处置惩罚中间件,将一个数组中的中间件转换为我们想要的洋葱模子花样的。
这里就用到了比较中心的koa-compose
实在它的功用上与co
相似,只不过把co
处置惩罚Generator
函数那部份逻辑悉数去掉了,自身co
的代码也就是一两百行,所以精简后的koa-compose
代码唯一48行。
我们晓得,async
函数现实上剥开它的语法糖今后是长这个模样的:
async function func () {
return 123
}
// ==>
function func () {
return Promise.resolve(123)
}
// or
function func () {
return new Promise(resolve => resolve(123))
}
所以拿上述use
的代码举例,现实上koa-compose
拿到的是如许的参数:
[
function (ctx, next) {
return new Promise(resolve => {
console.time('request')
next().then(() => {
console.timeEnd('request')
resolve()
})
})
},
function (ctx, next) {
return new Promise(resolve => {
next().then(() => {
ctx.body = ctx.body.toUpperCase()
resolve()
})
})
},
function (ctx, next) {
return new Promise(resolve => {
ctx.body = 'Hello World'
resolve()
})
},
function (ctx, next) {
return new Promise(resolve => {
console.log('never output')
resolve()
})
}
]
就像在第四个函数中输出示意的那样,第四个中间件不会被实行,由于第三个中间件并没有挪用next
,所以完成相似如许的一个洋葱模子是很有意义的一件事变。
起首抛开稳定的ctx
不谈,洋葱模子的完成中心在于next
的处置惩罚。
由于next
是你进入下一层中间件的钥匙,只要手动触发今后才会进入下一层中间件。
然后我们还须要保证next
要在中间件实行终了后举行resolve
,返回到上一层中间件:
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)
}
}
}
所以明白了这两点今后,上边的代码就会变得很清楚:
- next用来进入下一个中间件
- next在当前中间件实行完成后会触发还调关照上一个中间件,而完成的条件是内部的中间件已实行完成(
resolved
)
可以看到在挪用koa-compose
今后现实上会返回一个自实行函数。
在实行函数的开首部份,推断当前中间件的下标来防备在一个中间件中屡次挪用next
。
由于假如屡次挪用next
,就会致使下一个中间件的屡次实行,如许就破坏了洋葱模子。
其次就是compose
现实上供应了一个在洋葱模子悉数实行终了后的回调,一个可选的参数,现实上作用与挪用compose
后边的then
处置惩罚没有太大区分。
以及上边提到的,next
是进入下一个中间件的钥匙,可以在这一个柯里化函数的应用上看出来:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
将自身绑定了index
参数后传入本次中间件,作为挪用函数的第二个参数,也就是next
,效果就像挪用了dispatch(1)
,如许就是一个洋葱模子的完成。
而fn
的挪用假如是一个async function
,那末外层的Promise.resolve
会比及内部的async
实行resolve
今后才会触发resolve
,比方如许:
Promise.resolve(new Promise(resolve => setTimeout(resolve, 500))).then(console.log) // 500ms今后才会触发 console.log
P.S. 一个从koa1.x
切换到koa2.x
的暗坑,co
会对数组举行迥殊处置惩罚,运用Promise.all
举行包装,然则koa2.x
没有如许的操纵。
所以假如在中间件中要针对一个数组举行异步操纵,一定要手动增加Promise.all
,或许说等草案中的await*
。
// koa1.x
yield [Promise.resolve(1), Promise.resolve(2)] // [1, 2]
// koa2.x
await [Promise.resolve(1), Promise.resolve(2)] // [<Promise>, <Promise>]
// ==>
await Promise.all([Promise.resolve(1), Promise.resolve(2)]) // [1, 2]
await* [Promise.resolve(1), Promise.resolve(2)] // [1, 2]
吸收请求,处置惩罚返回值
经由上边的代码,一个koa
效劳已算是运转起来了,接下来就是接见看效果了。
在吸收到一个请求后,koa
会拿之条件到的context
与request
、response
来建立本次请求所运用的上下文。
在koa1.x
中,上下文是绑定在this
上的,而在koa2.x
是作为第一个参数传入进来的。
个人猜想可以是由于Generator
不能运用箭头函数,而async
函数可以运用箭头函数致使的吧:) 纯属个人YY
总之,我们经由历程上边提到的三个模块建立了一个请求所需的上下文,基础上是一通儿赋值,代码就不贴了,没有太多逻辑,就是有一个小细节比较有意义:
request.response = response
response.request = request
让二者之间产生了一个援用关联,既可以经由历程request
猎取到response
,也可以经由历程response
猎取到request
。
而且这是一个递归的援用,相似如许的操纵:
let obj = {}
obj.obj = obj
obj.obj.obj.obj === obj // true
同时如上文提到的,在context
建立的历程当中,将一大批的request
和response
的属性、要领代办到了自身,有兴致的可以本身翻看源码(看着有点晕):koa.js | context.js
这个delegate的完成也算是比较简朴,经由历程掏出原始的属性,然后存一个援用,在自身的属性被触发时挪用对应的援用,相似一个民间版的Proxy
吧,期待后续可以运用Proxy
替代它。
然后我们会将天生好的context
作为参数传入koa-compose
天生的洋葱中去。
由于不管何种状况,洋葱肯定会返回效果的(失足与否),所以我们还须要在末了有一个finished
的处置惩罚,做一些相似将ctx.body
转换为数据举行输出之类的操纵。
koa
运用了大批的get
、set
接见器来完成功用,比方最经常运用的ctx.body = 'XXX'
,它是来自response
的set body
。
这应该是request
、response
中逻辑最庞杂的一个要领了。
里边要处置惩罚许多东西,比方在body
内容为空时协助你修正请求的status code
为204,并移除无用的headers
。
以及假如没有手动指定status code
,会默许指定为200
。
以至还会依据当前传入的参数来推断content-type
应该是html
照样一般的text
:
// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
this.length = Buffer.byteLength(val)
return
}
以及还包含针对流(Stream
)的迥殊处置惩罚,比方假如要用koa
完成静态资本下载的功用,也是可以直接挪用ctx.body
举行赋值的,一切的东西都已在response.js
中帮你处置惩罚好了:
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val))
ensureErrorHandler(val, err => this.ctx.onerror(err))
// overwriting
if (null != original && original != val) this.remove('Content-Length')
if (setType) this.type = 'bin'
return
}
// 可以理解为是如许的代码
let stream = fs.createReadStream('package.json')
ctx.body = stream
// set body中的处置惩罚
onFinish(res, () => {
destory(stream)
})
stream.pipe(res) // 使response吸收流是在洋葱模子完整实行完今后再举行的
onFinish用来监听流是不是完毕、destory用来封闭流
其他的接见器基础上就是一些罕见操纵的封装,比方针对querystring
的封装。
在运用原生http
模块的状况下,处置惩罚URL中的参数,是须要本身引入分外的包举行处置惩罚的,最罕见的是querystring
。 koa
也是在内部引入的该模块。
所以对外抛出的query
大抵是这个模样的:
get query() {
let query = parse(this.req).query
return qs.parse(query)
}
// use
let { id, name } = ctx.query // 由于 get query也被代办到了context上,所以可以直接援用
parse为parseurl库,用来从request中提出query参数
亦或许针对cookies
的封装,也是内置了最盛行的cookies
。
在第一次触发get cookies
时才去实例化Cookie
对象,将这些烦琐的操纵挡在用户看不到的处所:
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
}
所以在koa
中运用Cookie
就像如许就可以了:
this.cookies.get('uid')
this.cookies.set('name', 'Niko')
// 假如不想用cookies模块,完整可以本身赋值为本身想用的cookie
this.cookies = CustomeCookie
this.cookies.mget(['uid', 'name'])
这是由于在get cookies
里边有推断,假如没有一个可用的Cookie实例,才会默许去实例化。
洋葱模子实行完成后的一些操纵
koa
的一个请求流程是如许的,先实行洋葱里边的一切中间件,在实行完成今后,还会有一个回调函数。
该回挪用来依据中间件实行历程当中所做的事变来决议返回给客户端什么数据。
拿到ctx.body
、ctx.status
这些参数举行处置惩罚。
包含前边提到的流(Stream
)的处置惩罚都在这里:
if (body instanceof Stream) return body.pipe(res) // 比及这里完毕后才会挪用我们上边`set body`中对应的`onFinish`的处置惩罚
同时上边另有一个迥殊的处置惩罚,假如为false则不做任何处置惩罚,直接返回:
if (!ctx.writable) return
实在这个也是response
供应的一个接见器,这里边用来推断当前请求是不是已挪用过end
给客户端返回了数据,假如已触发了response.end()
今后,则response.finished
会被置为true
,也就是说,本次请求已完毕了,同时接见器中还处置惩罚了一个bug
,请求已返回效果了,然则依旧没有封闭套接字:
get writable() {
// can't write any more after response finished
if (this.res.finished) return false
const socket = this.res.socket
// There are already pending outgoing res, but still writable
// https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
if (!socket) return true
return socket.writable
}
这里就有一个koa
与express
对照的劣势了,由于koa
采纳的是一个洋葱模子,关于返回值,假如是运用ctx.body = 'XXX'
来举行赋值,这会致使终究挪用response.end
时在洋葱悉数实行完成后再举行的,也就是上边所形貌的回调中,而express
就是在中间件中就可以自在掌握什么时刻返回数据:
// express.js
router.get('/', function (req, res) {
res.send('hello world')
// 在发送数据后做一些其他处置惩罚
appendLog()
})
// koa.js
app.use(ctx => {
ctx.body = 'hello world'
// 但是依旧发生在发送数据之前
appendLog()
})
不过幸亏照样可以经由历程直接挪用原生的response
对象来举行发送数据的,当我们手动挪用了response.end
今后(response.finished === true
),就意味着终究的回调会直接跳过,不做任何处置惩罚。
app.use(ctx => {
ctx.res.end('hello world')
// 在发送数据后做一些其他处置惩罚
appendLog()
})
非常处置惩罚
koa的全部请求,现实上照样一个Promise
,所以在洋葱模子后边的监听不仅唯一resolve
,对reject
也一样是有处置惩罚的。
时期任何一环出bug都邑致使后续的中间件以及前边守候回调的中间件停止,直接跳转到近来的一个非常处置惩罚模块。
所以,假如有相似接口耗时统计的中间件,一定要记得在try-catch
中实行next
的操纵:
app.use(async (ctx, next) => {
try {
await next()
} catch (e) {
console.error(e)
ctx.body = 'error' // 由于内部的中间件并没有catch 捕捉非常,所以抛出到了这里
}
})
app.use(async (ctx, next) => {
let startTime = new Date()
try {
await next()
} finally {
let endTime = new Date() // 抛出非常,然则不影响这里的一般输出
}
})
app.use(ctx => Promise.reject(new Error('test')))
P.S. 假如非常被捕捉,则会继承实行后续的response
:
app.use(async (ctx, next) => {
try {
throw new Error('test')
} catch (e) {
await next()
}
})
app.use(ctx => {
ctx.body = 'hello'
})
// curl 127.0.0.1
// > hello
假如本身的中间件没有捕捉非常,就会走到默许的非常处置惩罚模块中。
在默许的非常模块中,基础上是针对statusCode的一些处置惩罚,以及一些默许的毛病显现:
const code = statuses[err.status]
const msg = err.expose ? err.message : code
this.status = err.status
this.length = Buffer.byteLength(msg)
this.res.end(msg)
statuses是一个第三方模块,包含种种http code的信息: statuses
发起在最外层的中间件都本身做非常处置惩罚,由于默许的毛病提醒有点儿太难看了(纯文本),本身处置惩罚跳转到非常处置惩罚页面会好一些,以及防止一些接口由于默许的非常信息致使剖析失利。
redirect的注重事项
在原生http
模块中举行302
的操纵(俗称重定向),须要这么做:
response.writeHead(302, {
'Location': 'redirect.html'
})
response.end()
// or
response.statusCode = 302
response.setHeader('Location', 'redirect.html')
response.end()
而在koa
中也有redirect
的封装,可以经由历程直接挪用redirect
函数来完成重定向,然则须要注重的是,挪用完redirect
以后并没有直接触发response.end()
,它仅仅是增加了一个statusCode
及Location
罢了:
redirect(url, alt) {
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/'
this.set('Location', url)
// status
if (!statuses.redirect[this.status]) this.status = 302
// html
if (this.ctx.accepts('html')) {
url = escape(url)
this.type = 'text/html charset=utf-8'
this.body = `Redirecting to <a href="${url}">${url}</a>.`
return
}
// text
this.type = 'text/plain charset=utf-8'
this.body = `Redirecting to ${url}.`
}
后续的代码还会继承实行,所以发起在redirect
以后手动完毕当前的请求,也就是直接return
,不然很有可以后续的status
、body
赋值很可以会致使一些诡异的题目。
app.use(ctx => {
ctx.redirect('https://baidu.com')
// 发起直接return
// 后续的代码还在实行
ctx.body = 'hello world'
ctx.status = 200 // statusCode的转变致使redirect失效
})
小记
koa
是一个很好玩的框架,在浏览源码的历程当中,实在也发现了一些小题目:
- 多人协作保护一份代码,确切可以看出大家都有差别的编码作风,比方
typeof val !== 'string'
和'number' == typeof code
,很显然的两种作风。2333 - delegate的挪用体式格局在属性迥殊多的时刻并非很悦目,一大长串的链式挪用,假如换成轮回会更悦目一下
然则,koa
依旧是一个很棒的框架,很合适浏览源码来举行进修,这些都是一些小细节,无伤大雅。
总结一下koa
与koa-compose
的作用:
-
koa
注册中间件、注册http
效劳、天生请求上下文挪用中间件、处置惩罚中间件对上下文对象的操纵、返回数据完毕请求 -
koa-compose
将数组中的中间件鸠合转换为串行挪用,并供应钥匙(next
)用来跳转下一个中间件,以及监听next
猎取内部中间件实行完毕的关照
招人,招人
我司如今大批招人咯,前端、Node方向都有HC
公司名:Blued,坐标帝都旭日双井
重要手艺栈是React,也会有机会玩ReactNative和Electron
Node方向8.x版本+koa 新项目会以TS为主
有兴致的小伙伴可以私聊我,或许:
email: jiashunming@blued.com
wechat: github_jiasm