为什么需要区分环境

在开发网页的时候,一般都会有多套运行环境,例如:

  1. 在开发过程中方便开发调试的环境;
  2. 发布到线上给用户使用的运行环境。

这两套不同的环境虽然都是由同一套源代码编译而来,但是代码内容却不一样,其差异包括:

  • 线上代码被通过压缩代码中提到的方法压缩了;
  • 开发用的代码包含一些用于提示开发者的日志,普通用户不可能去看这些日志;
  • 开发用的代码所连接的后端数据接口地址也可能和线上环境不同,因为要避免在开发过程中造成对线上数据的影响。

为了尽可能复用代码,在构建的过程中需要根据目标代码要运行的环境输出不同的代码,我们需要一套机制在源码中去区分环境。幸运的是,Webpack已经为我们实现了这一点。

cross-env

通过Node中的process对象中的env属性来设置一个环境变量NODE_ENV来区分当前webpack环境。但是不同电脑上设置的方式是不一样的,cross-env可以跨平台设置环境和使用环境变量,解决了windows环境下的问题。

yarn add cross-env -D
1

在package.json里配置:

"build": "cross-env NODE_ENV=production webpack",
"dev": "cross-env NODE_ENV=development webpack-dev-server"
1
2

webpack-merge

设置好环境变量之后,还需要将Webpack配置进行拆分,先在根目录下新建一个build目录,新建如下几个文件:

  • webpack.config.common.js:公共配置;
  • webpack.config.dev.js:开发环境配置;
  • webpack.config.prod.js:生产环境配置。

使用webapck-merge插件,对配置进行合并使用。

yarn add webpack-merge -D
1

修改package.json里的配置:

"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.config.prod.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./build.\yanrwebpack.config.dev.js"
1
2

如何区分环境

具体区分方法很简单,在源码中通过如下方式:

if (process.env.NODE_ENV === 'production') {
  console.log('你正在线上环境');
} else {
  console.log('你正在使用开发环境');
}
1
2
3
4
5

其大概原理是通过环境变量的值去判断执行哪个分支。

当代码中出现了使用process模块的语句时,Webpack就会自动打包进process模块的代码以支持非Node.js的运行环境。当代码中没有使用process时就不会打包进 process模块的代码。这个注入的process模块作用是为了模拟Node.js中的 process,以支持上面使用的process.env.NODE_ENV === 'production'语句。

在构建线上环境代码时,需要给当前运行环境设置环境变量NODE_ENV = 'production'Webpack相关配置如下:

const DefinePlugin = require('DefinePlugin');

module.exports = {
  plugins: [
    new DefinePlugin({
      // 定义 NODE_ENV 环境变量为 production
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12

注意:在定义环境变量的值时用JSON.stringify包裹字符串的原因是,环境变量的值需要是一个由双引号包裹的字符串,而JSON.stringify('production')的值正好等于'"production"'

执行构建后,你会在输出的文件中发现如下代码:

if (true) {
  console.log('你正在使用线上环境');
} else {
  console.log('你正在使用开发环境');
}
1
2
3
4
5

Webpack定义的环境变量的值被代入到了源码中,process.env.NODE_ENV === 'production'被直接替换成了true。并且由于此时访问process的语句被替换且不存在了,Webpack也不会将process模块包含到输出文件中了。

DefinePlugin定义的环境变量只对Webpack需要处理的代码有效,而不会影响 Node.js运行时的环境变量的值。

通过Shell脚本的方式定义的环境变量,例如NODE_ENV = productionWebpack 是不认识的,对Webpack需要处理的代码中的环境区分语句是没有作用的。

也就是说,只需要通过DefinePlugin定义环境变量,就能使上面介绍的环境区分语句正常工作,没必要再次通过Shell脚本的方式定义一遍。

如果你想让Webpack使用通过Shell脚本的方式定义的环境变量,可以使用 EnvironmentPlugin,代码如下:

new webpack.EnvironmentPlugin(['NODE_ENV'])
// 以上这句代码实际上等价于:

new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
1
2
3
4
5
6

结合UglifyJS

其实以上输出的代码还可以进一步优化,因为if(true)语句永远只会执行前一个分支中的代码,也就是说最佳的输出应该直接是:

console.log('你正在线上环境');
1

Webpack没有实现去除死代码的功能,但是UglifyJS可以做这个事情,如何使用请阅读 压缩代码中的压缩JavaScript相关内容。

第三方库中的环境区分

除了在自己写的源码中可以有环境区分的代码,很多第三方库也做了环境区分的优化。以React为例,它做了两套环境区分,分别是:

  1. 开发环境:包含类型检查、HTML元素检查等针对开发者的警告日志代码;
  2. 线上环境:去掉了所有针对开发者的代码,只保留让React能正常运行的部分,以优化大小和性能。

例如React源码中有大量类似下面这样的代码:

if (process.env.NODE_ENV !== 'production') {
  warning(false, '%s(...): Can only update a mounted or mounting component.... ')
}
1
2
3

如果你不定义NODE_ENV=production,那么这些警告日志就会被包含到输出的代码中,输出的文件将会非常大。

process.env.NODE_ENV !== 'production'中的NODE_ENV'production' 两个值是社区的约定,通常使用这条判断语句在区分开发环境和线上环境。