Rollup — это сборщик javascript приложений и библиотек нового поколения. Многим он давно знаком как перспективный сборщик, который хорошо подходит для сборки библиотек, но плохо подходит для сборки приложений. Однако время идет, продукт активно развивается.
Я впервые попробовал его в начале 2017 года. Он сразу понравился мне за поддержку компиляции в ES2015, treeshaking, отсутствием модулей в сборке и конечно простым конфигом. Но тогда это был сырой продукт, с небольшим числом плагинов и очень ограниченной функциональностью, и я решил оставить его на потом и продолжил собирать через browserify. Вторая попытка была в 2018 году, тогда он уже значительно оброс комьюнити, плагинами и функционалом, но все еще не хватало качества в некоторых функциях, включая watcher. И вот наконец в начале 2019 года можно смело сказать — с помощью Rollup можно просто и удобно собирать современные приложения.
Для понимания преимуществ пройдемся по ключевым возможностям и сравним с Webpack (для Browserify ситуация такая же).
Простой конфиг
Сразу что бросается в глаза это очень простой и понятный конфиг:
export default [{
input: 'src/index.ts',
output: [{ file: 'dist/index.min.js', format: 'iife' }],
plugins: [
// todo: попозже накидаем сюда плагинов
],
}];
Вводим в косноли rollup -c и ваш бандл начинает собираться. На экспорт можно отдать массив бандлов для сборки, например если вы собираете отдельно полифилы, несколько программ, воркеры и прочее. В input можно подать массив файлов, тогда будут собираться чанки. В output можно подать массив выходных файлов и собирать в разные модульные системы: iife, commonjs, umd.
Поддержка iife
Поддержка сборки в само вызываемую функцию без модулей. Для понимания давайте возьмём самую известную программу:
console.log("Hello, world!");
прогоним её через Rollup в формат iife и увидим результат:
(function () {
'use strict';
console.log("Hello, world!");
}());
На выходе получаем очень компактный код, всего 69 байт. Если вы еще не поняли в чем преимущество, то Webpack/Browserify скомпилирует следующий код:
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
console.log("Hello, world!");
/***/ })
/******/ ]);
Как видим получилось «немного» больше из-за того что Webpack/Browserify может собирать только в CommonJS. Большое преимущество IIFE является компактность и отсутствие конфликтов между разными версиями CommonJS. Но есть и один недостаток, нельзя собрать чанки, для них надо переключиться на CommonJS.
Компиляция в ES2015
Название «сборщик следующего поколения» rollup еще в 2016 году получил за умение собирать в ES2015. И до конца 2018 года это был единственный сборщик который умел это делать.
Для примера если взять код:
export class TestA {
getData(){return "A"}
}
console.log("Hello, world!", new TestB().getData());
и прогнать через Rollup, то на выходе мы получим тоже самое. И да! На начало 2019 года уже 87% браузеров могут исполнить его нативно.
Тогда в 2016 году это выглядело прорывом, потому что существовало большое количество приложений которым не нужна поддержка старых браузеров: админки, киоски, не веб приложения, а инструментов сборки под них не было. А сейчас с Rollup мы за один проход можем собрать несколько бандлов, в es3, es5, es2015, exnext и в зависимости от браузера загружать необходимый.
Также большим преимуществом ES2015 является его размер и скорость исполнения. За счет отсутствия транспилинга в более низкий слой код получается значительно более компактным, а за счет отсутствия вспомогательного кода, который генерят транспиллеры, этот код еще и работает в 3 раза быстрее (по моим субъективным тестам).
Tree shaking
Это фишка Rollup, он его придумал! Webpack много лет подряд пытается его внедрить, но только с 4 версии что то начало получаться. У Browserify всё совсем плохо.
Что же это за зверь такой? Давайте для примера возьмем два следующих файла:
// module.ts
export class TestA {
getData(){return "A"}
}
export class TestB {
getData(){return "B"}
}
// index.ts
import { TestB } from './module';
const test = new TestB();
console.log("Hello, world!", test.getData());
прогоним через Rollup и получим:
(function () {
'use strict';
class TestB {
getData() { return "B"; }
}
const test = new TestB();
console.log("Hello, world!", test.getData());
}());
В результате TreeShaking'а еще на этапе разрешения зависимостей был отброшен мёртвый код. Благодаря чему сборки Rollup получаются значительно более компактны. А теперь посмотрим что сгенерирует Webpack:
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/module.ts
class TestA {
getData() { return "A"; }
}
class TestB {
getData() { return "B"; }
}
// CONCATENATED MODULE: ./src/index.ts
const test = new TestB();
console.log("Hello, world!", test.getData());
/***/ })
/******/ ]);
И тут можно сделать два вывода. Первый Webpack в конце 2018 все же научился понимать и собирать ES2015. Второй, абсолютно весь код попадает в сборку, а вот уже удаление мертвого кода происходит минификатором Terser (форк и наследник UglifyES). Результатом такого подхода более толстые бандлы чем у Rollup, на хабре про это уже много писали, не будем на этом останавливаться.
Плагины
Из коробки Rollup может собирать только голый ES2015+. Для того что бы обучить его дополнительному функционалу, такому как подключение модулей commonjs, typescript, подгрузка html и scss и пр., необходимо подключать плагины.
Делается это очень просто:
import nodeResolve from 'rollup-plugin-node-resolve';
import commonJs from 'rollup-plugin-commonjs';
import typeScript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import html from 'rollup-plugin-html';
import visualizer from 'rollup-plugin-visualizer';
import {sizeSnapshot} from "rollup-plugin-size-snapshot";
import {terser} from 'rollup-plugin-terser';
export default [{
input: 'src/index.ts',
output: [{ file: 'dist/index.r.min.js', format: 'iife' }],
plugins: [
nodeResolve(), // подключение модулей node
commonJs(), // подключение модулей commonjs
postcss(), // подключение препроцессора postcc, а также стилей scss и less
html(), // подключение html файлов
typeScript({tsconfig: "tsconfig.json"}), // подключение typescript
sizeSnapshot(), // напишет в консоль размер бандла
terser(), // минификатор совместимый с ES2015+, форк и наследник UglifyES
visualizer() // анализатор бандла
]
}];
Вот так просто, в одно слово подключается один плагин. Данный конфиг может собирать любое сложное приложение и на выходе даже сгенерирует анализ бандла.
Итог
А теперь вооружившись новыми знаниями давайте сделаем конфиг который будет собирать отдельно полифилы, раздельно приложение для старых и новых браузеров, сервисворкер для pwa и вебворкер для сложных вычислений в беграунде.
import nodeResolve from 'rollup-plugin-node-resolve';
import commonJs from 'rollup-plugin-commonjs';
import typeScript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import html from 'rollup-plugin-html';
import visualizer from 'rollup-plugin-visualizer';
import { sizeSnapshot } from "rollup-plugin-size-snapshot";
import { terser } from 'rollup-plugin-terser';
const getPlugins = (options) => [
nodeResolve(),
commonJs(),
postcss(),
html(),
typeScript({
tsconfig: "tsconfig.json",
tsconfigOverride: { compilerOptions: { "target": options.target } }
}),
sizeSnapshot(),
terser(),
visualizer()
];
export default [{
input: 'src/polyfills.ts',
output: [{ file: 'dist/polyfills.min.js', format: 'iife' }],
plugins: getPlugins({ target: "es5" })
},{
input: 'src/index.ts',
output: [{ file: 'dist/index.next.min.js', format: 'iife' }],
plugins: getPlugins({ target: "esnext" })
},{
input: 'src/index.ts',
output: [{ file: 'dist/index.es5.min.js', format: 'iife' }],
plugins: getPlugins({ target: "es5" })
},{
input: 'src/index.ts',
output: [{ file: 'dist/index.es3.min.js', format: 'iife' }],
plugins: getPlugins({ target: "es3" })
},{
input: 'src/serviceworker.ts',
output: [{ file: 'dist/serviceworker.min.js', format: 'iife' }],
plugins: getPlugins({ target: "es5" })
},{
input: 'src/webworker.ts',
output: [{ file: 'dist/webworker.min.js', format: 'iife' }],
plugins: getPlugins({ target: "es5" })
}];
Всем легких бандлов и быстрых веб приложений!
Автор: Евгений