前言
毋庸置疑,要説前端調試代碼用的最多的,肯定是console.log,雖然我現在 debugger 用的比較多,但對於生產環境、小程序真機調試,還是需要用到 log 來查看變量值,比如我下午遇到個場景:選擇完客户後返回頁面,根據條件判斷是否彈窗:
if (global.isXXX || !this.customerId || !this.skuList.length) return
// 到了這裏才會執行彈窗的邏輯
這個時候只能真機調試,看控制枱打印的值是怎樣的,但對於上面的條件,如果你這樣 log 的話,那控制枱只會顯示:
console.log(global.isXXX, !this.customerId, !this.skuList.length)
false false false
且如果參數比較多,你可能就沒法立即將 log 出的值對應到相應的變量,還得回去代碼裏面仔細比對。
還有一個,我之前遇到過一個項目裏一堆 log,同事為了方便看到 log 是在哪一行,就在 log 的地方加上代碼所在行數,但因為 log 那一刻已經硬編碼了,而代碼經常會添加或者刪除,這個時候行數就不對了:
比如你上面添加了一行,這裏的所有行數就都不對了
所以,我希望 console.log 的時候:
- 控制枱主動打印源碼所在行數
- 變量名要顯示出來,比如上面例子的 log 應該是
global.isXXX = false !this.customerId = false !this.skuList.length = false - 可以的話,每個參數都有分隔符,不然多個參數看起來就有點不好分辨
即源碼不做任何修改:
而控制枱顯示所在行,且有變量名的時候添加變量名前綴,然後你可以指定分隔符,如換行符\n:
因為之前有過 babel 插件的經驗,所以想着這次繼續通過寫一個 babel plugin 實現以上功能,所以也就有了babel-plugin-enhance-log,那究竟怎麼用?很簡單,下面 👇🏻 我給大家説説。
babel-plugin-enhance-log
老規矩,先安裝插件:
pnpm add babel-plugin-enhance-log -D
# or
yarn add babel-plugin-enhance-log -D
# or
npm i babel-plugin-enhance-log -D
然後在你的 babel.config.js 裏面添加插件:
module.exports = (api) => {
return {
plugins: [
'enhance-log',
...
],
}
}
看到了沒,就是這麼簡單,之後再重新啓動,去你的控制枱看看,小火箭咻咻咻為你刷起~
options
上面瞭解了基本用法後,這裏再給大家説下幾個參數,可以看下注釋,應該説是比較清楚的:
interface Options {
/**
* 打印的前綴提示,這樣方便快速找到log 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ...)
*/
preTip?: string
/** 每個參數分隔符,默認空字符串,你也可以使用換行符\n,分號;逗號,甚至豬豬🐖都行~ */
splitBy?: boolean
/**
* 是否需要endLine
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ..., 'line of 10 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀')
* */
endLine?: boolean
}
然後在插件第二個參數配置即可(這裏偷偷跟大家説下,通過/** @type {import('babel-plugin-enhance-log').Options} */可以給配置添加類型提示哦):
return {
plugins: [
['enhance-log', enhanceLogOption],
],
...
}
比如説,你不喜歡小 🚀,你喜歡豬豬 🐖,那可以配置 preTip 為 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖:
比如説,在參數較多的情況下,你希望 log 每個參數都換行,那可以配置 splitBy 為 \n:
或者分隔符是;:
當然,你也可以隨意指定,比如用個狗頭🐶來分隔:
又比如説,有個 log 跨了多行,你希望 log 開始和結束的行數,中間是 log 實體,那可以將 endLine 設置為 true:
我們可以看到開始的行數是13,結束的行數是44,跟源碼一致
實現思路
上面通過多個例子跟大家介紹了各種玩法,不過,我相信還是有些小夥伴想知道怎麼實現的,那我這裏就大致説下實現思路:
老規格,還是通過babel-ast-explorer來查看
1.判斷到 console.log 的 ast,即 path 是 CallExpression 的,且 callee 是 console.log,那麼進入下一步
2.拿到 console.log 的 arguments,也就是 log 的參數
3.遍歷 path.node.arguments 每個參數
- 字面量的,則無須添加變量名
- 變量的,添加變量名前綴,如
a = - 如果需要分隔符,則根據傳入的分隔符插入到原始參數的後面
4.拿到 console.log 的開始行數,創建一個包含行數的 StringLiteral,同時加上 preTip,比如上面的 🚀🚀🚀🚀🚀🚀🚀,或者 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖,然後 unshift,放在第一個參數的位置
5.拿到 console.log 的結束行數,過程跟第 4 點類似,通過 push 放到最後一個參數的位置
6.在這過程中需要判斷到處理過的,下次進來就要跳過,防止重複添加
以下是源碼的實現過程,有興趣的可以看看:
import { declare } from '@babel/helper-plugin-utils'
import generater from '@babel/generator'
import type { StringLiteral } from '@babel/types'
import { stringLiteral } from '@babel/types'
const DEFAULT_PRE_TIP = '🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀'
const SKIP_KEY = '@@babel-plugin-enhance-logSkip'
function generateStrNode(str: string): StringLiteral & { skip: boolean } {
const node = stringLiteral(str)
// @ts-ignore
node.skip = true
// @ts-ignore
return node
}
export default declare<Options>((babel, { preTip = DEFAULT_PRE_TIP, splitBy = '', endLine = false }) => {
const { types: t } = babel
const splitNode = generateStrNode(splitBy)
return {
name: 'enhance-log',
visitor: {
CallExpression(path) {
const calleeCode = generater(path.node.callee).code
if (calleeCode === 'console.log') {
// add comment to skip if enter next time
const { trailingComments } = path.node
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY
})
if (shouldSkip)
return
t.addComment(path.node, 'trailing', SKIP_KEY)
const nodeArguments = path.node.arguments
for (let i = 0; i < nodeArguments.length; i++) {
const argument = nodeArguments[i]
// @ts-ignore
if (argument.skip)
continue
if (!t.isLiteral(argument)) {
if (t.isIdentifier(argument) && argument.name === 'undefined') {
nodeArguments.splice(i + 1, 0, splitNode)
continue
}
// @ts-ignore
argument.skip = true
const node = generateStrNode(`${generater(argument).code} =`)
nodeArguments.splice(i, 0, node)
nodeArguments.splice(i + 2, 0, splitNode)
}
else {
nodeArguments.splice(i + 1, 0, splitNode)
}
}
// the last needn't split
if (nodeArguments[nodeArguments.length - 1] === splitNode)
nodeArguments.pop()
const { loc } = path.node
if (loc) {
const startLine = loc.start.line
const startLineTipNode = t.stringLiteral(`line of ${startLine} ${preTip}:\n`)
nodeArguments.unshift(startLineTipNode)
if (endLine) {
const endLine = loc.end.line
const endLineTipNode = t.stringLiteral(`\nline of ${endLine} ${preTip}:\n`)
nodeArguments.push(endLineTipNode)
}
}
}
},
},
}
})
對了,這裏有個問題是,我通過標記 path.node.skip = true 來跳過,但是還是會多次進入:
if (path.node.skip) return
path.node.skip = true
所以最終只能通過尾部添加註釋的方式來避免多次進入:
有知道怎麼解決的大佬還請提示一下,萬分感謝~
總結
國際慣例,我們來總結一下,對於生產環境或真機調試,或者對於一些偏愛 console.log 的小夥伴,我們為了更快在控制枱找到 log 的變量,通常會添加 log 函數,參數變量名,但前者一旦代碼位置更改,打印的位置就跟源碼不一致,後者又得重複寫每個參數變量名的字符串,顯得相當的麻煩。
為了更方便地使用 log,我們實現了個 babel 插件,功能包括:
- 自動打印行數
- 可以根據個人喜好加上 preTip,比如刷火箭 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀,或者可愛的小豬豬 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖
- 同時,對於有變量名的情況,可以加上變量名前綴,比如 const a = 1, console.log(a) => console.log('a = ', a)
- 還有,我們可以通過配置 splitBy、endLine 來自主選擇任意分隔符、是否打印結束行等功能
最後
不知道大家有沒有在追不良人,我是從高三追到現在。今天是週四,不良人第六季也接近尾聲了,那就謹以此文來紀念不良人第六季的完結吧~
好了,再説一句,如果你是個偏愛 console.log 的前端 er,那請你喊出:泰褲辣(逃~)