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