Rollup: уже можно собирать приложения

    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 скомпилирует следующий код:

    Результат сборки 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, 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:

    Результат сборки 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" })
    }];
    


    Всем легких бандлов и быстрых веб приложений!

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Чем вы собираете приложение?

    • 22,3%Gulp104
    • 2,6%Grunt12
    • 70,8%Webpack330
    • 15,9%Rollup74
    • 1,5%Browserify7
    • 0,9%Closure Compiler4
    • 7,3%Parcel34
    • 9,2%Что значит собирать?43
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 13

      +4
      в голосовалке не хватает пункта про исторический лисапед вокруг бабеля.
        +1
        rollup хорош, но у меня не получилось настроить быструю пересборку проекта для девелоперского режима, любое изменение — 10 секунд пересборки, вебпак это делает за полсекунды, пока альттабаюсь в браузер
          0

          Интересно, если наш проект webpack в watch-режиме каждый раз пересобирает минимум секунд пять, а если добавился новый импорт — то и все двадцать, сколько там rollup провозится...

            +1
            Это на самом деле палка на двух концах. С одной стороны Rollup быстрее собирает продакшен сборку, с другой в девелоперском режиме нельзя отключить treeshaking.

            Но облегчить ситуацию всё таки можно, дело в том что webpack в девелоперском режиме отключает все оптимизации, из-за чего в angular можно наблюдать бандлы по 14мб. Конфиг Rollup тоже можно настроить на отключение всех минификаций в плагинах в режиме девелоп, что сильно ускорит сборку.
              +3
              нельзя отключить treeshaking

              Так ведь можно же


              treeshake
              Type: boolean | { propertyReadSideEffects?: boolean, pureExternalModules?: boolean }
              CLI: --treeshake/--no-treeshake
              Default: true
              
              Whether or not to apply tree-shaking and to fine-tune the tree-shaking process. Setting this option to false will produce bigger bundles but may improve build performance. If you discover a bug caused by the tree-shaking algorithm, please file an issue! Setting this option to an object implies tree-shaking is enabled and grants the following additional options:
            +2
            Вы сравниваете результат сборки rollup'ом в iife с результатом сборки webpack'ом в commonjs. Это не честно. Что выдаёт rollup в формате commonjs?
              0
              Показан не результат сборки, а сама возможность сборки в iife. В Commonjs ситуация следующая:
              • Относительно Browserify и Webpack2 размер бандла iife меньше на 30%
              • Относительно Webpack 4 при сборке es5 из esnext бандл iife на 10% меньше
              • Относительно Webpack 4 при сборке es2015+ разница стремится к 0
              • Если оба бандлера собрать в commonjs и es2015, то они соберут одно и тоже, за той разницей что rollup поэффективнее мертвый код удалит, но commonjs придется отдельно подключать, тогда как webpack сам включит его в бандл.
              0
                0
                Спасибо, поправил.
                0
                Parcel с флагом --experimental-scope-hoisting тоже умеет в IIFE собирать.
                  0
                  А на сколько хорошо будет работать treeShaking на большом проекте? И на сколько будет отличаться результат от сборки webpack?
                    0
                    По сравнению с webpack 2, на 30% эффективнее, но если собирать в webpack 4 в es2015+, то разница стремится к 0.
                    0

                    Добавьте, пожалуйста, Broccoli в голосовалку.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое