阅读 73

KOA 中间件实现方式(一)

这是我参与更文挑战的第 10 天,活动详情查看: 更文挑战

Lynne,一个能哭爱笑永远少女心的前端开发工程师。身处互联网浪潮之中,热爱生活与技术。

前言

想要实现一个 KOA 中间件,首先你要懂中间件,昨天我们已经了解过了,其次你要了解 KOA。

今天,将从了解 KOA 开始,基于中间件,以日志中间件为例,分析一个 koa 中间件的实现。也许你已经发现啦,前面的 log4js 可不是白学的!

KOA 概念

从中间件出发了解 KOA

KOA,又称洋葱模型。假如把每个洋葱圈都看作一个中间件,每次当有一个请求进入的时候,每个中间件都会被执行两次。

正如下面代码展示的一样:

const Koa = require("koa")

const app = new Koa()

// 中间件A
app.use(async (ctx, next) => {
    console.log("A1")
    await next()
    console.log("A2")
});
// 中间件B
app.use(async (ctx, next) => {
    console.log("B1")
    await next()
    console.log("B2")
});
// 中间件C
app.use(async (ctx, next) => {
    console.log("C1")
    await next()
    console.log("C2")
});

app.listen(3000);

// 输出
// A1 -> B1 -> C1 -> C2 -> B2 -> A2
复制代码

KOA 实现原理

要想达到上面洋葱圈的运行效果,我们需要做什么呢?

  • 首先我们要知道当前中间件的数组集合
  • 然后构建一个组合方法,对这些中间件按照洋葱的结构进行组合,并执行
// middleware用来保存中间件
app.use = (fn) => {
    this.middleware.push(fn)
    return this
}

// compose组合函数来规定执行次序
function compose (middleware) {
  // context:上下文,next:传入的接下来要运行的函数
  return function (context, next) {
    function dispatch (i) {
      index = i
      // 中间件
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      try {
        // 假设有A、B、C三个中间件,通过dispatch(0)发起了第一个中间件A的执行
        // A执行之后,next作为dispatch(1)会被执行,从而发起下一个中间件B的执行
        // 然后是中间件C被执行,所有中间件都执行一遍后,执行Promise.resolve()
        // 最里面的中间件C的await next()运行结束,会继续执行console.log("C2")
        // 整个中间件C的运行结束又触发了Promise.resolve
        // 中间件B开始执行console.log("B2")
        // 同理,中间件A执行console.log("A2")
        return Promise.resolve(fn(context, () => {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
    return dispatch(0)
  }
}
复制代码

Koa利用了在中间件中间传入next参数的方法,再结合middleware中间件数组和compose组合函数,构建了洋葱圈的中间件执行结构。这也是为什么洋葱圈中间件机制可以运行起来的原因。

利用 koa 中间件机制实现一个koa中间件

前提

日志中间件实现准备

对于一个服务器应用来说,日志的记录是必不可少的,我们需要使用其记录项目程序每天都做了什么,什么时候发生过错误,发生过什么错误等等,便于日后回顾、实时掌握服务器的运行状态,还原问题场景。

那么你首先需要:

安装log4js npm i log4js -S

设置需要日志需要记录的信息段(log_info.js)

export default (ctx, message, commonInfo) => {
    const {
      method,  // 请求方法
      url,          // 请求链接
      host,      // 发送请求的客户端的host
      headers      // 请求中的headers
    } = ctx.request;
    const client = {
      method,
      url,
      host,
      message,
      referer: headers['referer'],  // 请求的源地址
      userAgent: headers['user-agent']  // 客户端信息 设备及浏览器信息
    }
    return JSON.stringify(Object.assign(commonInfo, client));
}
复制代码

设置通用获取配置后的log4js对象(logger.js)

const getLog = ({env, appLogLevel, dir}, name) => {
    
    //log4js基本说明配置项,可自定义设置键名,用于categories.appenders自定义选取
    let appenders = {
        // 自定义配置项1
        cheese: {
            type: 'dateFile', //输出日志类型
            filename: `${dir}/task`,  //输出日志路径
            pattern: '-yyyy-MM-dd.log', //日志文件后缀名(task-2019-03-08.log)
            alwaysIncludePattern: true
        }
    }
    // 如果为开发环境配置在控制台上打印信息
    if (env === "dev" || env === "local" || env === "development") {
        // 自定义配置项2
        appenders.out = {
          type: "stdout"
        }
    }
    // log4js配置
    let config = {
        appenders,
        //作为getLogger方法获取log对象的键名,default为默认使用
        categories: {
          default: {
            appenders: Object.keys(appenders), // 取出appenders中的所有配置项
            level: appLogLevel
          }
        }
    }
    log4js.configure(config) //使用配置项
    return log4js.getLogger(name)// 这个cheese参数值先会在categories中找,找不到就会默认使用default对应的appenders,信息会输出到yyyyMMdd-out.log
}
复制代码

总结

今天讲了日志中间件的 设置信息段和获取配置后的log4js对象,明天继续!

文章分类
前端
文章标签