logger.js

/**
 * @file 日志打印器
 * @author lisfan <goolisfan@gmail.com>
 * @version 1.3.2
 * @licence MIT
 */

import validation from '@~lisfan/validation'

/**
 * 从`localStorage`的`LOGGER_RULES`键中读取**打印规则**配置,以便可以在生产环境开启日志打印调试
 */
const LOGGER_RULES = JSON.parse(global.localStorage.getItem('LOGGER_RULES')) || {}

/**
 * 从`localStorage`的`IS_DEV`键中读取是否为**开发环境**配置,以便可以在生产环境开启日志打印调试
 */
const IS_DEV = JSON.parse(global.localStorage.getItem('IS_DEV')) || process.env.NODE_ENV === 'development'

// 私有方法
const _actions = {
  /**
   * 打印器工厂函数
   * 查找ls中是否存在打印命名空间配置项,若存在,则进行替换覆盖
   * 判断是否存在子命名空间,依次判断子命名空间的长度
   *
   * @since 1.3.0
   *
   * @param {Logger} self - Logger实例
   * @param {string} method - 打印方法
   * @param {string} color - 颜色值,web安全色 http://www.w3school.com.cn/tiy/color.asp?color=LightGoldenRodYellow
   *
   * @returns {function}
   */
  logFactory(self, method, color) {
    return (...args) => {
      return _actions.logProxyRun(self, method, color, ...args)
    }
  },
  /**
   * 代理运行打印方法
   *
   * @since 1.3.0
   *
   * @param {Logger} self - Logger实例
   * @param {string} method - 打印方法
   * @param {string} color - 打印颜色,颜色值,web安全色 http://www.w3school.com.cn/tiy/color.asp?color=LightGoldenRodYellow
   * @param {...*} args - 其他参数
   *
   * @returns {Logger}
   */
  logProxyRun(self, method, color, ...args) {
    // 处于非激活状态的话则不输出日志
    if (!self.isActivated(method)) {
      return self
    }

    let formatStr = `%c[${self.$name}]:%c`

    // 遍历参数列表,找出dom元素,进行转换
    args = args.map((arg) => {
      return validation.isElement(arg)
        ? [arg]
        : arg
    })

    /* eslint-disable no-console */
    console[method](formatStr, `color: ${color}`, '', ...args)
    /* eslint-enable no-console */

    return self
  },
  /**
   * 代理运行console方法
   * [注] 内部会进行判断是否允许日志输出
   *
   * @since 1.3.0
   *
   * @param {Logger} self - Logger实例
   * @param {string} method - 打印方法
   * @param {...*} args - 其他参数
   *
   * @returns {Logger}
   */
  proxyRun(self, method, ...args) {
    /* eslint-disable no-console */
    self.isActivated(method) && console[method](...args)
    /* eslint-enable no-console */

    return self
  }
}

/**
 * @classdesc 日志打印类
 *
 * @class
 */
class Logger {
  /**
   * 打印器命名空间规则配置项
   * - 可以配置整个命名空间是否输出日志
   * - 也可以配置命名空间下某个实例方法是否输出日志
   *
   * @since 1.2.0
   *
   * @static
   * @readonly
   * @memberOf Logger
   *
   * @type {object}
   * @property {object} rules - 打印器命名空间规则配置集合
   */
  static rules = LOGGER_RULES

  /**
   * 更改命名空间规则配置项
   * [注]从`localStorage`的`LOGGER_RULES`键中读取规则配置优先级最高,始终会覆盖其他规则
   *
   * @since 1.2.0
   *
   * @param {object} rules - 配置参数
   * @param {string} [rules.name] - 日志器命名空间
   * @param {boolean} [rules.debug] - 调试模式是否开启
   *
   * @returns {Logger}
   *
   * @example
   * // 定义规则
   * Logger.configRules = {
   *    utils-http:false // 整个utils-http不可打印输出
   *    utils-calc.log=true // utils-calc打印器的log方法不支持打印输出
   * }
   */
  static configRules(rules) {
    Logger.rules = {
      ...Logger.rules,
      ...rules,
      ...LOGGER_RULES,
    }

    return this
  }

  /**
   * 默认配置选项
   * 为了在生产环境能开启调试模式
   * 提供了从localStorage获取默认配置项的措施
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Logger
   *
   * @type {object}
   * @property {string} name='logger' - 日志器命名空间,默认为'logger'
   * @property {boolean} debug=true - 调试模式是否开启,默认开启
   */
  static options = {
    name: 'logger',
    debug: true,
  }

  /**
   * 更新默认配置选项
   *
   * @since 1.0.0
   *
   * @see Logger.options
   *
   * @param {object} options - 配置选项见{@link Logger.options}
   *
   * @returns {Logger}
   */
  static config(options) {
    // 以内置配置为优先
    Logger.options = {
      ...Logger.options,
      ...options
    }

    return this
  }

  /**
   * 构造函数
   *
   * @see Logger.options
   *
   * @param {object} options - 配置选项见{@link Logger.options},若参数为`string`类型,则表示设定为`options.name`的值
   */
  constructor(options) {
    this.$options = validation.isString(options)
      ? {
        ...Logger.options,
        name: options
      }
      : {
        ...Logger.options,
        ...options
      }
  }

  /**
   * 实例初始配置项
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {object}
   */
  $options = undefined

  /**
   * 获取实例的命名空间配置项
   *
   * @since 1.1.0
   *
   * @getter
   * @readonly
   *
   * @type {string}
   */
  get $name() {
    return this.$options.name
  }

  /**
   * 获取实例的调试模式配置项
   *
   * @since 1.1.0
   *
   * @getter
   * @readonly
   *
   * @type {boolean}
   */
  get $debug() {
    return this.$options.debug
  }

  /**
   * 检测当前是否调试模式是否激活:可以打印日志
   *
   * @since 1.1.0
   *
   * @param {string} [method] - 若指定了该参数,则精确检测具体的实例方法
   *
   * @returns {boolean}
   */
  isActivated(method) {
    // 如果不是开发模式
    if (!IS_DEV) {
      return false
    }

    // 以子命名空间的状态优先
    let status = Logger.rules[this.$name]
    // 先判断其子命名空间的状态

    // 如果存在放法名,则判断子命名空间
    // 当前方法名存在子命名空间里且明确设置为false时,则不打印
    // 当前子命名空间如果明确false,则不打印
    if (method) {
      const subStatus = Logger.rules[`${this.$name}.${method}`]

      if (validation.isBoolean(subStatus)) status = subStatus
    }

    // 如果明确指定该命名空间不开启日志打印,则不打印
    if (status === false) {
      return false
    }

    // 最后才判断实例是否禁用了调试
    if (!this.$debug) {
      return false
    }

    return true
  }

  /**
   * 创建一个指定颜色的打印方法
   *
   * @since 1.1.0
   *
   * @param {string} color - 颜色值
   *
   * @returns {Function}
   */
  color(color) {
    return _actions.logFactory(this, 'log', `${color}`)
  }

  /**
   * 启用日志输出
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  enable() {
    this.$options.debug = true

    return this
  }

  /**
   * 禁用日志输出
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  disable() {
    this.$options.debug = false

    return this
  }

  /**
   * 常规日志打印
   *
   * @since 1.0.0
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  log(...args) {
    return _actions.logProxyRun(this, 'log', 'lightseagreen', ...args)
  }

  /**
   * 警告日志打印
   *
   * @since 1.0.0
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  warn(...args) {
    return _actions.logProxyRun(this, 'warn', 'goldenrod', ...args)
  }

  /**
   * 调用栈日志打印
   *
   * @since 1.0.1
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  trace(...args) {
    return _actions.logProxyRun(this, 'trace', 'lightseagreen', ...args)
  }

  /**
   * 错误日志打印,同时会抛出错误,阻塞后续逻辑
   *
   * @since 1.0.0
   *
   * @param {...*} args - 参数列表
   *
   * @throws Error - 抛出错误提示
   */
  error(...args) {
    const message = args.map((value) => {
      return value.toString()
    }).join(' ')

    throw new Error(message)
  }

  /**
   * log的同名方法,使用方法请参考{@link Logger#log}
   *
   * @since 1.1.0
   *
   * @see Logger#log
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  info(...args) {
    return this.log(...args)
  }

  /**
   * log的同名方法,使用方法请参考{@link Logger#log}
   *
   * @since 1.1.0
   *
   * @see Logger#log
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  debug(...args) {
    return this.log(...args)
  }

  /**
   * 区别于console.table
   * - 对象或数组类型数据以表格的方式打印
   * - 若非这两种数据类型,则调用log方法打印
   *
   * @since 1.1.0
   *
   * @param {*} data - 任意数据
   *
   * @returns {Logger}
   */
  table(data) {
    return validation.isArray(data) && validation.isPlainObject(data)
      ? this.log(data)
      : _actions.proxyRun(this, 'table', data)
  }

  /**
   * 打印纯对象数据
   *
   * @since 1.1.0
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  dir(...args) {
    return _actions.proxyRun(this, 'dir', ...args)
  }

  /**
   * 打印纯对象数据
   *
   * @since 1.1.0
   *
   * @param {...*} args - 任意数据
   *
   * @returns {Logger}
   */
  dirxml(...args) {
    return _actions.proxyRun(this, 'dirxml', ...args)
  }

  /**
   * 创建一个组,接下来所有的打印内容,都会包裹在组内,直到调用groupEnd()方法结束,退出组
   *
   * @since 1.1.0
   *
   * @param {string} label - 标签名称
   *
   * @returns {Logger}
   */
  group(label) {
    return _actions.proxyRun(this, 'group', label)
  }

  /**
   * 类似group()方法,区别在于调用该方法后打印的内容都是折叠的,需要手动展开
   *
   * @since 1.1.0
   *
   * @param {string} label - 标签名称
   *
   * @returns {Logger}
   */
  groupCollapsed(label) {
    return _actions.proxyRun(this, 'groupCollapsed', label)
  }

  /**
   * 关闭组
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  groupEnd() {
    return _actions.proxyRun(this, 'groupEnd')
  }

  /**
   * 统计被执行的次数
   *
   * @since 1.1.0
   *
   * @param {string} label - 标签名称
   *
   * @returns {Logger}
   */
  count(label) {
    return _actions.proxyRun(this, 'count', label)
  }

  /**
   * 开始设置一个timer追踪操作任意的消耗时间,直到调用timeEnd()结束追踪,消耗时间单位为毫秒
   *
   * @since 1.1.0
   *
   * @param {string} label - 标签名称
   *
   * @returns {Logger}
   */
  time(label) {
    return _actions.proxyRun(this, 'time', label)
  }

  /**
   * 结束追踪
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  timeEnd() {
    return _actions.proxyRun(this, 'timeEnd')
  }

  /**
   * 结束追踪
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  timeStamp(...args) {
    return _actions.proxyRun(this, 'timeStamp', ...args)
  }

  /**
   * 开始记录一个性能分析简报,直到调用profileEnd()结束记录
   *
   * @since 1.1.0
   *
   * @param {string} label - 标签名称
   *
   * @returns {Logger}
   */
  profile(label) {
    return _actions.proxyRun(this, 'profile', label)
  }

  /**
   * 结束记录
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  profileEnd() {
    return _actions.proxyRun(this, 'profileEnd')
  }

  /**
   * 断言表达式,若结果为false,是抛出失败输出
   *
   * @since 1.1.0
   *
   * @param {boolean}  assertion - 表达式
   * @param {...*} args - 断言失败输出
   *
   * @returns {Logger}
   */
  assert(assertion, ...args) {
    return _actions.proxyRun(this, 'assert', assertion, ...args)
  }

  /**
   * 清空控制台
   *
   * @since 1.1.0
   *
   * @returns {Logger}
   */
  clear() {
    return _actions.proxyRun(this, 'clear')
  }
}

export default Logger