webpack 自定义 loader 解决原始文件行列号查看

6 篇文章 0 订阅
2 篇文章 0 订阅

webpack 自定义 loader

需求来源

需要在客户端中写入日志,并且需要知道报错日志的原文件路径和行号,不采用 sentry 的方式(需要额外部署)

思路

想法是在 logger 的最后两个参数中加入原始文件名和行号,所以这一步在 webpack 加载文件的时候就需要去解析文件,默认添加参数

自定义 loader

const path = require('path')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const parser = require('@babel/parser')
const t = require('@babel/types')

/**
 * 自己封装的方法
 * 使用方式:logger.log(...args)
 */
const loggers = [
  'log',
  'error',
  'warn',
  'info',
  'debug'
]

function loggerLoader(content) {
  // 得到源文件的路径
  const filename = path.relative(this.rootContext, this.resourcePath)
  // 解析 AST
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // 便利 AST
  traverse(ast, {
    // https://astexplorer.net/ 这里可以在这个网站中看见,遍历对应的 Expression
    CallExpression(path) {
      const memberExpression = path.get('MemberExpression')
      // 这里可以通过 debug 的方式一步步寻找自己想要的节点
      if (memberExpression && memberExpression.container && memberExpression.container.callee) {
        const callee = memberExpression.container.callee

        if (callee.object && callee.property) {
          // 验证 logger.log 
          if (t.isIdentifier(callee.object, {name: 'logger'}) &&
            loggers.find(v => t.isIdentifier(callee.property, {name: v}))) {
            // 构造一个 string ,传入文件路径
            const filenameNode = t.stringLiteral(filename)
            // 构造一个 number ,传入开始行号
            const lineNumStart = t.numericLiteral(path.node.loc.start.line)
            // 构造一个 number ,传入结束行号
            const lineNumEnd = t.numericLiteral(path.node.loc.end.line)

            // 放入到参数末尾中
            path.node.arguments.push(filenameNode)
            path.node.arguments.push(lineNumStart)
            path.node.arguments.push(lineNumEnd)
          }
        }
      }
    }
  })

  // 重新生成代码,则所有的 logger.log 在最后都加上了三个参数 (...args, filename, lineNumStart, lineNumEnd)
  return generate(ast, {}).code
}

// 默认导出
module.exports = loggerLoader

封装 logger

采用了 winston 进行日志写入

const {
  createLogger,
  format,
  transports
} = require('winston')
require('winston-daily-rotate-file')
const os = require('os')
const path = require('path')

// 构造 Symbol
const loggerSymbol = Symbol('loggerSymbol')

// winston 中需要的方法名,其他可以自行加入
const LOGGERS = [
  'error',
  'warn',
  'info',
  'debug'
]

class Logger {
  // winston 实例
  constructor(logger) {
    this.logger = logger
  }

  log(...args) {
    this.info(...args)
  }

  info(...args) {
    this[loggerSymbol]('log', ...args)
    this.logger.info(...args)
  }

  warn(...args) {
    this[loggerSymbol]('warn', ...args)
    this.logger.warn(...args)
  }

  error(...args) {
    this[loggerSymbol]('error', ...args)
    this.logger.error(...args)
  }

  debug(...args) {
    this[loggerSymbol]('debug', ...args)
    this.logger.debug(...args)
  }

  [loggerSymbol](type, ...args) {
    // 获取最后三个变量,则是原始文件路径,开始行号和结束行号
    const fileInfoArgs = args.splice(args.length - 3)
    if (process.env.NODE_ENV === 'development') {
      // 按需答应
      console[type](
        `[${fileInfoArgs[0]}] [${fileInfoArgs[1]}] [${fileInfoArgs[2]}]`,
        ...args
      )
    }
  }
}

const customFormat = format.combine(
  // format.label({ label: 'render' }),
  format.timestamp({
    format: 'YYYY-MM-DD HH:mm:ss'
  }),
  format.align(),
  // 构造打印日志的格式 
  format.printf((info) => {
    info.message = info.message.substring(1)
    // webpack 编译传入的参数对象
    const symbol = Object.getOwnPropertySymbols(info).find(symbol => symbol.description === 'splat')
    let message
    if (symbol) {
      // 通过 webpack loader 会传入文件所在的位置
      const args = info[symbol]
      const len = args.length
      if (len >= 3) {
        // 自定义日志格式,加入源文件路径,开始行号和结束行号
        message = `[${[info.timestamp]}] [${args[len - 3]}] [${args[len - 2]}] [${args[len - 1]}] [${info.level}] - ${info.message}`
      }
    }
    if (!message) {
      message = `[${[info.timestamp]}] [${info.level}] - ${info.message}`
    }

    return message
  })
)

const defaultOptions = {
  format: customFormat,
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
  // maxSize: '1m',
  maxFiles: '14d'
}

let filename = 'logs/%DATE%.log'
filename = process.env.NODE_ENV === 'development' ? filename : path.join(os.tmpdir(), filename)

const logger = createLogger({
  // format: customFormat,
  transports: [
    new transports.DailyRotateFile({
      filename,
      level: 'debug',
      ...defaultOptions
    })
  ]
})

const toMessage = (arg) => `${arg} `

// 重写 winston 实例,对路径和行号参数进行处理,把原有的参数变为字符串
LOGGERS.forEach(v => {
  const fn = logger[v]
  if (fn) {
    logger[v] = function (...args) {
      let message = ''
      const fileInfoArgs = args.splice(args.length - 3)

      args.forEach(arg => {
        if (typeof arg === 'object') {
          message += toMessage(JSON.stringify(arg))
        } else {
          message += toMessage(arg)
        }
      })

      fn(message, ...fileInfoArgs)
    }
  }
})

const hxLogger = new Logger(logger)

module.exports = {
  logger: hxLogger,
  loggerFilePath: filename
}

webpack 配置

// webpack 配置
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      // 制定自定义 loader 的路径
      use: [path.join(__dirname, './console-loader.js')]
    }]
  }
}

// 代码使用
logger.info('test')
logger.error('test')
logger.debug('test')
logger.warn('test')
logger.log('test')

日志样例

[2022-10-25 13:11:59] [src/renderer/App.vue] [17] [17] [info] - IPV4 111.111.111.111
[2022-10-25 13:11:59] [src/renderer/App.vue] [18] [18] [info] - mac 地址 11:11:11:11:11:11 
[2022-10-25 13:11:59] [src/renderer/App.vue] [19] [19] [info] - 磁盘序列号 1111111111
[2022-10-25 13:11:59] [src/renderer/App.vue] [20] [20] [info] - 系统序列号 111111111
[2022-10-25 13:11:59] [src/renderer/App.vue] [21] [21] [info] - 系统驱动版本 12.6 
[2022-10-25 13:11:59] [src/renderer/App.vue] [22] [22] [info] - 用户名 11111111

不足

vue 文件中的行号不是对应 script 标签中的行号,需要减去前面 template 占有的行号,基本可以满足对日志定位的需求

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值