本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
相信不少小伙伴都有听说过这样一个名词 - Webpack
配置工程师。😂,都需要设置专门的岗位去维护了,可见 Webpack
的配置是有多么复杂。
现实也确实如此。发展到现在 5.x
版本, Webpack
已经有 18+
大类的配置项,再加上大类中各种小的配置项,那就更多了。复杂的配置项,给新人入门 Webpack
带来了极大的门槛。光弄懂 Webpack
怎么用、熟练使用各个配置项就需要花费很长的功夫,更别提要了解其内部工作机制了。而且要吐槽一下,官方文档写的是真不咋地,好多配置项的用法都没说清楚。
小编一开始接触 Webpack
的时候,也是一脸懵逼,看着那么多的配置项,不知所措。但为了能在项目中更好的使用 Webpack
,只能硬着头皮去看官网文档,一个一个的去试各种配置项。如果看官网文档还不理解,就去尝试看源码中这个配置项是怎么工作的,来理解它在实际工作中该怎么用。
在学习和日常使用过程中,小编发现 Webpack
的各个配置项,理解起来也有一定的脉络可循的。本文,小编会根据自己的一些理解和社区存在的大量资料,对 Webpack
配置项的使用做一番梳理,希望能给到大家一些帮助。
本文的目录结构如下:
Webpack 的各个配置项
Webpack
官网里面罗列的配置项有 entry
、resolve
、module
、plugins
、output
、mode
、cache
、devServer
、devtools
、optimization
、watch
、externals
、performance
、node
、stats
等。
小编以为,要想更好的理解这些配置项,首先要对 Webpack
的工作机制有一个整体的认识。
在这里,先推荐两篇文章: 重新认识 Webpack: 旧时代的破局者 和 如何理解 Webpack 配置底层结构逻辑。这是范文杰大佬的掘金小册 - Webpack5 核心原理与应用实践 的前两个章节(给大佬点赞)。本来小编是想自己先给大家梳理一下 Webpack
的背景和工作机制的,但是发现大佬总结的更棒、更全面,就直接借花献佛了。还没有关注的小伙伴,可以先去看看,如果对自己胃口,可以尝试购买噢。
这里总结一下,Webpack
的整个工作过程可以归纳为: 以 entry
指定的入口文件为起点,分析源文件之间的依赖关系,构建一个模块依赖图,然后将这个模块依赖图拆分为多个 bundles
,并输出到 output
指定的位置。
上面罗列的这些配置项的使用,贯穿了 Webpack
的整个打包构建过程。
考虑到这么多的配置项,如果全放在本文里面,篇幅会很长,对读者也不是很友好,小编决定分成上下两篇来梳理。上篇,梳理 entry
、resolve
、module
、optimization
、output
;下篇,梳理 plugins
、cache
、externals
、devtool
、devServer
等。
entry
entry
,告诉 Webpack
配置模块依赖图的起点。
entry
值的类型有三种,string
、string
数组、对象。不同的类型,构建出来的模块依赖图各不相同:
-
string
, 单页面单入口文件打包,会构建一个模块依赖图,入口只有一个; -
string[]
, 单页面多入口文件打包,会构建一个模块依赖图,入口只有一个; -
object
,多页面(多入口/单入口文件)打包,会构建多个(或者 1 个)模块依赖图,入口有多个;
如果大家对这段描述不太能理解,我们可以通过一个简单的 demo
给大家演示一下。
// util.1.js
export const func1 = () => console.log('func1');
// util.2.js
export const func2 = () => console.log('func2');
// index.1.js
import { func1 } from './util.1.js';
func1();
// index.2.js
import { func1 } from './util.1.js';
import { func2 } from './util.2.js';
func1();
func2();
单页面单入口打包的 entry 配置:
entry: path.resolve(__dirname, '../src/index.1'),
对应的模块依赖图入口只有一个:
单页面多入口打包的 entry 配置:
entry: [path.resolve(__dirname, '../src/index.1'), path.resolve(__dirname, '../src/index.2')],
对应的模块依赖图入口只有一个:
多页面打包的 entry 配置:
entry: {
'main.1': path.resolve(__dirname, '../src/index.1'),
'main.2': path.resolve(__dirname, '../src/index.2'),
},
对应的模块依赖图入口有多个:
resolve
resolve
,配置源文件、第三方依赖包如何被解析,得到文件的绝对路径。
在日常开发中,用的比较多的配置项为 resolve.alias
别名配置和 resolve.extensions
扩展配置。
源文件的路径解析是构建模块依赖图的关键一环。拿不到源文件的绝对路径,就无法读取源文件、对源文件做 transform
、解析源文件、分析依赖关系等。resolve
失败了,打包构建就立即会终止。
默认情况下,Webpack
会自动解析相对路径和第三方依赖路径。但如果我们在项目中使用了别名,Webpack
就需要借助 resolve.alias
配置项来解析路径了。另外,如果源文件 url
没有携带后缀,webpack
默认会使用 .wasm
、.mjs
、.js
、.json
, 如果这些默认的后缀都不匹配,如 .tsx
,那么就无法读取源文件,导致打包构建报错。这时候就需要我们在 resolve.extensions
添加 '.tsx'
了,明确告诉 Webpack
使用 .tsx
后缀去读取源文件内容 。
module
module
,配置各个类型的源文件对应的 loader
。
在构建模块依赖图时,loader
会将源文件中各种类型的源文件,如 tsx
、ts
、jsx
、vue
、less
、sass
等,转换成浏览器可以支持的 js
、css
等类型。
通常我们会在 module.rules
配置一系列规则,指定每种类型的文件,使用什么样的 loader
。如 .tsx
类型的文件使用 ts-loader
,.scss
类型的文件使用 sass-loader
、css-loader
、style-loader
等。
loader
本质上是一个函数,它的入参是代码字符串,返回的结果也是一个代码字符串。如果一个源文件需要多个 loader
来处理,那么 loader
的使用是有先后顺序的。如处理 scss
类型的文件,使用 loader
的先后顺序为 sass-loader
、css-loader
、style-loader
,上一次 loader
的处理结果会做为下一个 loader
的入参。
Webpack
完成源文件绝对路径的解析以后,会根据解析出来的绝对路径去读取源文件的内容,然后作为入参传递给 loader
函数。loader
处理完以后会返回新的代码字符串,传递给下一个 loader
函数或者 parser
解析器。
loader
转换,是 Webpack
打包构建时间久的原因之一。
当 parser
收到 loader
转换以后的内容以后,会将内容转换为一个 AST
对象,分析并收集源文件的依赖,然后对收集到的依赖继续做路径解析、内容读取、内容转换、依赖解析。这一套流程会一直持续到项目涉及的所有源文件都解析完成,构建出一个模块依赖图。
构建好的模块依赖图,各个模块之间是静态依赖还是动态依赖、各个模块的 export
是否有被使用都一清二楚。
optimization
模块依赖图构建完成以后,接下来要做的就是将模块依赖图拆分为多个 bundle
。
这一过程,可以分成 4
个步骤:
- 对模块依赖图做预处理 -
tree shaking
; - 初次分离,将模块依赖图分离为
initial chunk
和async chunks
; - 二次分离,分离
common chunks
、runtime chunk
、custome chunks
; - 构建
bundles
;
optimization
提供了很多配置项来指导 Webpack
做更好的完成上面四个步骤。
tree shaking
在正式拆分模块依赖图前,Webpack
会先对模块依赖图做预处理。预处理主要做一件事情 - tree shaking
, 将模块依赖图中没有用到的模块、模块中没有用到的 deadcode
移除掉。不过有一个前提哈,模块源代码要符合 ESM
规范。
做 tree shaking
,要用到 optimization
中的 minimize
(是否压缩 bundle 代码)、usedExports
(是否标记模块中被使用的 exports)、sideEffects
(是否可以将未使用的模块移除)这 3
个配置项。
Webpack
的 tree shaking
有两种 level
: module level
- 将未使用的 module
移除和 statement level
- 将 module
中的 deadcode
移除。
在构建模块图的过程中,各个模块内部的 export
是否有被其他模块已经可以确定。如果一个模块内某个 export
没有被使用,那么这个 export
对应的 deadcode
是可以被移除的。如果一个模块的所有 export
都没有被使用,那么整个模块是可以被移除的。
好多小伙伴知道开启 tree shaking
,需要把 minimize
、usedExports
、sideEffects
这三个属性一股脑设置为 true
,但可能并不知道这里面还有一点小细节。
tree shaking
也是有一套配置规则的:
-
单单将
optimization.sideEffects
设置为true
, 只会开启module level
的tree shaking
,并不会开启statement level
的tree shaking
; -
开启
statement level
的tree shaking
,需要将optimization.usedExports
和optimization.minimize
都设置为true
,缺一不可; -
开启
statement level
的tree shaking
,和optimization.sideEffects
没有关系,即使optimization.sideEffects
为false
;
Webpack
在实现 statement level
的 tree shaking
时,会通过 usedExports
标记被使用的 exports
,然后在最后压缩打包代码时,再一次借助 AST
分析代码,将未被标记的 export
,也就是 deadcode
移除掉。也就是说,如果只配置了 usedExports
为 true
,没有配置 minimize
为 true
,那么 statement level
的 tree shaking
是不会生效的。
初次分离 - initial chunk 和 async chunks
做完 tree shaking
以后,接下来就是将拆分模块依赖图。
Webpack
默认会将一个模块依赖图,根据模块之间的静态依赖和动态依赖,拆分成 initial chunk
和 async chunks
。
entry
所在的 chunk
,称为 initial chunk
。从 entry
开始,沿着静态依赖能遍历到的所有模块,都会分配到 initial chunk
中。
需要动态加载的模块,遇到就会单独的为它构建一个新的 async chunk
。以动态加载模块开始,沿着静态依赖能遍历到所有未分配的模块,都会分配到对应的 async chunk
中。
由于这一步是 Webpack
的默认操作,optimization
没有提供什么配置项。
二次分离 - common chunks、runtime chunk、custome chunks
分离好 initial chunk
和 async chunks
以后,Webpack
还提供了自定义分包策略,让开发人员根据实际需要进行分包, 对应的是 optimization.splitChunks
配置项。
首先先来看看这三种类型的 chunk
是怎样定义的。
common chunks
,通用模块组成的 chunk
。在拆分模块依赖图的过程中,如果一个模块被多个 chunk
使用,那么这个模块就会被单独的分离为一个 common chunk
。这一项对应的配置项是 optimization.splitChunks.minChunks
。miniChunks
,指定模块被共享的次数,如果一个模块被共享的次数 >= miniChunks
, 那么该模块就会被分离成一个单独的 common chunk
。 miniChunks
默认值为 1
, 共享一次,意味着只要一个模块同时存在于两个 chunk
中,那么这个模块就会被分离。这个配置项实际用于多页面打包,单页面打包没有任何用处。因为单页面打包,一个模块永远只属于一个 chunk
,只有多页面打包才会出现一个模块被多个 chunk
公用的情况。
runtime chunk
,运行时 chunk
, 里面包含 Webpack
的自定义模块加载机制,对应的配置项为 optimization.runtimeChunk
, 默认为 false
。默认情况下,Webpack
的自定义模块加载机制是包含在 initial chunk
中的。如果配置 optimization.runtimeChunk
为 ture
,那么这一套逻辑就会被单独分离为一个 runtime chunk
。
custome chunks
,用户自定义 chunks
, 即开发人员自己配置分离的 chunks
,对应的配置项为 optimization.splitchunks.cacheGroups
。通过这个配置项,开发人员可以自定义分包策略,如把第三方依赖分离为 vendors chunk
。
在分离 common chunks
和 customer chunks
时,Webpack 规定了一些限制条件。如果不满足这些限制条件,分包会失败。
这些限制条件如下:
-
chunks
, 选定二次分离的范围,默认值为async
, 即对async chunks
做二次分离。也可配置为initial
(对initial chunk
做二次分离)和all
(对async chunks
和initial chunk
做二次分离)。一般配置为all
。 -
maxAsyncRequests
, 异步加载时允许的最大并行请求数, 可以理解为如果maxAsyncRequests
为n
,那么可以最多从async chunks
里二次分离出n - 1
个common/customer chunks
。如果超出这个限制,分包失败。 -
maxInitialRequests
, 入口点处的最大并行请求数, 可以理解为如果maxInitialRequests
为n
,runtime chunk
为m
(optimization.runtime
为ture
,m = 1
; 为false
,m
=0
),从initial chunk
分离出的async chunks
数量为k
,那么最多可以initial chunk
中分离出n - m - k - 1
个common/customer chunks
。如果超出这个限制,分包失败。 -
minSize
, 分出来的common/custome chunk
的最小体积。如果小于minSize
,分包失败。 -
minSizeReduction
, 对initial chunk
和async chunks
做二次分离时,如果initial chunk
和async chunks
减小的体积小于minSizeReduction
,分包失败。
很多时候,我们配置了 optimization.splitchunks.cacheGroups
,却没有分离出相应的包,这个时候就要看看是不是受到了上面的限制条件的影响。
大多情况下, Webpack
会默认帮我们把一个应用分离为 initial chunk
、async chunks
、vendors chunk
,如果还需要更定制化的分包,可以自行调整上面提到的配置项。
构建 bundle
分离好 chunks
以后,接下来 Webpack
会根据 chunks
构建输出的 bundle
。 构建时,webpack
会先为每个 chunk
包含的模块构建内容,然后根据模块的内容,生成 chunk
的内容。
在这个过程中,optimization
中有两个配置项很重要 - moduleIds
和 chunkIds
。这两个配置项,决定了最后的 bundle
文件名。
通常我们会在 output.filename
配置项中设置 bundle
文件的文件名,常用配置为 [name].[chunkhash:8].js
,而 moduleIds
和 chunksIds
会影响 name
和 chunkhash
值。
chunkIds
,指定每一个 chunk id
的命名方式,直接回影响 name
的值。举个 🌰,如果 chunkIds
为 'named'
,那么 async chunk
的 name
就是源文件的文件名;如果 chunksIds
为 'natural'
, 那么 async chunk
的 name
是一个数字。
moduleIds
,指定一个 module id
的命名方式,规则和 chunksId
一样,会直接影响 chunkhash
的值。 chunkhash
是由 chunk
中包含的模块的 id
、模块的内容、chunk id
生成的一个 md5
,变更 module id
的命名规则,就会改变 chunkhash
的值。
构建好内容、确定好文件名以后,如果配置了 optimization.minimize
为 ture
, Webpack
会对构建内容做压缩处理。做完这一步,Webpack
就会把内容输出到 output
配置项指定的位置。
output
output
, 用于配置 webpack
如何输出 bundle
内容, 包括确定 bundle
文件名、指定输出位置、如何对外暴露变量等,是 Webpack
打包构建的最后一环。
output.path
, 指定 bundle
的输出目录,即打包后文件在硬盘中的存储位置, 是一个绝对路径。
output.filename
, 指定 initial
类型的 bundle
的文件名,常用配置 [name].[chunkhash:8].js
。
output.chunkFilename
, 指定非 initial
类型的 bundle
的文件名,常用配置为 [name].[chunkhash:8].chunk.js
。
之所以要在文件命名中添加 chunkhash
,是因为这样可以有效利用浏览器的缓存策略,达到优化的目的。bundle
的 chunkhash
是根据 moduleId
、module
的源文件内容、chunkId
生成的。这三项,只要有一项发生变化,chunkhash
就会发生变化。
通常,我们会将项目中用到的 js
、css
、img
等静态资源设置为持久缓存。如果静态资源的文件名没有变化,那么静态资源的请求链接就不会变化,就可以一直使用浏览器本地缓存。当源文件发生变化后,对应的打包文件的 chunkhash
会发生变化,导致文件名发生变化。当重新请求静态资源时,如果请求 url
发生变化,本地缓存失效,会从 server
端请求新的静态资源;如果请求 url 没有变化,直接使用本地缓存。
output.library
, 常用与组件库开发,将入口文件的返回值赋值给 library
指定的变量。
output.libraryTarget
, 常和 output.library
一起使用,配置如何暴露 library
。
output.publicPath
, 对页面里面引入的资源的路径做对应的补全。
在这里,我们只列举这几个常用的配置项,其他配置项用的比较少,就不一一介绍了。
结束语
到这里,上篇就结束了。在本文,我们完成了对 entry
、resolve
、module
、optimization
、output
这 5
个配置项的梳理。这 5
个配置项,基本上对应了 Webpack
构建打包过程的各个关键阶段。只要配置好这 5
个配置项,就可以用 Webpack
顺利完成打包构建工作。
在下篇中,小编将会继续梳理剩余的配置项,其中 plugins
是重中之重,敬请期待哦。