创见博客
在 Webpack Loader 中使用 Babel:以 AST 删除 console.log 为例
七崽爱吃小饼干2026/09/20阅读 0专栏 webpack原理

为什么要用 Babel

之前的 clean-log-loader 用正则删除 console.log,简单但脆弱:它分不清代码里的字符串和注释,也处理不好嵌套调用。要做到精确,就得理解代码结构——这正是 Babel 的强项。

Babel 把源码解析成 AST(抽象语法树),我们基于节点类型做增删改,再生成回代码。整个过程分三步:

源码 ──parse──▶ AST ──transform──▶ AST ──generate──▶ 源码

Loader 恰好处于「源码进、源码出」的位置,所以用 Babel 再合适不过。

核心 API

只需安装一个包:

bash
npm install -D @babel/core

最常用的入口是 transformSync:

js
const babel = require('@babel/core');

const { code, map } = babel.transformSync(source, {
  filename: 'a.js',
  babelrc: false,
  configFile: false,
  sourceMaps: true,
  plugins: [myPlugin],
});
  • plugins:插件数组,每个插件负责一种转换。
  • babelrc / configFile:示例里关掉,避免读取外部配置导致行为不可控。
  • sourceMaps:是否生成 sourcemap。
  • 返回值里 code 是转换后的代码,map 是源码映射。

编写插件:从节点类型说起

一个 Babel 插件就是一个返回 visitor 对象的函数:

js
module.exports = function myPlugin({ types: t }) {
  return {
    name: 'my-plugin',
    visitor: {
      // 节点类型: 处理函数
    },
  };
};

types(通常简写 t)是官方提供的工具集,用来判断和创建节点。

要删除 console.log(x),先看它对应的 AST 结构:

ExpressionStatement
└── CallExpression
    ├── callee: MemberExpression
    │   ├── object:   Identifier  name="console"
    │   └── property: Identifier  name="log"
    └── arguments: [...]

于是我们在 CallExpression 上做判断。

完整插件

js
module.exports = function removeConsoleLog({ types: t }) {
  function isConsoleLog(callee) {
    if (!t.isMemberExpression(callee)) return false;
    if (!t.isIdentifier(callee.object, { name: 'console' })) return false;

    if (t.isIdentifier(callee.property)) {
      return callee.property.name === 'log';
    }
    if (t.isStringLiteral(callee.property)) {
      return callee.property.value === 'log';
    }
    return false;
  }

  return {
    name: 'remove-console-log',
    visitor: {
      CallExpression(path) {
        if (!isConsoleLog(path.node.callee)) return;

        const parent = path.parentPath;
        if (parent.isExpressionStatement()) {
          parent.remove();
        } else {
          path.replaceWith(t.identifier('undefined'));
        }
      },
    },
  };
};

逐段解读

  1. path 而非 node visitor 回调拿到的是 path,它描述节点在树中的位置及父子关系。path.node 是节点本身,path.parentPath 是父路径。

  2. isXxx 类型判断 t.isMemberExpression、t.isIdentifier 既能判类型,传第二个参数还能顺手校验字段,例如 t.isIdentifier(obj, { name: 'console' })。

  3. 兼容两种写法 console.log 的 property 是 Identifier;console['log'] 的 property 是 StringLiteral。两者都覆盖,避免漏网。

  4. 分场景删除

    • 独立语句 console.log(x);:父节点是 ExpressionStatement,直接 parent.remove()。
    • 嵌套表达式 const v = console.log(x);:不能删父节点,否则会留下 const v = ;,所以把这次调用替换成 undefined,得到 const v = undefined;。
  5. replaceWith 创建节点 t.identifier('undefined') 生成一个标识符节点,交给 Babel 生成代码。

接到 Loader 上

Loader 只负责调用 Babel 并把结果交出去:

js
const babel = require('@babel/core');
const removeConsoleLog = require('../babel-plugins/remove-console-log');

module.exports = function babelCleanLogLoader(source) {
  const { code, map } = babel.transformSync(source, {
    filename: this.resourcePath,
    babelrc: false,
    configFile: false,
    sourceMaps: this.sourceMap,
    plugins: [removeConsoleLog],
  });

  this.callback(null, code, map);
};
  • this.resourcePath 作为 filename,报错时能定位到具体文件。
  • sourceMaps: this.sourceMap 尊重 Webpack 的 sourcemap 配置。
  • 用 this.callback(null, code, map) 同时返回代码和 sourcemap。

Webpack 配置:

js
{
  test: /\.js$/i,
  exclude: /node_modules/,
  use: [
    {
      loader: path.resolve(__dirname, 'loaders/insert-comment-loader.js'),
      options: { content: 'Generated by webpack-demo' },
    },
    path.resolve(__dirname, 'loaders/babel-clean-log-loader.js'),
  ],
}

效果对比

输入:

js
console.log('1 + 2 =', sum(1, 2));
const s = 'console.log 在字符串里';
console.warn('保留');
const v = console.log(foo());
// console.log('注释');
console['log']('computed');

输出:

js
const s = 'console.log 在字符串里';
console.warn('保留');
const v = undefined;
// console.log('注释');

字符串、注释、console.warn 全部原样保留——这是正则方案做不到的。注意 console['log'] 也被正确删除,而注释中的内容因为不在 AST 里,天然不会受影响。

注意事项

  • Babel 只处理语法,不理解作用域:如果代码里有个局部变量也叫 console,插件无法区分。生产级插件会结合 path.scope.getBinding('console') 判断它是否是全局对象。
  • 未使用的返回值会变成 undefined:这是嵌套场景的取舍,可根据需求改成 void 0 或保留。
  • 性能:transformSync 是同步阻塞的,大项目可改用 transformAsync 并在 loader 里返回 Promise。
  • 别重复造轮子:若只是做语法降级,直接配置 @babel/preset-env 即可;写自定义插件是为了业务定制转换。

小结

用 Babel 写 loader 的收益,是能基于 AST 做精确、可靠的代码变换。掌握三步模型(parse / transform / generate)、visitor 与 path、以及 types 工具后,你就能实现删除日志、按需引入、自动埋点、i18n 提取等各类转换。

评论
0/100