koa middleware

1,169 阅读4分钟

koa

koa -- 基于 Nodejs 平台的下一代 web 开发框架

前言

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

尽管提供了相当多的有用的方法 Koa 仍保持了一个很小的体积,因为没有捆绑中间件。这包括诸如内容协商,缓存清理,代理支持和重定向等常见任务的方法。

虽然 koa 官网的学习文档只有简简单单的一页,但是它所有的精髓都体现得淋漓尽致,总是能够让你想要深入其原理的冲动

初出茅庐

我们以现已成为传统的 “hello world” 案例来开始吧

Koa 中间件以更传统的方式级联,使用 async 功能,我们可以实现 “真实” 的中间件。通过一系列功能直接传递控制,直到一个返回,Koa 调用“下游”,然后控制流回“上游”。

const Koa = require('koa')        // 引入 koa
const app = new Koa()             // 实例化 koa
app.use(async (ctx, next) => {    // 所有请求都经过该异步处理函数(中间件)
  ctx.body = 'Hello World'        // 响应内容
})
app.listen('3000')                // 监听端口

深入理解 Koa 中间件之洋葱模型

先来测试一段代码

const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})
app.use(async (ctx, next) => {
  console.log(3)
  next()
  console.log(4)
})

你如果说输出的是 1 2 3 4

最终结果应该是 1 3 4 2

是不是一脸懵??????这就是接下来要说的洋葱模型

middleware

  • koa 中间件的执行很像一个洋葱,但并不是一层一层的执行,而是以 next 为分界,先执行本层中 next 以前的部分,当下一层中间件执行完后,再执行本层 next 以后的部分。

  • 一个洋葱结构,从上往下一层一层进来,再从下往上一层一层回去,是不是越来越有 feel

如果你愿意 一层一层一层的剥开我的心

middleware 的核心原理

middleware 是从 http 请求开始到响应结束的过程中处理的逻辑,所以需要拿到请求和响应对它们进行一定的逻辑处理。还需要考虑的一个问题是多个中间件共存的问题,考虑怎么样让多个中间件自动执行。

那么,middleware 的模型大致也就出来了

const middleware = (req, res, next) => {
  next()
}

接下来,我们来写几个简单的案例来看看中间件的实现过程

// 定义几个中间件函数
const m1 = (req, res, next) => {
  console.log('m1 run')
  next()
}
const m2 = (req, res, next) => {
  console.log('m2 run')
  next()
}
const m3 = (req, res, next) => {
  console.log('m3 run')
  next()
}
// 中间件集合
const middlewares = [m1, m2, m3]

function use(req, res) {
  const next = () => {
    // 获取第一个中间件
    const middleware = middlewares.shift()
    if (middleware) {
      middleware(req, res, next)
    }
  }
  next()
}
// 第一次请求流进入
use()
// 结果
m1 run
m2 run
m3 run

当然考虑到中间件中有异步的场景,那么我们就要把 next 放在该中间件的异步回调中执行,这样才能保证其执行顺序的正确性

const m2 = (req, res, next) => fetch('xxx').then(() => next())

还有一种中间件的场景,比如说日志中间件、请求监控中间件等,他们会在业务处理前和处理后都会执行相关逻辑,这个时候就要求我们需要能对 next 函数进行二次处理,我们可以把它的返回值包装成 Promise,使得其在业务处理完成之后通过 then 回调继续处理中间件逻辑

function use(req, res, next) {
  const next = () => {
    const middleware = middlewares.shift()
    if (middleware) {
      return Promise.resolve(middleware(req, res, next))
    } else {
      return Promise.resolve('end')
    }
  }
  next()
}

这个时候我们就可以通过如下方式调用了

const m1 = (req, res, next) => {
  console.log('m1 start')
  next().then(() => {
    console.log('m1 end')
  })
}

以上就是实现 middleware 的一个设计模式,当然,我们可以使用 async/await 实现,写法会更加优雅

const m1 = async (req, res, next) => {
  let result1 = await next()
}
const m2 = async (req, res, next) => {
  let result2 = await next()
}
const m3 = async (req, res, next) => {
  let result3 = await next()
}

const middlewares = [m1, m2, m3]

function use(req, res) {
  const next = () => {
    const middleware = middlewares.shift()
    if (middleware) {
      return Promise.resolve(middleware(req, res, next))
    } else {
      return Promise.resolve('end')
    }
  }
  next()
}

use()

在 koa2 框架中,中间件的实现方式也是将 next 方法返回值封装成 Promise 对象,实现了洋葱模型的调用流程

koa middleware 的实现方式

function compose(middleware) {
  // 判断中间件类型
  if (!Array.iaArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    // 中间件必须为函数类型
    if (typeof fn !== 'function') throw new TypeError('Middleware stack must be composed of functions!')
  }
  return function(context, next) {
    // 采用闭包将索引缓存,来实现调用计数
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      // 防止 next 方法重复调用
      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)
      }
    }
  }
}

小结

所以,今天你又成就感满满了吗??????

最后 在此感谢徐小夕提供的思路,并且期待这周六我们的聚餐(算算也有半年多没有聚了)