第11讲:如何优化Webpack的构建速度和打包结果
我们分别尝试一下通过这两种方式,为开发环境和生产环境创建不同配置。
首先我们来看在配置文件中添加判断的方式。我们回到配置文件中,Webpack 配置文件还支持导出一个函数,然后在函数中返回所需要的配置对象。这个函数可以接收两个参数,第一个是 env,是我们通过 CLI 传递的环境名参数,第二个是 argv,是运行 CLI 过程中的所有参数。具体代码如下:
那我们就可以借助这个特点,为开发环境和生产环境创建不同配置。我先将不同模式下公共的配置定义为一个 config 对象,具体代码如下:
然后通过判断,再为 config 对象添加不同环境下的特殊配置。具体如下:
例如这里,我们判断 env 等于 development(开发模式)的时候,我们将 mode 设置为 development,将 devtool 设置为 cheap-eval-module-source-map;而当 env 等于 production(生产模式)时,我们又将 mode 和 devtool 设置为生产模式下需要的值。
当然,你还可以分别为不同模式设置其他不同的属性、插件,这也都是类似的。
通过这种方式完成配置过后,我们打开命令行终端,这里我们再去执行 webpack 命令时就可以通过 --env 参数去指定具体的环境名称,从而实现在不同环境中使用不同的配置。
那这就是通过在 Webpack 配置文件导出的函数中对环境进行判断,从而实现不同环境对应不同配置。这种方式是 Webpack 建议的方式。
你也可以直接定义环境变量,然后在全局判断环境变量,根据环境变量的不同导出不同配置。这种方式也是类似的,这里我们就不做过多介绍了。
不同环境的配置文件
通过判断环境名参数返回不同配置对象的方式只适用于中小型项目,因为一旦项目变得复杂,我们的配置也会一起变得复杂起来。所以对于大型的项目来说,还是建议使用不同环境对应不同配置文件的方式来实现。
一般在这种方式下,项目中最少会有三个 webpack 的配置文件。其中两个用来分别适配开发环境和生产环境,另外一个则是公共配置。因为开发环境和生产环境的配置并不是完全不同的,所以需要一个公共文件来抽象两者相同的配置。具体配置文件结构如下:
首先我们在项目根目录下新建一个 webpack.common.js,在这个文件中导出不同模式下的公共配置;然后再来创建一个 webpack.dev.js 和一个 webpack.prod.js 分别定义开发和生产环境特殊的配置。
在不同环境的具体配置中我们先导入公共配置对象,然后这里可以使用 Object.assign 方法把公共配置对象复制到具体环境的配置对象中,并且同时去覆盖其中的一些配置。具体如下:
如果你熟悉 Object.assign 方法,就应该知道,这个方法会完全覆盖掉前一个对象中的同名属性。这个特点对于普通值类型属性的覆盖都没有什么问题。但是像配置中的 plugins 这种数组,我们只是希望在原有公共配置的插件基础上添加一些插件,那 Object.assign 就做不到了。
所以我们需要更合适的方法来合并这里的配置与公共的配置。你可以使用 Lodash 提供的 merge 函数来实现,不过社区中提供了更为专业的模块 webpack-merge,它专门用来满足我们这里合并 Webpack 配置的需求。
我们可以先通过 npm 安装一下 webpack-merge 模块。具体命令如下:
npm i webpack-merge --save-dev
# or yarn add webpack-merge --dev
安装完成过后我们回到配置文件中,这里先载入这个模块。那这个模块导出的就是一个 merge 函数,我们使用这个函数来合并这里的配置与公共的配置。具体代码如下:
使用 webpack-merge 过后,我们这里的配置对象就可以跟普通的 webpack 配置一样,需要什么就配置什么,merge 函数内部会自动处理合并的逻辑。
分别配置完成过后,我们再次回到命令行终端,然后尝试运行 webpack 打包。不过因为这里已经没有默认的配置文件了,所以我们需要通过 --config 参数来指定我们所使用的配置文件路径。例如:
webpack --config webpack.prod.js
当然,如果你觉得这样操作让我们的命令变得更复杂了,那你可以把这个构建命令定义到 npm scripts 中,方便使用。
生产模式下的优化插件
在 Webpack 4 中新增的 production 模式下,内部就自动开启了很多通用的优化功能。对于使用者而言,开箱即用是非常方便的,但是对于学习者而言,这种开箱即用会导致我们忽略掉很多需要了解的东西。以至于出现问题无从下手。
如果你想要深入了解 Webpack 的使用,我建议你去单独研究每一个配置背后的作用。这里我们先一起学习 production 模式下几个主要的优化功能,顺便了解一下 Webpack 如何优化打包结果。
Define Plugin
首先是 DefinePlugin,DefinePlugin 是用来为我们代码中注入全局成员的。在 production 模式下,默认通过这个插件往代码中注入了一个 process.env.NODE_ENV。很多第三方模块都是通过这个成员去判断运行环境,从而决定是否执行例如打印日志之类的操作。
这里我们来单独使用一下这个插件。我们回到配置文件中,DefinePlugin 是一个内置的插件,所以我们先导入 webpack 模块,然后再到 plugins 中添加这个插件。这个插件的构造函数接收一个对象参数,对象中的成员都可以被注入到代码中。具体代码如下:
例如我们这里通过 DefinePlugin 定义一个 API_BASE_URL,用来为我们的代码注入 API 服务地址,它的值是一个字符串。
然后我们回到代码中打印这个 API_BASE_URL。具体代码如下:
完成以后我们打开控制台,然后运行 webpack 打包。打包完成过后我们找到打包的结果,然后找到 main.js 对应的模块。具体结果如下:
这里我们发现 DefinePlugin 其实就是把我们配置的字符串内容直接替换到了代码中,而目前这个字符串的内容为 https://api.example.com,字符串中并没有包含引号,所以替换进来语法自然有问题。
正确的做法是传入一个字符串字面量语句。具体实现如下:
这样代码内的 API_BASE_URL 就会被替换为 "https://api.example.com"。具体结果如下:
另外,这里有一个非常常用的小技巧,如果我们需要注入的是一个值,就可以通过 JSON.stringify 的方式来得到表示这个值的字面量。这样就不容易出错了。具体实现如下:
DefinePlugin 的作用虽然简单,但是却非常有用,我们可以用它在代码中注入一些可能变化的值。
Mini CSS Extract Plugin
对于 CSS 文件的打包,一般我们会使用 style-loader 进行处理,这种处理方式最终的打包结果就是 CSS 代码会内嵌到 JS 代码中。
mini-css-extract-plugin 是一个可以将 CSS 代码从打包结果中提取出来的插件,它的使用非常简单,同样也需要先通过 npm 安装一下这个插件。具体命令如下:
npm i mini-css-extract-plugin --save-dev
安装完成过后,我们回到 Webpack 的配置文件。具体配置如下:
我们这里先导入这个插件模块,导入过后我们就可以将这个插件添加到配置对象的 plugins 数组中了。这样 Mini CSS Extract Plugin 在工作时就会自动提取代码中的 CSS 了。
除此以外,Mini CSS Extract Plugin 还需要我们使用 MiniCssExtractPlugin 中提供的 loader 去替换掉 style-loader,以此来捕获到所有的样式。
这样的话,打包过后,样式就会存放在独立的文件中,直接通过 link 标签引入页面。
不过这里需要注意的是,如果你的 CSS 体积不是很大的话,提取到单个文件中,效果可能适得其反,因为单独的文件就需要单独请求一次。个人经验是如果 CSS 超过 200KB 才需要考虑是否提取出来,作为单独的文件。
Optimize CSS Assets Webpack Plugin
使用了 Mini CSS Extract Plugin 过后,样式就被提取到单独的 CSS 文件中了。但是这里同样有一个小问题。
我们回到命令行,这里我们以生产模式运行打包。那按照之前的了解,生产模式下会自动压缩输出的结果,我们可以打开打包生成的 JS 文件。具体结果如下:
然后我们再打开输出的样式文件。具体结果如下:
这里我们发现 JavaScript 文件正常被压缩了,而样式文件并没有被压缩。
这是因为,Webpack 内置的压缩插件仅仅是针对 JS 文件的压缩,其他资源文件的压缩都需要额外的插件。
Webpack 官方推荐了一个 Optimize CSS Assets Webpack Plugin 插件。我们可以使用这个插件来压缩我们的样式文件。
我们回到命令行,先来安装这个插件,具体命令如下:
npm i optimize-css-assets-webpack-plugin --save-dev
安装完成过后,我们回到配置文件中,添加对应的配置。具体代码如下:
这里同样先导入这个插件,导入完成以后我们把这个插件添加到 plugins 数组中。
那此时我们再次回到命令行运行打包。
打包完成过后,我们的样式文件就会以压缩格式输出了。具体结果如下:
不过这里还有个额外的小点,可能你会在这个插件的官方文档中发现,文档中的这个插件并不是配置在 plugins 数组中的,而是添加到了 optimization 对象中的 minimizer 属性中。具体如下:
那这是为什么呢?
其实也很简单,如果我们配置到 plugins 属性中,那么这个插件在任何情况下都会工作。而配置到 minimizer 中,就只会在 minimize 特性开启时才工作。
所以 Webpack 建议像这种压缩插件,应该我们配置到 minimizer 中,便于 minimize 选项的统一控制。
但是这么配置也有个缺点,此时我们再次运行生产模式打包,打包完成后再来看一眼输出的 JS 文件,此时你会发现,原本可以自动压缩的 JS,现在却不能压缩了。具体 JS 的输出结果如下:
那这是因为我们设置了 minimizer,Webpack 认为我们需要使用自定义压缩器插件,那内部的 JS 压缩器就会被覆盖掉。我们必须手动再添加回来。
内置的 JS 压缩插件叫作 terser-webpack-plugin,我们回到命令行手动安装一下这个模块。
npm i terser-webpack-plugin --save-dev
安装完成过后,这里我们再手动添加这个模块到 minimizer 配置当中。具体代码如下:
# 精选评论
# **春:
老师,你的 git 仓库地址是啥。想看看代码
# 讲师回复:
https://github.com/zce/webpack-demo
# **程:
我想问下 loader 是倒序执行的,那 optimization 的 minimizer 也是倒序 执行 吗?
# 讲师回复:
每种资源最终只需要使用一个 minimizer,不回涉及到多个压缩器同时压缩同一个资源的情况,也就不会有先后执行的问题
# **德:
webpack5 可以用 '...' 把默认的 minimizer 展开,方便很多了