babel插件去除console示例詳解
起因
已經頹廢瞭很久 因為實在不知道寫啥瞭 突然我某個同事對我說 寶哥 你看這個頁面好多console.log
不僅會影響性能 而且可能會被不法分子所利用 我覺得很有道理 所以我萌生瞭寫一個小插件來去除生產環境
的console.log的想法
介紹
我們籠統的介紹下babel
,之前我有一篇寫精度插件的babel
文章,babel一共有三個階段:第一階段是將源代碼轉化為ast語法樹、第二階段是對ast語法樹進行修改,生成我們想要的語法樹、第三階段是將ast語法樹解析,生成對應的目標代碼。
窺探
我們的目的是去除console.log
,我們首先需要通過ast查看語法樹的結構。我們以下面的console為例:
註意 因為我們要寫babel插件 所以我們選擇@babel/parser
庫生成ast,因為babel內部是使用這個庫生成ast的
console.log("我會被清除");
初見AST
AST是對源碼的抽象,字面量、標識符、表達式、語句、模塊語法、class語法都有各自的AST。
我們這裡隻說下本文章中所使用的AST。
Program
program 是代表整個程序的節點,它有 body 屬性代表程序體,存放 statement 數組,就是具體執行的語句的集合。
可以看到我們這裡的body隻有一個ExpressionStatement語句,即console.log。
ExpressionStatement
statement 是語句,它是可以獨立執行的單位,expression是表達式,它倆唯一的區別是表達式執行完以後有返回值。所以ExpressionStatement表示這個表達式是被當作語句執行的。
ExpressionStatement類型的AST有一個expression屬性,代表當前的表達式。
CallExpression
expression 是表達式,CallExpression表示調用表達式,console.log就是一個調用表達式。
CallExpression類型的AST有一個callee屬性,指向被調用的函數。這裡console.log就是callee的值。
CallExpression類型的AST有一個arguments屬性,指向參數。這裡“我會被清除”就是arguments的值。
MemberExpression
Member Expression通常是用於訪問對象成員的。他有幾種形式:
a.b a["b"] new.target super.b
我們這裡的console.log就是訪問對象成員log。
- 為什麼MemberExpression外層有一個CallExpression呢?
實際上,我們可以理解為,MemberExpression中的某一子結構具有函數調用,那麼整個表達式就成為瞭一個Call Expression。
MemberExpression有一個屬性object表示被訪問的對象。這裡console就是object的值。
MemberExpression有一個屬性property表示對象的屬性。這裡log就是property的值。
MemberExpression有一個屬性computed表示訪問對象是何種方式。computed為true表示[],false表示. 。
Identifier
Identifer 是標識符的意思,變量名、屬性名、參數名等各種聲明和引用的名字,都是Identifer。
我們這裡的console就是一個identifier。
Identifier有一個屬性name 表示標識符的名字
StringLiteral
表示字符串字面量。
我們這裡的log就是一個字符串字面量
StringLiteral有一個屬性value 表示字符串的值
公共屬性
每種 AST 都有自己的屬性,但是它們也有一些公共的屬性:
- type:AST節點的類型
- start、end、loc:start和end代表該節點在源碼中的開始和結束下標。而loc屬性是一個對象,有line和column屬性分別記錄開始和結束的行列號
- leadingComments、innerComments、trailingComments:表示開始的註釋、中間的註釋、結尾的註釋,每個 AST 節點中都可能存在註釋,而且可能在開始、中間、結束這三種位置,想拿到某個 AST 的註釋就通過這三個屬性。
如何寫一個babel插件?
babel插件是作用在第二階段即transform階段。
transform階段有@babel/traverse,可以遍歷AST,並調用visitor函數修改AST。
我們可以新建一個js文件,其中導出一個方法,返回一個對象,對象存在一個visitor屬性,裡面可以編寫我們具體需要修改AST的邏輯。
+ export default () => { + return { + name: "@parrotjs/babel-plugin-console", + visitor, + }; + };
構造visitor方法
path 是記錄遍歷路徑的 api,它記錄瞭父子節點的引用,還有很多增刪改查 AST 的 api
+ const visitor = { + CallExpression(path, { opts }) { + //當traverse遍歷到類型為CallExpression的AST時,會進入函數內部,我們需要在函數內部修改 + } + };
我們需要遍歷所有調用函數表達式 所以使用CallExpression
。
去除所有console
我們將所有的console.log去掉
path.get 表示獲取某個屬性的path
path.matchesPattern 檢查某個節點是否符合某種模式
path.remove 刪除當前節點
CallExpression(path, { opts }) { + //獲取callee的path + const calleePath = path.get("callee"); + //檢查callee中是否符合“console”這種模式 + if (calleePath && calleePath.matchesPattern("console", true)) { + //如果符合 直接刪除節點 + path.remove(); + } },
增加env api
一般去除console.log都是在生產環境執行 所以增加env參數
AST的第二個參數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,傳一個數組,可以去除想要去除的console類型
.... + const isArray = (arg) => Object.prototype.toString.call(arg) === "[object Array]"; - const { env } = opts; + const { env,exclude } = opts; if (env === "production" || isProduction) { - path.remove(); + //封裝函數進行操作 + removeConsoleExpression(path, calleePath, exclude); } +const removeConsoleExpression=(path, calleePath, exclude)=>{ + if (isArray(exclude)) { + const hasTarget = exclude.some((type) => { + return calleePath.matchesPattern("console." + type); + }); + //匹配上直接返回不進行操作 + 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同級,所以我們需要獲取他的父節點 然後獲取父節點的屬性
path.parentPath 獲取父path
path.node 獲取當前節點
- 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; + }; + //判斷是否有關鍵字匹配 默認no remove || reserve 且如果commentWords和默認值是相斥的 + 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; + //標識是否有前綴註釋 + let leadingReserve = false; + //標識是否有後綴註釋 + 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; + } + }); + } + //如果沒有前綴節點和後綴節點 直接刪除節點 + if (!leadingReserve && !trailReserve) { + path.remove(); + } }
細節完善
我們大致完成瞭插件 我們引進項目裡面進行測試
console.log("測試1"); //no remove console.log("測試2"); console.log("測試3");//reserve console.log("測試4"); //新建.babelrc 引入插件 { "plugins":[["../dist/index.cjs",{ "env":"production" }]] }
理論上應該移除測試1、測試4,但是我們驚訝的發現 竟然一個console沒有刪除!!經過排查 我們大致確定瞭問題所在
因為測試2的前綴註釋同時也被AST納入瞭測試1的後綴註釋中瞭,而測試3的後綴註釋同時也被AST納入瞭測試4的前綴註釋中瞭
所以測試1存在後綴註釋 測試4存在前綴註釋 所以測試1和測試4沒有被刪除
那麼我們怎麼判斷呢?
對於後綴註釋
我們可以判斷後綴註釋是否與當前的調用表達式處於同一行,如果不是同一行,則不將其歸納為後綴註釋
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; + } + //屬於當前行才將其設置為後綴註釋 - if (isReserveComment(comment, commentWords)) + if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) { trailReserve = true; } }); }
我們修改完進行測試 發現測試1 已經被刪除
對於前綴註釋
那麼對於前綴註釋 我們應該怎麼做呢 因為我們在後綴註釋的節點中添加瞭一個變量belongCurrentLine,表示該註釋是否是和節點屬於同一行。
那麼對於前綴註釋,我們隻需要判斷是否存在belongCurrentLine,如果存在belongCurrentLine,表示不能將其當作前綴註釋。
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; + } + //屬於當前行才將其設置為後綴註釋 - if (isReserveComment(comment, commentWords)) + if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) { trailReserve = true; } }); }
發佈到線上
我現已將代碼發佈到線上
安裝
yarn add @parrotjs/babel-plugin-console
使用
舉個例子:新建.babelrc
{ "plugins":[["../dist/index.cjs",{ "env":"production" }]] }
git地址
以上就是babel插件去除console示例詳解的詳細內容,更多關於babel插件去除console的資料請關註WalkonNet其它相關文章!