Хабрахабр

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

Rollup — это сборщик javascript приложений и библиотек нового поколения. Многим он давно знаком как перспективный сборщик, который хорошо подходит для сборки библиотек, но плохо подходит для сборки приложений. Однако время идет, продукт активно развивается.

Он сразу понравился мне за поддержку компиляции в ES2015, treeshaking, отсутствием модулей в сборке и конечно простым конфигом. Я впервые попробовал его в начале 2017 года. Вторая попытка была в 2018 году, тогда он уже значительно оброс комьюнити, плагинами и функционалом, но все еще не хватало качества в некоторых функциях, включая watcher. Но тогда это был сырой продукт, с небольшим числом плагинов и очень ограниченной функциональностью, и я решил оставить его на потом и продолжил собирать через browserify. И вот наконец в начале 2019 года можно смело сказать — с помощью Rollup можно просто и удобно собирать современные приложения.
Для понимания преимуществ пройдемся по ключевым возможностям и сравним с Webpack (для Browserify ситуация такая же).

Простой конфиг

Сразу что бросается в глаза это очень простой и понятный конфиг:

export default [], 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% браузеров могут исполнить его нативно.

А сейчас с Rollup мы за один проход можем собрать несколько бандлов, в es3, es5, es2015, exnext и в зависимости от браузера загружать необходимый. Тогда в 2016 году это выглядело прорывом, потому что существовало большое количество приложений которым не нужна поддержка старых браузеров: админки, киоски, не веб приложения, а инструментов сборки под них не было.

За счет отсутствия транспилинга в более низкий слой код получается значительно более компактным, а за счет отсутствия вспомогательного кода, который генерят транспиллеры, этот код еще и работает в 3 раза быстрее (по моим субъективным тестам). Также большим преимуществом ES2015 является его размер и скорость исполнения.

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

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»