簡單好用的除錯 log 函數
相信一般人寫程式最開始 debug 都是用 printf 或 console.log 之類的函數, 簡單把想要的資料丟到輸出,但事後又要刪除 debug 用的函數很麻煩。 最近弄出了一種稍微包裝過的 log 函數, 可以控制在 production 階段時就不輸出東西,有點像通用的 log 函式庫, 但控制實作複雜度在一定範圍內。 簡單來說就是幫每條 log 加上一個名稱, 然後可以用一個變數控制哪些名稱的 log 才要輸出。
const flag = {
exclude: ''.split(/ /g),
dict: null
}
function log(name, m) {
let dict = flag.dict
if (!dict) {
dict = flag.dict = {}
for (const name of flag.exclude) dict[name] = false
}
if (!m) {
m = name
name = 'debug'
}
if (!(name in dict)) dict[name] = true
if (!dict[name]) return
if (typeof m != 'string') m = String(m)
let s
if (m.indexOf('\n') == -1) {
s = `[${name}] ${m}`
}
else {
m = m.trim()
s =
`[${name}]
${m}
[/${name}]`
}
console.log(s)
}
其實文章到這裡就可以結束了,我想說的都在 code 裡了。
這個函數的目標大概就是,讓每條 log 都有一個名字,事後可以關掉。 使用情境就是對次一次除錯的過程; 在除錯時,會先在想檢查的地方加入 log,然後幫 log 取一個名字。 在事後解決問題後,就可以把名字加入 exclude 的清單裡, 程式就不會再輸出這則訊息,但又保留如果有需要可以再打開的彈性。
多行訊息
比較有趣的是對多行訊息的處理。 由於用方括號加在行首的作法只適合單行訊息:
[error-message] ls: cannot access 'foo': No such file or directory
對於多行訊息,我加入了一種類似 html 開關標籤的寫法, 實際效果會像這樣:
[error-message]
Cannot find module 'foo'
Require stack:
- <repl>
[/error-message]
用方括號是來自 BBCode 的語法,實際上就是把 html 的大小於換成方括號而已。 主要是用來顯示預先格式化好的錯誤訊息。 因為我自己蠻常遇到多行的錯誤訊息, 所以就想了一個符合既有方括號格式語法的顯示方式。
其他功能
其它也有一些可以更彈性的地方, 像是加入一個全域 flag debug,能按命令列參數一次開啟所有的 log。 或是加入萬用字元或 regexp 的支援, 可以開啟某些或某個命名空間下的所有 log, 或是二層的先 exclude 再 include 策略。 當然,會不會用到就是另外一回事了。 實際也不是那麼實用,所以也不一定有加。
執行開銷
如果用 c 語言寫,甚至可以用巨集的方式定義 log, 在 production 階段完全把 log 函數從程式中移除, 減少效能的損耗。 雖然很多完整的 log 函式庫,也都有類似的功能。
用 js 最多就是把整個 log 函數換成一個什麼都不做的 nop 函數, 或用一個全域 flag 關掉。 至於要減少格式化訊息的開銷的話, 像如果要錯誤訊息是序列化整個物件, 可能可以用 callback 惰性求值來避免開銷, 只要在確定要輸出錯誤訊息時再執行 callback。
function log(name, m) {
// ...
if (typeof m == 'function') m = m()
// ...
}
log('html-full', () => $.html())
使用策略
而且因為每則 log 都有名稱,
在解析上可以朝機器可讀的方式寫 log,
也就是在訊息中不用帶太多的說明,直接輸出數字、字串就好。
像如果想驗證目前到迴圈第幾次,
可以直接寫: log('loop-count', i)
而不是
log(`the ${i}th loop`)
。
這樣寫的好處是,來自各處的訊息混在一起時,可以依名稱分辨, 不用想某個寫法的 count 到底是來自哪個變數的 count。 其實就是變成替 log 取名字時就要確保最好名稱沒有重複。