使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

lxf2023-04-16 09:11:01

相信很多前端开发者都会遇到过类似的场景:在开发阶段打印一些日志用于调试,在生产环境需要把这些日志去掉,并且不能让它们打包进去。比如以下代码在开发阶段会打印日志,在生产环境下则不会:

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 命令,可以看到输出结果是: 使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

执行 npx webpack --config webpack.config.js --env production && node dist/main.js 命令后输出结果是: 使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

产物结果里面已经没有 log 相关信息,默认已经被 Webpack Tree shaking 去掉了:

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

可以看到现在的表现已经满足了我们的需求,在开发阶段能正常输出日志,在生产环境下则不输出。可是为什么要对 define 的值做一次 JSON.stringify() 的转换呢?

为什么需要 JSON.stringify()

在 DefinePlugin 文档中提及到:

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

拿上面的例子来说,字符串 '1.0.0' 经过 JSON.stringify() 后的结果是 '"1.0.0"',替换到源码中的 __VERSION__ 后,源码就变成了:

console.log('当前版本:' + '1.0.0');

查看 DefinePlugin 中的 toCode() 方法实现 我们可以知道,在 DefinePlugin 内部默认会先对部分数据类型(包括 nullundefinedfunctionobject 等) 进行处理,通过 JSON.stringify() 或者原型链上的 toString() 方法,保证原来的数据类型都变成 string 字符串。

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

然后当 Webpack 发现我们的源码有变量需要替换时,它会以字符串拼接的方式,把我们要替换的真实的值或表达式替换上去:

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

这样,我们的 __VERSION__ 会被替换成 '1.0.0',最终生成的代码才能够正常执行。如果一开始我们的 1.0.0 字符串不通过 JSON.stringify() 处理或者不多加一对引号,最后结果就会变成:

console.log('当前版本:' + 1.0.0);

这里很明显不是一个合法的表达式,所以代码执行肯定会报错

类似的,在 rollup-plugin-replace 插件中,调用了 replace() 方法把我们真实的值或表达式替换掉,和 Webpack 的 DefinePlugin 原理是类似的。

不是所有情况都需要 JSON.stringify()

情况一:objectfunctionnull 等数据类型

这种情况是针对 Webpack 的 DefinePlugin 插件的,因为它默认对 nullundefinednull-0RegExpfunctionobjectbigint 等数据类型转换成字符串了,比如:

// webpack.config.js
module.exports = function() {
  return {
    plugins: [
      new webpack.DefinePlugin({
        // ...
        __OBJ__: { a: 1, b: 2 },
      })
    ],
  }
}
console.log('OBJ: ', __OBJ__);

最终 Webpack DefinePlugin 会默认处理这个 __OBJ__ 对象打包出正确的结果:

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

但是对于其他打包编译工具来说,则来看它是否有默认的处理机制了,该进行字符串处理还是要的。

情况二:替换成表达式

细心的同学可能发现,DefinePlugin 内部为什么对其他的数据类型(nullundefinedfunctionobject)这些默认进行 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',
      })
    ],
  }
}

这时你会看到打包出来的结果是:

使用 Webpack DefinePlugin 在编译时替换变量的正确姿势

打包产物也可以正常执行并输出 1

对于字符串的数据类型来说,有可能是要替换成字符串字面量,也有可能是要替换成表达式,所以 Webpack 还是把操作的选择权交给用户吧。

总结

本文先介绍了如何使用 Webpack DefinePlugin 对源码中的变量在编译时进行替换,以在生产环境和本地调试环境下输出不一样的产物。然后通过解析两段 Webpack 源码来说明为什么要先对变量的值进行 JSON.stringify() 处理。最后介绍了有两种情况是不需要进行 JSON.stringify() 处理的,所以在实际的开发时还是要有选择的使用。