Webpack 4 中新增了一个 sideEffects 特性,它允许我们通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。
TIPS:模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情。
这个特性一般只有我们去开发一个 npm 模块时才会用到。因为官网把对 sideEffects 特性的介绍跟 Tree-shaking 混到了一起,所以很多人误认为它们之间是因果关系,其实它们没有什么太大的关系。
我们先把 sideEffects 特性本身的作用弄明白,你就更容易理解为什么说它跟 Tree-shaking 没什么关系了。
这里我先设计一个 sideEffects 能够发挥效果的场景,案例具体结构如下:

  1. .
  2. ├── src
  3. ├── components
  4. ├── button.js
  5. ├── heading.js
  6. ├── index.js
  7. └── link.js
  8. └── main.js
  9. ├── package.json
  10. └── webpack.config.js

基于上一个案例的基础上,我们把 components 模块拆分出多个组件文件,然后在 components/index.js 中集中导出,以便于外界集中导入,具体 index.js 代码如下:

  1. // ./src/components/index.js
  2. export { default as Button } from './button'
  3. export { default as Link } from './link'
  4. export { default as Heading } from './heading'

这也是我们经常见到一种同类文件的组织方式。另外,在每个组件中,我们都添加了一个 console 操作(副作用代码),具体代码如下:

  1. // ./src/components/button.js
  2. console.log('Button component~') // 副作用代码
  3. export default () => {
  4. return document.createElement('button')
  5. }
  6. 我们再到打包入口文件(main.js)中去载入 components 中的 Button 成员,具体代码如下:
  7. // ./src/main.js
  8. import { Button } from './components'
  9. document.body.appendChild(Button())

那这样就会出现一个问题,虽然我们在这里只是希望载入 Button 模块,但实际上载入的是 components/index.js,而 index.js 中又载入了这个目录中全部的组件模块,这就会导致所有组件模块都会被加载执行。
我们打开命令行终端,尝试运行打包,打包完成过后找到打包结果,具体结果如下:
image.png根据打包结果发现,所有的组件模块都被打包进了 bundle.js。
此时如果我们开启 Tree-shaking 特性(只设置 useExports),这里没有用到的导出成员其实最终也可以被移除,打包效果如下:
image.png但是由于这些成员所属的模块中有副作用代码,所以就导致最终 Tree-shaking 过后,这些模块并不会被完全移除。
可能你会认为这些代码应该保留下来,而实际情况是,这些模块内的副作用代码一般都是为这个模块服务的,例如这里我添加的 console.log,就是希望表示一下当前这个模块被加载了。但是最终整个模块都没用到,也就没必要留下这些副作用代码了。
所以说,Tree-shaking 只能移除没有用到的代码成员,而想要完整移除没有用到的模块,那就需要开启 sideEffects 特性了

sideEffects 作用

我们打开 Webpack 的配置文件,在 optimization 中开启 sideEffects 特性,具体配置如下:

  1. // ./webpack.config.js
  2. module.exports = {
  3. mode: 'none',
  4. entry: './src/main.js',
  5. output: {
  6. filename: 'bundle.js'
  7. },
  8. optimization: {
  9. sideEffects: true
  10. }
  11. }

TIPS:注意这个特性在 production 模式下同样会自动开启。
那此时 Webpack 在打包某个模块之前,会先检查这个模块所属的 package.json 中的 sideEffects 标识,以此来判断这个模块是否有副作用,如果没有副作用的话,这些没用到的模块就不再被打包。换句话说,即便这些没有用到的模块中存在一些副作用代码,我们也可以通过 package.json 中的 sideEffects 去强制声明没有副作用。
那我们打开项目 package.json 添加一个 sideEffects 字段,把它设置为 false,具体代码如下:

  1. {
  2. "name": "09-side-effects",
  3. "version": "0.1.0",
  4. "author": "zce <w@zce.me> (https://zce.me)",
  5. "license": "MIT",
  6. "scripts": {
  7. "build": "webpack"
  8. },
  9. "devDependencies": {
  10. "webpack": "^4.43.0",
  11. "webpack-cli": "^3.3.11"
  12. },
  13. "sideEffects": false
  14. }

这样就表示我们这个项目中的所有代码都没有副作用,让 Webpack 放心大胆地去“干”。
完成以后我们再次运行打包,然后同样找到打包输出的 bundle.js 文件,结果如下:
image.png此时那些没有用到的模块就彻底不会被打包进来了。那这就是 sideEffects 的作用。
这里设置了两个地方:

  • webpack.config.js 中的 sideEffects 用来开启这个功能;
  • package.json 中的 sideEffects 用来标识我们的代码没有副作用。

目前很多第三方的库或者框架都已经使用了 sideEffects 标识,所以我们再也不用担心为了一个小功能引入一个很大体积的库了。例如,某个 UI 组件库中只有一两个组件会用到,那只要它支持 sideEffects,你就可以放心大胆的直接用了。

sideEffects 注意

使用 sideEffects 这个功能的前提是确定你的代码没有副作用,或者副作用代码没有全局影响,否则打包时就会误删掉你那些有意义的副作用代码。
例如,我这里准备的 extend.js 模块:

  1. // ./src/extend.js
  2. // 为 Number 的原型添加一个扩展方法
  3. Number.prototype.pad = function (size) {
  4. const leadingZeros = Array(size + 1).join(0)
  5. return leadingZeros + this
  6. }

在这个模块中并没有导出任何成员,仅仅是在 Number 的原型上挂载了一个 pad 方法,用来为数字添加前面的导零,这是一种很早以前常见的基于原型的扩展方法。
我们回到 main.js 中去导入 extend 模块,具体代码如下:

  1. // ./src/main.js
  2. import './extend' // 内部包含影响全局的副作用
  3. console.log((8).pad(3)) // => '0008'

因为这个模块确实没有导出任何成员,所以这里也就不需要提取任何成员。导入过后就可以使用它为 Number 提供扩展方法了。
这里为 Number 类型做扩展的操作就是 extend 模块对全局产生的副作用。
此时如果我们还是通过 package.json 标识我们代码没有副作用,那么再次打包过后,就会出现问题。我们可以找到打包结果,如下图所示:
image.png我们看到,对 Number 的扩展模块并不会打包进来。
缺少了对 Number 的扩展操作,我们的代码再去运行的时候,就会出现错误。这种扩展的操作属于对全局产生的副作用。
这种基于原型的扩展方式,在很多 Polyfill 库中都会大量出现,比较常见的有 es6-promise,这种模块都属于典型的副作用模块。
除此之外,我们在 JS 中直接载入的 CSS 模块,也都属于副作用模块,同样会面临这种问题。
所以说不是所有的副作用都应该被移除,有一些必要的副作用需要保留下来。
最好的办法就是在 package.json 中的 sideEffects 字段中标识需要保留副作用的模块路径(可以使用通配符),具体配置如下:

  1. {
  2. "name": "09-side-effects",
  3. "version": "0.1.0",
  4. "author": "zce <w@zce.me> (https://zce.me)",
  5. "license": "MIT",
  6. "scripts": {
  7. "build": "webpack"
  8. },
  9. "devDependencies": {
  10. "webpack": "^4.43.0",
  11. "webpack-cli": "^3.3.11"
  12. },
  13. "sideEffects": [
  14. "./src/extend.js",
  15. "*.css"
  16. ]
  17. }

这样 Webpack 的 sideEffects 就不会忽略确实有必要的副作用模块了。

Tree-shaking 的本身没有太多需要你理解和思考的地方,你只需要了解它的效果,以及相关的配置即可。
而 sideEffects 可能需要你花点时间去理解一下,重点就是想明白哪些副作用代码是可以随着模块的移除而移除,哪些又是不可以移除的。总结下来其实也很简单:对全局有影响的副作用代码不能移除,而只是对模块有影响的副作用代码就可以移除。
总之不管是 Tree-shaking 还是 sideEffects,我个人认为,它们都是为了弥补 JavaScript 早期在模块系统设计上的不足。随着 Webpack 这类技术的发展,JavaScript 的模块化确实越来越好用,也越来越合理。
除此之外,我还想强调一点,当你对这些特性有了一定的了解之后,就应该意识到:尽可能不要写影响全局的副作用代码。