写在前面
Rollup was designed with libraries rather than apps in mind, and it is a perfect fit for React’s use case.
在Behind the Scenes: Improving the Repository Infrastructure – React Blog看到了这个,有些惊讶,这样好的东西,为什么只是面向类库呢?什么原因致使它不适合用来构建App?
零.webpack
webpack致力于复杂SPA的模块化构建,非常吸引人的是各种loader:
Essentially, webpack loaders transform all types of files into modules that can be included in your application’s dependency graph.
以一致的方式处理各种资源依赖,通过loader屏蔽掉了资源类型差异(js是module,css是module,img也是module……),优势如下:
No more carefully placing your files in the right folders and hacked-together scripts for adding hashes to file URLs — webpack can take care of it for you.
另一些非常强大的特性包括:
- Code Splitting:生产环境按需加载/并行加载
- Tree Shaking:构建时去掉无用代码(export)
- HMR:开发中模块热替换
- Commons Chunk:构建时提取公共依赖
- Dependency Graph:构建完毕输出模块依赖图,让bundle有了可读性
一.初衷
rollup一开始就是面向ES6 module的:
Next-generation ES6 module bundler.
当时AMD、CMD、UMD的格式之争还很火热,ES6 module还没有浏览器实现。rollup就这样冒了出来
Rollup was created for a different reason: to build flat distributables of JavaScript libraries as efficiently as possible, taking advantage of the ingenious design of ES2015 modules.
(引自Webpack and Rollup: the same but different,rollup作者亲述)
希望充分利用ES6 module机制,构建出结构扁平,性能出众的类库bundle,即面向library设计的
二.核心优势
It solves one problem well: how to combine multiple modules into a flat file with minimal junk code in between.
rollup让人惊艳的是其bundle的干净程度,尤其是iife
格式,内容非常干净,没什么多余代码,真的只是把各模块按依赖顺序,先后拼接起来了
这与rollup的模块处理思路有关:
To achieve this, instead of turning modules into functions like many other bundlers, it puts all the code in the same scope, and renames variables so that they don’t conflict. This produces code that is easier for the JavaScript engine to parse, for a human to read, and for a minifier to optimize.
把所有模块都扁平地放在bundle文件内最外层作用域中,模块之间没有作用域隔离,依靠重命名来解决同一作用域下命名冲突的问题。几个显而易见的好处:
- 运行时性能(代码结构扁平,便于解析)
- bundle源码可读性(自然的顺序结构,没有模块定义/跳转)
- 压缩可优化性(没有模块定义之类的压缩不掉的样板代码)
这样做的缺点也很明显:
- 模块系统过于静态化,HMR之类的特性很难实现
- 仅面向ES6 module,无法可靠地处理cjs,umd依赖(每次用rollup-plugin-commonjs都会遇到问题)
如果只是面向lib的话,第一点不支持也不要紧,但第二点着实头疼,二级依赖是不可控的,总是不可避免地会遇到cjs模块无法转自动换到ES6 module的一些问题,例如:
‘foo’ is not exported by bar.js (imported by baz.js)
一些场景可以按照Troubleshooting通过namedExports
的方式不太愉快地解决,另一些时候通过external
或globals
绕过去,甚至还有需要调整plugin应用顺序的解法……但没有办法彻底解决这类问题:
Webpack gets around the need for namedExports by keeping everything as CommonJS modules and implementing Node’s require system in the browser, which is why the resulting bundles are larger and take longer to start up. Both options are valid, but that’s the tradeoff to be aware of — more config (Rollup, when using modules like React), or larger bundles (webpack).
(引自Is “named exports” feature or bug?)
虽然cjs终将成为历史,但目前以及眼下,npm仍然存在相当多的cjs模块,无论是SPA还是library,仍然面经常临处理cjs模块依赖的问题
三.选用原则
Use webpack for apps, and Rollup for libraries
构建App的话,webpack比较合适,如果是类库,当然rollup更好
webpack构建App的优势体现在以下几方面:
- 强大的插件生态,主流前端框架都有对应的loader
- 面向App的特性支持,比如之前提到的HMR,Code Splitting,Commons Chunk等都是开发App必要的特性
- 简化Web开发各个环节,包括图片自动base64,资源缓存(chunkId),按路由做代码拆分,懒加载等,都不难实现
- 可靠的依赖模块处理,不像rollup面临cjs的问题,
__webpack_require__
没这些烦恼
而rollup没有这些优势,做代码拆分等会遇到一些不太容易解决的问题,没有足够的时间和把握的话,不要轻易尝试把rollup作为App构建工具
rollup的优势在于高效率的bundle,这正是类库所追求的,即便费一点周折(正如React 16所做的),为了性能也是值得的
注意,这个原则只是说用合适的工具做合适的事情,适用于多数一般场景,用rollup构建App,用webpack构建类库的也很常见:
That’s not a hard and fast rule — lots of sites and apps are built with Rollup, and lots of libraries are built with webpack. But it’s a good rule of thumb.
典型的,如果业务本身没太多第三方模块依赖,并且风格约定遵循ES6 module,用rollup构建App也很合适(Code Splitting等也不是完全做不到)
P.S.另外,rollup也不太容易像glup或webpack一样进行基于stream的扩展,比如从一个vue
文件中分离出三部分分别处理(vue插件好像还不支持ts)
四.外部依赖
对于React之类的类库,应该尽可能地作为第三方依赖独立出去,而不是build进bundle,几个原因:
- 性能会变差,比如React 16费了好大劲切换到了rollup GCC(Google Closure Compiler)达到了109kb,自己编一次立马回到解放前
- 不利于缓存,类库不经常更新,可以当做静态资源充分发挥缓存优势,而手动build的内容受工具配置影响
rollup下可以通过external
globals
配置来标记外部依赖:
external: ['react', 'react-dom'],
output: {
globals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
}
这样生成的bundle为:
代码语言:javascript复制// iife
(function (React,ReactDOM) {
//...
}(React,ReactDOM));// cjs
var React = _interopDefault(require('react'));
var ReactDOM = _interopDefault(require('react-dom'));
所以一般把业务代码打包成iife
,再通过script
引用CDN第三方类库:
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<!-- 或者聚合的版本 -->
<script crossorigin src="//cdn.jsdelivr.net/combine/npm/react@16.0.0/umd/react.production.min.js,npm/react-dom@16.0.0/umd/react-dom.production.min.js"></script>
P.S.rollup的external
与globals
有些奇怪,无论是key
还是value
,还是这两个东西竟然要配合使用,更多信息请查看[question] Difference between externals and globals
参考资料
- Webpack and Rollup: the same but different:很有意思的文章,发现得太晚了
- Handling 3rd-party JavaScript with Rollup