关于webpack的loader小教程:如何删除代码中的console
在开发环境中,我们经常会加入很多console.log来做代码的调试,但是我们并不希望当项目上线后,还会有打印的值,因此我们需要将这些console在上线前全部删掉。虽然webpack4中已经集成了去除console的功能,但webpack3没有这个功能,需要我们自己去处理。
如果有浏览过 webpack 官网的同学一定见过这张图,这是webpack官方对自己的功能描述图。webpack 能把左侧各种类型的文件(在webpack中 它们都是模块)打包为右边被通用浏览器支持的文件。
什么是 Loader ?
在撸一个 loader 前,我们需要先知道它到底是什么。本质上来说,loader 就是一个 node 模块,在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块(resource)的时候调用该函数。在这个函数内部,我们可以通过传入 this 上下文给 Loader API 来使用它们。
把 webpack 想像成一个工厂,loader 就是一个个身怀绝技的流水线工人,有的会处理 svg,有的会压缩 css 或者图片,有的会处理 less,有的会将 es6 转换为 es5。
回顾一下头图左边的那些模块,他们就是所谓的源模块,会被 loader 转化为右边的通用文件,因此我们也可以概括一下 loader 的功能:把源模块转换成通用模块。
下面我来展示一个loader的基础小功能,字符串转换
基本示例:
// 如果有配置项,则会挂载到this上
module.exports = function (source) { // source 为引入文件的源代码(内容)
return source.replace('aaaaa', 'bbbbb') // 讲代码中所有的'aaaaa'转换为'bbbbb'
}
当然实际的loader并不会这么粗暴的去做转换,这里仅仅是作为一个入门案例
注意:我们删除console的loader一定不能使用正则等其他方式,因为正则删除的时候,会将匹配到的内容全部删除,可能会将我们写在字符串中的相同内容一起删掉
例:
console.log('我们想要删除的console')
alert('这是一个字符串 console') // 如果是通过正则来做删除,那么连此处的字符串中的console也会被一起删掉,这是错误的
正式开始
安装依赖
下面我们正式开始写一个可以删除代码中console的loader,首先我们需要做一些前置工作,下载我们在开发中所需要的一些依赖
- @babel/parser ---- 帮助我们分析代码,并将代码转换为抽象语法树(AST)
- @babel/traverse ---- 帮助我们对抽象语法树进行遍历
- @babel/generator ---- 将抽象语法树转换回js代码 注: 这里也可以使用@babel/core,任选一个即可,只是使用上会稍有区别
- @babel/types ---- 对具体的AST节点进行增删改查
书写测试代码
console.log('测试测试')
因为我们的主要目的是删除console,因此,在此处就不再书写过多的代码,我们直奔主题,删掉它!
loader编写
首先,我们创建一个deleteConsoleLoader.js,并将我们需要的依赖全部导入进来
const parser = require('@babel/parser') //将源代码解析成AST
const traverse = require('@babel/traverse').default //对AST节点进行递归遍历,生成一个便于操作、转换的path对象
const generator = require('@babel/generator').default //将AST解码回js代码
const bableTypes = require('@babel/types') //对具体的AST节点进行增删改查
module.exports = function (source) {
const ast = parser.parse(sourceStr, { sourceType: 'module' }) //支持ES6的module方式
console.log(ast); //打印一下我们的ast,看看他是个什么东西
return null
}
打印结果:
Node {
type: 'File',
start: 0,
end: 28,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 28 }
},
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 28,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [[Node]], // 这里就是我们的代码节点,可以看到只有一个node节点,就是刚刚我们测试代码中的console
directives: []
},
comments: []
}
我们继续通过console.log(ast.program.body)
把这个body打印出来,看看它的详细内容
Node {
type: 'ExpressionStatement', // 代表是一个表达式语句,我们的console.log()
start: 0,
end: 28,
loc: SourceLocation { start: [Position], end: [Position] },
expression:
Node {
type: 'CallExpression', // 这里是它的具体类型
start: 0,
end: 28,
loc: [SourceLocation],
callee: [Node],
arguments: [Array]
}
}
那么下面就好办了,通过@babel/traverse,我们可以很轻松的遍历AST,同时,它还为我们提供了很方便的获取某种类型节点的方法,下面放上代码
const parser = require('@babel/parser') //将源代码解析成AST
const traverse = require('@babel/traverse').default //对AST节点进行递归遍历,生成一个便于操作、转换的path对象
const generator = require('@babel/generator').default //将AST解码生成js代码
const bableTypes = require('@babel/types') //对具体的AST节点进行增删改查
module.exports = function (source) {
const ast = parser.parse(source, { sourceType: 'module' })
traverse(ast, { // 对ast进行遍历
CallExpression (path) { // 如果节点类型为CallExpression ,则会执行此函数,我们的console的type就是这个
console.log(path.node.callee) // 在这里,我们打印path.node.callee
}
})
}
结果:
{
type: 'MemberExpression', // 这是整个console.log的类型
start: 0,
end: 11,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 11 }
},
object:
Node {
type: 'Identifier', // 这里是console和log的类型
start: 0,
end: 7,
loc:
SourceLocation { start: [Position], end: [Position], identifierName: 'console' },
name: 'console' // 这里可以看到,是console.log的第一部分,log在下面的第二部分
},
property:
Node {
type: 'Identifier',
start: 8,
end: 11,
loc:
SourceLocation { start: [Position], end: [Position], identifierName: 'log' },
name: 'log'
},
computed: false
}
有了以上的铺垫,我们就可以完成最后一步了,在上面那些步骤完成后,我们就获得了删除console所需要的所有条件
接下来,我们只需要将console所对应的类型及name作为删除的条件,就可以将代码中所有的console.log删除掉
直接上代码
const parser = require('@babel/parser') //将源代码解析成AST
const traverse = require('@babel/traverse').default //对AST节点进行递归遍历,生成一个便于操作、转换的path对象
const generator = require('@babel/generator').default //将AST解码生成js代码
const bableTypes = require('@babel/types') //对具体的AST节点进行增删改查
module.exports = function (source) {
const ast = parser.parse(sourceStr, { sourceType: 'module' })
traverse(ast, {
CallExpression (path) {
// 删除console
// 使用bableTypes 来对node节点的类型做判断,如果节点的整体类型为MemberExpression,并且子节点object的类型为Identifier,同时节点中的name又为console
if (bableTypes.isMemberExpression(path.node.callee) && bableTypes.isIdentifier(path.node.callee.object, { name: "console" })) {
path.remove() // 那么将这个节点删除掉
}
}
})
let output = generator(ast, {}); // 通过@babel/generator将AST重新解码回js
return output.code // 最后将解码好的代码返回,给下一个loader使用
}
loader开发完成
那么一个简单的删除console的loader就开发完成了,为了验证,我们可以对比使用loader前后打包代码的区别
使用前:
/*! no static exports found */
/***/ (function(module, exports) {
eval("console.log('测试测试');\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
使用后:
/***/ (function(module, exports) {
eval("\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
可以看到console.log已经被删除掉了,那么我们的loader就完成了