天天看點

webpack 自定義 loader 解決原始檔案行列号檢視webpack 自定義 loader

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 占有的行号,基本可以滿足對日志定位的需求

繼續閱讀