Eggjs快速入门3:使用中间件实现日志功能和全局错误处理

1,989 阅读2分钟

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

1.中间件原理

Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。

image.png

所有的请求经过一个中间件的时候都会执行两次,对比 Express 形式的中间件,Koa 的模型可以非常方便的实现后置处理逻辑,对比 Koa 和 Express 的 Compress 中间件就可以明显的感受到 Koa 中间件模型的优势。

2.egg中间件的编写

  1. 新建middleware文件件(框架约定middleware)

image.png

2.编写测试中间jstest.js,简单打印ctx.request,ctx.response

module.exports = () => {
    return async (ctx, next) => {
        const req = ctx.request;
        await next();
        const res = ctx.response;
    };
};

3.中间件挂载,config.default.js

  // 配置中间件
  config.middleware = ["test"];

4.postman随意请求此服务一个接口,能打印出请求对象和响应对象 image.png 5.配置

一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:

  • options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
  • app: 当前应用 Application 的实例。
  1. 在框架和插件中使用中间件

应用层定义的中间件(app.config.appMiddleware)和框架默认中间件(app.config.coreMiddleware)都会被加载器加载,并挂载到 app.middleware 上。所以直接app.config.middleware调用即可

3.全局错误处理中间件

使用中间件编写全局异常处理的好处就是每一个请求都会经过中间件,那么可以使用try catch的方式执行,await next()参数就是指的整个请求和响应的逻辑执行过程


async function onerror(ctx, next) {
  try {
    await next();
  } catch (err) {
    ctx.app.emit('error', err);
    ctx.body = 'server error';
    ctx.status = err.status || 500;
  }

参照网上的一些方案,可以写出更加完善的错误捕捉中间件,主要是将try catch的状态捕捉的错误进行标准化处理,比如对于输出信息标准化,对无法处理的异常不抛给前端展示,防止数据库错误字段暴露给用户,对敏感信息进行了处理

'use strict';

const { HttpExceptions } = require('../exceptions/http_exceptions');

module.exports = () => {
  return async function errorHandler(ctx, next) {
    try {
      await next();
    } catch (err) {
      // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
      ctx.app.emit('error', err, ctx);

      let status = err.status || 500;
      const error = {};

      if (err instanceof HttpExceptions) {
        status = err.httpCode;
        error.requestUrl = `${ctx.method} : ${ctx.path}`;
        error.msg = err.msg;
        error.code = err.code;
        error.httpCode = err.httpCode;
      } else {
        // 未知异常,系统异常,线上不显示堆栈信息
        // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
        error.errsInfo = status === 500 && ctx.app.config.env === 'prod'
          ? 'Internal Server Error'
          : err.message;
      }
      // 从 error 对象上读出各个属性,设置到响应中
      ctx.body = error;
      if (status === 422) {
        ctx.body.detail = err.errors;
      }
      ctx.status = status;
    }
  };
};


http_exception

使用一个通用请求状态库来规范响应的请求格式,包括状态码,错误信息,http状态码。注意区分httpCode和code的区别,一个是浏览器的http状态码httpCode,一个是自定义装状态码code

'use strict';

class HttpExceptions extends Error {
  constructor(msg = '服务器异常', code = 1, httpCode = 400) {
    super();
    this.code = code;
    this.msg = msg;
    this.httpCode = httpCode;
  }
}

module.exports = { HttpExceptions };

3.使用中间件开发全局自定义日志

开发全局日志中间件,我们要考虑的三个点是

  • 如何打印出准确的时间?

    实施方案:使用dayjs日期库,用于日期的计算

  • 如何输出规范的日志格式?

    实施方案:自定义统一日志格式,需记录请求方法,请求url,请求host,响应状态,日期

  • 输出在哪个位置?

    实施方案:使用fs文件写功能,将文件写在项目目录下

    完全实现代码如下

'use strict';

const dayjs = require('dayjs');
const fs = require('fs');
const path = require('path');

module.exports = () => {
  return async (ctx, next) => {
    const sTime = Date.now();
    const req = ctx.request;
    const res = ctx.response;
    await next();
    const log = {
      method: req.method,
      url: req.url,
      host: req.header.host,
      responeStatus: res.status,
      data: res.body,
      timeLen: Date.now() - sTime,
    };
    const data = dayjs(sTime).format('YYYY-MM-DD HH:mm:ss') + '[test-httpLog]' + JSON.stringify(log) + '\r\n';
    if (ctx.app.env === 'local') {
      fs.appendFileSync(path.resolve(ctx.app.baseDir, './httpLog/httpLog-local.js'), data);
    } else {
      fs.appendFileSync(path.resolve(ctx.app.baseDir, './httpLog/httpLog-prod.js'), data);
    }

  };
}
;

日志打印出来会相当的完善 image.png