首页IT科技babel-plugin-import原理(babel插件去除console示例详解)

babel-plugin-import原理(babel插件去除console示例详解)

时间2025-06-20 14:22:16分类IT科技浏览3844
导读:起因 已经颓废了很久 因为实在不知道写啥了 突然我某个同事对我说 宝哥 你看这个页面好多console.log 不仅会影响性能 而且可能会被不法分子所利用 我觉得很有道理 所以我萌生了写一个小插件来去除生产环境的console.log的想法...

起因

已经颓废了很久 因为实在不知道写啥了 突然我某个同事对我说 宝哥 你看这个页面好多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的资料请关注本站其它相关文章!

声明:本站所有文章       ,如无特殊说明或标注      ,均为本站原创发布      。任何个人或组织                  ,在未征得本站同意时          ,禁止复制                 、盗用       、采集      、发布本站内容到任何网站                  、书籍等各类媒体平台        。如若本站内容侵犯了原著者的合法权益   ,可联系我们进行处理                。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
网站多语言切换怎么实现代码(实现多语言功能,根据浏览器语言标识自动切换)