1. 概述

Rollup是一款基于ES Module的打包器,类似于webpack可以将项目中散落的细小模块打包为整块的代码,不用点是Rollup打包的模块可以更好的运行在浏览器或nodeJs环境。

从作用上来看Rollupwebpack非常类似,相比于webpack来说Rollup要小巧很多。webpack配合插件几乎可以完成前端工程化的绝大多数工作。Rollup仅是ES Module的打包器,并没有其他额外的功能。

webpack有对于开发者十分友好的hmr功能Rollup中则无法支持。Rollup诞生的目的并不是与webpack工具竞争,他的初衷只是希望提供一个高效的ES Module打包器,充分利用ES Module的各项特性构建出结构比较扁平,性能比较出众的类库。

2. 使用

这里准备一个简单的示例,使用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程序,通过yarn rollup运行程序。可以发现,在不传递任何参数的情况下rollup会自动打印出帮助信息,帮助信息开始的位置就表示应该通过参数去指定一个打包入口文件。打包入口是src下面的index.js文件。

yarn rollup ./src/index.js

此时还应该指定一个代码的输出格式,可以使用--format参数指定输出的格式比如iife也自执行函数的格式。

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

还可以通过--file指定文件的输出路径,比如这里指定dist文件夹下的bundle.js,这样打包结果就会输出到dist文件当中。

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

rollup打包结果惊人的简洁,基本上和手写的代码是一样的,相比于webpack中大量的引导代码,这里的输出结果几乎没有任何的多余代码。

rollup只是把打包过程中各个模块按照模块的依赖顺序先后的拼接到一起,而且仔细观察打包结果会发现,在输出结果中只会保留用到的部分,对于未引用的部分都没有输出。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);

}());

3. 配置文件

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'
    }
}

需要通过--config参数来表明使用项目中的配置文件,默认是不去读取配置文件的。

yarn rollup --config rollup.config.js

4. 使用插件

rollup自身的功能就是ES模块的合并打包,如果项目有更高级别的需求,例如想去加载其他类型的资源文件,或者是要在代码中导入CommonJS模块,又或者是想要编译ECMAScript的新特性。这些额外的需求,rollup同样支持使用插件的方式扩展实现,而且插件是rollup唯一的扩展方式,他不像webpack中划分了loaderpluginminimize这三种扩展方式。

尝试使用一个可以在代码中导入JSON文件的插件,这里使用的插件名字叫做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文件了。

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中的nameversion正常被打包进来了,而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);

}());

5. 使用Babel和TS

"devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/preset-env": "^7.15.6",
    "@babel/preset-typescript": "^7.15.0",
    "rollup-plugin-babel": "^4.4.0",
    "typescript": "^4.4.3"
}

.babelrc

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-typescript"
    ]
}

别忘了初始化ts的配置文件tsconfig.json

rollup配置文件中plugins加入babel插件。

const babel = require('rollup-plugin-babel');
const extensions = ['.js', '.ts'];

export default {
    input: {
        utils: 'src/index.ts',
    },
    output: {
        dir: 'dist',
        format: 'iife'
    },
    plugins: [
        babel({
          exclude: 'node_modules/**',
          extensions,
        }),
    ]
}

6. 编译less

"devDependencies": {
    "rollup-plugin-less": "^1.1.2",
    "rollup-plugin-postcss": "^3.1.1"
}

rollup配置文件中plugins加入postcss插件。

import postcss from 'rollup-plugin-postcss';

export default {
    input: {
        utils: 'src/index.ts',
    },
    output: {
        dir: 'dist',
        format: 'iife'
    },
    plugins: [
        postcss({
            extensions: [ '.less' ],
        }),
    ]
}

7. 压缩代码

import { uglify } from 'rollup-plugin-uglify';

const isDev = process.env.NODE_ENV !== 'production';

export default {
    input: {
        utils: 'src/index.ts',
    },
    output: {
        dir: 'dist',
        format: 'iife'
    },
    plugins: [
        !isDev && uglify()
    ]
}

8. 加载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模块,这个模块是loadashES 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'))

这里我们使用的loadsh ES Module的版本而不是使用普通版本是因为rollup默认只能取处理ES Module模块,如果需要使用普通版本需要做额外的处理。

9. 加载CommonJS模块

为了兼容CommonJS方式模块官网给出了rollup-plugin-commonjs插件。

yarn add rollup-plugin-commonjs --dev
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: [
        commonjs(),
        json(),
        resolve()
    ]
}

配置过后就可以使用commonjs模块了,在src下添加commonjs模块cjs.module.js文件。

module.exports = {
    foo: 'bar'
} 

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里面了。

10. 代码拆分

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

使用动态导入的方式导入logger对应的模块,import方法返回的是promise对象,在promisethen方法里面可以拿到模块导入过后的对象。

// 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一样的引导代码,所以也就没有办法实现代码拆分。

要想使用代码拆分必须要使用amdcommonjs等其他标准,浏览器环境只能使用amd标准。同样因为输出多个文件,这里就不能使用file的这种配置方式,因为file是执行一个单个文件输出的文件名。

如果需要输出多个文件可以使用dir的参数将输出的目录设置为dist

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的标准输出的。

11. 多入口打包

rollup支持多入口打包,而且对于不同入口中公共的部分,会自动提取到单个文件当中,作为独立的bundle。实例中有两个入口分别是indexalbum,共用fetch.jslogger.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标准的库去加载。

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

require可以通过data-main参数来制定require加载的模块的入口模块路径。

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

12. rollup-watch 监听文件

"devDependencies": {
    "rollup-watch": "^4.3.1"
  },

package.json,文件变化时重新打包。

"scripts": {
    "watch": "rollup -c -w",
}

13. 选用原则

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插件去完成。也可以实现类似的输出。

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 rollup打包工具  "隐冬"
请输入评论...