babel插件去除console示例详解

目录
  • 起因
  • 介绍
  • 窥探
  • 初见AST
    • Program
    • ExpressionStatement
    • CallExpression
    • MemberExpression
    • Identifier
    • StringLiteral
    • 公共属性
  • 如何写一个babel插件?
  • 构造visitor方法
  • 去除所有console
  • 增加env api
  • 增加exclude api
  • 增加commentWords api
  • 细节完善
    • 对于后缀注释
    • 对于前缀注释
  • 发布到线上
    • 安装
    • 使用

起因

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

(0)

相关推荐

  • Node.js基础模块babel使用详解

    目录 安装配置 使用babel 实际例子 类的转化 babel-polyfill 前言: 由于ES6到ES7增加了很多新的语法,新特性的出现使得大家都希望通过新语法来提升自身的开发效率,但在之前的最新的node可能也没有百分之百的支持ES2017的新特性,而且开发者在开发环境和生产环境中的版本一般是不同的,所以新特性的代码可能不能完美的运行在线上环境中,为了解决难题,babel提供一系列的api来将新特性的语法转化成低版本环境中能够运行的代码 安装配置 babel是由一系列的组件构成,所以我们在

  • react源码层深入刨析babel解析jsx实现

    目录 jsx v16.x及以前版本 v17及之后版本 ReactElement React.createElement ReactElement React.Component 总结 经过多年的发展,React已经更新了大版本16.17.18,本系列主要讲的是 version:17.0.2,在讲这个版本之前,我们先看一看在babel的编译下,每个大版本之下会有什么样的变化. jsx <div className='box'> <h1 className='title' style={{'

  • Vue 3.0的attribute强制行为理解学习

    目录 理解property和attribute property 形容词 名词 attribute vue3中的property和attribute xml中的属性节点 vue3.0的attribute强制行为 源代码分析 vue3.0的变化 理解property和attribute 这个要看具体的语境了.不过我们可以从词源的角度来区分一下这两者: property 形容词 property的词源可追溯到proper,意为合适的,适当的. 短语示例: a proper job 一份合适(自己的)

  • Babel自动生成Attribute文档实现详解

    目录 1. 前言 2. 开发自动生成属性文档插件 2.1 生成Babel插件模板: 2.2 转换思路详解: 2.3 单元测试用例: 2.4 AST分析详解: 2.5 插件开发过程: 2.5.1 定义Comment.ApiTable类型对象: 2.5.2 插件主逻辑分析: 2.5.3 主逻辑实现: 2.5.4 注释解析函数: 2.5.5 Markdown表格拼装: 2.5.6生成结果展示~ 3. 总结 1. 前言 利用Babel自动解析源码属性上的注释生成对应Markdown文档,这个场景的应用主

  • Babel 插件开发&访问节点实例详解

    目录 访问节点 获取子节点的Path: 检查节点的类型: 检查路径(Path)类型: 检查标识符(Identifier)是否被引用: 找到特定的父路径: 获取同级路径: 停止遍历: 访问节点 获取子节点的Path: 我们在处理节点的属性之前必须要拿到节点对象才能进行操作,我们使用path.node.property来访问属性~ BinaryExpression(path) { path.node.left; path.node.right; path.node.operator; } 我们还可以

  • vue3中的透传attributes教程示例详解

    目录 引言 绑定样式 对象 数组 透传的attributes 透传 attributes 之样式绑定 透传 attributes 之事件绑定 特殊1:组件嵌套 特殊2:禁用透传attributes 特殊3:在 javascript 中访问透传的attributes 总结 引言 最近两年都是在使用 react 进行项目开发,看技术博客都是针对 react 和 javaScript 高级方面的,对 vue 的知识基本上遗忘的差不多了.最近开始慢慢回顾 vue 的知识以及对新的语法进行学习,为后面的计

  • babel插件去除console示例详解

    目录 起因 介绍 窥探 初见AST Program ExpressionStatement CallExpression MemberExpression Identifier StringLiteral 公共属性 如何写一个babel插件? 构造visitor方法 去除所有console 增加env api 增加exclude api 增加commentWords api 细节完善 对于后缀注释 对于前缀注释 发布到线上 安装 使用 起因 已经颓废了很久 因为实在不知道写啥了 突然我某个同事对

  • jQuery.Validate表单验证插件的使用示例详解

    jQuery Validate 插件为表单提供了强大的验证功能,让客户端表单验证变得更简单,同时提供了大量的定制选项,满足应用程序各种需求. 请在这里查看示例 validate示例 示例包含 验证错误时,显示红色错误提示 自定义验证规则 引入中文错误提示 重置表单需要执行2句话 源码示例 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <

  • Android热修复及插件化原理示例详解

    目录 1.前言 2.类加载机制 3.Android类加载 4.Tinker原理 代码实现 5.插件化 5.1 Activity启动流程简单介绍 5.2 插件化原理 5.2.1 绕开验证 5.2.2还原插件Activity 5.3 加载插件资源 5.3.1 Resources&AssetManager 5.3.2 id冲突 1.前言 热修复一直是这几年来很热门的话题,主流方案大致有两种,一种是微信Tinker的dex文件替换,另一种是阿里的Native层的方法替换.这里重点介绍Tinker的大致原

  • mysql查看死锁与去除死锁示例详解

    1.查询进程 show processlist 2. 查询到相对应的进程,然后 kill id 验证(kill后再看是否还有锁) 2.查询是否锁表 show OPEN TABLES where In_use > 0; 示例: 新建一个会话执行如下的显示锁示例 LOCK TABLES account_data.account READ; SELECT SLEEP(160); UNLOCK TABLES account_data.account; 另开启一个会话检查锁表情况: mysql> sho

  • 独立使用umi的核心插件模块示例详解

    目录 引言 实践 结语 引言 今天我们做一个有趣的尝试,将 umi 的核心插件模块独立出来作为另一个框架的基础架构,这里我们将它称为 konos. 介于 umi 自身的源码的独立拆分,要实现这个功能其实非常的简单.只需要单独使用 @umijs/core 就好. 实践 先看具体实践吧.以下步骤都是常规编写 cli 的一些步骤,我就不做过多的说明,如果你看不懂其中的某些代码,可以评论区留言,或者查看我的其他文章. 新建空白文件夹,mkdir konos 你可以根据你使用的电脑执行对应的命令来新建一个

  • vue3自定义插件的作用场景及使用示例详解

    目录 插件的作用场景 插件的定义(注册) 插件的安装 插件的使用 插件中的Provide/inject 插件的作用场景 在vue2的插件那篇文章我们介绍过插件其实就是vue的增强功能.通常来为vue添加全局功能的.在vue3中插件的功能也是一样的,只是它们在定义上有所不同. 通过app.component()和app.directive()注册一到多个全局组件或自定义指令 通过app.provide()使一个资源可被注入进整个应用 向app.config.globalProperties中添加一

  • JavaScript console对象与控制台使用示例详解

    目录 1. console对象 2. console的静态方法 3. 自定义console 4. 控制台命令行API 4.1 $_ 4.2 $0-$4 4.3 $(selector) 4.4 $x(path) 4.5 inspect(obj) 4.6 keys()和values() 4.7 其它的命令 1. console对象 console对象是JavaScript的原生对象,提供了很多用于调试的方法,如console.log输出信息,console.count记录执行次数 console.l

  • kotlin android extensions 插件实现示例详解

    目录 前言 原理浅析 总体结构 源码分析 插件入口 配置编译器插件传参 编译器插件接收参数 注册各种Extension IrGenerationExtension ExpressionCodegenExtension StorageComponentContainerContributor ClassBuilderInterceptorExtension PackageFragmentProviderExtension 总结 前言 kotlin-android-extensions 插件是 Ko

  • webpack 5.68.0版本教程示例详解

    目录 起步 1. 基本安装 2. 配置出入口 plugin 1. html-webpack-plugin 2. progress-bar-webpack-plugin loader 1. css-loader与style-loader 2. url-loader与file-loader 3. sass-loader 4. postcss-loader 5. babel-loader 搭建环境 1. 开发环境与生产环境 2. 配置别名 代码分离 1. webpack-bundle-analyzer

  • React Redux应用示例详解

    目录 一 React-Redux的应用 1.学习文档 2.Redux的需求 3.什么是Redux 4.什么情况下需要使用redux 二.最新React-Redux 的流程 安装Redux Toolkit 创建一个 React Redux 应用 基础示例 Redux Toolkit 示例 三.使用教程 安装Redux Toolkit和React-Redux​ 创建 Redux Store​ 为React提供Redux Store​ 创建Redux State Slice​ 将Slice Reduc

随机推荐