章节简介
Webpack 是目前最为流行的前端构建工具。同时在前端工程化中,Webpack在开发/编译/构建中都起到了最关键的作用。所以在当下阶段,webpack的基本配置,是每一个前端程序员应当掌握的基本技能。
2014年2月,Webpack发布了第一个正式版,而如今已经到了Webpack@4。Webpack版本变更似乎是参考了同行的一些功能。Rollup 推出后,Webpack2通过UglifyJs实现了tree-shaking,Webpack3实现了作用域提升(scope-hosting)。而在Parcel 推出后,Webpack4也开始降低配置化。在这之前,用户需要引入大量插件来做构建优化,开发环境与生产环境还需要做不同配置。而Webpack4通过设置模式(mode),可以减少开发与生产环境的插件引入与相应逻辑。
所以鉴于此,本章节立足当下,主要介绍Webpack@4的基本配置。根据开发构建的不同流程环节与构建的不同需求,我将webpack基本配置分为如下几块介绍:
- 基本配置
- 开发配置
- 构建配置
- 库开发相关
由于搭建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工程,首先至少要有一个入口,配置也很简单,官网有介绍:
module.exports = {
entry: './index.js'
};
当然,因为工程可能是多个HTML页面的,每个页面都希望有各自的入口,那可能就是这样:
module.exports = {
entry: {
pageA: './pageA.js',
pageB: './pageB.js'
}
};
entry也可以为数组,如:
module.exports = {
entry: ['./fileA.js', './fileB.js']
};
// 也可以这样:
module.exports = {
entry: {
main: ['./fileA.js', './fileB.js']
}
};
这样配置后会将多个js文件,最终打包成一个入口。数组型的入口,在工程开发中使用较少。某些插件可能会利用此特性来实现一些功能,如往入口中注入热更新相关代码以实现热更新[HMR]。
出口[output]
我们所定义的「入口」是开发时的程序入口,最终编译构建后,真正被浏览器加载的资源文件则是「出口」文件。「出口」的相关配置定义了输出文件的路径与文件名。最基本的配置为:
module.exports = {
output: {
filename: 'output.js', // 文件名
path: __dirname + '/dist' // 文件输出路径,必须为系统绝对路径
}
};
由于「入口」会存在多个,同样的「出口」文件也会有多个,多出口的定义如下:
module.exports = {
entry: {
pageA: './a.js',
pageB: './b.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
};
这样配置后,将会输出 /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
,如:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
pageA: './a.js',
pageB: './b.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist',
publicPath: 'https://cdn.antfin.com'
},
plugins: [
new HtmlWebpackPlugin()
]
};
这样,html中引用的js资源从 pageA.js
变为 [https://cdn.antfin.com/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
。
目前已有的模式有:production
、development
、none
,默认为production
。设置为 production
与 development
时会同时默认设置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配置即为:
module.exports = {
//...
module: {
rules: [
{
test: /\.(js)$/,
use: 'babel-loader'
}
]
}
};
其中 test
可以为一个正则,其匹配的对象是 引用模块的绝对路径,我们通过配置上述配置将所有引入的js文件都通过babel-loader来转化。
作为loader,可能需要一些额外的配置,比如 babel-loader
可能需要做一些编译配置,来设置最终转化后的结果。这需要 use
属性值为对象,不可简写为字符串,如:
{
test: /\.(js)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] // 根据目标浏览器自动转换为相应es5代码
}
}
}
// ps: babel的配置我们更多是以.babelrc配置文件的方式存在项目根目录
一般来说,node_modules中我们加载的外部库文件已经被babel编译成es5代码,因此是不需要再进行一次babel编译的。为了节省开发、构建性能,我们会通过配置 exclude
或 include
来过滤或者指定需要执行loader的文件目录。如:
{
test: /\.(js)$/,
use: 'babel-loader',
exclude: path.resolve(process.cwd(), './node_modules'), // 过滤node_modules目录
include: path.resolve(process.cwd(), './src') // 只匹配src目录
}
有时候,我们一个模块文件需要转化多次,需要多个loader。比如一个css文件,先通过 css-loader
解释css文件内的 @import
和 url()
, 最后通过 style-loader
将css以