本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
在上一篇文章 如何快速成为一名熟练的 Webpack 配置工程师 - 上篇 中 ,小编完成了对 entry、resolve、module、optimization、output 这 5 个配置项的梳理。这 5 个配置项,基本上对应了 Webpack 构建打包过程的各个关键阶段。只要配置好这 5 个配置项,就可以用 Webpack 顺利完成打包构建工作。
本文,小编会继续对剩下的配置项进行梳理,其中会重点介绍 plugins。
本文的目录结构如下:
Webpack 配置项详解
本章节,小编会继续给大家梳理剩下的几个配置项: plugins、cache、external、mode、devtool、devserver 等。
这些配置项都属于功能性配置,可以帮助我们更好的使用 Webpack 完成打包构建工作。
plugins
可以这么说,配置 entry、resolve、module、optimization、output,只是可以让我们用 Webpack 顺利完成打包构建工作。整个过程对我们来说是一个黑盒。如果我们想介入打包过程,做一些自定义操作,那么就要用到 plugins 这个配置项了。
plugins,给我们提供了介入 Webpack 打包构建的机会。
Webpack 在整个打包构建过程,一共提供了 130+ 的 hooks。这些 hooks 可以分为 Compiler、Compilation、ContextModuleFactory、JavascriptParser、NormalModuleFactory 五大类。这五大类 hooks 基本上涵盖打包构建过程的各个生命周期。通过这些 hooks,我们可以在期望的某个阶段做一些自定义操作。
在这里,小编要对这五类的 hooks 稍微介绍一下,让大家对这几类 hooks 有个认识。
Webpack 在做实际打包构建时,内部会先创建一个编译器 compiler 实例。compiler 在 Webpack 打包过程中负责做配置项初始化、打包构建准备、将 bundles 输出到 output 指定位置等工作。对应的, Webpack 提供了 Compiler 类型的 hooks,如 initialize、beforeRun、run、beforeCompile、compile、shouldEmit、emit 等,通过这些 hooks,我们可以介入 Webpack 初始化配置项、输出 bundles 等阶段。
真正负责构建模块依赖图、分离模块依赖图、构建 bundle 内容工作的是 compiler 构建的一个实例对象:编译过程 - compilation。我们可以通过开发模式来更好的理解这两个实例的区别。开发模式下,每次修改源文件,Webpack 都会重新做打包构建。在这整个过程中,Webpack 只会构建一个 compiler 实例对象,做一次配置项初始化工作,每次打包构建时,都会创建一个新的 compilation 实例对象来做模块依赖图构建、分离、bundle 内容构建。 对应的,Webpack 提供了 Compilation 类型的 hooks,让我们来介入模块依赖图的构建、分离等阶段,如 buildModule、finishModules、seal、optimize、recordHash 等。
compilation 在做模块依赖图构建的时候,会根据源文件创建一个 module 对象,并借助 AST 来解析 module 的依赖关系。对应的,Webpack 也提供了 ContextModuleFactory / NormalModuleFactory 和 JavascriptParser 类型的 hooks,让我们来介入 module 构建和依赖解析的过程。
plugin 的工作原理非常简单,可以直接用订阅/发布设计模式来理解。通过 Webpack 提供的 tap、tapAsync、tapPromise 这几个 api,我们可以给需要的 hook 注册 callback,然后等 Webpack 运行到我们选择的阶段时,就会触发 callback。
举个 🌰:
class CustomePlugin {
constructor(option) {
...
}
apply(compiler) {
compiler.hooks.initialize.tap('CustomePlugin', (compiler) => {
...
});
}
}
在这个 🌰 中,我们订阅了 initiallize hook。当 compiler 对象构建并完成初始化以后,就会触发 initiallize hook 注册的 callback。
定义一个自己需要的 plugin,还是蛮简单的。只要像上面 🌰 一样,定义一个 plugin class,在 class 中定义一个 apply 方法,然后在 apply 方法中订阅想要的 hook 就可以了。
不过这里面有一些门道是我们需要注意的:
-
首先,
Webpack提供的hook分为Compiler、Compilation、ContextModuleFactory、JavascriptParser、NormalModuleFactory五大类。不同类型的hooks,可订阅的时机不同。Compiler类型的hooks,需要在compiler对象创建完成以后才可订阅。compiler创建好以后,Webpack会依次执行plugin配置项中各个插件实例的apply方法,订阅Compiler类型的hooks。Compilation类型的hook,需要在compilation对象构建完成以后才可以订阅。要订阅Compilation类型的hooks,我们需要先订阅compiler的compilation hook, 等compilation创建以后,会触发compiler的compilation hook的callback,compilation对象会做为callback的入参,在callback中我们就可以订阅Compilation类型的hooks。同理,
ContextModuleFactory / NormalModuleFactory类型的hook,需要在contextModuleFactory / normalModuleFactory对象构建完成以后才可以订阅。要订阅ContextModuleFactory / NormalModuleFactory类型的hook, 我们需要先订阅compiler的contextModuleFactory / normalModuleFactory hook, 等contextModuleFactory / NormalModuleFactory对象创建以后,会触发compiler的contextModuleFactory / NormalModuleFactory hook,contextModuleFactory / normalModuleFactory对象会做为callback的入参,在callback中我们就可以订阅ContextModuleFactory / NormalModuleFactory类型的hooks。JavascriptParser类型的hooks,需要parser对象构建完成以后才可以订阅。要订阅该类型的hooks,我们需要先订阅compiler的normalModuleFactory的hook, 在normalModuleFactory hook的callback中,订阅normalModuleFactory对象的parser hook,在parser hook的callback中,才可以订阅JavascriptParser类型的hooks。说实话,这一块的逻辑还是蛮复杂的,大家在实际项目中自己写
plugin时,一定要找准hook的订阅时机。 -
其次,
Webpack提供的hook可以分为sync hook和async hook中两大类。这两大类hook,又可具体细分为SyncHook、SyncBailHook、SyncWaterfallHook,SyncLoopHook,AsyncParallelHook,AsyncParallelBailHook,AsyncSeriesHook,AsyncSeriesBailHook,AsyncSeriesWaterfallHook这 9 小类。不同类型的hook,订阅方式也不相同。要区分一个
hook是sync还是async,关键要看这个hook的callback的内部是不是可以出现异步代码,如xhr、setTimeout、Promise等。如果可以出现异步代码,那就是async hook,否则就是sync hook。在解释为什么
Webpack要提供sync和async两种类型的hook之前,我们要先了解一点前置知识。Webpack在打包构建过程中,如果完成了某个阶段,就会依次执行该阶段hook对应的callback。callback执行的顺序,和订阅时的顺序保持一致,即哪个plugin先订阅,对应的callback先执行。等所有的callback处理完毕,才会进入下一个阶段。如果
callback内部全部是同步代码,刚刚提到的完全没有问题,Webpack会依次处理完所有callback,然后顺利进入下一个阶段。这种情况下,我们可以直接使用sync hook,通过tap这种方式订阅。举个 🌰:
// initialize 是 sync hook, 直接使用 tap 订阅 compiler.hooks.initialize.tap('CustomePlugin', (compiler) => { ... });如果
callback中有异步代码,如果不做特殊处理,那么callback就有可能不会正确处理,甚至会出现Webpack构建过程进入下一个阶段的情况。这时,我们就要使用async hook来处理这种情况了。常见的异步代码,可以分为
promise类型和非promise类型,对应的async hook也提供了tapPromise和tapAsync这两种方式订阅。再举个 🌰:
compiler.hooks.run.tapAsync('CustomePlugin', (compiler, callback) => { setTimeout(() => { ... callback(); }); }); compiler.hooks.run.tapPromise('CustomePlugin', (compiler) => { return Promise.resolve(1).then((res) => { ... }); });要注意哦,通过
tapAsync订阅async hook时,回调函数的最后一个入参,必须时callback,而且callback必须在异步代码执行完毕以后调用; 使用tapPromise时,必须要返回一个已经注册onFullfilled的promise对象。只有这样才能保证回调函数按序执行,Webpack可以顺利进入下一个阶段。了解完
sync和async这两大类hook之后,我们再来了解一下细分的9小类hook。这
9种类型,是基于订阅同一种hook的callback的不同处理方式来划分的,具体如下:series, 顺序,所有callback按订阅hook的顺序按序执行。sync对应的是SyncHook,async对应的是AsyncSeriesHook。如果是AsyncSeriesHook,会在上一个callback的异步代码执行完毕以后,才会处理下一个callback。bail, 熔断,如果某一个callback有返回非undefined的值,那么后面的所有callback都不处理。sync对应SyncBailHook,async对应AsyncSeriesBailHook、AsyncParallelBailHook。waterfall, 瀑布,上一个callback的返回值会作为下一个callback的入参。sync对应SyncWaterfallHook,async对应AsyncSeriesWaterfallHook。parallel, 并行,只有在async hook中使用,AsyncParallelHook。callback可并行执行,即不用等上一个callback的异步代码执行,就可以开始处理下一个callback。noop,逐次循环处理callback,直到所有的callback返回undefined,只有在sync hook中使用,SyncLoopHook。
有了这两点说明,相信大家对如何写一个合适的自定义 plugin,有初步的认识了吧。
cache
cache,配置 Webpack 将打包构建过程中生成的 module、chunk 缓存起来, 供二次构建使用。
使用 cache,可以有效提升二次构建的速度。
externals
externals,配置 Webpack 选择不参与打包构建的资源,可有效提升打包构建速度和减小 bundle 体积。
通常,如果应用程序中引入了第三方依赖,webpack 会自动把第三方依赖也打包到 bundle 中。在运行 bundle 代码时,会先运行第三方依赖代码,拿到第三方依赖的 exports,然后使进行下一步操作。
如果使用了 externals 配置项指定不参与编译打包的第三方依赖,那我们在运行打包以后的 bundle 代码时,由于 bundle 并没有第三方依赖代码,直接使用第三方依赖的 export 是会报错的。此时我们必须先加载好第三方依赖代码。
使用 externals 配置项时, 会受到 output.libraryTarget 配置项的影响。
举个 🌰,如果 output.libraryTarget 的值为 'var', 应用程序会通过一个变量来获取第三方依赖的输出结果,此时当前上下文环境 - window 中必须存在已定义变量。
对应的打包代码如下:
// main.js
(function(modules){
...
return __webpack_require__(__webpack_require__.s = "./src/main.js");
}({
'main': {
...
var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("react")
...
},
'react': (function(module, exports) {
module.exports = React; // React 变量在使用 main.js 前,必须已经存在 window 中
})
}))
mode
mode, 配置 Webpack 的工作模式。
Webpack 的工作模式有两种:development 和 production。各种模式都有各自的默认配置项。
比如:
-
devTool,production模式下默认为false,不生成.map文件;development模式下为eval,使用eval包裹代码块。 -
optimization.minimize,production模式下默认为true,bundle文件会被压缩;development模式下默认为false,不压缩bundle文件。 -
optimization.moduleIds,production模式下默认为natural,module id为数字;development模式下默认为named,module id为源文件url。 -
optimization.chunkIds,production模式下默认为natural,chunk id为数字;development模式下默认为named,chunk id中会包含module的文件名。 -
...
mode 是 webpack4 新出现的配置项,减少了开发人员人员的心智负担。
devtool
devtool, 配置 Webpack 在是否在打包过程中生成 .map 文件和生成什么样的 .map 文件。.map 文件不仅可以在本地开发时帮助我们调试源文件代码,还可以在线上环境出现问题时帮助我们快速定位问题出现在源文件的哪个位置,非常有用。
devtool 的配置项,多达 27 种,看着难以记忆,但找到了窍门以后就非常简单了。
devtool 的各个配置项,其实是由 source-map、cheat、module、inline、hidden、eval、nosource 这 7 个关键字组合生成的。
这几个关键字各自的含义如下:
-
source-map, 只有devtool中包含source-map关键字,才会生成.map文件; -
cheap, 需要配合source-map一起使用,.map文件中只包含行映射关系,没有列映射关系,常用于减小.map文件的体积; -
module, 需要配合source-map、cheap一起使用,可以将bundle代码映射到源文件代码,即 loader 处理前的代码; -
inline, 需要配合source-map一起使用,不单独生成.map文件,.map文件作为DataUrl嵌入bundle中; -
hidden, 需要配合source-map一起使用, 会生成.map文件,但不会在bundle文件中添加.map文件的引用注释; -
nosources, 需要配合source-map一起使用, 会生成.map文件,但是sourcesContent的内容为空,可以帮忙定位到代码对应的原始位置,但无法映射到源代码; -
eval,使用eval()包裹模块代码,配合source-map一起使用时,source-map文件会内联到bundle中;
这些关键字的组合规则如下:
[inline-|hidden-|eval-][nosource-][cheap-[module-]]source-map
通过上面的组合规则,我们可以将上面 7 个关键字根据实际需要自由组合成需要的配置项。
如:
-
cheap-module-source-map,生成只有行映射、没有列映射的.map文件,调试时bundle代码可以映射到源文件; -
source-map, 生成包含行映射、列映射的.map文件,调试时bundle代码会映射到转换之前的代码; -
cheap-source-map, 生成只有行映射、没有列映射的.map文件,调试时bundle代码会映射到转换之前的代码; -
eval-nosources-cheap-source-map, 以eval包裹模块代码 ,且.map映射文件中不带源码,也不带列映射; -
...
开发模式下,常用的配置项为 cheap-module-source-map、 cheap-module-eval-source-map。
生产模式下,devtool 默认是为 false,不生成 .map 文件。但是我们通常会接入类似 Sentry 的异常监控,需要我们将 .map 文件上传到 Sentry 方便我们定位问题,这就要求 devtool 需要配置为 source-map。这样做又会带来一个新的问题,就是源代码会暴露给外部用户。
针对这个问题,我们可以分 4 步来处理他,先完成打包构建,然后上传 .map 到 Sentry,然后再将 .map 文件移除,最后将删除 .map 文件以后的静态资源放置到合适的位置。这样就既可以保证源码不被暴露,又可以很方便的定位线上问题。
devServer
开发模式下,我们会启动一个本地服务 webpack-dev-server 来进行本地开发,而 devServer 配置项就是用来指引 wepack-dev-server 工作的。
devServer 中最受人关注的是 HMR 配置项。
要正常使用 HRM 功能,需要三个前置条件:
-
devServer.hot配置项为true; -
启用
inline模式; -
模块中必须声明
module.hot.accept(url, callback);
这里有的小伙伴们可能会有疑惑,自己在本地开发的时候,源文件里面并没有声明 module.hot.accept(url, callback),为什么 HMR 还是可以正常运行呢?
答案很简单。这是因为我们在使用 react / vue 开发项目时,会使用对应的 loader 处理源文件。处理过程中,loader 会给源文件自动添加 module.hot.accept(url, callback) 逻辑。这一点,大家可以打开浏览器的源代码自己去看看噢。
其他配置项
剩下几个配置项,如 target、performance、node、stats 等,由于在实际项目中用的比较少,本文就不再做介绍了。如果有小伙伴感兴趣,可以自行去官方文档了解噢。
结束语
到这里,关于 webpack 配置项梳理的下篇就结束了。结合上篇,我们一共梳理了常用的 11 种配置项的用法,并对 optimization、plugins 做了比较深入的介绍。文章篇幅较长,阅读起来需要花一定的时间,希望小伙伴们能有所收获。
如果觉得本文还不错,一定要给小编点赞噢,😄。