创见博客
如何实现一个 clean-log-loader
七崽爱吃小饼干2026/09/19阅读 0专栏 webpack原理

背景

开发阶段我们习惯用 console.log 调试,但上线前手动一个个删除既繁琐又容易漏。Webpack 的 loader 机制让我们可以在构建阶段自动清理这些日志,源码保持原样,产物里却不含调试信息。

本文从零实现一个 clean-log-loader,并讲清其中的关键细节。

Loader 是什么

Loader 本质上是一个导出函数的 JS 模块。Webpack 会把文件内容作为参数传进来,loader 处理后再把结果返回给下一个 loader。

js
module.exports = function (source) {
  return transformedSource;
};

因此我们的目标很明确:输入一段源码,去掉其中的 console.log(...),返回新源码。

第一版:正则替换

最直接的想法是用正则:

js
module.exports = function (source) {
  return source.replace(/console\.log\s*\([^)]*\);?/g, '');
};

这能处理大多数单行调用,但 [^)]* 遇到嵌套括号就失效了:

js
console.log('结果', sum(1, 2));

因为 sum(1, 2) 里的第一个 ) 会让匹配提前结束,结果是:

js
;

明显不对。所以正则方案不够健壮。

第二版:括号配对扫描

正确的做法是:先定位 console.log,再手动做括号配对,找到与之匹配的右括号,从而准确切出整条语句。

完整实现

js
module.exports = function cleanLogLoader(source) {
  const keyword = /console\s*\.\s*log/g;
  let result = '';
  let lastIndex = 0;
  let match;

  while ((match = keyword.exec(source)) !== null) {
    let i = match.index + match[0].length;
    while (i < source.length && /\s/.test(source[i])) i++;
    if (source[i] !== '(') continue;

    let depth = 0;
    let j = i;
    for (; j < source.length; j++) {
      if (source[j] === '(') depth++;
      else if (source[j] === ')') {
        depth--;
        if (depth === 0) {
          j++;
          break;
        }
      }
    }

    let end = j;
    if (source[end] === ';') end++;

    result += source.slice(lastIndex, match.index);
    lastIndex = end;
    keyword.lastIndex = end;
  }

  result += source.slice(lastIndex);
  return result;
};

逐段解读

  1. 全局匹配关键字 /console\s*\.\s*log/g 中的 \s* 允许 console . log(...) 这种写法。

  2. 跳过空白,确认是函数调用 关键字后面可能跟空格或换行,只有遇到 ( 才说明这是一次调用;否则跳过(避免误伤 console.log = fn 之类的赋值)。

  3. 括号计数找到结束位置 遇到 ( 计数 +1,遇到 ) 计数 -1,当计数归零时,当前位置就是匹配的右括号。这个思路天然支持多行和任意深度的嵌套。

  4. 吃掉末尾分号 如果右括号后紧跟 ;,一并删除,避免留下空语句。

  5. 拼接结果 用 lastIndex 记录上一次删除的终点,把未处理的部分原样保留,最后补上尾部内容。

  6. 同步 lastIndex 删除后把 keyword.lastIndex 指向新的位置,防止在已删除区间里重复匹配。

配置到 Webpack

js
const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/i,
        exclude: /node_modules/,
        use: path.resolve(__dirname, 'loaders/clean-log-loader.js'),
      },
    ],
  },
};

test 匹配 .js 文件,exclude 排除依赖包,use 指向 loader 的绝对路径。

效果验证

处理前:

js
console.log('1 + 2 =', sum(1, 2));
console.warn('保留我');

处理后:

js
console.warn('保留我');

构建后检查产物:

bash
grep -r "console.log" dist/

没有输出即表示清理成功。

注意事项

  • 只匹配 console.log:不会误删 console.warn、console.error 等。
  • 不能识别字符串和注释:如果 "console.log(x)" 出现在字符串或注释里,也会被删除。生产级实现建议接入 acorn 等解析器,遍历 AST 精确删除 CallExpression。
  • 保留行号:当前实现直接删除语句,会留下空行。若在意 sourcemap 行号,可以把删除内容替换为等量空白/换行。
  • 正则字面量风险:/console\.log(1)/ 这类正则里的内容也可能被误伤,体积不大的项目影响有限。

小结

实现 loader 的核心是理解「输入源码 → 输出源码」这一模型。正则能解决 80% 的场景,但要正确处理嵌套和多行,括号配对扫描是简单又可靠的方案。掌握这个例子后,你也能写出删除 debugger、替换接口地址之类的自定义 loader。

评论
0/100