从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 Script和Evaluate Script上,其余部分可以忽略不计,如下图:
【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
umd比cjs有优势,看起来很奇怪,但实际结果确实是这样。看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-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是唯一的选择 参考资料 rollup-plugin-babel Polyfill What are Babel “plugins” and “presets”? (And how to use them) Rollup.js Tutorial, Part 1: How to Set Up Smaller, More Efficient JavaScript Bundling Using Rollup
- 前端打包构建工具Gulp、Rollup、Webpack、Webpack-stream
- webpack与rollup背后的acorn
- Rollup 与 webpack的区别
- 使用模块化工具打包自己开发的JS库(webpack/rollup)对比总结
- vue - vue-cli脚手架安装和webpack-simple模板项目生成
- webpack ---loader,plugin下载命令
- 详解vue+webpack+express中间件接口使用
- Webpack基础教程之名词解释
- webpack & react项目搭建一:环境
- ES6模块化及webpack配置
- webpack3 配置详解
- webpack 加载css模块2 样式处理
- 使用 Babel + React + Webpack 搭建 Web 应用
- webpack-生产环境最佳实践(https://webpack.js.org/guides/production/)
- vue-cli中的webpack配置
- 使用vue脚手架工具搭建vue-webpack项目
- webpack 小技巧:动态批量加载文件
- webpack入门demo(一)打包js
- 使用vue+webpack的多页面框子
- webpack2 项目