从webpack到rollup

一.放弃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 ScriptEvaluate Script上,其余部分可以忽略不计,如下图:

rollup-performance

rollup-performance

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

umdcjs有优势,看起来很奇怪,但实际结果确实是这样。看bundle差异主要在于函数名简化,cjsbundle中很多长函数名保留下来了,没有被混淆掉

三.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-rollupexternal-helpers的作用后面介绍

如果想保留ES6风格,需要这样的babel配置:

{
  "presets": [
    ["stage-0"]
  ],
  "plugins": [
    "external-helpers"
  ]
}

转换后得到的是把项目各模块文件拼在一起的ES6模块,代码里的classconstlet都会保留,因为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是唯一的选择

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code