在 webpack 2 版本, 增加了对 ES Module 的支持, 使得 webpack 能够分析出未使用的 export 内容, 然后将其 tree-shrking 掉
但是模块中那些具备副作用的代码, webpack 会将其保留
举一个例子, 项目中存在 utils/a.js
模块和 /utils/b.js
模块, 并通过 utils/index.js
提供统一入口
其中 b 模块包含一条打印语句, 是具有副作用的
1 | // utils/a.js |
添加主入口 app.js
, 只引用 a 模块, 我们期望未使用的 b 模块被 tree-shaking 掉
1 | // app.js |
我们看一下打包后的结果, 注意要在 production 模式下打包. 结果如下所示, 我去掉了无关的 webpack 启动代码
1 | // output |
打包结果中, 不包含 b 模块, 但是 b.js
中的副作用代码被保留了, 这是合乎情理的
sideEFfects 作用
下面修改下 b.js
的内容
1 | // utils/b.js |
我们在 Array 原型链上定义了一个新方法 sum
, 这是具有副作用的. 然后在 b 模块中调用了该方法, 但是作为 b 模块的维护者, 我又希望 sum
是”纯粹”的, 只被我使用, 外部并不依赖它的实现
修改 package.json, 新增字段 "sideEffects": false
, 该字段表明整个工程是”无副作用”的
重新调用 webpack 编译, 期待在 b 模块没被使用的情况下, b 中定义的 sum 方法也被 tree-shaking 掉, 结果如下
1 | ([ |
如期望那样, 整个 b 模块都被 tree-shaking 掉了, 包括包含副作用的代码
所以, sideEffects 可以优化打包体积, 并且一定程度上可以减少 webpack 对源码分析过程, 加快打包速度
你可以再试试引用 b 模块、sideEffects 值设为 true、去掉 sideEffects 等情况的打包结果
sideEffects 配置
sideEffects 除了能设置 boolean 值, 还可以设置为数组, 传递需要保留副作用的代码文件(例如: “./src/polyfill.js”) 或者传递模糊匹配符(例如: “src/*/.css”)
1 | sideEffects: boolean | string[] |
sideEffects 注意事项
实际项目中, 通常并不能简单的设置为 "sideEffects": false
, 有些副作用是需要保留的, 比如引入样式文件
webpack 会认为所有 import 'xxx'
语句是仅引入而未使用, 如果你错误的将其声明成了”无副作用”, 它们就会被 tree-shaking 掉, 并且由于 tree-shaking 仅在 production 模式生效, 本地开发时可能一切仍是正常的, 生产环境并不能及时发现问题.
下面这些都是”仅引入而未使用”的例子
1 | import './normalize.css'; |
相应的, 下面这种就不算
1 | import icon from './icon.png'; |
这些有副作用的文件, 我们要正确声明, 修改 sideEffects 值
1 | // package.json |
|
sideEffects 局限性
sideEffects 配置是以文件为维度的, 只要你配置了文件具备副作用, 即便你只用了该文件中没有副作用的那部分功能, 仍然会将副作用保留
比如将 b.js
修改为
1 | Object.defineProperty(Array.prototype, 'sum', { |
在 app.js
中仅引入 c 方法, b 方法会被 tree-shaking, 但 sum 方法不会
再谈”副作用”的含义
初次看到 sideEffects 配置可能会很奇怪, 代码明明是有副作用的, 为什么要声明它是”无副作用”呢?
其实可以换个角度来想, sideEffects 是通知 webpack 该模块是可以安全的 tree-shaking 的, 无需关心其副作用。
后话
sideEffects 对 webpack 构建过程有着很大影响, 对 npm 模块尤为重要. 使用中要特别注意声明的正确性。
题外话 【由 allowSyntheticDefaultImports 引起的思考】
来百度开始在使用 TypeScript,有些童鞋会像下面这样写1
2
3import React from "react";
console.log(React);
因为 react
是以 commonJS 的形式导出的,所以上面的代码会报错1
Module '"react"' has no default export.
提示 react
并没有 default
字段导出。这个问题解决也很简单,换成下面这种形式的写法就可以了。1
import * as React from "react";
babel 允许第一种写法,因为帮你做了兼容。而 TS 是不能这样写的。后面仔细看了文档,发现自己的观点是不对的。
TS 是允许你这么写的,但是需要在 tsconfig.json
中配置两个字段1
2
3
4{
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
先说下 allowSyntheticDefaultImports
,这个字段实际只起到检查的作用,不会对编译后的代码有任何影响。
而 esModuleInterop
就不一样,实际上开启这个字段的时候,默认也是会开启 allowSyntheticDefaultImports
,并且对于编译后的代码也做了兼容。还要注意的是这个字段只有当把代码编译成 commonJS 的时候才会起作用。
所以对于第一种写法,如果配置是下面这样1
2
3
4{
"module": "commonjs",
"esModuleInterop": true
}
编译之后的代码为1
2
3
4
5
6var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));
console.log(react_1.default);
可以看到加了很多兼容性的代码,如果配置换成下面这样1
2
3
4
5{
"module": "es2015",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
因为导出模块是 es2015
,实际会忽略 esModuleInterop
字段,所以要加上 allowSyntheticDefaultImports
防止报错,输出的代码为1
2import React from "react";
console.log(React);
可以看到和源代码一模一样。实际上 babel 的行为也是这样的,只有当导出 commonjs 的时候,才会加上兼容性的代码,如果是 ES6 的格式则原样输出。
因为 webpack 的 tree shaking,所以一般我们都会让 babel 或者 TS 把模块导出成 ES6。而 npm 上面绝大部分的包都是 commonjs 形式的,那是谁做了这个兼容呢。答案也很明显,就是 webpack。
只要是 webpack 能识别的模块格式,不管是 commonjs,ES6 还是 amd,webpack 都能让它们正常地相互引用。