如何设计一个日志打印器

需求

需求是构建一个可以传入预定义好类型级别,得到一个新的函数或对象,可以通过调用其上面的方法来生成当前对象上允许的级别日志。日志级别共分为 4 个级别,分别为:

  1. silent:静默执行,不会输出任何级别的日志
  2. error:只会输出 error 级别的日志
  3. warn:可以输出 error、warn 级别的日志
  4. info:可以输出 error、warn、info 等任何类型的日志

举个例子,比如传入了 warn,那么调用其上面的 info 方法则不会有任何输出,但是 error warn 方法依旧可以输出对应的日志。

这种输入决定输出类型的函数通常会使用使用工厂模式来处理。我们先了解一下工厂模式的实现思路,其实很多设计模式都是听起来很牛x,但实际上这些模式都是我们日常开发中一些很普遍的做法,我们按照需求一步一步去做就有了以下代码:

function createLogger(level) {
  function error(msg) {
    console.error(`[error] ${msg}`)
  }

  function warn(msg) {
    console.warn(`[warn] ${msg}`)
  }

  function info(msg) {
    console.info(`[info] ${msg}`)
  }

  let logger = {}
  if (level === 'silent') {
    logger = {
      error: () => {},
      warn: () => {},
      info: () => {},
    }
  } else if (level === 'error') {
    logger = {
      error,
      warn: () => {},
      info: () => {},
    }
  } else if (level === 'warn') {
    logger = {
      error,
      warn,
      info: () => {},
    }
  } else if (level === 'info') {
    logger = {
      error,
      warn,
      info,
    }
  }

  return logger
}

日志的权重

以上虽然可以实现需求,但这对我们的学习毫无帮助,因为这只不过是将每天都在写的代码又重复了一遍。后来我发现了另一种思路,即使用一个对象来存储日志的级别,用对象的 key 来保存日志的级别,用对象的 value 来保存日志的权重,这样我们就可以通过权重来判断日志是否可以输出了。这样我们就可以重构一下代码:

const LEVELS = {
  silent: 0,
  error: 1,
  warn: 2,
  info: 3,
}

function createLogger(level) {
  const thresh = LogLevels[level]

  function print(method) {
    if (thresh < LEVELS[level]) {
      return () => {}
    }

    return (msg) => console[method](`[${method}] ${msg}`)
  }

  return {
    error: print.bind(null, 'error'),
    warn: print.bind(null, 'warn'),
    info: print.bind(null, 'info'),
  }
}

只不过在使用的时候还是需要判断一下运行环境,比如说在 "TTY"( Teletypes 或 teletypewriters:虚拟控制台)和服务器 CI 环境中就需要谨慎操作了,因为通常这些日志都是需要保留用于排查错误,或是查看系统当前状态的,如果贸然清除打印的日志的话,则会使得出现问题是排查十分困难,所以建议在调用前判断一下 process.stdout.isTTY && !process.env.CI,如果结果为 true,则表示可以安全调用,否则还是使用一个空函数来替代较为稳妥。

以上。