教你巧用webpack在日志中記錄文件行號(hào)
前言
在做前端項(xiàng)目時(shí),會(huì)在各個(gè)關(guān)鍵節(jié)點(diǎn)打印日志,方便后續(xù)數(shù)據(jù)分析和問(wèn)題排查。當(dāng)日志越來(lái)越多之后,又會(huì)遇到通過(guò)日志反查代碼所在文件和所在行的場(chǎng)景,于是一個(gè)很自然的需求就出來(lái)了:
在打印日志的時(shí)候,自動(dòng)注入當(dāng)前文件名、行號(hào)、列號(hào)。
舉個(gè)例子,有個(gè) logger 函數(shù),我們?cè)?index.js 的業(yè)務(wù)代碼某一行添加打印邏輯:
const { logLine } = require('./utils')
function getJuejinArticles() {
const author = 'keliq'
const level = 'LV.5'
// ... 業(yè)務(wù)代碼省略,獲取文章列表
logLine(author, level)
// ...
}
getJuejinArticles()正常情況下會(huì)輸出:
keliq LV.5
但是希望能夠輸出帶文件名和行號(hào),即:
[index.js:7:3] keliq LV.5
表明當(dāng)前這次打印輸出來(lái)源于 index.js 文件中的第 7 行第 3 列代碼,也就是 logLine 函數(shù)所在的具體位置。那如何實(shí)現(xiàn)這個(gè)需求呢?我的腦海中浮現(xiàn)了兩個(gè)思路:
通過(guò)提取 Error 錯(cuò)誤棧
因?yàn)?error 錯(cuò)誤棧里面天然帶有此類信息,可以人工制造了一個(gè) Error,然后捕獲它:
exports.logLine = (...args) => {
try {
throw new Error()
} catch (e) {
console.log(e.stack)
}
}仔細(xì)觀察打印的結(jié)果:
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
第三行的內(nèi)容不正是我們想要的結(jié)果嗎?只需要把這一行的字符串進(jìn)行格式化一下,提取出 index.js:7:3 即可:
at getJuejinArticles (/test/src/index.js:7:3)
由于代碼結(jié)構(gòu)是這樣的:
.
└── 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
問(wèn)題似乎完美解決,然而還是想的太簡(jiǎn)單了,上述場(chǎng)景僅限于 node.js 環(huán)境,而在 web 環(huán)境,所有的產(chǎn)物都會(huì)被 webpack 打到一個(gè)或多個(gè) js 文件里面,而且做了壓縮混淆處理,由于 error 是在運(yùn)行時(shí)被捕獲到的 ,所以我沒(méi)根本無(wú)法拿到開(kāi)發(fā)狀態(tài)下的文件名、行號(hào)和列號(hào),如下圖所示:

通過(guò) webpack 預(yù)處理
那怎么辦呢?解鈴還須系鈴人,既然 webpack 對(duì)代碼進(jìn)行了加工處理,那就只能在預(yù)處理最開(kāi)始的階段介入進(jìn)來(lái),寫一個(gè)自定義的 loader 來(lái)解析源碼文件,拿到文件名、行號(hào)和列號(hào)。說(shuō)干就干,創(chuàng)建一個(gè) 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) // 打印文件內(nèi)容
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'),
},
],
},
],
},
}一切準(zhǔn)備就緒,先運(yùn)行一下看看輸出:

可以看到,index.js 和 utils.js 被自定義的 inject-line.loader.js 給加載到了,通過(guò) this.resourcePath 能夠拿到文件名稱,行號(hào)和列號(hào)的話只能通過(guò)分析 content 字符串進(jìn)行提取了,處理的代碼如下:
// 拿到文件路徑
const fileName = this.resourcePath.split('/src/').pop()
// 文本內(nèi)容按行處理后再拼接起來(lái)
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')這里面的邏輯,如果光看代碼的話可能會(huì)云里霧里,其實(shí)思路很簡(jiǎn)單,就是下面這樣的:

這樣的話,即使代碼經(jīng)過(guò)各種壓縮轉(zhuǎn)換,也不會(huì)改變開(kāi)發(fā)狀態(tài)下代碼所在的文件名、行與列的位置了。打開(kāi) webpack 打包后的文件看一下:

到這里,功能就已經(jīng)開(kāi)發(fā)完了,不過(guò)還有一個(gè)小小的缺陷就是 logLine 函數(shù)名是寫死的,能不能讓用戶自己定義這個(gè)函數(shù)名呢?當(dāng)然可以,在 webpack 配置文件中,支持利用 options 屬性傳遞 config 配置參數(shù):
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 代碼中通過(guò) this.query.config 拿到該配置即可,不過(guò)正則表達(dá)式也要根據(jù)這個(gè)配置動(dòng)態(tài)創(chuàng)建,字符串替換的時(shí)候也要換成該配置變量,最終代碼如下:
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總結(jié)
到此這篇關(guān)于如何巧用webpack在日志中記錄文件行號(hào)的文章就介紹到這了,更多相關(guān)webpack日志記錄文件行號(hào)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript實(shí)現(xiàn)圖像模糊化的方法實(shí)例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)圖像模糊化的方法,文中先進(jìn)行簡(jiǎn)單介紹,而后給出了完整的實(shí)例代碼,有需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-01-01
基于Three.js實(shí)現(xiàn)酷炫3D地圖效果
這篇文章主要為大家詳細(xì)介紹了如何利用Three.js實(shí)現(xiàn)酷炫3D地圖的效果,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以嘗試一下2022-10-10
js+CSS簡(jiǎn)單實(shí)現(xiàn)瀑布流布局
瀑布流布局,是一種視覺(jué)表現(xiàn)為參差不齊的多欄布局,常用于內(nèi)容以圖片為主的頁(yè)面展示,本文將使用css和js兩種方式來(lái)實(shí)現(xiàn)瀑布流布局,需要的可以參考下2023-11-11
js數(shù)據(jù)向上翻滾_數(shù)據(jù)滾動(dòng)
方便做一些問(wèn)題提交等宣傳效果,多用于文字滾動(dòng)2008-10-10

