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

habr.png

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" })
}];

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

© Habrahabr.ru