Пресеты и плагины Babel в связке с Webpack

Webpack отвечает за построение графа зависимостей, упаковку модулей и оптимизацию сборки, тогда как Babel занимается преобразованием Javascript-кода. Основная задача Babel — транспиляция современного синтаксиса Javascript в код, поддерживаемый старыми браузерами или средами выполнения.

Связка Webpack + Babel решает сразу несколько задач:

  • преобразование современного синтаксиса ES2015+;
  • поддержка JSX;
  • трансформация TypeScript;
  • подключение экспериментальных возможностей языка;
  • автоматическая полифилизация;
  • оптимизация итогового бандла;
  • унификация кода под разные платформы.

Внутри Webpack Babel обычно используется через babel-loader.

Пример базовой интеграции:

npm install --save-dev babel-loader @babel/core @babel/preset-env

Конфигурация:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    }
};

Архитектура Babel

Babel состоит из нескольких уровней:

Ядро

Пакет:

@babel/core

Выполняет:

  • парсинг AST;
  • трансформацию дерева;
  • генерацию итогового кода.

Без ядра остальные компоненты работать не могут.


Пресеты

Пресет представляет собой набор готовых Babel-плагинов.

Например:

@babel/preset-env

включает множество трансформаций ES6+.

Пресеты позволяют не подключать плагины вручную по одному.


Плагины

Плагин выполняет конкретную трансформацию:

  • преобразование optional chaining;
  • поддержка decorators;
  • трансформация class properties;
  • удаление console;
  • оптимизация runtime.

Плагины подключаются точечно.


Конфигурационные файлы Babel

Babel поддерживает несколько вариантов конфигурации.

.babelrc

{
    "presets": ["@babel/preset-env"]
}

babel.config.json

Подходит для крупных проектов и monorepo.

{
    "presets": ["@babel/preset-env"]
}

Конфигурация внутри package.json

{
    "babel": {
        "presets": ["@babel/preset-env"]
    }
}

Конфигурация прямо в Webpack

{
    test: /\.js$/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: ['@babel/preset-env']
        }
    }
}

Такой подход используется реже, поскольку отдельный файл Babel удобнее поддерживать.


Пресет @babel/preset-env

Назначение preset-env

@babel/preset-env — главный пресет Babel.

Он автоматически определяет:

  • какие возможности языка используются;
  • какие браузеры должны поддерживаться;
  • какие трансформации необходимы.

Это позволяет не подключать десятки плагинов вручную.

Установка:

npm install --save-dev @babel/preset-env

Конфигурация:

{
    "presets": ["@babel/preset-env"]
}

Настройка targets

Секция targets определяет поддерживаемые окружения.

Пример:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "90",
                    "firefox": "88"
                }
            }
        ]
    ]
}

Поддержка Node.js

{
    "targets": {
        "node": "18"
    }
}

Использование Browserslist

Babel умеет использовать настройки Browserslist.

Пример:

{
    "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead"
    ]
}

Webpack, Babel и Autoprefixer могут использовать один и тот же список браузеров.


Автоматическое подключение полифилов

Современный синтаксис и современные API — разные вещи.

Babel преобразует синтаксис:

const fn = () => {};

но не добавляет API вроде:

Promise
Map
Set
Array.from

Для этого используются полифилы.


Использование core-js

Установка:

npm install core-js

Режим useBuiltIns: "usage"

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ]
    ]
}

Babel автоматически подключит только необходимые полифилы.

Например:

const arr = Array.from(document.querySelectorAll('div'));

будет дополнен импортом нужного полифила.


Режим entry

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "entry",
                "corejs": 3
            }
        ]
    ]
}

В entry-файле:

import "core-js";
import "regenerator-runtime/runtime";

Разница между usage и entry

Режим Поведение
usage Анализирует код и добавляет точечные полифилы
entry Подключает набор полифилов через entry

В большинстве современных проектов используется usage.


Preset для React

@babel/preset-react

Позволяет Babel понимать JSX.

Установка:

npm install --save-dev @babel/preset-react

Конфигурация:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}

Трансформация JSX

Исходный код:

const element = <h1>Hello</h1>;

После Babel:

const element = React.createElement("h1", null, "Hello");

Automatic Runtime

Современный React поддерживает автоматический JSX runtime.

Конфигурация:

{
    "presets": [
        [
            "@babel/preset-react",
            {
                "runtime": "automatic"
            }
        ]
    ]
}

Теперь импорт React не требуется:

export default function App() {
    return <div>Hello</div>;
}

Preset для TypeScript

@babel/preset-typescript

Babel умеет удалять TypeScript-аннотации.

Установка:

npm install --save-dev @babel/preset-typescript

Конфигурация:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-typescript"
    ]
}

Webpack:

{
    test: /\.ts$/,
    exclude: /node_modules/,
    use: 'babel-loader'
}

Особенности Babel TypeScript

Babel:

  • удаляет типы;
  • преобразует синтаксис.

Но Babel не выполняет:

  • type checking;
  • проверку интерфейсов;
  • анализ типов.

Для проверки типов обычно используется:

tsc --noEmit

или:

fork-ts-checker-webpack-plugin

Babel Plugins

Подключение плагинов

Пример:

npm install --save-dev @babel/plugin-transform-runtime

Конфигурация:

{
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

Plugin Transform Runtime

Проблема дублирования helper-функций

Babel может вставлять вспомогательные функции в каждый файл:

function _extends() {}
function _classCallCheck() {}

При большом проекте это увеличивает размер бандла.


Решение через runtime

Установка:

npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime

Конфигурация:

{
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

Теперь helper-функции импортируются централизованно.


Преимущества runtime

  • уменьшение размера бандла;
  • отсутствие дублирования;
  • более чистый output;
  • улучшение кеширования.

Plugin Proposal

Экспериментальные возможности Javascript

Некоторые возможности языка ещё не входят в официальный стандарт ECMAScript.

Babel позволяет использовать их заранее.


Class Properties

Установка:

npm install --save-dev @babel/plugin-proposal-class-properties

Пример:

class User {
    name = 'Alex';
}

Decorators

Установка:

npm install --save-dev @babel/plugin-proposal-decorators

Конфигурация:

{
    "plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "legacy": true
            }
        ]
    ]
}

Optional Chaining

Современный Babel уже включает поддержку через preset-env.

Пример:

const city = user?.address?.city;

Трансформируется в безопасные проверки.


Nullish Coalescing

const value = input ?? 'default';

Использование Babel в Webpack Loader

Полная конфигурация

const path = require('path');

module.exports = {
    mode: 'development',

    entry: './src/index.js',

    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },

    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },

    resolve: {
        extensions: ['.js', '.jsx']
    }
};

Кеширование Babel

Проблема медленной сборки

Транспиляция больших проектов может занимать значительное время.


cacheDirectory

{
    loader: 'babel-loader',
    options: {
        cacheDirectory: true
    }
}

Webpack начнёт сохранять кеш Babel.

Повторные сборки существенно ускоряются.


Кеш в node_modules

Обычно кеш располагается:

node_modules/.cache/babel-loader

Исключение node_modules

Причины исключения

Большинство npm-пакетов уже поставляется в совместимом виде.

Поэтому:

exclude: /node_modules/

значительно ускоряет сборку.


Когда исключение не подходит

Некоторые библиотеки публикуются в современном синтаксисе:

node_modules/some-esm-package

Тогда используется selective transpilation.


Точечная транспиляция

{
    test: /\.js$/,
    exclude: /node_modules\/(?!some-esm-package)/,
    use: 'babel-loader'
}

Source Maps и Babel

Поддержка source map

Webpack и Babel умеют сохранять связь между исходным кодом и итоговым bundle.

Конфигурация:

module.exports = {
    devtool: 'source-map'
};

Babel и source maps

Дополнительная настройка обычно не требуется.

Webpack автоматически объединяет source maps.


Production-конфигурация Babel

Минимизация кода

Babel сам по себе не минифицирует код.

Минификация обычно выполняется:

TerserWebpackPlugin

Удаление console.log

Плагин:

npm install --save-dev babel-plugin-transform-remove-console

Конфигурация:

{
    "env": {
        "production": {
            "plugins": [
                "transform-remove-console"
            ]
        }
    }
}

Секция env

Разные настройки Babel

Babel позволяет использовать отдельные конфигурации.

Пример:

{
    "presets": ["@babel/preset-env"],

    "env": {
        "development": {
            "sourceMaps": true
        },

        "production": {
            "comments": false
        }
    }
}

Использование NODE_ENV

Webpack:

NODE_ENV=production webpack

Babel автоматически выберет production-конфигурацию.


Babel и Tree Shaking

Влияние Babel на tree shaking

Некоторые трансформации могут ломать tree shaking.

Особенно это касается преобразования ES Modules в CommonJS.


Сохранение ES Modules

Правильная настройка:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false
            }
        ]
    ]
}

Теперь Webpack сможет выполнять tree shaking.


Babel и динамический импорт

Поддержка import()

Webpack использует:

import('./module.js')

для code splitting.

Babel должен сохранить такой синтаксис.


Современный preset-env

Новые версии Babel корректно работают с dynamic import без дополнительных плагинов.


Совместная работа Babel и Webpack

Зоны ответственности

Babel

  • синтаксические преобразования;
  • полифилы;
  • AST-трансформации;
  • JSX;
  • TypeScript.

Webpack

  • граф зависимостей;
  • bundling;
  • splitting;
  • оптимизация;
  • asset management;
  • HMR.

Babel Loader Pipeline

Этапы обработки

Webpack передаёт файл в babel-loader.

Далее Babel:

  1. парсит код;
  2. строит AST;
  3. применяет пресеты;
  4. применяет плагины;
  5. генерирует итоговый код;
  6. возвращает результат Webpack.

Порядок применения пресетов и плагинов

Плагины

Плагины выполняются раньше пресетов.


Пресеты

Пресеты применяются снизу вверх.

Пример:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}

Порядок выполнения:

  1. preset-react
  2. preset-env

Babel в monorepo

Проблемы monorepo

В monorepo может быть несколько пакетов:

packages/
    frontend/
    backend/
    shared/

babel.config.js

Для monorepo обычно используется корневая конфигурация:

module.exports = {
    presets: ['@babel/preset-env']
};

Использование babel.config.js

Динамическая конфигурация

module.exports = function(api) {

    api.cache(true);

    return {
        presets: ['@babel/preset-env']
    };
};

api.cache

Позволяет кешировать результат конфигурации Babel.


Ошибки и проблемы

Конфликт preset-env и modules

Если tree shaking перестал работать, причиной часто становится:

{
    "modules": "commonjs"
}

Webpack хуже оптимизирует CommonJS.


Дублирование полифилов

Ошибка:

import "core-js";

и одновременно:

"useBuiltIns": "usage"

может привести к дублированию.


Несовместимость decorators

Decorators чувствительны к порядку плагинов.

Обычно:

{
    "plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "legacy": true
            }
        ],
        "@babel/plugin-proposal-class-properties"
    ]
}

Практическая production-конфигурация

Babel

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": "> 0.25%, not dead",
                "useBuiltIns": "usage",
                "corejs": 3,
                "modules": false
            }
        ],
        [
            "@babel/preset-react",
            {
                "runtime": "automatic"
            }
        ]
    ],

    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

Webpack

module.exports = {
    mode: 'production',

    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true
                    }
                }
            }
        ]
    }
};