章节简介

Webpack 是目前最为流行的前端构建工具。同时在前端工程化中,Webpack在开发/编译/构建中都起到了最关键的作用。所以在当下阶段,webpack的基本配置,是每一个前端程序员应当掌握的基本技能。

2014年2月,Webpack发布了第一个正式版,而如今已经到了Webpack@4。Webpack版本变更似乎是参考了同行的一些功能。Rollup 推出后,Webpack2通过UglifyJs实现了tree-shaking,Webpack3实现了作用域提升(scope-hosting)。而在Parcel 推出后,Webpack4也开始降低配置化。在这之前,用户需要引入大量插件来做构建优化,开发环境与生产环境还需要做不同配置。而Webpack4通过设置模式(mode),可以减少开发与生产环境的插件引入与相应逻辑。

所以鉴于此,本章节立足当下,主要介绍Webpack@4的基本配置。根据开发构建的不同流程环节与构建的不同需求,我将webpack基本配置分为如下几块介绍:

  1. 基本配置

  2. 开发配置

  3. 构建配置

  4. 库开发相关

由于搭建webpack工程,会涉及多个维度的相关配置。但分散的配置介绍又显得凌乱,故而我选择系统的介绍相关配置,方便大家阅读后上手。我会从最基础的开始讲,尽量让一个webpack小白,看完文章以后,能利用webpack搭建基本的前端工程。但是具体详细的配置,我不会穷举,因为无论文章怎么写,必定不如官方文档详实与及时。我不指望一篇文章会成为一本工具书,只希望本文可以告诉入门用户webpack的大致套路与某些场景下的配置情况。建议实际操作需要查阅配置时,还是要查阅此时官网的相应文档。

配置前言

介绍配置之前,我们应该对webpack有个初始认识,不然对配置会缺乏一些基本概念。官网对自身有着明确定义:

webpack 是一个模块打包器。它的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用,但它也能够胜任转换(transform)、打包(bundle)或包裹(package)任何资源(resource or asset)。

在开发web应用时,假如我们没有任何构建工具。我们一开始会编写一个index.html 、index.js、index.less。随着功能的增多,我们的代码开始变的复杂、甚至可能会引用一些开源的库或者框架,这不可避免的要将代码拆分成模块,然后模块之间可能会有依赖。怎么管理不同模块的依赖引入、又如何打包和输出就成了一个大问题。

但不管模块如何多,开发人员总要先编写一个最初始的js文件。webpack就以这个初始js文件为「入口」,根据代码中声明的模块引用来加载其他资源文件,根据webpack配置来对加载的模块进行编译、打包,最终「输出」开发人员期望的打包结果。

(不同于webpack,另一款打包工具Parcel更倾向于用html作为打包的入口文件)。

基本配置

入口[entry]

从上节中,我们明白了「入口」的意义。一个webpack工程,首先至少要有一个入口,配置也很简单,官网有介绍:

  1. module.exports = {
  2. entry: './index.js'
  3. };

当然,因为工程可能是多个HTML页面的,每个页面都希望有各自的入口,那可能就是这样:

  1. module.exports = {
  2. entry: {
  3. pageA: './pageA.js',
  4. pageB: './pageB.js'
  5. }
  6. };

entry也可以为数组,如:

  1. module.exports = {
  2. entry: ['./fileA.js', './fileB.js']
  3. };
  4. // 也可以这样:
  5. module.exports = {
  6. entry: {
  7. main: ['./fileA.js', './fileB.js']
  8. }
  9. };

这样配置后会将多个js文件,最终打包成一个入口。数组型的入口,在工程开发中使用较少。某些插件可能会利用此特性来实现一些功能,如往入口中注入热更新相关代码以实现热更新[HMR]。

出口[output]

我们所定义的「入口」是开发时的程序入口,最终编译构建后,真正被浏览器加载的资源文件则是「出口」文件。「出口」的相关配置定义了输出文件的路径与文件名。最基本的配置为:

  1. module.exports = {
  2. output: {
  3. filename: 'output.js', // 文件名
  4. path: __dirname + '/dist' // 文件输出路径,必须为系统绝对路径
  5. }
  6. };

由于「入口」会存在多个,同样的「出口」文件也会有多个,多出口的定义如下:

  1. module.exports = {
  2. entry: {
  3. pageA: './a.js',
  4. pageB: './b.js'
  5. },
  6. output: {
  7. filename: '[name].js',
  8. path: __dirname + '/dist'
  9. }
  10. };

这样配置后,将会输出 /dist/pageA.js/dist/pageB.js 。占位符 [name]chunkName ,在此处则即是 entry 中多入口配置对象的key值,即pageA 与 pageB。

chunk

在这里又引入了一个 chunk 的概念。chunk 的中文意思是“块”,在这里即是代码块。我们可以将一个webpack打包出的一个js文件认为是一个chunk 。如果我们不做任何的拆包处理,那我们的每一个entry就会输出一个chunk。output的filename的配置中,占位符即代表了chunk的一些属性。如[id]代表chunkId,[chunkhash]代表chunk文件的内容哈希值。当然 filename 也可以配置为函数,函数入参则为包含chunk信息的对象。

path与publicPath

path 配置决定了最终打包后输出资源的文件路径,且它必须要求为系统绝对路径(这不等同于html中最终引入的路径)。如果使用了html插件(这在后续章节会讲到),那webpack会智能的判断生成的html与chunk的相对路径,再以script方式引入。

有时候我们html与静态资源并非分发到同一个地方。如html为服务端渲染输出、亦或者发至专门的html输出服务器以便方便控制版本,而js/css等静态资源则专门分发到cdn。这样就需要html中引入资源时以完整路径引入。这时就需要用使用 publicPath ,如:

  1. const HtmlWebpackPlugin = require('html-webpack-plugin')
  2. module.exports = {
  3. entry: {
  4. pageA: './a.js',
  5. pageB: './b.js'
  6. },
  7. output: {
  8. filename: '[name].js',
  9. path: __dirname + '/dist',
  10. publicPath: 'https://cdn.antfin.com'
  11. },
  12. plugins: [
  13. new HtmlWebpackPlugin()
  14. ]
  15. };

这样,html中引用的js资源从 pageA.js 变为 https://cdn.antfin.com/pageA.js

还有一种常见的情况是“按需加载”。这时js资源会异步的去加载,而不是直接以script资源方式构建在html文件中。这种情况下,webpack无法判断js与html的相对路径(因为这是在js文件中执行脚本引入的,这个js文件也可能被不同的页面引入,不确定此时具体的html文件与其路径)。所以,需要配置 publicPath 固化此 异步chunk 的地址。

其他

output 还有个很重要的配置属性是 library ,这个属性决定了webpack构建出来的包是做什么用途的,能被以何种方式引用、加载执行。但对于日常工程构建来说,可以默认不配置。后续在 「库开发相关」这一章节中,我会再具体介绍。

除此外,output还有非常多的配置,但都比较偏门,同学们可以在输出文件遇到问题时,查询官方文档。

模式[mode]

模式是webpack@4新增的配置属性,目的是为了简化webpack繁琐的配置。上两节中我们介绍了文件的入口与输出。在一个工程化的项目中:生产环境下,我们希望输出的文件是经过压缩、混淆(丑化)的。而开发环境时由于调试需要,我们希望文件是未压缩与混淆的。

在webpack@4以前,我们往往通过执行不同的npm script命令,从而设置不同的process.env.NODE_ENV,进而在webpack配置文件中判断此时环境变量,加载不同的webpack插件,输出不同的配置。

由于大部分工程项目的开发环境与生产环境有着较为统一的需求,因此webpack@4+通过配置不同模式[mode],来默认执行该模式下的一些通用操作。如生产模式时,默认引入代码压缩混淆插件 UglifyJsPlugin 与作用域提升的插件 ModuleConcatenationPlugin

目前已有的模式有:productiondevelopmentnone ,默认为production。设置为 productiondevelopment 时会同时默认设置process.env.NODE_ENV为production或development。 设置 none 时,webpack不做任何附加操作。

模块[module]

我们知道webpack 是一个模块打包器,它不仅仅能处理js文件,还能处理css、图片。而且也能将ES6的代码、甚至是TypeScript的代码引用并打包输出成当下可执行的js文件。而webpack自身不可能穷举处理所有的相关文件。于是就采用了 loader 方式。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 “加载” 模块时预处理文件。

不同的文件类型可能需要匹配不同的loader,做不同的文件转化。而有的模块可能需要处理,有的模块可能需要忽略,这一切相关的配置就在 module 中。

rules

module 的主要配置项为 rules 。这是一个数组配置,rules中的每一项rule即配置了如何去处理一个模块。比如我们希望将ES6代码转成ES5代码,这需要引用 babel-loader 。它的rule配置即为:

  1. module.exports = {
  2. //...
  3. module: {
  4. rules: [
  5. {
  6. test: /\.(js)$/,
  7. use: 'babel-loader'
  8. }
  9. ]
  10. }
  11. };

其中 test 可以为一个正则,其匹配的对象是 引用模块的绝对路径,我们通过配置上述配置将所有引入的js文件都通过babel-loader来转化。

作为loader,可能需要一些额外的配置,比如 babel-loader 可能需要做一些编译配置,来设置最终转化后的结果。这需要 use 属性值为对象,不可简写为字符串,如:

  1. {
  2. test: /\.(js)$/,
  3. use: {
  4. loader: 'babel-loader',
  5. options: {
  6. presets: ['@babel/preset-env'] // 根据目标浏览器自动转换为相应es5代码
  7. }
  8. }
  9. }
  10. // ps: babel的配置我们更多是以.babelrc配置文件的方式存在项目根目录

一般来说,node_modules中我们加载的外部库文件已经被babel编译成es5代码,因此是不需要再进行一次babel编译的。为了节省开发、构建性能,我们会通过配置 excludeinclude 来过滤或者指定需要执行loader的文件目录。如:

  1. {
  2. test: /\.(js)$/,
  3. use: 'babel-loader',
  4. exclude: path.resolve(process.cwd(), './node_modules'), // 过滤node_modules目录
  5. include: path.resolve(process.cwd(), './src') // 只匹配src目录
  6. }

有时候,我们一个模块文件需要转化多次,需要多个loader。比如一个css文件,先通过 css-loader 解释css文件内的 @importurl() , 最后通过 style-loader 将css以