初探Node服务器——koa

170 阅读5分钟

Koa简介

Koa概述

Koa是由Express原班人马打造的一个小型框架。相比于Express,Koa是一个更加轻量级,拓展性更高的微型框架。因为Express内部还内置了路由、视图等常见的功能,而Koa.js完全没有提供,而是由各种中间件组成,像常用的路由,社区中就有很多实现方法。此外,Koa对中间件的处理,不仅仅是在请求层面进行拦截,还能对响应进行拦截,这一点是Express所做不到的。

Koa主要由两个版本,v1.x(统称为v1)和v2.x(统称为v2),这两个版本的核心功能其实都差不多。只不过v1版本是一个过渡版本,现阶段我们常用的版本还是以v2为主,也就是我们常说的koa2。他们之间主要的差别如下:

  1. v1基于ES6 Generator写法,而v2主打async函数
  2. 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的优势

  1. 使用async函数做异步流程控制时,代码更容易理解。
  2. 性能非常好,比Express要好。
  3. 搭建应用非常快速

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没有杂糅太多的功能,而是以中间件的形式交给开发者自由拓展。基于其对中间件的特殊处理,使得中间件功能更加强大。