背景
开发阶段我们习惯用 console.log 调试,但上线前手动一个个删除既繁琐又容易漏。Webpack 的 loader 机制让我们可以在构建阶段自动清理这些日志,源码保持原样,产物里却不含调试信息。
本文从零实现一个 clean-log-loader,并讲清其中的关键细节。
Loader 是什么
Loader 本质上是一个导出函数的 JS 模块。Webpack 会把文件内容作为参数传进来,loader 处理后再把结果返回给下一个 loader。
module.exports = function (source) {
return transformedSource;
};
因此我们的目标很明确:输入一段源码,去掉其中的 console.log(...),返回新源码。
第一版:正则替换
最直接的想法是用正则:
module.exports = function (source) {
return source.replace(/console\.log\s*\([^)]*\);?/g, '');
};
这能处理大多数单行调用,但 [^)]* 遇到嵌套括号就失效了:
console.log('结果', sum(1, 2));
因为 sum(1, 2) 里的第一个 ) 会让匹配提前结束,结果是:
;
明显不对。所以正则方案不够健壮。
第二版:括号配对扫描
正确的做法是:先定位 console.log,再手动做括号配对,找到与之匹配的右括号,从而准确切出整条语句。
完整实现
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;
};
逐段解读
-
全局匹配关键字
/console\s*\.\s*log/g中的\s*允许console . log(...)这种写法。 -
跳过空白,确认是函数调用 关键字后面可能跟空格或换行,只有遇到
(才说明这是一次调用;否则跳过(避免误伤console.log = fn之类的赋值)。 -
括号计数找到结束位置 遇到
(计数 +1,遇到)计数 -1,当计数归零时,当前位置就是匹配的右括号。这个思路天然支持多行和任意深度的嵌套。 -
吃掉末尾分号 如果右括号后紧跟
;,一并删除,避免留下空语句。 -
拼接结果 用
lastIndex记录上一次删除的终点,把未处理的部分原样保留,最后补上尾部内容。 -
同步
lastIndex删除后把keyword.lastIndex指向新的位置,防止在已删除区间里重复匹配。
配置到 Webpack
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 的绝对路径。
效果验证
处理前:
console.log('1 + 2 =', sum(1, 2));
console.warn('保留我');
处理后:
console.warn('保留我');
构建后检查产物:
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。