Rollup
Rollup 同样也是一款 ES Module 的打包器,它也可以将我们项目中散落的细小模块打包为整块的代码,从而可以使这些划分的模块可以更好的运行在浏览器或者 Nodejs 环境中。
从作用上来看,Rollup 与 webpack 非常类似,不过相比于 webpack,Rollup 要小巧的多。webpack 在配合一些插件的试用下,几乎可以完成我们开发过程总前端工程化的绝大多数工作,而 Rollup 仅仅可以说是一款 ESM 打包器,并没有其他额外的功能。
例如 webpack 中有对我们十分友好的 HMR,在 Rollup 中并不支持类似 HMR 这种高级特性。
Rollup 并不是要与 webpack 全面竞争,它的初衷只是希望提供一个充分利用 ESM各项特性的高效打包器,充分利用 ESM 的各项特性,构建出结构比较扁平,性能比较出众的类库。
快速上手
首先有一个小的demo,项目结构如下:
src/logger.js:
export const log = msg => {
console.log('---------- INFO ----------')
console.log(msg)
console.log('--------------------------')
}
export const error = msg => {
console.error('---------- ERROR ----------')
console.error(msg)
console.error('---------------------------')
}
src/messages.js:
export default {
hi: 'Hey Guys, I am zce~'
}
src/index.js:
// 导入模块成员
import { log } from './logger'
import messages from './messages'
// 使用模块成员
const msg = messages.hi
log(msg)
开始使用 rollup,首先安装依赖 yarn add rollup --dev。
然后运行以下命令,rollup 后边是入口文件,传入参数 --format 为 iife 表示编译浏览器端的结果
yarn rollup ./src/index.js --format iife
可以在命令行看到编译结果,想要输出到 dist 中,可以执行
yarn rollup ./src/index.js --format iife --file dist/bundle.js
运行后看到 dist/bundle.js 中打包结果:
(function () {
'use strict';
const log = msg => {
console.log('---------- INFO ----------');
console.log(msg);
console.log('--------------------------');
};
var messages = {
hi: 'Hey Guys, I am zce~'
};
// 导入模块成员
// 使用模块成员
const msg = messages.hi;
log(msg);
}());
可以发现 rollup 的打包结果惊人的简洁,就跟我们以前手写的代码是一样的,相比于 webpack 中大量的引导代码和一堆的模块函数,这里的输出结果几乎没有任何多余的代码,它就是把我们的模块按照引入的顺序先后拼接在一起。而且仔细观察打包结果会发现,打包结果中只会保留用到的部分,对于未引用的部分都没有输出,这是因为 rollup 默认会开启 Tree Shaking,去优化我们输出的结果。Tree Shaking 这样的一个概念,最早也是在 Rollup 中提出的。
配置文件
Rollup 同样支持以配置文件的方式打包。在项目根目录下新建 rollup.config.js 文件。这个文件同样也可以运行在 node 环境中,不过 rollup 自身会额外处理这个配置文件,所以可以直接使用 ESM。
rollup.config.js:
export default {
// input: 指定打包入口文件路径
input: 'src/index.js',
// output: 指定输出的相关配置
output: {
// file: 指定输出的文件名
file: 'dist/bundle.js',
// format: 指定输出格式
format: 'iife'
}
}
运行时需要用 --config 参数表明要使用项目中的配置文件,默认是不会读取配置文件的。 yarn rollup --config
也可以通过这个参数指定不同配置文件的名称,例如 .dev, .prod,等针对不同环境的配置文件
yarn rollup --config rollup.config.js
或者 yarn rollup --config rollup.dev..js
使用插件
Rollup 自身的功能就只是 ESM 模块的合并打包,如果项目有更高级的需求,例如想加载其他类型的资源模块,或者要在代码中导入 CommonJS 模块,又或者想要它区编译 ECMAScript 新特性,这些额外的需求,rollup 同样支持使用插件的方式实现,而插件是 Rollup 唯一的扩展途径。
插件是 Rollup 唯一的扩展途径,它不像 webpack 有 loader, plugins, minimizer 三种扩展方式.
我们先尝试使用可以让我们在代码中导入 JSON 文件的插件,通过这个过程了解如何在 rollup 中使用插件。这个插件叫 rollup-plugin-json.
// 这个插件默认导出的是插件函数
import json from 'rollup-plugin-json'
export default {
// input: 指定打包入口文件路径
input: 'src/index.js',
// output: 指定输出的相关配置
output: {
// file: 指定输出的文件名
file: 'dist/bundle.js',
// format: 指定输出格式
format: 'iife'
},
plugins:[
// 将插件的函数调用结果放入 plugins 的数组中。
json()
]
}
这样我们就可以在代码中导入 json 文件了。我们尝试导入 package.json 文件,这个文件中的属性就会作为单独的导出成员,然后我们打印它们。
src/index.js:
// 导入模块成员
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 使用模块成员
const msg = messages.hi
log(msg)
log(name)
log(version)
运行 yarn rollup --config
在 bundle.js 中可以看到 name 和 version 被打包进来了。package.json 中没有被引用到的属性会被 Tree Shaking 移除掉。
(function () {
'use strict';
const log = msg => {
console.log('---------- INFO ----------');
console.log(msg);
console.log('--------------------------');
};
var messages = {
hi: 'Hey Guys, I am zce~'
};
var name = "01-getting-started";
var version = "0.1.0";
// 导入模块成员
// 使用模块成员
const msg = messages.hi;
log(msg);
log(name);
log(version);
}());
加载 npm 模块
Rollup 默认只能按照文件路径加载模块,对于 node_modules 中的第三方模块,不能像 webpack 一样直接通过模块的名称导入对应的模块。为了抹平这样一个差异,rollup 官方给出了一个插件 - rollup-plugin-node-resolve,通过使用这个插件就可以在代码中直接使用模块名称导入对应的模块了。
在 rollup.config.js 中引入插件 import resolve from 'rollup-plugin-node-resolve'
,然后在 plugins 中添加 resolve()
,就可以在项目中使用名称引入 node_modules 中的模块了。
我们来使用一下,装一个第三方依赖‘lodash-es’,这是 lodash 模块的 es 版本,在 index.js 中引入。
src/index.js:
// 导入模块成员
import _ from 'lodash-es'
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 使用模块成员
const msg = messages.hi
log(msg)
log(name)
log(version)
log(_.camelCase('hello world'))
执行 yarn rollup --config
进行打包,bundle.js 中就有了 loadsh 的方法
这里使用 lodash-es,而不是 lodash 是因为rollup 默认只能处理 ESM 模块,如果需要使用普通版本,需要做额外的处理。
加载 CommonJS 模块
目前还是会有大量 npm 模块使用 CommonJS 导出成员,所以为了兼容这些模块,官方给出了一个插件 - rollup-plugin-commonjs。
新建文件 src/cjs.module.js:
module.exports = {
foo: 'bar'
}
src/index.js:
// 导入模块成员
import _ from 'lodash-es'
import { log } from './logger'
import messages from './messages'
import { name, version } from '../package.json'
// 引入 CommonJS 文件
import cjs from './cjs.module'
// 使用模块成员
const msg = messages.hi
log(msg)
log(name)
log(version)
log(_.camelCase('hello world'))
log(cjs)
打包后插件 bundle.js 会发现,CommonJS 中的内容以一个对象的形式出现在我们的文件中了
Code Splitting - 代码拆分
可以使用 ESM 的动态导入的方式实现模块的按需加载,rollup 内部也会自动处理代码的拆分,也就是分包。
src/index.js 中代码都注释掉,然后使用动态加载的方式来写。
import('./logger').then(({ log }) => {
log('code splitting~')
})
import('./logger') 动态引入模块,并返回一个 Promise 对象,在它的 then 方法中可以拿到文件导出的内容,由于导出的是多个函数,可以通过解构的方式拿到想要的方法。
执行打包命令,会报错 IIFE 格式输出不支持代码拆分。
原因很简单,因为 IIFE - 自执行函数会把所有的模块都放入同一个函数当中,并没有像 webpack 一样有一些引导代码,所以说它没有办法实现代码拆分。
想要使用代码拆分就要使用 AMD 或是 CommonJS 这样的一些其他标准,在浏览器中只能使用 AMD 的标准。
执行yarn rollup --config --format amd
,传入参数 --format 为 amd 覆盖默认的格式,执行后还是报错。
报错内容是说当使用 Code Splitting 时需要输出多个文件,不能再使用 output.file 的配置方式,因为 file 是指定单个文件,需要输出多个文件可以使用 dir 的参数。修改后:
再次运行 yarn rollup --config
打包,就会输出 index 和动态导入的 log 的 bundle,他们都是通过 amd 输出的。
多入口打包
Rollup 同样支持多入口打包,而且对于不同入口那些公共的部分也会自动提取到独立的文件中作为独立的 bundle。
有这样一个 demo,src 中的 index.js 和 album.js 共用了src/fetch.js 和 src/logger.js。
rollup 的多入口打包非常简单,只需要把 input 设为数组就可以了。
export default {
// 多入口打包
input: ['src/index.js', 'src/album.js'],
output: {
dir: 'dist',
format: 'amd'
}
}
也可以像 webpack 一样设置一个对象,属性名和属性值分别为导入文件的文件名和路径
export default {
// 多入口打包
// input: ['src/index.js', 'src/album.js'],
input: {
foo: 'src/index.js',
bar: 'src/album.js'
},
output: {
dir: 'dist',
format: 'amd'
}
}
打包后会输出三个文件,分别是 foo,bar 和公共模块的文件。
需要注意的是,对于 amd 这种格式输出的 js 文件,我们不能直接引用到页面上,必须要用实现 amd 标准的库区加载。
我们在 dist 目录下手动创建一个文件 index.html,尝试在这个文件中使用打包生成的 bundle。采用 Require.js 这个库来加载 amd 标准输出的 bundle。
<script src="https://unpkg.com/requirejs@2.3.6/require.js" data-main="foo.js"></script>
Rollup / Webpack 选用原则
Rollup 优点:
- 输出结果更加扁平,执行效率自然就会更高
- 自动移除未引用代码
- 打包结果依然完全可读
Rollup 缺点:
- 加载非 ESM 的第三方模块比较复杂
- 模块最终都被打包到一个函数中,无法实现 HMR
- 浏览器环境中,代码拆分功能依赖 AMD 库
如果我们正在开发应用程序,肯定需要大量引入第三方模块,也需要 HMR 提升我们的开发体验,而且我们的应用一旦大了,必须要去分包,这些需求在满足上都有所欠缺。
而如果我们开发的是一个框架或者类库,这些优点就特别有必要,而这些缺点都可以忽略。
所以很多知名框架、库都在使用 Rollup 打包器,而并非 Webpack。
但是目前为止,社区中还是希望这两者能够共同存在,共同发展,并且能够相互支持和借鉴,就是希望能够让更专业的工具做更专业的事情。
webpack 大而全,rollup 小而美。
- 应用程序 - webpack
- 库/框架 - rollup
不过随着近几年的发展,rollup 中的这些优势已经被抹平了,例如它的扁平化输出,webpack 中可以使用插件完成。
Parcel
Parcel - 零配置的前端应用打包器。提供了近乎傻瓜式的体验。我们只需要了解它提供的几个简单的命令,就可以直接使用它构建我们的前端应用程序。
快速上手
创建一个 parcel-demo 的项目,通过 yarn init 初始化一个 package.json 文件。安装 parcel-bundler。
新建 src/index.html 作为打包入口(parcel 支持任意类型的文件作为打包入口,但是官方还是建议使用 html 类型的文件作为打包入口,给出的理由是 html 是应用在浏览器端的入口)。
在入口文件中可以正常编写,也可以正常引入资源文件,被引入的资源最后都会被 parcel 打包到一起,最终输出到输出目录。
在 html 中引入 main.js
src/main.js:
import foo from './foo'
foo.bar()
src/foo.js:
export default {
bar:() => {
console.log('hello bar~')
}
}
Parcel 同样支持对 ESM 的打包。
执行yarn parcel src/index.html
后,parcel 不仅进行了打包,还启动了一个开发服务器。
打开这个地址,打开控制台,尝试修改foo.js 中打印的值,会发现浏览器会自动刷新执行最新的打包结果。
HMR
如果需要的是模块热替换的体验,parcel 同样也支持。
src/index.js:
import foo from './foo'
foo.bar()
// 先判断是否存在
if (module.hot) {
// 处理模块热替换的逻辑
// 这里的 accept 和 webpack 提供的 api 不一样
// webpack 中支持接受两个参数,用来处理指定模块更新过后的逻辑
// 而 parcel 提供的 accept 只可以接受一个参数
// 作用就是当当前这个模块更新,或当前模块所依赖的模块更新后,自动执行
module.hot.accept(() => {
console.log('hmr')
})
}
自动安装依赖
除了热替换的功能,parcel 还可以自动安装依赖。试想一下,你正在开发一个应用,突然想要使用某个第三方模块,此时就需要先停止正在运行的dev serve,然后去安装依赖,再重新启动 Dev serve,有了自动安装依赖的功能,我们就不用这么麻烦了。
我们只需要在开发的时候直接引入使用,保存以后 parcel 会自动安装依赖并重新编译。
其他类型资源模块
parcel 同样支持加载其他类型的资源模块,而且相比于其他的模块打包器,在 parcel 中加载任意类型的资源模块,同样还是零配置。
例如添加一个 css 样式文件,或者图片,在 index 中导入,它可以立即生效,不需要添加额外的插件或loader。
Parcel 希望给开发者的体验就是,你想要做什么你就只管去做,额外的事就由工具负责处理。另外 Parcel 同样支持动态导入,如果使用了动态导入,也会自动拆分代码。
生产模式打包
yarn parcel build src/index.html
打包后的文件都会被压缩,而且样式代码也都单独提取到单个文件中了。
对于相同体量的项目打包,parcel 会比 webpack 快很多,因为在 parcel 的内部使用的是多进程同时工作,充分发挥了多核CPU的性能,webpack 中也可以使用一个叫做 happypack 的插件实现这一点。