babel插件去除console示例詳解
起因
已經(jīng)頹廢了很久 因為實在不知道寫啥了 突然我某個同事對我說 寶哥 你看這個頁面好多console.log 不僅會影響性能 而且可能會被不法分子所利用 我覺得很有道理 所以我萌生了寫一個小插件來去除生產(chǎn)環(huán)境的console.log的想法
介紹
我們籠統(tǒng)的介紹下babel,之前我有一篇寫精度插件的babel文章,babel一共有三個階段:第一階段是將源代碼轉(zhuǎn)化為ast語法樹、第二階段是對ast語法樹進(jìn)行修改,生成我們想要的語法樹、第三階段是將ast語法樹解析,生成對應(yīng)的目標(biāo)代碼。

窺探
我們的目的是去除console.log,我們首先需要通過ast查看語法樹的結(jié)構(gòu)。我們以下面的console為例:
注意 因為我們要寫babel插件 所以我們選擇@babel/parser庫生成ast,因為babel內(nèi)部是使用這個庫生成ast的

console.log("我會被清除");

初見AST
AST是對源碼的抽象,字面量、標(biāo)識符、表達(dá)式、語句、模塊語法、class語法都有各自的AST。
我們這里只說下本文章中所使用的AST。
Program
program 是代表整個程序的節(jié)點,它有 body 屬性代表程序體,存放 statement 數(shù)組,就是具體執(zhí)行的語句的集合。
可以看到我們這里的body只有一個ExpressionStatement語句,即console.log。
ExpressionStatement
statement 是語句,它是可以獨立執(zhí)行的單位,expression是表達(dá)式,它倆唯一的區(qū)別是表達(dá)式執(zhí)行完以后有返回值。所以ExpressionStatement表示這個表達(dá)式是被當(dāng)作語句執(zhí)行的。
ExpressionStatement類型的AST有一個expression屬性,代表當(dāng)前的表達(dá)式。
CallExpression
expression 是表達(dá)式,CallExpression表示調(diào)用表達(dá)式,console.log就是一個調(diào)用表達(dá)式。
CallExpression類型的AST有一個callee屬性,指向被調(diào)用的函數(shù)。這里console.log就是callee的值。
CallExpression類型的AST有一個arguments屬性,指向參數(shù)。這里“我會被清除”就是arguments的值。
MemberExpression
Member Expression通常是用于訪問對象成員的。他有幾種形式:
a.b a["b"] new.target super.b
我們這里的console.log就是訪問對象成員log。
- 為什么MemberExpression外層有一個CallExpression呢?
實際上,我們可以理解為,MemberExpression中的某一子結(jié)構(gòu)具有函數(shù)調(diào)用,那么整個表達(dá)式就成為了一個Call Expression。
MemberExpression有一個屬性object表示被訪問的對象。這里console就是object的值。
MemberExpression有一個屬性property表示對象的屬性。這里log就是property的值。
MemberExpression有一個屬性computed表示訪問對象是何種方式。computed為true表示[],false表示. 。
Identifier
Identifer 是標(biāo)識符的意思,變量名、屬性名、參數(shù)名等各種聲明和引用的名字,都是Identifer。
我們這里的console就是一個identifier。
Identifier有一個屬性name 表示標(biāo)識符的名字
StringLiteral
表示字符串字面量。
我們這里的log就是一個字符串字面量
StringLiteral有一個屬性value 表示字符串的值
公共屬性
每種 AST 都有自己的屬性,但是它們也有一些公共的屬性:
- type:AST節(jié)點的類型
- start、end、loc:start和end代表該節(jié)點在源碼中的開始和結(jié)束下標(biāo)。而loc屬性是一個對象,有l(wèi)ine和column屬性分別記錄開始和結(jié)束的行列號
- leadingComments、innerComments、trailingComments:表示開始的注釋、中間的注釋、結(jié)尾的注釋,每個 AST 節(jié)點中都可能存在注釋,而且可能在開始、中間、結(jié)束這三種位置,想拿到某個 AST 的注釋就通過這三個屬性。
如何寫一個babel插件?
babel插件是作用在第二階段即transform階段。
transform階段有@babel/traverse,可以遍歷AST,并調(diào)用visitor函數(shù)修改AST。
我們可以新建一個js文件,其中導(dǎo)出一個方法,返回一個對象,對象存在一個visitor屬性,里面可以編寫我們具體需要修改AST的邏輯。
+ export default () => {
+ return {
+ name: "@parrotjs/babel-plugin-console",
+ visitor,
+ };
+ };
構(gòu)造visitor方法
path 是記錄遍歷路徑的 api,它記錄了父子節(jié)點的引用,還有很多增刪改查 AST 的 api

+ const visitor = {
+ CallExpression(path, { opts }) {
+ //當(dāng)traverse遍歷到類型為CallExpression的AST時,會進(jìn)入函數(shù)內(nèi)部,我們需要在函數(shù)內(nèi)部修改
+ }
+ };
我們需要遍歷所有調(diào)用函數(shù)表達(dá)式 所以使用CallExpression。
去除所有console
我們將所有的console.log去掉
path.get 表示獲取某個屬性的path
path.matchesPattern 檢查某個節(jié)點是否符合某種模式
path.remove 刪除當(dāng)前節(jié)點
CallExpression(path, { opts }) {
+ //獲取callee的path
+ const calleePath = path.get("callee");
+ //檢查callee中是否符合“console”這種模式
+ if (calleePath && calleePath.matchesPattern("console", true)) {
+ //如果符合 直接刪除節(jié)點
+ path.remove();
+ }
},
增加env api
一般去除console.log都是在生產(chǎn)環(huán)境執(zhí)行 所以增加env參數(shù)
AST的第二個參數(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都會清除,所以我們加一個exclude api,傳一個數(shù)組,可以去除想要去除的console類型
....
+ 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
某些時候 我們希望一些console 不被刪除 我們可以給他添加一些注釋 比如
//no remove
console.log("測試1");
console.log("測試2");//reserse
//hhhhh
console.log("測試3")

如上 我們希望帶有no remove前綴注釋的console 和帶有reserse后綴注釋的console保留不被刪除
之前我們提到 babel給我們提供了leadingComments(前綴注釋)和trailingComments(后綴注釋)我們可以利用他們 由AST可知 她和CallExpression同級,所以我們需要獲取他的父節(jié)點 然后獲取父節(jié)點的屬性
path.parentPath 獲取父path
path.node 獲取當(dāng)前節(jié)點
- 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)識是否有前綴注釋
+ let leadingReserve = false;
+ //標(biāo)識是否有后綴注釋
+ 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;
+ }
+ });
+ }
+ //如果沒有前綴節(jié)點和后綴節(jié)點 直接刪除節(jié)點
+ if (!leadingReserve && !trailReserve) {
+ path.remove();
+ }
}
細(xì)節(jié)完善
我們大致完成了插件 我們引進(jìn)項目里面進(jìn)行測試
console.log("測試1");
//no remove
console.log("測試2");
console.log("測試3");//reserve
console.log("測試4");
//新建.babelrc 引入插件
{
"plugins":[["../dist/index.cjs",{
"env":"production"
}]]
}
理論上應(yīng)該移除測試1、測試4,但是我們驚訝的發(fā)現(xiàn) 竟然一個console沒有刪除??!經(jīng)過排查 我們大致確定了問題所在
因為測試2的前綴注釋同時也被AST納入了測試1的后綴注釋中了,而測試3的后綴注釋同時也被AST納入了測試4的前綴注釋中了
所以測試1存在后綴注釋 測試4存在前綴注釋 所以測試1和測試4沒有被刪除
那么我們怎么判斷呢?
對于后綴注釋
我們可以判斷后綴注釋是否與當(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)行測試 發(fā)現(xiàn)測試1 已經(jīng)被刪除
對于前綴注釋
那么對于前綴注釋 我們應(yīng)該怎么做呢 因為我們在后綴注釋的節(jié)點中添加了一個變量belongCurrentLine,表示該注釋是否是和節(jié)點屬于同一行。
那么對于前綴注釋,我們只需要判斷是否存在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
使用
舉個例子:新建.babelrc
{
"plugins":[["../dist/index.cjs",{
"env":"production"
}]]
}
以上就是babel插件去除console示例詳解的詳細(xì)內(nèi)容,更多關(guān)于babel插件去除console的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS中const對于復(fù)雜類型變量和普通類型變量的區(qū)別詳解
我們在開發(fā)的過程中一定常常發(fā)現(xiàn)const關(guān)鍵字定義的簡單類型變量不可以改變,但是你如果定義的是一個復(fù)雜類型變量(比如對象)的話對里面屬性的增刪改查是可以的,那這又是為什么呢,接下來就來和小編一起探討一下吧2023-11-11
Js得到radiobuttonlist選中值的兩種方法(推薦)
下面小編就為大家?guī)硪黄狫s得到radiobuttonlist選中值的兩種方法(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08

