Webpack 的概念

基本组件

  • webpack:核心实现,并提供了 API
  • webpack-cli:在命令行使用上述 API 的命令行工具。可以传参使用,也可以使用 webpack 配置文件

基本概念

module

可以被某代码语句(例子见下述列表)引入的东西,会被 Webpack 视为 module。

  • ES 的 import/import() 语句
  • CJS 的require语句
  • AMD 的definerequire语句
  • css/sass/less 的@import语句、url(...)
  • HTML 的 <img src="">
  • ...

可见,JS 模块文件、CSS/SASS/LESS 文件、图片文件等均可作为 Webpack module。而且不同类型的 module 可以互相引用(例如 JS module 中可以引入 SASS 文件、图片文件等)。

Webpack 使用不同的 loader(默认自带 ES Module/CJS/AMD JavaScript loader 和 JSON loader) 解析不同类型的 Webpack module,并将其转化为某种其它形式(例如将图片转换为 base64 字符串/相对 URL 等)。

各 module 间通过上述语句建立起依赖关系。如果在一个模块中, Webpack 无法通过require语句得知该模块具体需引入哪个其它模块(例如require的参数是在运行时求得的),则所有可能引入的模块都将被视为该模块的依赖。这背后的机制是 webpack 自动生成了一个 require.context(一个编译期信息。可以由程序员手动在编译期生成)。

各 module 以及它们互相间的依赖关系组成了 Dependency Graph。 Webpack 总是从一个作为 entry point 的 module 开始,分析、建立起一个 Dependency Graph的。在一次构建中可能建立起多个 Dependency Graph。

处理以特殊形式进行导出的模块

本节介绍如何用 webpack 导入前 ESM 时代的可复用代码,这些代码通常以不标准的方式进行导出。但我们没必要对它们的源码进行修改,因为 webpack 提供了如下解决方案:

以全局变量形式导出的模块

有一些老旧的库并未使用任何模块化语法进行导出,而是把自己注册为一个全局变量。这种情况可使用 exports-loader 将其纳入正常的 webpack module 体系内,而无需对库源码做任何改动。

以混合模块化语法导出的模块

一些老旧的库使用了混合的模块化导出语法,例如先检查有无define函数(AMD),再检查有无require(CJS),最后以注册为全局变量为兜底。这种情况一般通过 imports-loader 的配置,强制将其视为以 CJS 语法进行导出的模块。

以非上述方式导出的模块

在 noParse 选项中标记该库,将其原样打包进打包产物中。

处理使用了全局变量的消费者模块

有一些老旧模块通过全局变量使用它的依赖,而这些依赖现已有模块化导出的版本了。此时可使用 ProvidePlugin,在不改动该模块源代码的前提下使源代码正常运作。

不打包某些被使用的模块

这称为模块的“外化(Externalize)”,常见于:

  • 库作者不希望将自己依赖的第三方模块打包进构建产物中(例如已经将这些第三方模块列在 package.json 中的dependenciespeerDependencies字段里了)
  • 依赖的第三方模块是运行时环境的一部分(例如在浏览器里通过引入这些第三方模块的 script 生成了全局变量)

这种情况下应使用externals配置字段向 webpack 列举出此类依赖,还可列举出这些依赖可能使用的模块化语法,以便 webpack 进行额外处理。注意,externals配置不侵入消费者模块的代码,这意味着在使用这些依赖的模块内,该 import 哪些符号就 import 哪些符号。

通过配置字段target,webpack 可以自动认得一些运行时环境变量,而无需手动列举(例如当 target 为 node 时,webpack 会自动外化fspath等依赖)。

chunk

chunk 是 Webpack 打包构建后生成的代码文件。

一般来说,通过构建过程,源于一个 entry point 的一个或多个(当以数组形式指定一个 entry point 时) Dependency Graph 中的所有 modules 被打包进一个 chunk。但有时可能会被打包为多个 chunk,此时称这些 chunk 组成一个 chunks group。一个 chunks group 中,必有一个 initial chunk 以及任意个 non-initial chunk(或称为 on-demand chunk)。后者的加载由前者触发。

为什么有时需要多个 entry point

可能的场景如下:

注意,用一个数组作为 entry point,并不能称“该项目具有多个 entry point”。这么做的例子见此处

为什么会有 non-initial chunk

可能的场景如下:

其中,1、3 两点基本上依赖于插件 SplitChunksPlugin。该插件提供了各种划分 chunk 的方法。

注意:懒加载(按需加载)的 chunk 不一定是 non-initial chunk,non-initial chunk 也不一定是懒加载(按需加载)的。

webpack runtime

作为构建过程的产物的代码中,额外存在一个由 Webpack 提供的runtime chunk(一个 chunk group 默认产生一个 runtime chunk,它直接被插入到 HTML 的 script 里作为内联脚本了。但可以通过配置,使得所有 chunk group 共用同一个 runtime chunk)。其中的代码负责进行所有其它 chunk 的加载和内部各 module 的连接。其中会用到一种称为 manifest 的信息,它包含了各模块间关系的描述。manifest 信息可以被 webpack plugin 加以利用。

针对不同的目标环境(浏览器/node/electron等),Webpack 生成的 runtime 代码也不同。目标环境由配置字段target控制。

环境变量

给 Webpack 配置文件提供环境变量

可以在 Webpack 的配置文件中额外使用 Webpack CLI(而非操作系统或 Node)提供的环境变量(--env),以动态地选择配置。当然,也能使用操作系统或 Node 提供的环境变量。

给项目代码提供环境变量

使用 DefinePlugin 插件可实现该功能。在项目中使用process.env.xxx来使用定义好的环境变量。注意,此处的process.env和 NodeJS 的 process.env 没有任何关系,即,和操作系统或是 Node 提供的环境变量无关。process.env.xxx只是会被进行单纯的字符串替换,因此也不能对process.env使用解构语法等。

Tree shaking

Webpack 在 production mode 下的打包过程中会删掉所有模块中并未被实际使用到的符号背后的代码(这背后是 optimization.usedExports 配置在起作用),但这要求所有模块均使用了 ESM 语法。因此使用 NPM 上的库时,应当尽量寻找使用了 ESM 的库;同时也不能让 babel 等转译工具对import语句进行转译(通常 babel 会将 ESM 语法转为 CJS)。

有时我们 import 一个模块并非是为了使用它导出的符号,而是为了触发副作用。如果不做任何配置,Webpack 会因为该模块被不存在被外部使用的符号而将其删除。因此:

被标注为有副作用对模块中可能还是存在着纯函数的,这些纯函数可以被安全删除。使用/*#__PURE__*/标记出纯函数。可以在函数的声明前使用它,也可在函数的调用前使用它

除此之外,我们还需要注意:

  • Babel 等转译工具可能将看上去没有副作用的代码转译出副作用,从而导致 tree-shaking 失败。如果对ES6语义特性要求不是特别严格,可以开启babel的loose模式;并且确保转译发生在打包之后
  • 如果你是库作者,且库中难以避免地产生各种副作用代码,可以将功能函数或者组件,打包成单独的文件或目录,以便于用户可以通过目录去加载。如有条件,也可为自己的库开发单独的webpack-loader(或者类似于 babel-lodash-plugin 之类的插件),便于用户按需加载
  • 如果你是库作者,记得提供

作为库作者

如果你的代码需要由 Webpack 打包后进行分发供其他开发者使用,则需要注意以下事项: