# babel-plugin-console-omit

实现 线上删除 log 语句的 babel 插件

# 前言

在之前的一篇文章中,介绍了babel的基本的概念,以及如何写一个简单的babel插件,这篇文章中就实现一个在生产环境中去掉 console 的插件 babel-plugin-console

有如下代码,我们要把函数中的 console语句去掉。

function code(x) {
  if (true) { 
    console.log(x)
  }
  console.log(x)
  return x * x
};

这段代码在astexplorer (opens new window)解析后得到的 ast如下:

ast

# 版本1

基于此,这里先初步实现一版,去掉 console即可:

module.exports = ({ types: t, template }) => {
  return {
    visitor: {
      CallExpression(path, state, scope) {
        if (
          path.node.callee &&
          t.isIdentifier(path.node.callee.object, { name: "console" })
        ) {
          path.remove()
        }
      },
    },
  };
};

这里判断如果是 console语句,直接使用 path.remove() 删除即可

最终得到的结果如下:

function code(x) {
  console.log(x) 
  if (true) {
  }
  return x * x
};

发现 console语句都可被去掉

# 版本2

上面一版本是去掉所有的 console语句,非常简单直接。但是在有些情况下,我们需要保留某些 console语句供线上调试使用,这时候我们约定,通过给 console语句添加特定注释,以确保不会被去掉。

function code(x) {
  console.log(x) 
  if (true) {
    // 其他注释
    // no remove
    console.log(x)
    // hhhhhhh
    console.log(x) // reserve
    console.log(x)
  }
  return x * x
};

如上所示:

  • 第一条 console 没加上注释,期望被删掉
  • 第二条 console 有多条前置注释,其中有一个 no remove的注释,期望该条 console被保留
  • 第三条 console 有一条非保留前置注释,但是有一个 reserve的后置保留注释
  • 第四条 console 没有加上任何注释,期望被删掉

基本上,这四种情况可以涵盖了所有的注释情况了(这里先不考虑块注释)。生成的 ast如下:

ast2

可以发现,在生成的ast中,注释并没有被表示成一个独立的节点类型,而是被当作了一个 node节点的属性。所以这里我们就判断一下,当前节点的 前置注释(leadingComments)后置注释(trailingComments) 是否有保留的字段,如果都没有保留的字段,那么就删除掉该节点即可,实现如下:

function removeConsoleExpression(path, state) {
  const parentPath = path.parentPath;
  const node = parentPath.node;

  let leadingReserve = false;
  let trailReserve = false;

  if (hasLeadingComments(node)) {
    // 遍历所有的前缀注释
    node.leadingComments.forEach((comment) => {
      // 有保留字 并且不是上个兄弟节点的尾注释
      if (isReserveComment(comment, state)) {
        leadingReserve = true;
      }
    });
  }
  if (hasTrailingComments(node)) {
    // 遍历所有的后缀注释
    node.trailingComments.forEach((comment) => {
      // 有保留字 并且是本行的
      if (isReserveComment(comment, state)) {
        trailReserve = true;
      }
    });
  }
  if (!leadingReserve && !trailReserve) {
    path.remove();
  }
}

最终得到的结果如下:

function code(x) {
  if (true) {
    // 其他注释
    // no remove
    console.log(x);
    // hhhhhhh
    console.log(x); // reserve
    console.log(x);
  }
  return x * x;
};

我们发现:

  • 第一条可正常删除
  • 第二条和第三条如期望一样被保留了下来
  • 第三条却出现了意外,没有加上任何保留注释,却没有被去掉。

第三天没有被去掉是什么原因呢?通过观察 ast的结果发现,第三条console尾部的reserve注释同时也是第四条语句的头部注释

trailComments ast3

leadingComments ast4

# 版本3

所以,这就需要在遍历时注意:

  • 遍历 leadingComments 的时候需要注意,它时候含有上个兄弟节点的保留注释
  • 遍历 trailComments 的时候,要注意如果当前尾部包含保留注释,需要对其进行标记,以便于下个兄弟节点决定是否删除节点的时候可以排除这个影响因素
function removeConsoleExpression(path, state) {
  const parentPath = path.parentPath;
  const node = parentPath.node;

  let leadingReserve = false;
  let trailReserve = false;

  if (hasLeadingComments(node)) {
    // 遍历所有的前缀注释
    node.leadingComments.forEach((comment) => {
      // 有保留字 并且不是上个兄弟节点的尾注释
      if (isReserveComment(comment, state) && !comment.belongPrevTrail) {
        leadingReserve = true;
      }
    });
  }
  if (hasTrailingComments(node)) {
    // 遍历所有的后缀注释
    node.trailingComments.forEach((comment) => {
      const {loc: { start: { line: commentLine }}} = comment;
      // 标记下一个sibling节点遍历的key
      nextSibilingKey = parentPath.key + 1;

      // 对于尾部注释 需要标记出 该注释是属于当前的尾部 还是属于下个节点的头部 通过其所属的行来判断
      const {loc: { start: { line: expressionLine }}} = node.expression;
      if (commentLine === expressionLine) {
        comment.belongPrevTrail = true;
      }
      // 有保留字 并且是本行的
      if (isReserveComment(comment, state) && comment.belongPrevTrail) {
        trailReserve = true;
      }
    });
  }
  if (!leadingReserve && !trailReserve) {
    path.remove();
  }
}

如上所述,关键在于 主要通过注释所在的行和 console所在的行来判断,此注释是属于当前 console的尾部注释,还是属于下个兄弟节点的头部注释。如果是尾部注释就标记出来:

if (commentLine === expressionLine) {
  comment.belongPrevTrail = true;
}

如此一来,删除节点的逻辑就变得清晰了:

  • 遍历前置注释,只需要知道该注释含有约定的保留字段,并且它不是上个兄弟节点的尾部注释
  • 遍历后置注释,只需要判断该注释包含约定的保留字段,并且它是当前节点的尾部注释

最终得到的就是我们想要的结果了

# 测试

测试可以使用两种方法

  1. 使用 babel-core提供的api

这里需要安装 bable-corebabel-cli

const babel = require("babel-core");
const consolePlugin = require("../plugins/consolePlugin")

const result = babel.transform(code, {
  plugins: [[consolePlugin, { removeMethods: null }]]
});

package.json中添加脚本:

{
  "scripts": {
    "console": "NODE_ENV='production' babel-node ./test/console.js",
  },
}
  1. 使用 babel-cli

添加.babelrc 文件

{
  "plugins": ["./plugins/2consolePlugin"]
}

package.json中添加脚本:

{
  "script": {
    "babel": "NODE_ENV='production' babel ./test/console.js",
    "babel2": "NODE_ENV='production' babel ./test/console.js -o result.js",
  }
}

这两个脚本的区别是:

  • 脚本1可将编译结果输出到控制台,
  • 脚本2将结果输出到 result.js文件中

完整项目地址:点这里 (opens new window)

最后更新时间: 6/21/2021, 5:27:24 PM