前言
一般来说,笔者在看源码的时候都会先过一遍代码的依赖库,这样一来我们就会对眼前的这堆逻辑到底在干一个什么事情有一个大致的轮廓,然后再深入代码逻辑的时候就是填充骨架,肉体的思想工作了。
当我在看Vue.js源码的时候,首先打开了 package.json
。
于是我注意到了一个奇怪的属性:
{
...
"sideEffects": false,
...
}
sideEffects 这个属性可没在别的npm包中见到过,于是我去查阅npm的官方文档,尝试去搜索关于这个字段的信息。
结果是什么都没有。
这个时候我就要去github查Vue.js的commit信息了,看看提交这行代码的人有没有给到充足的说明。
得益于vscode强大的插件GitLens,我的鼠标浮动在他身上的时候就显示出了该行代码的提交信息。 点击#8099链接,就可到达当时的PR情况。>传送门
很快地,在这位热心的开源贡献者的描述中,我找到了这一句话:
This PR adds the
"sideEffects": false
property in vue'spackage.json
file. This allow's webpack (for those who want to opt-in to requiring vue's original source files (instead of the flattened esm bundles) and want to remove flow type through a babel-transform, then this will allow webpack to aggressively ignore and treeshake unused exports throughout the module system.
大致意思是这是一个让那些想要直接 require Vue.js的源码而不是从Vue.js的esm bundles那里去导入代码的人来使用的一个功能,这个配置能让webpack以更击进的方式来treeshake将要打包的代码。
原来是webpack的一个和treeshake相关的配置啊,赶紧去查阅一下webpack文档:
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如
import
和export
。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。 webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过package.json
的"sideEffects"
属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
概括一下,就是sideEffects是提供给 webpack 的 treeshake 功能的一项配置,告诉他能不能“进击地“(aggressively)移除代码中的一些未引用代码(dead-code)。
那么怎么才算是 dead-code 呢?以下是一个简单的例子:
目录结构: demo |- module.js |- index.js
// module.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
// index.js
import { square } from './module'
square(2)
当我们的webpack配置的entry是index.js的时候,可以看到我们没有引入到code这个函数,于是
export function cube(x) {
return x * x * x
}
这段代码就会被称为dead-code,没有任何地方会引用到。
然而,我们是通过ES2015的import,export来很清晰且断定地去判断它是dead-code的,但是没有被import他就一定可以安全地被删除吗?请看下面这个例子,来自Polaris React的Button组件。
import hoistStatics from 'hoist-non-react-statics';
function Button(_ref) {
// ...
}
function merge() {
var _final = {};
for (
var _len = arguments.length, objs = new Array(_len), _key = 0;
_key < _len;
_key++
) {
objs[_key] = arguments[_key];
}
for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
var obj = _objs[_i];
mergeRecursively(_final, obj);
}
return _final;
}
function withAppProvider() {
return function addProvider(WrappedComponent) {
var WithProvider =
/*#__PURE__*/
(function (_React$Component) {
// ...
return WithProvider;
})(Component);
WithProvider.contextTypes = WrappedComponent.contextTypes
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
return FinalComponent;
};
}
var Button$1 = withAppProvider()(Button);
export {
// ...,
Button$1,
};
假设我们的项目没有 import 过 Button 的话,那么这段关于 Button 的代码就应该被判定为 dead-code 。
但是仔细一看里面的withAppProvider
这个函数,我们的不仅调用了它,还以Button为参数调用了它的返回值。
**
这个时候我们很难判定这段代码的删除对这整个代码库有没有额外的逻辑影响,也就是副作用(sideEffects)。比如polyfill的代码,我们可能会修改一个对象的原型,或者定义、修改一些全局的函数,通常这样的代码都不会export,这就是webpack做treeshaking的时候考虑到的场景。
总结
sideEffects: false
属性就是为了告诉webpack,我这个代码库没有包含任何有副作用的代码,你可以“进击地(aggresively)”在打包的时候忽略掉这些没有被 import 的代码。
更详细的解释可以查阅webpack的介绍:treeshaking和sideEffects
到此,我还知道了:
- treeshaking的功能依赖于ES2015的模块语法
import
和export
,难怪 Vue 的代码全都使用ES2015的模块化方式。 - sideEffects作为一个指定打包的移除无用代码从而减小包体的方式的属性,在rollup中也被认真地探讨:
- 在2016年,Rollup对tree-shaking功能提出的issue,其中关于如何精确地(explicitly)判断一段代码是否dead-code引起了众多开源作者的讨论;以及会引起的sideEffects的代码就不应该写出来:Treeshaking is defeated by Object.defineProperty
- 逛issue的时候发现这经历了一年多的讨论,看到了一个评论简直笑出了声:
- 不过也多亏了那么多开源大佬的反复推敲,才有那么好用的打包工具,开源社区的好处就在于此,集思广益!😆
- 最终看到这个问题是通过在使用uglify的过程中,给没有sideEffects的函数添加注释
/*#__PURE__*/
来指定它的处理方式,详情见:github.com/rollup/roll…。同时也给内部的treeshaking加入了一个moduleSideEffects的属性来指定对应的行为:rollupjs.org/guide/en/#d… - 一开始给Vue.js提PR让他的package.json加入
sideEffects:false
的人是Sean Larkin,webpack核心团队的开发者。即使Vue.js是使用rollup来打包,他也提了这个issue来benefit其他用webpack的人,这样一来相当于借助Vue.js这个火爆的开源项目给webpack打了个广告,真不错!很快地,这个PR就被尤大merge到master了(3 May 2018)。