You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// componetns.jsexportclassPerson{constructor({ name, age, sex }){this.className='Person'this.name=namethis.age=agethis.sex=sex}getName(){returnthis.name}}exportclassApple{constructor({ model }){this.className='Apple'this.model=model}getModel(){returnthis.model}}
function_classCallCheck(instance,Constructor){if(!(instanceinstanceofConstructor)){thrownewTypeError("Cannot call a class as a function");}}var_createClass=function(){functiondefineProperties(target,props){for(vari=0;i<props.length;i++){vardescriptor=props[i];descriptor.enumerable=descriptor.enumerable||!1,descriptor.configurable=!0,"value"indescriptor&&(descriptor.writable=!0),Object.defineProperty(target,descriptor.key,descriptor);}}returnfunction(Constructor,protoProps,staticProps){returnprotoProps&&defineProperties(Constructor.prototype,protoProps),staticProps&&defineProperties(Constructor,staticProps),Constructor;};}()varPerson=function(){functionPerson(_ref){varname=_ref.name,age=_ref.age,sex=_ref.sex;_classCallCheck(this,Person);this.className='Person';this.name=name;this.age=age;this.sex=sex;}_createClass(Person,[{key: 'getName',value: functiongetName(){returnthis.name;}}]);returnPerson;}();
varV8Engine=(function(){functionV8Engine(){}V8Engine.prototype.toString=function(){return'V8'}returnV8Engine}())varV6Engine=(function(){functionV6Engine(){}V6Engine.prototype=V8Engine.prototype// <---- side effectV6Engine.prototype.toString=function(){return'V6'}returnV6Engine}())console.log(newV8Engine().toString())
webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0
Tree-Shaking这个名词,很多前端coder已经耳熟能详了,它代表的大意就是删除没用到的代码。这样的功能对于构建大型应用时是非常好的,因为日常开发经常需要引用各种库。但大多时候仅仅使用了这些库的某些部分,并非需要全部,此时Tree-Shaking如果能帮助我们删除掉没有使用的代码,将会大大缩减打包后的代码量。
Tree-Shaking在前端界由rollup首先提出并实现,后续webpack在2.x版本也借助于UglifyJS实现了。自那以后,在各类讨论优化打包的文章中,都能看到Tree-Shaking的身影。
许多开发者看到就很开心,以为自己引用的elementUI、antd 等库终于可以删掉一大半了。然而理想是丰满的,现实是骨干的。升级之后,项目的压缩包并没有什么明显变化。
我也遇到了这样的问题,前段时间,需要开发个组件库。我非常纳闷我开发的组件库在打包后,为什么引用者通过ES6引用,最终依旧会把组件库中没有使用过的组件引入进来。
下面跟大家分享下,我在Tree-Shaking上的摸索历程。
Tree-Shaking的原理
这里我不多冗余阐述,直接贴百度外卖前端的一篇文章:Tree-Shaking性能优化实践 - 原理篇。
如果懒得看文章,可以看下如下总结:
很好,原理非常完美,那为什么我们的代码又删不掉呢?
先说原因:都是副作用的锅!
副作用
了解过函数式编程的同学对副作用这词肯定不陌生。它大致可以理解成:一个函数会、或者可能会对函数外部变量产生影响的行为。
举个例子,比如这个函数:
这个函数修改了全局变量location,甚至还让浏览器发生了跳转,这就是一个有副作用的函数。
现在我们了解了副作用了,但是细想来,我写的组件库也没有什么副作用啊,我每一个组件都是一个类,简化一下,如下所示:
用rollup在线repl尝试了下tree-shaking,也确实删掉了Person,传送门
可是为什么当我通过webpack打包组件库,再被他人引入时,却没办法消除未使用代码呢?
因为我忽略了两件事情:babel编译 + webpack打包
成也Babel,败也Babel
Babel不用我多解释了,它能把ES6/ES7的代码转化成指定浏览器能支持的代码。正是由于它,我们前端开发者才能有今天这样美好的开发环境,能够不用考虑浏览器兼容性地、畅快淋漓地使用最新的JavaScript语言特性。
然而也是由于它的编译,一些我们原本看似没有副作用的代码,便转化为了(可能)有副作用的。
比如我如上的示例,如果我们用babel先编译一下,再贴到rollup的repl,那么结果如下:传送门
如果懒得点开链接,可以看下Person类被babel编译后的结果:
我们的Person类被封装成了一个IIFE(立即执行函数),然后返回一个构造函数。那它怎么就产生副作用了呢?问题就出现在_createClass这个方法上,你只要在上一个rollup的repl链接中,将Person的IIFE中的
_createClass
调用删了,Person类就会被移除了。至于_createClass
为什么会产生副作用,我们先放一边。因为大家可能会产生另外一个疑问:Babel为什么要这样去声明构造函数的?假如是我的话,我可能会这样去编译:
因为我们以前就是这么写“类”的,那babel为什么要采用
Object.defineProperty
这样的形式呢,用原型链有什么不妥呢?自然是非常的不妥的,因为ES6的一些语法是有其特定的语义的。比如:for...of
的循环是通过遍历器(Iterator
)迭代的,循环数组时并非是i++,然后通过下标寻值。这里依旧可以看下阮老师关于遍历器与for...of的介绍,以及一篇babel关于for...of
编译的说明transform-es2015-for-of所以,babel为了符合ES6真正的语义,编译类时采取了
Object.defineProperty
来定义原型方法,于是导致了后续这些一系列问题。眼尖的同学可能在我上述第二点中发的链接transform-es2015-for-of中看到,babel其实是有一个
loose
模式的,直译的话叫做宽松模式。它是做什么用的呢?它会不严格遵循ES6的语义,而采取更符合我们平常编写代码时的习惯去编译代码。比如上述的Person
类的属性方法将会编译成直接在原型链上声明方法。这个模式具体的babel配置如下:
同样的,我放个在线repl示例方便大家直接查看效果:loose-mode
咦,如果我们真的不关心类方法能否被枚举,开启了
loose
模式,这样是不是就没有副作用产生,就能完美tree-shaking类了呢?我们开启了
loose
模式,使用rollup打包,发现还真是如此!传送门不够屌的UglifyJS
然而不要开心的太早,当我们用Webpack配合UglifyJS打包文件时,这个Person类的IIFE又被打包进去了? What???
为了彻底搞明白这个问题,我搜到一条UglifyJS的issue:Class declaration in IIFE considered as side effect,仔细看了好久。对此有兴趣、并且英语还ok的同学,可以快速去了解这条issue,还是挺有意思的。我大致阐述下这条issue下都说了些啥。
这个issue信息量比较大,也挺有意思,其中那位uglify贡献者kzc,当时提出rollup存在的问题后还给rollup提了issue,rollup认为问题不大不紧急,这位贡献者还顺手给rollup提了个PR,解决了问题。。。
我再从这个issue中总结下几点关键信息:
getter
或者setter
,而getter
、setter
是不透明的,有可能会产生副作用。有的同学可能会想,连获取对象的属性也会产生副作用导致不能删除代码,这也太过分了吧!事实还真是如此,我再贴个示例演示一下:传送门
代码如下:
打包结果如下:
而如果将
square
方法中的return x.a
改为return x
,则最终打包的结果则不会出现square
方法。当然啦,如果不在maths.js
文件中执行这个square
方法,自然也是不会在打包文件中出现它的。所以我们现在理解了,当时babel编译成的
_createClass
方法为什么会有副作用。现在再回头一看,它简直浑身上下都是副作用。查看uglify的具体配置,我们可以知道,目前uglify可以配置
pure_getters: true
来强制认为获取对象属性,是没有副作用的。这样可以通过它删除上述示例中的square
方法。不过由于没有pure_setters
这样的配置,_createClass
方法依旧被认为是有副作用的,无法删除。那到底该怎么办?
聪明的同学肯定会想,既然babel编译导致我们产生了副作用代码,那我们先进行tree-shaking打包,最后再编译bundle文件不就好了嘛。这确实是一个方案,然而可惜的是:这在处理项目自身资源代码时是可行的,处理外部依赖npm包就不行了。因为人家为了让工具包具有通用性、兼容性,大多是经过babel编译的。而最占容量的地方往往就是这些外部依赖包。
那先从根源上讨论,假如我们现在要开发一个组件库提供给别人用,该怎么做?
如果是使用webpack打包JavaScript库
先贴下webpack将项目打包为JS库的文档。可以看到webpack有多种导出模式,一般大家都会选择最具通用性的
umd
方式,但是webpack却没支持导出ES模块的模式。所以,假如你把所有的资源文件通过webpack打包到一个bundle文件里的话,那这个库文件从此与Tree-shaking无缘。
那怎么办呢?也不是没有办法。目前业界流行的组件库多是将每一个组件或者功能函数,都打包成单独的文件或目录。然后可以像如下的方式引入:
但是这样呢也比较麻烦,而且不能同时引入多个组件。所以这些比较流行的组件库大哥如antd,element专门开发了babel插件,使得用户能以
import { Button, Message } form 'antd'
这样的方式去按需加载。本质上就是通过插件将上一句的代码又转化成如下:这样似乎是最完美的变相tree-shaking方案。唯一不足的是,对于组件库开发者来说,需要专门开发一个babel插件;对于使用者来说,需要引入一个babel插件,稍微略增加了开发成本与使用成本。
除此之外,其实还有一个比较前沿的方法。是rollup的一个提案,在package.json中增加一个key:module,如下所示:
这样,当开发者以es6模块的方式去加载npm包时,会以
module
的值为入口文件,这样就能够同时兼容多种引入方式,(rollup以及webpack2+都已支持)。但是webpack不支持导出为es6模块,所以webpack还是要拜拜。我们得上rollup!(有人会好奇,那干脆把未打包前的资源入口文件暴露到
module
,让使用者自己去编译打包好了,那它就能用未编译版的npm包进行tree-shaking了。这样确实也不是不可以。但是,很多工程化项目的babel编译配置,为了提高编译速度,其实是会忽略掉node_modules
内的文件的。所以为了保证这些同学的使用,我们还是应该要暴露出一份编译过的ES6 Module。)使用rollup打包JavaScript库
吃了那么多亏后,我们终于明白,打包工具库、组件库,还是rollup好用,为什么呢?
我们只要通过rollup打出两份文件,一份umd版,一份ES模块版,它们的路径分别设为
main
,module
的值。这样就能方便使用者进行tree-shaking。那么问题又来了,使用者并不是用rollup打包自己的工程化项目的,由于生态不足以及代码拆分等功能限制,一般还是用webpack做工程化打包。
使用webpack打包工程化项目
之前也提到了,我们可以先进行tree-shaking,再进行编译,减少编译带来的副作用,从而增加tree-shaking的效果。那么具体应该怎么做呢?
首先我们需要去掉babel-loader,然后webpack打包结束后,再执行babel编译文件。但是由于webpack项目常有多入口文件或者代码拆分等需求,我们又需要写一个配置文件,对应执行babel,这又略显麻烦。所以我们可以使用webpack的plugin,让这个环节依旧跑在webpack的打包流程中,就像uglifyjs-webpack-plugin一样,不再是以loader的形式对单个资源文件进行操作,而是在打包最后的环节进行编译。这里可能需要大家了解下webpack的plugin机制。
关于uglifyjs-webpack-plugin,这里有一个小细节,webpack默认会带一个低版本的,可以直接用
webpack.optimize.UglifyJsPlugin
别名去使用。具体可以看webpack的相关说明而这个低版本的uglifyjs-webpack-plugin使用的依赖uglifyjs也是低版本的,它没有
uglify
ES6代码的能力,故而如果我们有这样的需求,需要在工程中重新npm install uglifyjs-webpack-plugin -D
,安装最新版本的uglifyjs-webpack-plugin
,重新引入它并使用。这样之后,我们再使用webpack的babel插件进行编译代码。
问题又来了,这样的需求比较少,因此webpack和babel官方都没有这样的插件,只有一个第三方开发者开发了一个插件babel-webpack-plugin。可惜的是这位作者已经近一年没有维护这个插件了,并且存在着一个问题,此插件不会用项目根目录下的
.babelrc
文件进行babel编译。有人对此提了issue,却也没有任何回应。那么又没有办法,就我来写一个新的插件吧----webpack-babel-plugin,有了它之后我们就能让webpack在最后打包文件之前进行babel编译代码了,具体如何安装使用可以点开项目查看。注意这个配置需要在
uglifyjs-webpack-plugin
之后,像这样:但是这样呢,有一个毛病,由于babel在最后阶段去编译比较大的文件,耗时比较长,所以建议区分下开发模式与生产模式。另外还有个更大的问题,
webpack
本身采用的编译器acorn不支持对象的扩展运算符(...)以及某些还未正式成为ES标准的特性,所以。。。。。所以如果特性用的非常超前,还是需要
babel-loader
,但是babel-loader
要做专门的配置,把还在es stage阶段的代码编译成ES2017的代码,以便于webpack
本身做处理。感谢掘金热心网友的提示,还有一个插件BabelMinifyWebpackPlugin,它所依赖的babel/minify也集成了uglifyjs。使用此插件便等同于上述使用UglifyJsPlugin + BabelPlugin的效果,如若有此方面需求,建议使用此插件。
总结
上面讲了这么多,我最后再总结下,在当下阶段,在tree-shaking上能够尽力的事。
loose
模式,这个要根据自身项目判断,如:是否真的要不可枚举class的属性。module
字段。pure_getters: true
,删除一些强制认为不会产生副作用的代码。故而,在当下阶段,依旧没有比较简单好用的方法,便于我们完整的进行tree-shaking。所以说,想做好一件事真难啊。不仅需要靠个人的努力,还需要考虑到历史的进程。
--阅读原文
The text was updated successfully, but these errors were encountered: