koa是经常使用的node端框架,它封装了一系列node方法,通过它的api让写服务变得更加方便。而且相比express,koa支持promise写法,更加符合现在的前端代码编写习惯,代码可读性更强。koa相比express更加短小,更多的功能是通过插件的形式实现的,它的设计思路很值得我们参考。
本篇文章将koa的逻辑用流程图表示,让我们理解起来更加简单。
代码结构
koa的代码十分短小精悍,有效的代码文件就lib目录下的4个文件,从package.json里的main
字段可知入口文件是lib/application.js
。koa的主要功能由application.js提供,context.js,request.js和response.js 这三个文件主要是对原生req和res进行扩展,给koa的请求上下文ctx提供更丰富的功能。
Koa创建服务
1 | const Koa = require('koa'); |
如上示例代码,三步就能够起一个简单的koa服务。👉 demo1
起服务各个步骤的详细解析如下:
1. new Koa()
调用了koa的constructor方法,进行了context、request、response的初始化
2. app.use()
将中间件函数存储在middleware中
3. app.listen()
- 起http服务
- 监听请求,如果有请求进来,创建koa的请求上下文ctx,依次调middleware里的中间件函数对请求进行处理。
中间件原理
koa的中间件使用的是洋葱模型,具体我们通过下面两个例子深入了解。
1. koa中间件使用
情况一: await next()
koa中间件正常的使用需要在next前加await,这样能确保请求被每一个中间件都处理完再返回响应。👉 demo2
情况二: 直接next()
,没用await
如果next
前没有加await
,那么中间件1相当于同步代码,如果中间件2有异步代码,中间件1不会等待中间件2执行完而是直接返回。这样会导致请求没有被后面的中间件处理就返回了,我们在应用中尽量避免此情况。👉 demo3
小结:
我们在使用koa中间件时确保使用的规范性,用await next()
这种方式,而不是next()
这种。因为我们没法确保每个中间件都没有异步代码,使用await next()
能等待异步代码执行完毕再往下执行。
2. koa中间件原理
koa中间件是通过koa-compose实现的。为了方便理解,我将compose函数简化一下。
compose函数简化版
1 | function compose(middlewares) { |
compose执行时会先通过dispatch(0)
执行middlewares的第一个函数,传入()=>{dispatch(1)}
作为next。中间件1运行到next时会触发dispatch(1)
的执行。dispatch(1)
则会执行中间件2,并且传入()=>{dispatch(2)}
作为next。中间件2执行到next()时相当于执行dispatch(2)
,从而触发中间件3执行…
具体的执行过程可以下载 👉 compose精简版,看简化版compose的运行结果。
koa上下文(ctx)
koa中使用的环境变量是ctx。通过Object.keys(ctx)
可知ctx上有 request, response, app, req, res, originalUrl, state
这几个属性。其中ctx.res, ctx.req是原生的请求和响应;ctx.response,ctx.request是针对原生请求的扩展。
koa要保证不同请求的ctx保持独立,并且将ctx的属性获取代理到response.js和request.js上,简化ctx取值操作。
1. koa的context实现了不同应用以及不同请求之间的环境隔离
如图所示:
① 每个koa应用都会拥有自己的context, (源码:koa/application.js 59行) 实现应用的隔离。
② 每个请求都会通过Object.create()拥有自己的ctx, (源码:koa/application.js 178行) 实现请求的隔离。
通过两次的Object.create
koa确保每次请求都能拥有独立的ctx变量,避免不同请求的ctx变量互相污染。
2. ctx能直接取request或response的值,因为ctx被代理到request.js和response.js上,这给开发带来了便利。
我们在编码时可以直接写ctx.path
来获取当前请求的路径,这是原生的req不提供的。当从ctx上取属性path时,因为ctx被做了代理,实际上是从cxt.request
上取path的(如图上步骤①,对应源码 koa/context.js 253行)。而cxt.request
是request.js提供的(如图上步骤②, 对应源码 koa/application.js 179行),它能通过this.req
获取到原生req(源码 koa/application.js 182行),然后经过解析处理转化成url返回给 ctx.path
。
再举一个例子,当我们写入ctx.body='ok'
时,实际上ctx被代理到ctx.response
上,在response.js内通过set body()
写入this._body变量。当返回请求时,通过get body()
获取并返回this._body。
小结: koa将context上的属性代理到request.js和response.js上,通过属性劫持给context扩展了一些新的属性和方法。(源码koa/context.js 199-251行)。我们可以直接从ctx上获取对应的属性和方法,给代码的编写提供了便利。
总结
希望读完这篇文章能够让你对koa的实现原理有进一步的了解。koa短小精简的代码非常适合阅读。这篇文章主要介绍了:
- koa中间件实现使用了洋葱模型。
- koa每次请求都会创建一个新的请求上下文,保证了context的请求隔离和应用隔离。
- koa将context上的属性代理给response.js和request.js处理,扩展了context功能。
参考
—
希望这篇文章能够给你有所帮助,对文章中的内容有疑问的地方欢迎一起探讨。