Koa简介
Koa概述
Koa是由Express原班人马打造的一个小型框架。相比于Express,Koa是一个更加轻量级,拓展性更高的微型框架。因为Express内部还内置了路由、视图等常见的功能,而Koa.js完全没有提供,而是由各种中间件组成,像常用的路由,社区中就有很多实现方法。此外,Koa对中间件的处理,不仅仅是在请求层面进行拦截,还能对响应进行拦截,这一点是Express所做不到的。
Koa主要由两个版本,v1.x(统称为v1)和v2.x(统称为v2),这两个版本的核心功能其实都差不多。只不过v1版本是一个过渡版本,现阶段我们常用的版本还是以v2为主,也就是我们常说的koa2。他们之间主要的差别如下:
- v1基于ES6 Generator写法,而v2主打async函数
- v1使用隐式的this作为上下文,而v2则使用显式的ctx作为上下文
快速搭建一个Koa应用
其实搭建一个Koa应用是非常简单的,可以自己动手建立一个应用,也能通过像koa-generator 之类的脚手架生成一个Koa应用。下面我们分别来看一下。
mkdir koa-example-1 && cd koa-example-1
npm init # 一直回车即可
touch app.js
准备完毕后,我们执行npm i koa操作,安装一下koa依赖。
然后就可以在app.js中进行开发。
const Koa = require('koa');
const app = new Koa();
const logMiddleware = async (ctx, next) => {
const start = new Date();
next();
const end = new Date();
const time = end.getTime() - start.getTime();
console.log(`${ctx.method} ${ctx.path} use time ${time} ms`);
}
app.use(logMiddleware)
app.use(async (ctx) => {
if(ctx.path === '/') {
ctx.body = 'hello Koa!';
}else if(ctx.path === '/get'){
ctx.body = 'get something.';
}
})
app.listen(3000, function () {
console.log('Server listening on port 3000');
});
如上图,我们成功了,我们搭建了一个能够访问,能够使用中间件的简单应用。
下面我们看看如何使用koa-generator 搭建的koa应用
首先使用npm i -g koa-generator 全局安装
然后执行koa2 koa-example-2 我们就搭建出了一个简单的koa2的脚手架
如上图,是生成的koa应用的目录结构,主要包含视图和路由。
访问localhost:3000 出现该页面即应用搭建成功
Koa的优势
- 使用async函数做异步流程控制时,代码更容易理解。
- 性能非常好,比Express要好。
- 搭建应用非常快速
Koa中间件
中间件概述
中间件是框架的扩展机制,主要用于抽象HTTP请求过程。在单一请求响应过程中加入中间件,可以更好地应对复杂的业务逻辑。每个中间件在HTTP处理过程中通过改写请求和响应数据、状态,实现了特定的功能。从代码层面来看,在App和server.listen中间的都是中间件,过滤的先后顺序和挂载中间件的顺序有关,越靠前的中间件越早执行。
此外,koa1和koa2的中间件写法不太相同,前面我们提到的,koa1的中间件写法是基于Generator,koa2支持三种写法(最常见,使用最多的还是async函数的写法),如下:
- 通用函数中间件,即通过回调函数的写法
- 生成器函数中间件,与Generator的写法类似
- async函数写法,可读性比较好的一种写法,推荐使用
常用中间件
- Koa-router:
express风格的路由中间件,对于熟悉express开发者使用koa开发很友好。 - Koa-view:内涵三种渲染模版引擎,不过在现在前后端分离横行的时代,很少使用了。
- Koa-static:可以生成静态服务的中间件。
- Koa-session:提供操作
session的中间件,操作cookie是koa自带的功能,通过ctx.cookie操作即可。 - Koa-bodyparse:用于解析表单的中间件,可以处理文件表单。
中间件原理
koa的中间件相比起express的中间件,不仅能够在请求时发起拦截,也能够对响应数据进行拦截,这得益于他中间件的处理模型——洋葱模型。

use
use函数的代码如下,除了判错和debug之外,它只做了一件事,。我们可以看到实际上这个方法只做了类型判断和存储中间件,最后返回一个 this 供用户链式调用。
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
callback
那么中间件是在哪里执行的呢?其实是在我们调用listen 函数建立一个http服务时,会调用callback 函数,它是我们在调用 listen 方法时实际传入 http.createServer 的回调函数,在其中整合了之前所有的方法。它又做了两件事1、创建ctx实例;2将上下文ctx实例和整合的中间件传入handleRequest函数中并返回结果。
callback () {
const fn = this.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
}
可以看到,在callback函数中,调用了compose函数,将所有的中间件整合成了一个函数,然后又在handleRequest中调用。到这里还是没有解决为什么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)
}
}
}
}
以上就是我们整个compose函数的代码。代码量不多,但是却非常巧妙。
首先函数做了一定的类型判断,确保中间件保存在一个数组中,并且确保每一个中间件都是一个函数。
再然后就是返回一个函数了。这个函数只有一个作用,就是把所有的中间件函数汇聚成一个函数。
// 假设有这样一个中间件数组
const middleware = [fn1,fn2,fn3]
// 经过compose后最终得到的函数会是这样的
const fnMiddleware = function(context){
return Promise.resolve(fn1(context,function(){
return Promise.resolve(fn2(context,function(){
return Promise.resolve(fn3(context,function(){
return Promise.resolve();
}))
}))
}))
}
当然上诉代码是在不考虑错误捕获的情况下,只要有一个中间件函数出错,中间件就会停止执行,近而返回上一个中间件继续执行。我们可以看一下compose的测试代码。
it('should catch downstream errors', async () => {
const arr = []
const stack = []
stack.push(async (ctx, next) => {
arr.push(1)
try {
arr.push(6)
await next()
arr.push(7)
} catch (err) {
arr.push(2)
}
arr.push(3)
})
stack.push(async (ctx, next) => {
arr.push(4)
throw new Error()
})
stack.push(async (ctx, next) => {
arr.push(8)
})
await compose(stack)({})
// 校验通过,并没有执行push(8)
expect(arr).toEqual([1, 6, 4, 2, 3])
})
总结
koa与其数以万计的中间件打造了一个新型的node服务框架。koa没有杂糅太多的功能,而是以中间件的形式交给开发者自由拓展。基于其对中间件的特殊处理,使得中间件功能更加强大。