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

в 20:43, , рубрики: bundler, javascript, rollup, TypeScript

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

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

Автор: Евгений

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js