概述

Rollup同样是一块ES Module的打包器,那他也可以将我们项目当中散落的细小模块打包为整块的代码,从而使这些划分的模块可以更好的运行在浏览器环境,或者是nodeJs环境。

那从作用上来看Rollup与webpack非常类似,不过相比于webpack,Rollup要小巧的多。

因为webpack再去配合一些插件的使用时几乎可以完成我们开发过程中前端工程化的绝大多数工作,而Rollup他仅仅可以说是一个ES Module的打包器,并没有其他额外的功能。

那例如webpack中有对于我们开发者十分友好的hmr也就是模块热替换这样的功能,在Rollup中就没办法支持。

那Rollup诞生的目的呢并不是要与webpack之类的工具去全面竞争,他的初衷只是希望能够提供一个高效的ES Module的打包器,然后充分利用ES Module的各项特性构建出结构比较扁平,性能比较出众的类库。

至于他其他的一些特点和优势,我们需要上手过后,才能了解。

Rollup使用

这里我们准备了一个简单的示例,在这个示例中我们使用的是ES Module的方式组织的代码模块化,在这个示例的源代码当中准备了三个文件,其中message.js中以默认导出的方式导出了一个对象。

export default {
    hi: 'Hey Guys, I am yd~'
}

而logger.js当中导出了两个函数成员。

export const log = msg => {
    console.log(msg);
}

export const error = msg => {
    conole.log('------ ERROR ------')
    console.log(msg);
}

在index.js中导入这两个模块,并且使用了他们。

import { log } from './logger';
import message from './message';

const msg = message.hi;
log(msg);

接下来我们尝试使用rollup对这个示例应用打包,我们需要先安装这个模块, 通过开发模式安装。

yarn add rollup --dev

安装完成过后rollup自带了cli程序,我们可以通过cli去使用rollup打包。

我们这里通过yarn rollup运行rollup程序。因为yarn可以自动找到node_modules当中给的cli程序。避免我们手动通过路径去查找cli。

执行过户我们可以发现,在不传递任何参数的情况下,rollup会自动打印出帮助信息,在帮助信息一开始的位置就已经告诉我们rollup的正确用法。我们这里应该通过参数去指定一个打包入口文件。

我们再次执行这个命令, 我们这里的打包入口应该是src下面的index.js文件。

yarn rollup ./src/index.js

此时我们的命令行会报出一个错误,大概的意思是说应该指定一个代码的输出格式,那输出格式的概念我们应该并不陌生,意思就是希望吧ES Modules的代码转换过后,以什么样的格式去输出。

我们这里可以使用--format参数指定输出的格式,这里我们先选择最适合浏览器端的iife 也就是自执行函数的格式。

yarn rollup ./src/index.js --format iife

此时我们打包的结果就被打印到控制台当中了,我们还可以通过--file指定文件的输出路径,我们这里指定到dist文件夹下的bundle.js,这样的话我们的打包结果就会输出到文件当中。

yarn rollup ./src/index.js --format iife --file dist/bundle.js

完成以后我们找到rollup的输出文件,打开这个文件我们第一印象就是rollup他的打包结果惊人的简洁,基本上和我们以前手写的代码是一样的,相比于webpack当中那些大量的引导代码,还有一堆的模块函数,这里的输出结果几乎没有任何的多与代码。

他就是把我们打包过程中各个模块按照模块的依赖顺序先后的拼接到一起,而且此时我们仔细去观察打包结果,你会发现,在我们的输出结果当中他只会去保留那些用到的部分,对于未引用的部分都没有输出。

这是因为rollup默认会自动开启tree-shaking去优化输出的结果,那tree-shaking这样一个概念,最早也就是在rollup这样一个工具中提出的。

(function () {
    'use strict';

    const log = msg => {
        console.log(msg);
    };

    var message = {
        hi: 'Hey Guys, I am yd~'
    };

    const msg = message.hi;
    log(msg);

}());

配置文件

rollup同样支持以配置文件的方式去配置我们打包过程中的各项参数,我们可以在项目的跟目录下去新建一个,rollup.config.js的配置文件。

那这个文件同样是运行在node环境中,不过rollup他自身会额外处理这个配置文件,所以这里我们可以直接使用ES Modules。

那在这个文件中他需要导出一个配置对象,那在这个对象中我们可以通过input属性指定我们打包的入口文件路径。

然后通过output去指定输出的相关配置,那output属性要求是一个对象,在output对象中我们可以使用file属性去指定我们输出的文件名。然后format属性可以用来指定我们输出格式。

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    }
}

我们可以运行rollup命令来测试配置,不过需要注意的是这里我们需要通过--config参数来去表明我们这去使用项目中的配置文件,默认的话他是不回去读取配置文件的,我们必须要使用这样一个参数。

yarn rollup --config rollup.config.js

那你也可以通过这样一个参数去指定不同配置文件的名称,例如我们可以有rollup.production.js或者rollup.development.js这样的对于开发和生产不同的配置文件。

使用插件

rollup自身的功能就是ES模块的合并打包,那如果我们的项目有更高级别的需求,例如我们想去加载其他类型的资源文件,或者是我们要在代码当中导入CommonJS模块,又或者是想要他帮我们编译ECMAScript的新特性。

那这些额外的需求,rollup同样支持使用插件的方式去扩展实现,而且插件是rollup唯一的扩展方式,他不像webpack中划分了loader,plugin和minimize这三种扩展方式。

那这里我们先尝试使用一个可以让我们在代码当中导入JSON文件的插件,那通过这样一个过程我们去了解如何在rollup当中使用插件。

那我们这里使用的插件名字叫做rollup-plugin-json,我们需要先安装这个插件。

yarn add rollup-plugin-json --dev

安装完成过后我们打开配置文件,由于rollup的配置文件可以直接使用ES Modules所以我们这里直接使用import的方式去导入这个插件模块。

那这个模块默认导出的是一个插件的函数,我们可以将这个函数的调用结果添加到配置对象的plugins数组当中。需要注意的是这里我们是将调用的结果放在数组当中,而不是直接将这个函数放进去。

import json from 'rollup-plugin-json';

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json()
    ]
}

那配置好这个插件过后我们就可以在代码中通过import方式导入json文件了,我们回到index.js当中,那这里我们通过import去导入项目根目录下的package.json文件。

那这个package.json当中的每一个属性呢就会作为一个单独的导出成员,我们这里可以提取一下json当中的name和version。然后我们通过log函数把他们打印出来。

import { log } from './logger';
import message from './message';
import { name, version } from '../package.json';

const msg = message.hi;
log(msg);

log(name);
log(version);
yarn rollup --config rollup.config.js

我们回到命令行打包完成之后,我们找到输出的bundle.js, 那此时就能看到json当中的name和version正常被打包进来了,而json当中那些没有用到的属性也都会被tree-shaking移除掉。

(function () {
    'use strict';

    const log = msg => {
        console.log(msg);
    };

    var message = {
        hi: 'Hey Guys, I am yd~'
    };

    var name = "rollup_p";
    var version = "1.0.0";

    const msg = message.hi;
    log(msg);

    log(name);
    log(version);

}());

那这就是我们在rollup中如何去使用插件。

加载NPM模块

rollup默认只能按照文件路径的方式去加载本地的文件模块,那对于node_modules当中那些第三方的模块,他并不能够像webpack一样直接去通过模块的名称去导入对应的模块。

那为了抹平这样的一个差异,rollup官方给出了一个rollup-plugin-node-resolve这样一个插件。

通过使用这个插件我们就可以在代码当中直接去使用模块名称导入对应的模块了,首先我们需要安装这个插件。

yarn add rollup-plugin-node-resolve --dev

在配置文件中,我们需要先将这个模块导入进来,然后我们将这个插件函数的调用结果配置到plugins数组中。

import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve'

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        resolve()
    ]
}

完成以后我们就可以回到代码当中直接去导入node_modules当中的那些第三方的npm模块了。这里我们导入一个loadash-es模块,需要提前安装。那这个模块就是我们非常常见的loadash的ES Modules版本。

导入过后我们就可以使用和这个模块所提供的一些工具方法了。

import _ from 'loadash-es';
import { log } from './logger';
import message from './message';
import { name, version } from '../package.json';

const msg = message.hi;
log(msg);

log(name);
log(version);
log(_.camelCase('hello world'))

此时loadash那些代码就能被打包到bundle.js当中了。这里我们使用的loadsh ES Module的版本而不是使用普通版本的原因是因为rollup默认只能取处理ES Module模块,如果说我们需要去使用普通版本我们需要做额外的处理。

加载CommonJS模块

正如我们刚刚所看到的一样,rollup设计的就是只处理ES Module的模块打包,那如果我们在代码当中去导入CommonJS模块,默认是不被支持的,但是目前还是有大量的npm模块使用CommonJS方式去导出成员。

所以为了兼容这些模块,官网又给出了一个插件,叫做rollup-plugin-commonjs, 我们需要先安装这个模块。

yarn add rollup-plugin-commonjs --dev

在rollup配置文件中,导入这个插件,然后把他配置在plugins中。

import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        resolve()
    ]
}

配置过后我们就可以回到代码当中去使用commonjs模块了,我们先在src下添加一个commonjs模块,cjs.module.js

module.exports = {
    foo: 'bar'
} 

我们回到index.js当中我们尝试通过import去导入这个commonjs模块,那commonjs他的导出整体会作为一个默认导出,我们这里打印出这个默认导出。

import _ from 'loadash-es';
import { log } from './logger';
import message from './message';
import { name, version } from '../package.json';
import cjs from './cjs.module';

const msg = message.hi;
log(msg);

log(name);
log(version);
log(_.camelCase('hello world'));
log(cjs);

打包之后我们可以发现这个commonjs也被打包进bundle.js里面了。

代码拆分

在rollup最新的版本中也进行开始支持代码拆分了,我们同样可以使用符合ES Module标准的动态导入的方式(Dynamic Imports)去实现模块的按需加载,rollup内部也会自动取处理代码的拆分,也就是我们说的分包。

我们在打包入口文件当中一起来尝试一下,我们先注释掉之前的代码,然后我们使用动态导入的方式去导入一下logger对应的这个模块,那这里import方法同样返回的是一个promise对象,在这个promise的then方法里面我们就可以拿到模块导入过后的对象。

由于这里模块导出的成员都会放在这个对象当中,所以我们这里可以使用解构的方式提取出里面的log方法,然后我们使用log方法打印一个日志消息。

// import { log } from './logger';
// import message from './message';

// const msg = message.hi;
// log(msg);

import('./logger').then(({ log }) => {
    log('code splitting');
});
yarn rollup --config

我们打开命令行终端尝试打包,这里我们直接运行打包会报出一个错误,说的是我们使用代码拆分这种方式去打包,他要求我们的format也就是输出格式不能是iife也就是自执行函数这种形式。

原因也很简单,因为自执行函数他会把所有的模块都放到同一个函数当中,他并没有像webpack一样有一些引导的代码,所以说他没有办法实现代码拆分。

那你要想使用代码拆分的话就必须要使用amd或者是commonjs这样一些其他的标准,那我们再浏览器环境中我们只能使用amd标准,所以说我们这里需要使用amd的格式去输出打包结果。

那我们这里继续使用打包命令,我们这里使用--format这样一个参数去覆盖住配置文件当中的format设置。我们把它设置为amd。

再次运行打包这里同样报出的一个错误,说的是我们这里code splitting输出输出多个文件,因为输出多个文件,我们这里就不能在使用file的这种配置方式,因为file是执行一个单个文件输出的文件文件名。

我们如果需要输出多个文件我们可以使用dir的参数。这里我们回到配置文件当中,注释掉原本配置文件当中的file和format,我们添加一个dir属性和一个format属性,我们将输出的目录设置为dist,将输出的格式设置为amd。

export default {
    input: 'src/index.js',
    output: {
        // file: 'dist/bundle.js',
        // format: 'iife'
        dir: 'dist',
        format: 'amd'
    },
}
yarn rollup --config

打包完成过后我们就可以看到dist目录中,这里他就会根据我们刚刚的动态导入生成一个入口的bundle.js以及我们动态导入的那个bundle。他们都是采用amd的标准输出的。

多入口打包

rollup同样支持多入口打包,而且对于不同入口当中那些公共的部分,也会自动提取到单个文件当中,作为独立的bundle,那我们具体来看如何去配置。

在我们的实例中分别有两个入口,分别是index和album, 他们共用的fetch.js和logger.js这两个模块。

index.js

import fetchApi from './fetch';
import { log } from './logger';

fetchApi('/posts').then(data => {
    data.forEach(item => {
        log(item);
    })
})

album.js

import fetchApi from './fetch';
import { log } from './logger';

fetchApi('/photos?albumId=1').then(data => {
    data.forEach(item => {
        log(item);
    })
})

fetch.js

export default endpoint => {
    return fetch('xxxxx').then(response => response.json())
}

logger.js

export const log = msg => {
    console.log(msg);
}

export const error = msg => {
    conole.log('------ ERROR ------')
    console.log(msg);
}

我们在配置文件当中配置多入口打包的方式非常简单,我们只需要将input属性修改为一个数组就可以了,也可以使用与webpack相同的对象配置方式,不过这里我们需要注意,因为多入口打包,内部会自动提取公共模块,也就是说我们内部会使用代码拆分。

那这里我们就不能再去使用iife这种输出格式了,我们需要将输出格式修改为amd。

export default {
    // input: ['src/index.js', 'src/album.js'],
    input: {
        foo: 'src/index.js',
        bar: 'src/album.js'
    },
    output: {
        dir: 'dist',
        // format: 'iife',
        format: 'amd'
    },
}

打包过后我们dist目录下就会多出三个js文件,分别是两个不同打包入口的打包结果和我们公共提取出来的公共模块。

另外我们需要注意一点的是,对于amd这种输出格式的js文件, 我们不能直接去引用到页面上,而必须要去通过实现amd标准的库去加载,那这个我们之前在介绍amd标准的时候就应该有所了解了。

我们这里在dist目录下手动去创建一个html文件。然后尝试在html当中去使用打包生成的bundle.js, 我们这里采用requirejs的库去加载以amd标准输出的bundle。

require可以通过data-main这样一个参数来去制定我们require他去加载的模块的入口模块路径。我们这里就是foo.js

<body>
    <script src="...require.js" data-main="foo.js"></script>
</body>

完成以后我们启动一个服务,打开浏览器查看,你就可以看到我们的打包结果正常的加载进来了,也正常的工作了。

选用原则

通过以上的探索和尝试我们发现,rollup确实有他的优势,首先就是他输出的结果会更加扁平一些,执行效率自然就会更高。

其次就是他会自动取移除那些位引用代码,也就是tree-shaking。

在一个就是他的打包结果基本上跟我们手写的代码是一致的,也就是打包结果对于开发者而言是可以正常阅读的。

但是他的缺点同样也很明显,首先就是我们去加载一些非ES Module的第三方模块就会比较复杂,我们需要配置一大堆插件。

因为这些模块最终都被打包到一个函数当中了,所以说我们没有办法像webpack一样去实现HMR这种模块热替换的这种开发体验。

在一个就是在浏览器环境中,代码拆分必须要使用像requireJS这样的amd库,因为他的代码拆分就必须要使用像amd这样的输出格式。

那综合以上的这些特点,我们发现,如果我们是正在开发一个应用程序,那我们肯定要面临大量引入第三方模块这样的需求。同时我们又需要像HMR这样的功能去提升我们的开发体验。而且我们应用一旦大了以后还需要必须要去分包。

那这些需求呢rollup他在满足上都会有一些欠缺。

如果我们正在开发的是一个js的框架或者是一个类库,那这些优点就特别有必要,而这些缺点基本可以忽略。

我们就拿加载第三方模块来说,在我们开发类库的时候会很少的在我们代码当中去依赖一些第三方模块。

所以说很多像react或者vue之类的框架中他都是使用rollup作为模块打包器,而并非是webpack。

但是到目前为止,开源社区中大多数人还是希望这两个工具可以共同存在共同发展,并且能够相互支持和借鉴。原因也很简单,就是希望能够让更专业的工具去做更专业的事情。

总结一下就是Webpack他的感觉是大而全,而Rollup是小而美。

在对他们两者之前的选择上,我们基本的原则就是如果我们正在开放应用程序,我们建议大家使用Webpack, 如果你是正在开发类库或者开发框架的话那我们建议选择Rollup。不过这也并不是一个绝对的标准。只是一个经验法则。

因为Rollup他同样也可以去构建绝大多数的应用程序,而Webpack也同样可以去构建类库或者是框架。只不过相对来讲的话术业有专攻的感觉。

另外一点就是随着近几年Webpack的发展Rollup中的很多优势几乎已经被抹平了,例如像我们Rollup当中这种扁平化输出,我们在Webpack中就可以死使用concatenateModules这样一个插件去完成。也可以实现类似的这样一个输出。