一.放弃webpack的原因
1.webpack模块可读性太低
// 引用模块
var _myModule1 = __webpack_require__(0);
var _myModule2 = __webpack_require__(10);
var _myModule3 = __webpack_require__(24);
// 模块定义
/* 10 */
/***/function (module, exports, __webpack_require__) {...}
// 源码
_myModule2.default.xxx()
这种代码读起来相当费劲,先找到_myModule2
对应的__webpack_require__
id,再找对应的模块定义,最后看该模块exports
身上挂了什么东西。模块定义这个部分很讨厌,延长了阅读引用链
当然,一般不需要读bundle,这一点并不致命
2.文件很大
如上面提到的,这些额外的bundle代码(子模块定义、子模块引用等等)导致文件体积膨胀,因为:
源码每个独立文件外面都包了一层模块定义
模块内对其它模块的引用都插了一条
__webpack_require__
声明__webpack_require__
工具函数自身的体积
文件体积不但会带来传输负担,还会影响Compile时间,打包方案的bundle size是一项重要指标
3.执行很慢
子模块定义和运行时依赖处理(__webpack_require__
),不仅导致文件体积增大,还会大幅拉低性能,如下图:
(图片来自webpack_require is too slow)
打包方案对性能产生大幅影响,这是一点最为致命,无法忍受
二.rollup的优势
1.文件很小
几乎没什么多余代码,除了必要的cjs, umd
头外,bundle代码基本和源码没什么差异,没有奇怪的__webpack_require__, Object.defineProperty
bundle大小对比如下:
webpack 132KB
rollup 82KB
2.执行很快
因为没什么多余代码,如上文提到的,webpack bundle不仅体积大,非业务代码(__webpack_require__, Object.defineProperty
)执行耗时也不容小视
rollup没有生成这些额外的东西,执行耗时主要在于Compile Script
和Evaluate Script
上,其余部分可以忽略不计,如下图:
3.es模块及iife格式支持
// rollup
amd – Asynchronous Module Definition, used with module loaders like RequireJS
cjs – CommonJS, suitable for Node and Browserify/Webpack
es – Keep the bundle as an ES module file
iife – A self-executing function, suitable for inclusion as a <script> tag. (If you want to create a bundle for your application, you probably want to use this, because it leads to smaller file sizes.)
umd – Universal Module Definition, works as amd, cjs and iife all in one
// webpack
"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root
支持打包es6模块,对于基础库之类的东西很合适,因为es6项目一般会用babel转一遍,这样保证一次统一的babel翻译
支持打包成iife,非常小。另外,单从最终bundle大小来看:
default uglify
cjs 81KB 34K
amd 81KB 30KB
iife 81KB 30KB
umd 82KB 30KB
umd
比cjs
有优势,看起来很奇怪,但实际结果确实是这样。看bundle差异主要在于函数名简化,cjs
bundle中很多长函数名保留下来了,没有被混淆掉
三.rollup的缺陷
目前最新版本(0.50.0)仍然处于0.x的不稳定状态,版本相关的问题比较多(甚至某些问题还需要通过版本降级来解决)
插件生态相对较弱,一些常见需求无法满足
比如打包多个依赖库,把公共依赖项提出来(webpack的CommonsChunkPlugin)
早些版本(0.43)循环依赖处理得不好,会出现打包/执行出错
文档相对较少,遇到问题无法快速解决
比如常见错误
'foo' is not exported by bar.js (imported by baz.js)
,Troubleshooting算是FAQ,但没有提供详细可靠的解决方案(即照做了也不一定能解决)
四.babel配置
babel翻译一般是必不可少的,作为rollup/webpack打包过程的一个中间处理环节,都提供了相应的包装插件,可以把babel配置嵌进来,实际需要掌握的是babel配置
babel preset
In Babel, a preset is a set of plugins used to support particular language features.
常见的有:
es2015:仅支持ES6特性,如果preset里含有该项,会把ES6语法转换为ES5
stage-0:还支持最新的es7甚至es8特性,实际上是指ES Stage 0 Proposals,如果preset里含有该项,会把ESn转换为ES6
react:支持React JSX
stage-0
是最激进的做法,表示想要用babel能转的所有JS新特性,无论是否稳定。es2015
最保守,规范已经发布了,没有特性不稳定的风险。像stage-0
一样能打的还有4个(TC39规范制定流程):
- stage-0 – Strawman: just an idea, possible Babel plugin.
- stage-1 – Proposal: this is worth working on.
- stage-2 – Draft: initial spec.
- stage-3 – Candidate: complete spec and initial browser implementations.
- stage-4 – Finished: will be added to the next yearly release.
P.S.最近babel提供了babel-preset-env,根据目标平台环境来自动添加preset,就不需要装一堆esxxx了,但只提供ES支持,react和polyfill并不会内置,也不应该内置。关于env的更多信息,请查看babel-preset-env: a preset that configures Babel for you
注意,各preset仅负责一步转换,比如stage-0
能把ESn转ES6,而不是ES5,也就是说,对于一个语法很激进的项目,想要转换成ES5的话,需要这样的babel配置:
{
"presets": [
["stage-0"],
["es2015", {"modules": false}]
],
"plugins": [
"external-helpers"
]
}
P.S.其中,{"modules": false}
是rollup
需要,用来代替babel-preset-es2015-rollup,external-helpers
的作用后面介绍
如果想保留ES6风格,需要这样的babel配置:
{
"presets": [
["stage-0"]
],
"plugins": [
"external-helpers"
]
}
转换后得到的是把项目各模块文件拼在一起的ES6模块,代码里的class
、const
、let
都会保留,因为ES6支持这些特性,但async&await
之类的更高级特性会被转换到ES6
babel plugin
在babel的3个处理环节中:
parsing -> transforming -> generation
插件作用于第2个环节(transforming),即解析完源语法之后,把它转换为等价的目标语法,在这个阶段可以通过插件做进一步处理,例如简单的:
// 把标识符成员访问转换为字面量形式,例如a.catch -> a['catch']
es3-member-expression-literals
// 把标识符成员声明转换为字面量形式,例如{catch: xxx} -> {'catch': xxx}
es3-property-literals
还有常用的:
// 支持class静态属性和实例属性,例如class A{instanceProp = 1; static staticProp = 2;}
transform-class-properties
// 把babel自己用的公共方法提出来,例如_createClass, _inherits等等
external-helpers
// 常量修改检查,const声明的常量被修改时报错
check-es2015-constants
所以babel plugin大致分3类:
ES5/ES6补丁,修补更低环境相关的问题(es3-xxx,es2015-xxx)
静态检查,比如const修改报错提前到“编译”阶段
风险特性,比如class-properties等不适合放在stage里的争议特性
补丁针对生产环境,静态检查是质量保证的一部分,风险特性则是更激进的一些JS语法
babel polyfill
babel把ESn高级语法转换到ES5/ES3会遇到4种情况:
简单语法糖。无脑转换,例如
for...of, arrow function
复杂语法糖。需要工具函数处理,例如
createClass, inherits
low环境缺少的基础特性。需要polyfill,例如
Symbol, Promise, String.repeat
无法被polyfill的特性。例如
Proxy
对于low环境缺少的基础特性,babel默认不提供polyfill(babel翻译结果不含polyfill),可以引入babel-polyfill,或者引入想要的特殊polyfill(更轻量小巧的,或者更可靠的重量级的)
babelHelpers
babel有一些转换相关的工具函数,例如:
_typeof
_instanceof
_createClass
_interopRequireDefault
_classCallCheck
_inherits
asyncGenerator
这些工具函数都属于babelHelpers,完整的helpers可以通过命令生成:
npm install babel-cli --save-dev
// type可选global/umd/var
./node_modules/.bin/babel-external-helpers -t umd > helpers.js
P.S.关于生成babelHelpers的更多信息,请查看External helpers
默认配置下,这些工具函数会被生成多份,也就是说bundle中会存在多个_createClass
声明,是冗余代码。可以通过插件配置优化或去掉
默认配置,bundle中存在多份helper声明:
{
"presets": [
["es2015"]
]
}
添上external-helpers
插件,把helper声明提到bundle顶部,不存在多份声明:
{
"presets": [
["es2015"]
],
"plugins": [
"external-helpers"
]
}
引用外部babelHelpers
,bundle中不含helper声明:
{
"presets": [
["es2015"]
],
"plugins": [
"external-helpers"
],
externalHelpers: true
}
一般添上external-helpers
把helper提到bundle顶部就能满足优化要求,所以babel配置都建议至少添上external-helpers
插件去除冗余helper代码
externalHelpers: true
是针对多bundle(multi entry)的情况,不添的话每个bundle顶部都有一份helper声明,添上之后bundle都引用外部helper,例如:
babelHelpers.createClass(xxx)
babelHelpers在bundle里是未定义的,需要提前引入,比如web环境:
<script src="babelHelpers.js"></script>
<script src="bundle.js"></script>
五.总结
相比webpack,rollup拥有无可比拟的性能优势,这是由依赖处理方式决定的,编译时依赖处理(rollup)自然比运行时依赖处理(webpack)性能更好,但对循环依赖的处理不是百分百可靠。尽量通过内部实现(或设计)来避免,解决循环依赖的常用技巧有:
依赖提升,把需要相互依赖的部分提升一层
依赖注入,运行时从模块外部注入依赖
依赖查找,运行时由模块内部查找依赖
依赖提升针对不合理的设计,此类循环依赖是本能够避免的,例如A->B, B->A
可能可以通过提出C
来转换为A->C, B->C
对于无法避免的循环依赖,可以通过运行时依赖注入和依赖查找来解决,例如factory->A, A->factory
,一种简单的依赖注入方案是:
// factory.js
import A from './A';
export create() {
// 构造函数注入
return new A(create);
// 属性注入
// let a = new A();
// a._createFromFactory = create;
// return a;
}
// A.js
class A {
constructor(create) {
this._createFromFactory = create;
}
// Will be injected from factory
_createFromFactory() {
return null;
}
}
所以循环依赖是可以从设计/实现上解决的,不是大问题
就应用场景而言,rollup最适合打包成单文件,因为目前rollup对multi entry不很友好(公共依赖项都提不出来)。另外,稳定性及插件生态、文档等还不如webpack,但在苛求性能的场景,rollup是唯一的选择