babel插件去除console示例詳解
起因
已經(jīng)頹廢了很久 因?yàn)閷?shí)在不知道寫(xiě)啥了 突然我某個(gè)同事對(duì)我說(shuō) 寶哥 你看這個(gè)頁(yè)面好多console.log
不僅會(huì)影響性能 而且可能會(huì)被不法分子所利用 我覺(jué)得很有道理 所以我萌生了寫(xiě)一個(gè)小插件來(lái)去除生產(chǎn)環(huán)境
的console.log的想法
介紹
我們籠統(tǒng)的介紹下babel
,之前我有一篇寫(xiě)精度插件的babel
文章,babel一共有三個(gè)階段:第一階段是將源代碼轉(zhuǎn)化為ast語(yǔ)法樹(shù)、第二階段是對(duì)ast語(yǔ)法樹(shù)進(jìn)行修改,生成我們想要的語(yǔ)法樹(shù)、第三階段是將ast語(yǔ)法樹(shù)解析,生成對(duì)應(yīng)的目標(biāo)代碼。
窺探
我們的目的是去除console.log
,我們首先需要通過(guò)ast查看語(yǔ)法樹(shù)的結(jié)構(gòu)。我們以下面的console為例:
注意 因?yàn)槲覀円獙?xiě)babel插件 所以我們選擇@babel/parser
庫(kù)生成ast,因?yàn)閎abel內(nèi)部是使用這個(gè)庫(kù)生成ast的
console.log("我會(huì)被清除");
初見(jiàn)AST
AST是對(duì)源碼的抽象,字面量、標(biāo)識(shí)符、表達(dá)式、語(yǔ)句、模塊語(yǔ)法、class語(yǔ)法都有各自的AST。
我們這里只說(shuō)下本文章中所使用的AST。
Program
program 是代表整個(gè)程序的節(jié)點(diǎn),它有 body 屬性代表程序體,存放 statement 數(shù)組,就是具體執(zhí)行的語(yǔ)句的集合。
可以看到我們這里的body只有一個(gè)ExpressionStatement語(yǔ)句,即console.log。
ExpressionStatement
statement 是語(yǔ)句,它是可以獨(dú)立執(zhí)行的單位,expression是表達(dá)式,它倆唯一的區(qū)別是表達(dá)式執(zhí)行完以后有返回值。所以ExpressionStatement表示這個(gè)表達(dá)式是被當(dāng)作語(yǔ)句執(zhí)行的。
ExpressionStatement類(lèi)型的AST有一個(gè)expression屬性,代表當(dāng)前的表達(dá)式。
CallExpression
expression 是表達(dá)式,CallExpression表示調(diào)用表達(dá)式,console.log就是一個(gè)調(diào)用表達(dá)式。
CallExpression類(lèi)型的AST有一個(gè)callee屬性,指向被調(diào)用的函數(shù)。這里console.log就是callee的值。
CallExpression類(lèi)型的AST有一個(gè)arguments屬性,指向參數(shù)。這里“我會(huì)被清除”就是arguments的值。
MemberExpression
Member Expression通常是用于訪問(wèn)對(duì)象成員的。他有幾種形式:
a.b a["b"] new.target super.b
我們這里的console.log就是訪問(wèn)對(duì)象成員log。
- 為什么MemberExpression外層有一個(gè)CallExpression呢?
實(shí)際上,我們可以理解為,MemberExpression中的某一子結(jié)構(gòu)具有函數(shù)調(diào)用,那么整個(gè)表達(dá)式就成為了一個(gè)Call Expression。
MemberExpression有一個(gè)屬性object表示被訪問(wèn)的對(duì)象。這里console就是object的值。
MemberExpression有一個(gè)屬性property表示對(duì)象的屬性。這里log就是property的值。
MemberExpression有一個(gè)屬性computed表示訪問(wèn)對(duì)象是何種方式。computed為true表示[],false表示. 。
Identifier
Identifer 是標(biāo)識(shí)符的意思,變量名、屬性名、參數(shù)名等各種聲明和引用的名字,都是Identifer。
我們這里的console就是一個(gè)identifier。
Identifier有一個(gè)屬性name 表示標(biāo)識(shí)符的名字
StringLiteral
表示字符串字面量。
我們這里的log就是一個(gè)字符串字面量
StringLiteral有一個(gè)屬性value 表示字符串的值
公共屬性
每種 AST 都有自己的屬性,但是它們也有一些公共的屬性:
- type:AST節(jié)點(diǎn)的類(lèi)型
- start、end、loc:start和end代表該節(jié)點(diǎn)在源碼中的開(kāi)始和結(jié)束下標(biāo)。而loc屬性是一個(gè)對(duì)象,有l(wèi)ine和column屬性分別記錄開(kāi)始和結(jié)束的行列號(hào)
- leadingComments、innerComments、trailingComments:表示開(kāi)始的注釋、中間的注釋、結(jié)尾的注釋,每個(gè) AST 節(jié)點(diǎn)中都可能存在注釋,而且可能在開(kāi)始、中間、結(jié)束這三種位置,想拿到某個(gè) AST 的注釋就通過(guò)這三個(gè)屬性。
如何寫(xiě)一個(gè)babel插件?
babel插件是作用在第二階段即transform階段。
transform階段有@babel/traverse,可以遍歷AST,并調(diào)用visitor函數(shù)修改AST。
我們可以新建一個(gè)js文件,其中導(dǎo)出一個(gè)方法,返回一個(gè)對(duì)象,對(duì)象存在一個(gè)visitor屬性,里面可以編寫(xiě)我們具體需要修改AST的邏輯。
+ export default () => { + return { + name: "@parrotjs/babel-plugin-console", + visitor, + }; + };
構(gòu)造visitor方法
path 是記錄遍歷路徑的 api,它記錄了父子節(jié)點(diǎn)的引用,還有很多增刪改查 AST 的 api
+ const visitor = { + CallExpression(path, { opts }) { + //當(dāng)traverse遍歷到類(lèi)型為CallExpression的AST時(shí),會(huì)進(jìn)入函數(shù)內(nèi)部,我們需要在函數(shù)內(nèi)部修改 + } + };
我們需要遍歷所有調(diào)用函數(shù)表達(dá)式 所以使用CallExpression
。
去除所有console
我們將所有的console.log去掉
path.get 表示獲取某個(gè)屬性的path
path.matchesPattern 檢查某個(gè)節(jié)點(diǎn)是否符合某種模式
path.remove 刪除當(dāng)前節(jié)點(diǎn)
CallExpression(path, { opts }) { + //獲取callee的path + const calleePath = path.get("callee"); + //檢查callee中是否符合“console”這種模式 + if (calleePath && calleePath.matchesPattern("console", true)) { + //如果符合 直接刪除節(jié)點(diǎn) + path.remove(); + } },
增加env api
一般去除console.log都是在生產(chǎn)環(huán)境執(zhí)行 所以增加env參數(shù)
AST的第二個(gè)參數(shù)opt中有插件傳入的配置
+ const isProduction = process.env.NODE_ENV === "production"; CallExpression(path, { opts }) { .... + const { env } = opts; + if (env === "production" || isProduction) { path.remove(); + } .... },
增加exclude api
我們上面去除了所有的console,不管是error、warning、table都會(huì)清除,所以我們加一個(gè)exclude api,傳一個(gè)數(shù)組,可以去除想要去除的console類(lèi)型
.... + const isArray = (arg) => Object.prototype.toString.call(arg) === "[object Array]"; - const { env } = opts; + const { env,exclude } = opts; if (env === "production" || isProduction) { - path.remove(); + //封裝函數(shù)進(jìn)行操作 + removeConsoleExpression(path, calleePath, exclude); } +const removeConsoleExpression=(path, calleePath, exclude)=>{ + if (isArray(exclude)) { + const hasTarget = exclude.some((type) => { + return calleePath.matchesPattern("console." + type); + }); + //匹配上直接返回不進(jìn)行操作 + if (hasTarget) return; + } + path.remove(); +}
增加commentWords api
某些時(shí)候 我們希望一些console 不被刪除 我們可以給他添加一些注釋 比如
//no remove console.log("測(cè)試1"); console.log("測(cè)試2");//reserse //hhhhh console.log("測(cè)試3")
如上 我們希望帶有no remove前綴注釋的console 和帶有reserse后綴注釋的console保留不被刪除
之前我們提到 babel給我們提供了leadingComments(前綴注釋)和trailingComments(后綴注釋)我們可以利用他們 由AST可知 她和CallExpression同級(jí),所以我們需要獲取他的父節(jié)點(diǎn) 然后獲取父節(jié)點(diǎn)的屬性
path.parentPath 獲取父path
path.node 獲取當(dāng)前節(jié)點(diǎn)
- const { exclude, env } = opts; + const { exclude, commentWords, env } = opts; + const isFunction = (arg) =>Object.prototype.toString.call(arg) === "[object Function]"; + // 判斷是否有前綴注釋 + const hasLeadingComments = (node) => { + const leadingComments = node.leadingComments; + return leadingComments && leadingComments.length; + }; + // 判斷是否有后綴注釋 + const hasTrailingComments = (node) => { + const trailingComments = node.trailingComments; + return trailingComments && trailingComments.length; + }; + //判斷是否有關(guān)鍵字匹配 默認(rèn)no remove || reserve 且如果commentWords和默認(rèn)值是相斥的 + const isReserveComment = (node, commentWords) => { + if (isFunction(commentWords)) { + return commentWords(node.value); + } + return ( + ["CommentBlock", "CommentLine"].includes(node.type) && + (isArray(commentWords) + ? commentWords.includes(node.value) + : /(no[t]? remove\b)|(reserve\b)/.test(node.value)) + ); +}; - const removeConsoleExpression = (path, calleePath, exclude) => { + const removeConsoleExpression = (path, calleePath, exclude,commentWords) => { + //獲取父path + const parentPath = path.parentPath; + const parentNode = parentPath.node; + //標(biāo)識(shí)是否有前綴注釋 + let leadingReserve = false; + //標(biāo)識(shí)是否有后綴注釋 + let trailReserve = false; + if (hasLeadingComments(parentNode)) { + //traverse + parentNode.leadingComments.forEach((comment) => { + if (isReserveComment(comment, commentWords)) { + leadingReserve = true; + } + }); + } + if (hasTrailingComments(parentNode)) { //traverse + parentNode.trailingComments.forEach((comment) => { + if (isReserveComment(comment, commentWords)) { + trailReserve = true; + } + }); + } + //如果沒(méi)有前綴節(jié)點(diǎn)和后綴節(jié)點(diǎn) 直接刪除節(jié)點(diǎn) + if (!leadingReserve && !trailReserve) { + path.remove(); + } }
細(xì)節(jié)完善
我們大致完成了插件 我們引進(jìn)項(xiàng)目里面進(jìn)行測(cè)試
console.log("測(cè)試1"); //no remove console.log("測(cè)試2"); console.log("測(cè)試3");//reserve console.log("測(cè)試4"); //新建.babelrc 引入插件 { "plugins":[["../dist/index.cjs",{ "env":"production" }]] }
理論上應(yīng)該移除測(cè)試1、測(cè)試4,但是我們驚訝的發(fā)現(xiàn) 竟然一個(gè)console沒(méi)有刪除?。〗?jīng)過(guò)排查 我們大致確定了問(wèn)題所在
因?yàn)闇y(cè)試2的前綴注釋同時(shí)也被AST納入了測(cè)試1的后綴注釋中了,而測(cè)試3的后綴注釋同時(shí)也被AST納入了測(cè)試4的前綴注釋中了
所以測(cè)試1存在后綴注釋 測(cè)試4存在前綴注釋 所以測(cè)試1和測(cè)試4沒(méi)有被刪除
那么我們?cè)趺磁袛嗄兀?/p>
對(duì)于后綴注釋
我們可以判斷后綴注釋是否與當(dāng)前的調(diào)用表達(dá)式處于同一行,如果不是同一行,則不將其歸納為后綴注釋
if (hasTrailingComments(parentNode)) { + const { start:{ line: currentLine } }=parentNode.loc; //traverse // @ts-ignore parentNode.trailingComments.forEach((comment) => { + const { start:{ line: currentCommentLine } }=comment.loc; + if(currentLine===currentCommentLine){ + comment.belongCurrentLine=true; + } + //屬于當(dāng)前行才將其設(shè)置為后綴注釋 - if (isReserveComment(comment, commentWords)) + if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) { trailReserve = true; } }); }
我們修改完進(jìn)行測(cè)試 發(fā)現(xiàn)測(cè)試1 已經(jīng)被刪除
對(duì)于前綴注釋
那么對(duì)于前綴注釋 我們應(yīng)該怎么做呢 因?yàn)槲覀冊(cè)诤缶Y注釋的節(jié)點(diǎn)中添加了一個(gè)變量belongCurrentLine,表示該注釋是否是和節(jié)點(diǎn)屬于同一行。
那么對(duì)于前綴注釋,我們只需要判斷是否存在belongCurrentLine,如果存在belongCurrentLine,表示不能將其當(dāng)作前綴注釋。
if (hasTrailingComments(parentNode)) { + const { start:{ line: currentLine } }=parentNode.loc; //traverse // @ts-ignore parentNode.trailingComments.forEach((comment) => { + const { start:{ line: currentCommentLine } }=comment.loc; + if(currentLine===currentCommentLine){ + comment.belongCurrentLine=true; + } + //屬于當(dāng)前行才將其設(shè)置為后綴注釋 - if (isReserveComment(comment, commentWords)) + if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) { trailReserve = true; } }); }
發(fā)布到線上
我現(xiàn)已將代碼發(fā)布到線上
安裝
yarn add @parrotjs/babel-plugin-console
使用
舉個(gè)例子:新建.babelrc
{ "plugins":[["../dist/index.cjs",{ "env":"production" }]] }
以上就是babel插件去除console示例詳解的詳細(xì)內(nèi)容,更多關(guān)于babel插件去除console的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
fabric.js實(shí)現(xiàn)diy明信片功能
這篇文章主要為大家詳細(xì)介紹了fabric.js實(shí)現(xiàn)diy明信片功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03D3.js實(shí)現(xiàn)散點(diǎn)圖和氣泡圖的方法詳解
這篇文章將會(huì)給大家介紹了另外兩種可視化圖表,利用D3.js實(shí)現(xiàn)散點(diǎn)圖和氣泡圖,文章通過(guò)多個(gè)方面介紹的非常詳細(xì),下面來(lái)一起看看吧。2016-09-09C#程序員入門(mén)學(xué)習(xí)微信小程序的筆記
這篇文章主要給大家分享了一位C#程序員入門(mén)學(xué)習(xí)微信小程序的筆記,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03微信小程序?qū)崿F(xiàn)點(diǎn)擊圖片旋轉(zhuǎn)180度并且彈出下拉列表
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)點(diǎn)擊圖片旋轉(zhuǎn)180度并且彈出下拉列表,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11微信小程序外賣(mài)選購(gòu)頁(yè)實(shí)現(xiàn)切換分類(lèi)與數(shù)量加減功能案例
這篇文章主要介紹了微信小程序外賣(mài)選購(gòu)頁(yè)實(shí)現(xiàn)切換分類(lèi)與數(shù)量加減功能,結(jié)合具體實(shí)例形式分析了微信小程序狀態(tài)記錄、判定及數(shù)值運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2019-01-01利用原生js和jQuery實(shí)現(xiàn)單選框的勾選和取消操作的方法
下面小編就為大家?guī)?lái)一篇利用原生js和jQuery實(shí)現(xiàn)單選框的勾選和取消操作的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09公眾號(hào)SVG動(dòng)畫(huà)交互實(shí)戰(zhàn)代碼
這篇文章主要介紹了公眾號(hào)SVG動(dòng)畫(huà)交互實(shí)戰(zhàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05利用Echarts如何實(shí)現(xiàn)多段圓環(huán)圖
這篇文章主要給大家介紹了關(guān)于利用Echarts如何實(shí)現(xiàn)多段圓環(huán)圖的相關(guān)資料,文中通過(guò)實(shí)例代碼代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03javascript中獲取class的簡(jiǎn)單實(shí)現(xiàn)
下面小編就為大家?guī)?lái)一篇javascript中獲取class的簡(jiǎn)單實(shí)現(xiàn)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07JavaScript作用域、閉包、對(duì)象與原型鏈概念及用法實(shí)例總結(jié)
這篇文章主要介紹了JavaScript作用域、閉包、對(duì)象與原型鏈,結(jié)合實(shí)例形式總結(jié)分析了javascript中變量與函數(shù)的作用域、閉包、對(duì)象、原形鏈相關(guān)概念、用法及注意事項(xiàng),需要的朋友可以參考下2018-08-08