相信很多前端开发者都会遇到过类似的场景:在开发阶段打印一些日志用于调试,在生产环境需要把这些日志去掉,并且不能让它们打包进去。比如以下代码在开发阶段会打印日志,在生产环境下则不会:
console.log('This is index.js.');
if (__DEV__) {
console.log('当前版本:' + __VERSION__);
}
如何在编译时替换变量
熟悉 Webpack 的同学,第一时间会想都可以使用 Webpack 官方提供的 DefinePlugin 来完成上述的需求。按照 Webpack 官方文档指引,有以下的配置内容:
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
/**
* @returns {import('webpack').Configuration}.config
*/
module.exports = function(env) {
return {
entry: './src/index',
output: {
path: path.resolve(__dirname, 'dist'),
},
mode: env.production ? 'production' : 'development',
plugins: [
new webpack.DefinePlugin({
__DEV__: !env.production,
__VERSION__: JSON.stringify('1.0.0'),
})
],
}
}
上面的 DefinePlugin
入参是一个对象,对象的 key __DEV__
和 __VERSION__
对应着源码中的变量,value 代表着要替换的值或表达式。
现在让我们执行 npx webpack --config webpack.config.js && node dist/main.js
命令,可以看到输出结果是:
执行 npx webpack --config webpack.config.js --env production && node dist/main.js
命令后输出结果是:
产物结果里面已经没有 log 相关信息,默认已经被 Webpack Tree shaking 去掉了:
可以看到现在的表现已经满足了我们的需求,在开发阶段能正常输出日志,在生产环境下则不输出。可是为什么要对 define 的值做一次 JSON.stringify()
的转换呢?
为什么需要 JSON.stringify()
在 DefinePlugin 文档中提及到:
拿上面的例子来说,字符串 '1.0.0'
经过 JSON.stringify()
后的结果是 '"1.0.0"'
,替换到源码中的 __VERSION__
后,源码就变成了:
console.log('当前版本:' + '1.0.0');
查看 DefinePlugin 中的 toCode()
方法实现 我们可以知道,在 DefinePlugin 内部默认会先对部分数据类型(包括 null
,undefined
,function
,object
等) 进行处理,通过 JSON.stringify()
或者原型链上的 toString()
方法,保证原来的数据类型都变成 string 字符串。
然后当 Webpack 发现我们的源码有变量需要替换时,它会以字符串拼接的方式,把我们要替换的真实的值或表达式替换上去:
这样,我们的 __VERSION__
会被替换成 '1.0.0'
,最终生成的代码才能够正常执行。如果一开始我们的 1.0.0
字符串不通过 JSON.stringify()
处理或者不多加一对引号,最后结果就会变成:
console.log('当前版本:' + 1.0.0);
这里很明显不是一个合法的表达式,所以代码执行肯定会报错。
类似的,在 rollup-plugin-replace 插件中,调用了 replace()
方法把我们真实的值或表达式替换掉,和 Webpack 的 DefinePlugin 原理是类似的。
不是所有情况都需要 JSON.stringify()
情况一:object
、function
、null
等数据类型
这种情况是针对 Webpack 的 DefinePlugin 插件的,因为它默认对 null
、undefined
、null
、-0
、RegExp
、function
、object
、bigint
等数据类型转换成字符串了,比如:
// webpack.config.js
module.exports = function() {
return {
plugins: [
new webpack.DefinePlugin({
// ...
__OBJ__: { a: 1, b: 2 },
})
],
}
}
console.log('OBJ: ', __OBJ__);
最终 Webpack DefinePlugin 会默认处理这个 __OBJ__
对象打包出正确的结果:
但是对于其他打包编译工具来说,则来看它是否有默认的处理机制了,该进行字符串处理还是要的。
情况二:替换成表达式
细心的同学可能发现,DefinePlugin 内部为什么对其他的数据类型(null
,undefined
,function
,object
)这些默认进行 JSON.stringify()
处理,而不对字符串也处理一下呢?
原因很简单,假如有以下的代码:
const a = {
b: {
c: 1
}
}
console.log('OBJ: ', C);
我就是要 C 替换成 a.b.c
,这时你可以写以下的 webpack 配置:
// webpack.config.js
module.exports = function() {
return {
plugins: [
new webpack.DefinePlugin({
// ...
C: 'a.b.c',
})
],
}
}
这时你会看到打包出来的结果是:
打包产物也可以正常执行并输出 1
。
对于字符串的数据类型来说,有可能是要替换成字符串字面量,也有可能是要替换成表达式,所以 Webpack 还是把操作的选择权交给用户吧。
总结
本文先介绍了如何使用 Webpack DefinePlugin 对源码中的变量在编译时进行替换,以在生产环境和本地调试环境下输出不一样的产物。然后通过解析两段 Webpack 源码来说明为什么要先对变量的值进行 JSON.stringify()
处理。最后介绍了有两种情况是不需要进行 JSON.stringify()
处理的,所以在实际的开发时还是要有选择的使用。