教你巧用webpack在日志中記錄文件行號
前言
在做前端項目時,會在各個關鍵節點打印日志,方便後續數據分析和問題排查。當日志越來越多之後,又會遇到通過日志反查代碼所在文件和所在行的場景,於是一個很自然的需求就出來瞭:
在打印日志的時候,自動註入當前文件名、行號、列號。
舉個例子,有個 logger 函數,我們在 index.js 的業務代碼某一行添加打印邏輯:
const { logLine } = require('./utils') function getJuejinArticles() { const author = 'keliq' const level = 'LV.5' // ... 業務代碼省略,獲取文章列表 logLine(author, level) // ... } getJuejinArticles()
正常情況下會輸出:
keliq LV.5
但是希望能夠輸出帶文件名和行號,即:
[index.js:7:3] keliq LV.5
表明當前這次打印輸出來源於 index.js 文件中的第 7 行第 3 列代碼,也就是 logLine 函數所在的具體位置。那如何實現這個需求呢?我的腦海中浮現瞭兩個思路:
通過提取 Error 錯誤棧
因為 error 錯誤棧裡面天然帶有此類信息,可以人工制造瞭一個 Error,然後捕獲它:
exports.logLine = (...args) => { try { throw new Error() } catch (e) { console.log(e.stack) } }
仔細觀察打印的結果:
Error
at logLine (/test/src/utils.js:3:11)
at getJuejinArticles (/test/src/index.js:7:3)
at Object.<anonymous> (/test/src/index.js:11:1)
at Module._compile (node:internal/modules/cjs/loader:1105:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
at node:internal/main/run_main_module:17:47
第三行的內容不正是我們想要的結果嗎?隻需要把這一行的字符串進行格式化一下,提取出 index.js:7:3 即可:
at getJuejinArticles (/test/src/index.js:7:3)
由於代碼結構是這樣的:
.
└── src
├── index.js
└── utils.js
隻需要改成下面的代碼即可:
exports.logLine = (...args) => { try { throw new Error() } catch (e) { const lines = e.stack.split('\n') const fileLine = lines[2].split('/src/').pop().slice(0, -1) console.log(`[${fileLine}]`, ...args) } }
命令行試一試:
$ test node src/index.js [index.js:7:3] keliq LV.5
問題似乎完美解決,然而還是想的太簡單瞭,上述場景僅限於 node.js 環境,而在 web 環境,所有的產物都會被 webpack 打到一個或多個 js 文件裡面,而且做瞭壓縮混淆處理,由於 error 是在運行時被捕獲到的 ,所以我沒根本無法拿到開發狀態下的文件名、行號和列號,如下圖所示:
通過 webpack 預處理
那怎麼辦呢?解鈴還須系鈴人,既然 webpack 對代碼進行瞭加工處理,那就隻能在預處理最開始的階段介入進來,寫一個自定義的 loader 來解析源碼文件,拿到文件名、行號和列號。說幹就幹,創建一個 inject-line.loader.js,寫下模板代碼:
module.exports = function (content) { content = content.toString('utf-8') if (this.cacheable) this.cacheable() console.log(this.resourcePath) // 打印文件路徑 console.log(content) // 打印文件內容 return content } module.exports.raw = true
然後在 webpack.config.js 中做配置:
module.exports = { entry: './src/index.js', output: { filename: 'index.js', }, module: { rules: [ { test: /.js$/, exclude: [/node_modules/], use: [ { loader: require.resolve('./loaders/inject-line.loader'), }, ], }, ], }, }
一切準備就緒,先運行一下看看輸出:
可以看到,index.js 和 utils.js 被自定義的 inject-line.loader.js 給加載到瞭,通過 this.resourcePath 能夠拿到文件名稱,行號和列號的話隻能通過分析 content 字符串進行提取瞭,處理的代碼如下:
// 拿到文件路徑 const fileName = this.resourcePath.split('/src/').pop() // 文本內容按行處理後再拼接起來 content = content .split('\n') .map((line, row) => { const re = /logLine((.*?))/g let result let newLine = '' let cursor = 0 while ((result = re.exec(line))) { const col = result.index newLine += line.slice(cursor, result.index) + `logLine('${fileName}:${row + 1}:${col + 1}', ` + result[1] + ')' cursor += col + result[0].length } newLine += line.slice(cursor) return newLine }) .join('\n')
這裡面的邏輯,如果光看代碼的話可能會雲裡霧裡,其實思路很簡單,就是下面這樣的:
這樣的話,即使代碼經過各種壓縮轉換,也不會改變開發狀態下代碼所在的文件名、行與列的位置瞭。打開 webpack 打包後的文件看一下:
到這裡,功能就已經開發完瞭,不過還有一個小小的缺陷就是 logLine 函數名是寫死的,能不能讓用戶自己定義這個函數名呢?當然可以,在 webpack 配置文件中,支持利用 options 屬性傳遞 config 配置參數:
module.exports = { entry: './src/index.js', output: { filename: 'index.js', }, module: { rules: [ { test: /.js$/, exclude: [/node_modules/], use: [ { loader: require.resolve('./loaders/inject-line.loader'), options: { config: { name: 'customLogName', }, }, }, ], }, ], }, }
然後在 inject-line.loader.js 代碼中通過 this.query.config 拿到該配置即可,不過正則表達式也要根據這個配置動態創建,字符串替換的時候也要換成該配置變量,最終代碼如下:
module.exports = function (content) { content = content.toString('utf-8') if (this.cacheable) this.cacheable() const { name = 'logLine' } = this.query.config || {} const fileName = this.resourcePath.split('/src/').pop() content = content .split('\n') .map((line, row) => { const re = new RegExp(`${name}\((.*?)\)`, 'g') let result let newLine = '' let cursor = 0 while ((result = re.exec(line))) { const col = result.index newLine += line.slice(cursor, result.index) + `${name}('${fileName}:${row + 1}:${col + 1}', ` + result[1] + ')' cursor += col + result[0].length } newLine += line.slice(cursor) return newLine }) .join('\n') return content } module.exports.raw = true
總結
到此這篇關於如何巧用webpack在日志中記錄文件行號的文章就介紹到這瞭,更多相關webpack日志記錄文件行號內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- style-loader為什麼要使用pitch方法原理解析
- webpack搭建vue環境時報錯異常解決
- webpack cjs運行時分析示例詳解
- webpack模塊化的原理解析
- JavaScript webpack5配置及使用基本介紹