首頁 > 軟體

教你巧用webpack在紀錄檔中記錄檔案行號

2022-11-17 14:00:29

前言

在做前端專案時,會在各個關鍵節點列印紀錄檔,方便後續資料分析和問題排查。當紀錄檔越來越多之後,又會遇到通過紀錄檔反查程式碼所在檔案和所在行的場景,於是一個很自然的需求就出來了:

在列印紀錄檔的時候,自動注入當前檔名、行號、列號。

舉個例子,有個 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紀錄檔記錄檔案行號內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com