原文地址


经过以上对三个打包工具多方面的使用对比,相信大家都有了一个初步的印象;我们来简单总结一下三个打包工具的使用环境:

  • 如果我们需要构建一个简单的小型应用并让它快速运行起来,可以使用Parcel;
  • 如果需要构建一个类库只需要导入很少第三方库,可以使用Rollup;
  • 如果需要构建一个复杂的应用,需要集成很多第三方库,并且需要代码分拆、HMR等功能,推荐使用Webpack。

我们在之前用了两篇文章来介绍了Webpack的配置和优化,那么为什么这篇文章还要来对比Parcel和Rollup呢?配置过Webpack的童鞋可能会发现了,虽然它具有很高的可配置性和扩展性,以及丰富的插件系统,但是这些无一不给我们的上手带来限制,有很高的上手门槛。相信大多童鞋在配置时遇到都会遇到莫名其妙的报错和各种查资料的烦恼,那么今天就来看一下Parcel和Rollup能给我们的打包带来哪些便利。

Parcel

Parcel官网的定义就是极速零配置Web应用打包工具,它利用多核处理提供了极快的速度,并且不需要任何配置。

它有以下优点:

  • 极速打包:Parcel使用worker进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。
  • 将你所有的资源打包:Parcel 具备开箱即用的对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。
  • 自动转换:如若有需要,Babel, PostCSS, 和PostHTML甚至 node_modules 包会被用于自动转换代码.
  • 零配置代码分拆:使用动态import()语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。
  • 热模块替换:Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。
  • 友好的错误日志:当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。

我们从以下几个方面分别来看下Parcel如何进行配置。

入口文件

Parcel可以使用任何类型的文件作为入口,但是最好还是使用Html或者JS文件。我们新建一个项目,创建html和js文件:

  1. <!-- index.html -->
  2. <html>
  3. <body>
  4. <script src="./index.js"></script>
  5. </body>
  6. </html>
  1. //index.js
  2. console.log('hello parcel')

文件创建好后我们就可以启动Parcel了,它默认内置了一个用于开发环境的服务器,如果将入口设置为js文件,我们可以在浏览器里查看到js文件内容,但这样看不到任何页面效果,因此我们将入口设置为html文件:

  1. parcel index.html

现在可以在浏览器打开http://localhost:1234/,也可以添加-p 覆盖默认的1234端口号;这个开发服务器也支持模块热更新,我们改变js或者html内容,浏览器就会自动更新。同时,Parcel也支持多页面入口,假设我们的项目结构如下:

  1. - pages
  2. - home
  3. index.html
  4. index.js
  5. - list
  6. index.html
  7. index.js

我们可以繁琐的手动指定需要加载入口的文件名称,比如:

  1. parcel ./pages/home/index.html ./pages/list/index.html

也可以通过glob文件匹配模式来匹配所有的html文件:

  1. parcel ./pages/**/*.html

这样,我们就可以通过http://localhost:1234/home/index.html来访问到页面了。
我们发现在入口文件这方面,Parcel相较于Webpack要灵活不少,在Webpack中我们需要通过entry字段来指定入口,而且入口只能是JS文件,要以html为入口还需要使用第三方插件如html-webpack-plugin;而Parcel在入口文件则宽松很多,可以用html文件作为入口,Parcel会自动加载html文件中引用的脚本或者样式进行打包;我们甚至还可以将一张图片或者vue文件直接作为入口文件。

模块转换

通常打包工具只知道如何处理JS文件,在处理其他资源时,比如less/sass、图片、vue文件等,都需要通过通过转换器进行转换,比如webpack的loader就是充当了转换器的角色;而Parcel支持许多开箱即用的转换器,比如在项目根目录配置.babelrc和.postcssrc,Parcel就会自动在所有的JS和css上进行转换。
再比如我们一般在项目中使用sass或者less,然后在js中import来引入样式,如果在webpack中也是需要通过配置loader才能解析;而我们在Parcel可以不通过任何配置直接import进来,Parcel会自动给我们安装sass的依赖包:
image.png

代码分割

虽然项目代码的增多,打包出来的文件也会随之增大;通过代码分割我们可以将首屏不加载的页面或者模块拆分到独立的包中,然后进行按需加载。
在Webpack中实现代码分割主要是通过配置splitChunks和模块内的内联函数动态引入来实现代码分割,而Parcel支持零配置代码分割,并且开箱即用。我们可以将代码拆分成单独的包,这些包可以按需加载;主要是通过动态import()函数来实现,这个函数与普通的import语句类似,但返回一个Promise对象:

  1. //about.js
  2. export function render() {
  3. console.log("about run");
  4. }
  5. //index.js
  6. let body = document.getElementsByTagName('body')[0]
  7. body.addEventListener("click", ()=>{
  8. import("./about.js").then((page) => {
  9. page.render();
  10. console.log("index import");
  11. });
  12. });

这样,Parcel在打包时会将about.js单独进行打包,然后点击body时才按需加载。

模块热更新

模块热更新(HMR)我们在Webpack中也介绍过,是通过在运行时自动更新浏览器中的模块而无需刷新整个页面,从而改善了开发体验;其实现的原理主要是在服务器和浏览器之间维护了一个websocket连接,服务器自动推送每次代码改变。
在webpack中主要是通过官方的webpack-dev-server服务器来实现的,而在Parcel中HMR服务器是内置的,开箱即用,在启动开发环境服务器时自动启用,无需配置。

Tree Shaking

Parcel不支持Tree Shaking

小结

我们体验完整个Parcel,发现确实一行配置代码都没有写,和官网的slogan极速零配置确实符合;在Webpack最难配置的模块转换方面,Parcel做到了自动识别处理导入的模块,这是让我们眼前一亮的地方;而且内部启用了多进程工作,在打包相同体量的项目时,Parcel会比Webpack快很多;不过Webpack的很多功能Parcel也并不具备,比如常用的Tree Shaking、提取公共代码等,因此我们在使用它时需要考虑到这些功能是否是我们项目所必须的。

Rollup

我们有时候在开发一些自己项目中用的JS类库时,比如弹框组件、校验组件、工具组件等,如果使用Webpack,在打包时会产生很多冗余代码,导致一个简单的类库打包出来体积也比较庞大;而Rollup就是专门针对类库进行打包,它的优点是小巧而专注,因此现在很多我们熟知的库都都使用它进行打包,比如:Vue、React和three.js等。
image.png
Rollup 是一个JavaScript模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。

入口文件

Rollup既然是打包类库文件,那么它的入口也就只能是JS文件了(通过第三方插件可以支持Html,这里不作展开),因此我们新建一个main.js作为入口文件,打包出来的文件我们命名为bundle.js,我们可以简单的通过命令行进行打包:

  1. # 针对浏览器环境打包
  2. rollup main.js --file bundle.js --format iife
  3. # 针对Nodejs环境打包
  4. rollup main.js --file bundle.js --format cjs

但是和Webpack一样,推荐使用配置文件进行打包

  1. # 默认使用rollup.config.js配置文件
  2. rollup --config
  3. # 使用自定义my.config.js配置文件
  4. $ rollup --config my.config.js

我们来看下rollup.config.js配置文件需要包含哪些选项:

  1. export default {
  2. // 核心选项
  3. input, // 必须
  4. external,
  5. plugins,
  6. // 额外选项
  7. onwarn,
  8. // 高危选项
  9. acorn,
  10. context,
  11. moduleContext,
  12. legacy
  13. // 必须 (如果要输出多个,可以是一个数组)
  14. output: {
  15. // 核心选项
  16. file, // 必须
  17. format, // 必须
  18. name,
  19. globals,
  20. // 额外选项
  21. paths,
  22. banner,
  23. footer,
  24. intro,
  25. outro,
  26. sourcemap,
  27. sourcemapFile,
  28. interop,
  29. // 高危选项
  30. exports,
  31. amd,
  32. indent
  33. strict
  34. },
  35. };

我们发现这里input、output.file和output.format都是必传的,因此,一个基础配置文件如下:

  1. // rollup.config.js
  2. export default {
  3. input: "main.js",
  4. output: {
  5. file: "bundle.js",
  6. format: "cjs",
  7. },
  8. };

这里的format字段大家看了可能不太理解,尤其是里面的cjs代表什么意思;由于JS有多种模块化方式,Rollup可以针对不同的模块规范打包出不同的文件,它有以下五种选项;

  • amd: 异步模块定义,用于像RequireJS这样的模块加载器
  • cjs:CommonJS,适用于 Node 和 Browserify/Webpack
  • es:ES模块文件
  • iife:自执行模块,适用于浏览器环境script标签
  • umd:通用模块定义,以amd,cjs 和 iife 为一体

和Webpack一样,Rollup也支持配置多个文件入口,我们新建foo.js和bar.js两个入口文件:

  1. export default {
  2. input: {
  3. foo: "./foo.js",
  4. bar: "./bar.js",
  5. },
  6. output: {
  7. dir: "dist",
  8. format: "cjs",
  9. },
  10. };

插件

插件拓展了Rollup处理其他类型文件的能力,它的功能有点类似于Webpack的loader和plugin的组合;不过配置比webpack中要简单很多,不用逐个声明哪个文件用哪个插件处理,只需要在plugins中声明,在引入对应文件类型时就会自动加载;我们来看几个常用的插件。
首先是rollup-plugin-json,让Rollup可以从JSON文件中读取数据:

  1. import json from "rollup-plugin-json";
  2. export default {
  3. input: "main.js",
  4. output: {
  5. file: "bundle.js",
  6. format: "iife",
  7. },
  8. plugins: [json()],
  9. };

Rollup默认只能加载ES6模块的js,但是我们项目中通常也会用到CommonJS的模块,这样Rollup解析就会出现问题,比如:

  1. //add.js
  2. let count = 1;
  3. let add = () => {
  4. return count + 1;
  5. };
  6. module.exports = {
  7. count,
  8. add,
  9. };
  10. //main.js
  11. //报错
  12. import add from "./add";
  13. console.log("foo", add.count);

由于ES6模块导入默认会去找default,因此这里打包会报错;这时就需要用到rollup-plugin-commonjs插件来进行转换:

  1. import commonjs from "rollup-plugin-commonjs";
  2. export default {
  3. input: "./main.js",
  4. output: {
  5. file: "bundle.js",
  6. format: "iife",
  7. },
  8. plugins: [commonjs()],
  9. };

而且目前npm中大多数依赖包都是以CommonJS模块形式出现,都需用通过这个插件来进行模块化解析;除此之外,我们引用node_modules中的第三方模块,还需要用到rollup-plugin-node-resolve进行解析,这两个插件通常组合使用:

  1. import resolve from "rollup-plugin-node-resolve";
  2. import commonjs from "rollup-plugin-commonjs";
  3. export default {
  4. input: "./main.js",
  5. output: {
  6. file: "bundle.js",
  7. format: "iife",
  8. },
  9. plugins: [commonjs(), resolve()],
  10. };

除此之外还有很多有用的插件,这里就不一一赘述使用方法,列出来有需要的童鞋可以根据情况进行使用:

  • rollup-plugin-vue:解析vue文件
  • rollup-plugin-postcss:解析、转换、提取css文件
  • rollup-plugin-alias:提供modules名称的alias和reslove功能
  • rollup-plugin-babel:babel转换
  • rollup-plugin-eslint:eslint代码检测
  • rollup-plugin-uglify:js代码压缩
  • rollup-plugin-replace:类似Webpack的DefinePlugin, 可在源码中通过process.env.NODE_ENV用于构建区分环境.


模块热更新

Rollup本身不支持启动开发服务器,我们可以通过rollup-plugin-serve第三方插件来启动一个静态资源服务器:

  1. import serve from "rollup-plugin-serve";
  2. export default {
  3. input: "./main.js",
  4. output: {
  5. file: "dist/bundle.js",
  6. format: "iife",
  7. },
  8. plugins: [serve("dist")],
  9. };

不过由于其本质就是一个静态资源的服务器,因此不支持模块热更新

Tree Shaking

由于Rollup本身支持ES6模块化规范,因此不需要额外配置即可进行Tree Shaking

代码分割

Rollup代码分割和Parcel一样,也是通过按需导入的方式;但是我们输出的格式format不能使用iife,因为iife自执行函数会把所有模块放到一个文件中,可以通过amd或者cjs等其他规范。

  1. export default {
  2. input: "./main.js",
  3. output: {
  4. //输出文件夹
  5. dir: "dist",
  6. format: "amd",
  7. },
  8. };

这样我们通过import()动态导入的代码就会单独分割到独立的js中,在调用时按需引入;不过对于这种amd模块的文件,不能直接在浏览器中引用,必须通过实现AMD标准的库加载,比如Require.js。

小结

通过对Rollup的使用介绍,我们发现它有以下优点:

  1. 配置简单,打包速度快
  2. 自动移除未引用的代码(内置tree shaking)

但是他也有以下不可忽视的缺点:

  1. 开发服务器不能实现模块热更新,调试繁琐
  2. 浏览器环境的代码分割依赖amd
  3. 加载第三方模块比较复杂


总结

经过以上对三个打包工具多方面的使用对比,相信大家都有了一个初步的印象;我们来简单总结一下三个打包工具的使用环境,如果我们需要构建一个简单的小型应用并让它快速运行起来,可以使用Parcel;如果需要构建一个类库只需要导入很少第三方库,可以使用Rollup;如果需要构建一个复杂的应用,需要集成很多第三方库,并且需要代码分拆、HMR等功能,推荐使用Webpack。