Как транспилировать ES модули с помощью Webpack

В экосистеме JavaScript webpack выделяется как основной инструмент для объединения нашего кода. Но прежде чем мы углубимся в то, как webpack это делает, разберемся, что такое ES-модули.

Что такое ES-модули?

ES модули (ESM) являются рекомендуемым способом написания кода как для Node.js, так и для браузера. В настоящее время модули ES поддерживаются всеми основными браузерами и представляют собой официальный стандартный формат для упаковки и повторного использования кода JavaScript в Интернете. Мы можем определять и использовать пакеты в модульной системе с ключевыми словами export и import.

Импорт и экспорт функций

Ниже приведен пример импорта и экспорта функции с синтаксисом ES модулей:
// smallestNumber.js
export function smallestNumber(arr) {
    let smallestNumber = arr[0]
    let smallestIndex = 0
    for(let i = 1; i < arr.length; i++) {
       if(arr[i] < smallestNumber) {
         smallestNumber = arr[i]
         smallestIndex = i 
       }
    }
     return smallestNumber;
}
Обратите внимание, ключевое слово export полезно, поскольку оно позволяет нам сделать нашу функцию smallestNumber доступной для других модулей, которым необходимо ее вызвать. Помимо обычных функций, мы можем экспортировать константы, классы и даже просто переменные. Также обратите внимание, что у нас может быть экспорт по умолчанию с ESM.
// result.js
import { smallestNumber } from './smallestNumber.js';

console.log(smallestNumber([3,2,5,6,0,-1]))

// возвращает -1

Транспилирование кода

В конце концов, мы сосредоточимся на механике процесса транспиляции кода в следующем разделе, но перед этим давайте в первую очередь разберемся в важности этого процесса. Как мы уже упоминали ранее, старые версии JavaScript, такие как ES5, должны иметь возможность запускать наш новый код, а также понимать его синтаксис. Это означает, что должен быть способ полной обратной совместимости языка. Обратная совместимость является одним из наиболее важных аспектов, которые следует учитывать и расставлять приоритеты при добавлении новых функций в язык программирования. Для ESM среда выполнения Node.js может определить систему/формат модуля, который она должна использовать по умолчанию, на основе поля type, если оно установлено в module в package.json, который находится в корне проекта.
// package.json 
{
  "type": "module"
}
Приведенная выше настройка позволяет создавать все файлы, которые находятся на том же уровне структуры папок, что и package.json по умолчанию ESM. В качестве альтернативы мы можем установить тип commonjs, и среда выполнения Node заставит все файлы соответствовать системе Common JS Module по умолчанию. Хотя обычно это поведение по умолчанию, если поле type не указано. По сути, когда модули помечены как модули ECMAScript, среда выполнения Node использует другой шаблон или метод для разрешения импорта файлов. Например, импорт теперь более строгий, что означает, что мы должны добавить полное имя файла с их расширениями для относительных путей или запросов, в зависимости от обстоятельств.

Что такое транспиляция кода?

С введением новой версии JavaScript и изменениями в синтаксисе (также известным как ES2015), введением TypeScript, надмножества JavaScript и других достижений, таких как, например, CoffeeScript, написание JavaScript, который работает везде, уже не так просто, как раньше. Как мы, возможно, уже знаем, разные браузеры имеют разные движки JavaScript, и их различные уровни принятия и поддержки этих новых функций JS могут различаться, потому что они не соответствовали спецификации языка в одинаковое время. Это привело к тому, что код может работать в одном браузере и не работать в другом. Таким образом, суть транспиляции заключается в том, чтобы иметь возможность конвертировать новый синтаксис JS ES2015 в старый синтаксис ES5, чтобы код мог работать в старых браузерах. Например, шаблонные литералы или, оператор слияения null (??) и другие функции ES2015 или ES2015+ по-прежнему не имеют полной поддержки браузеров и серверной среды выполнения, поэтому нам может потребоваться транспилировать наш код для поддержки этих версий. В основном, инструменты, называемые загрузчиками (loaders), такие как Babel, Traceur и т. д., используются в сочетании с webpack для транспиляции кода. На высоком уровне транспиляторы работают на разных языках программирования, считывая исходный код строка за строкой и выдавая эквивалентный результат. Например, мы можем захотеть транспилировать кодовую базу TypeScript в старый добрый JavaScript. В целом, транспиляторы позволяют нам уверенно использовать новые, нестандартизированные возможности JavaScript. В настоящее время лучшим подходом к работе с модулями ES как в среде Node.js, так и в среде браузера является их транспилирование в формат модуля CommonJS с помощью Babel.

Что такое Webpack ?

Webpack — это инструмент сборки, который помогает объединить наш код и его зависимости в один файл JavaScript. Можно также сказать, что webpack — это своего рода сборщик статических модулей для JavaScript-приложений. Это связано с тем, что он применяет такие методы, как встряхивание дерева и компиляция (которая состоит из этапов транспиляции и минификации) к нашему исходному коду. Упаковщики, такие как webpack, работают рука об руку с транспиляторами. Это означает, что это совершенно разные, но дополняющие друг друга наборы инструментов. Поэтому нам нужно настроить webpack для работы с транспилятором — скажем, Babel. Как мы упоминали ранее, транспиляторы либо выполняют работу по компиляции одного языка в другой, либо делают язык обратно совместимым. Webpack довольно хорошо работает с Babel, а также легко настраивается. Например, мы можем настроить Babel для работы с webpack, создав конфигурационный файл webpack (webpack.config.js) использование плагина Babel — по сути, экосистема плагинов webpack — это то, что делает webpack тем, что есть. С другой стороны, Babel может быть сконфигурирован с помощью babel.config.js файл или .babelrc файл.

Почему webpack?

Как вы, возможно, знаете, webpack поддерживает несколько типов модулей из коробки, включая модули CommonJS и ES. Webpack также работает как на клиентском, так и на серверном JavaScript, поэтому с помощью webpack мы также можем легко работать с ресурсами, такими как изображения, шрифты, таблицы стилей и так далее. Он остается действительно мощным инструментом, поскольку автоматически строит и выводит граф зависимостей на основе импорта и экспорта файлов (поскольку, по сути, каждый файл является модулем). Сочетание этого с загрузчиками и плагинами делает webpack отличным инструментом в нашем арсенале. Более подробно о том, как это работает под капотом, можно прочитать в документации. Что касается плагинов, webpack также имеет богатую экосистему плагинов. Плагины поддерживают webpack в выполнении некоторой грязной работы, такой как оптимизация пакетов, управление ресурсами и так далее. Подводя итог, можно сказать, что комплектация с такими инструментами, как webpack, является самым быстрым способом работы или обеспечения обратной совместимости с модулями в наши дни, поскольку ESM постепенно набирает обороты в качестве официального стандарта для повторного использования кода в экосистеме. Webpack также поддерживает загрузчики, которые помогают ему решать, как обрабатывать, объединять и обрабатывать несобственные модули или файлы. Важно отметить, как webpack относится к загрузчикам. Загрузчики оцениваются и выполняются снизу вверх. Таким образом, последний загрузчик выполняется первым, и так далее, и тому подобное, именно в таком порядке. В этой статье мы сосредоточимся на том, как webpack транспилирует или обрабатывает модули ECMAScript.

Использование загрузчиков для транспиляции

Загрузчики преобразуют файлы из одного языка программирования в другой. Например, ts-loader может преобразовывать или транспилировать TypeScript в JavaScript. Обычно мы используем загрузчики в качестве зависимостей разработки. Например, давайте посмотрим, как мы можем использовать ts-loader. Для установки мы можем выполнить следующее:
npm install --save-dev ts-loader
Затем мы можем использовать этот загрузчик, чтобы указать webpack правильно обрабатывать все файлы TypeScript в нашем исходном коде. Ознакомьтесь с примером webpack.config.js файл ниже.
module.exports = {
  module: {
    rules: [
      { test: /.ts$/, use: 'ts-loader' },
    ],
  },
};
Здесь, как мы уже упоминали, мы говорим webpack обрабатывать все пути к файлам, оканчивающиеся на .ts и транспилировать их в синтаксис JavaScript, понятный браузеру и среде выполнения Node. Это означает, что загрузчики также могут работать в среде Node.js и, следовательно, также следовать стандарту разрешения модулей.

Соглашения об именовании загрузчиков

Общий способ именования загрузчиков является последовательным, так как загрузчики именуются своим именем и дефисом — обычно как xxx-loader. Например, babel-loader, ts-loader и так далее. Для нашего примера использования нас особенно интересует ESNext или babel-loader, загрузчик, созданный и поддерживаемый сообществом. Более подробную информацию о загрузчиках можно найти в документации webpack.

Некоторые основные понятия webpack

Чтобы понять, как работает webpack, в этом разделе рассматриваются некоторые высокоуровневые концепции, о которых должны знать читатели. Как мы упоминали ранее, webpack использует граф зависимостей, что означает, что он рекурсивно строит связь, включающую каждый модуль, который нужен приложению или от которого зависит, а затем объединяет все эти модули в выходной файл, готовый к использованию. Это означает, что webpack должен иметь точку входа — и это действительно так. При настройке конфигурации webpack точкой входа является начало проверок webpack пути к файлу, прежде чем он начнет строить внутренний граф зависимостей для нашего приложения. Обратите внимание, что webpack также поддерживает несколько точек входа для нашего приложения.
module.exports = {
  entry: ['./path/to/my/entry1/file1.js', './path/to/my/entry2/file2.js']
};
Чтобы добавить несколько точек входа, мы можем использовать массив. В webpack v5 теперь у нас может быть пустой объект входа. После того, как webpack построит граф зависимостей внутри и завершит процесс объединения, он должен вывести пакет в другой путь к файлу, который нам затем нужно обработать. Это то, что делает output. Он сообщает webpack, какой путь выдавать созданные пакеты и как называются файлы.
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};
Начиная с версии 4.0.0, webpack не требует конфигурационного файла для объединения проекта, хотя и предполагает, что вы определили точку входа в приложение с папкой и путем к файлу как src/index.js, и что вывод упакован в dist/main.js путь к папке, минимизированный, оптимизированный и готовый к продакшену.

Настройка webpack и Babel

В webpack была реализована полная поддержка использования встроенного синтаксиса модуля ES2015. Это означает, что мы можем использовать операторы import и export, не полагаясь на внешние инструменты транспиляции или зависимости, такие как Babel. Тем не менее, все же рекомендуется настроить Babel, на случай, если есть другие, более новые функции ES2015+, которые webpack еще не принял во внимание. В зависимости от форматов модулей webpack проверяет ближайший package.json и обеспечивает соблюдение соответствующих рекомендаций. При использовании webpack для объединения нашего кода обычно рекомендуется придерживаться синтаксиса одного модуля, чтобы позволить webpack правильно обрабатывать собранный вывод и, следовательно, предотвращать нежелательные ошибки. Чтобы начать, нам нужно убедиться, что на наших компьютерах установлен интерфейс командной строки webpack. Затем мы можем использовать команду CLI init, чтобы быстро запустить конфигурацию webpack в соответствии с требованиями нашего проекта. Мы можем сделать это, просто выполнив npx webpack-cli init и соответствующим образом отвечая на запросы. Теперь нам нужно скомпилировать наш код ES2015 в ES5, чтобы мы могли использовать его в различных средах браузера или средах выполнения. Для этого нам нужно установить Babel и все его зависимости, необходимые для webpack. Давайте установим следующее:
  • Babel Core
  • Babel Loader, webpack загрузчик, который взаимодействует с Babel Core
Для установки мы можем выполнить:
npm i webpack webpack-cli webpack-dev-server @babel/core @babel/preset-env babel-loader rimraf  -D
По окончанию установки наш package.json должно выглядеть следующим образом:
{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "A demo of webpack with babel",
  "main": "dist/bundle.js",
  "scripts": {
    "build": "node_modules/.bin/webpack --config webpack.config.js --mode=production",
    "watch": "node_modules/.bin/webpack --config webpack.config.js --mode=development -w",
    "prebuild:dev": "rimraf dist"
  },
  "author": "Name",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.4",
    "babel-loader": "^8.2.3",
    "rimraf": "^3.0.2",
    "webpack": "^5.64.3",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.5.0"
  },
  "type": "module"
}
Обратите внимание, что наши зависимости закреплены, чтобы обеспечить согласованность при запуске нашего приложения в будущем. Пакет @babel/preset-env преобразует весь код ES2015–ES2020 в ES5 или практически в любую целевую среду, которую мы укажем в целевых параметрах. Обычно он нам нужен, когда мы намереваемся установить конкретную цель, которая затем позволяет Babel нацелиться на эту конкретную среду. Этот пакет в основном проверяет указанное целевое окружение по своему внутреннему отображению, а затем составляет список плагинов, которые он передает в Babel для транспиляции кода. Это приводит к уменьшению количества пакетов JavaScript. Поэтому, если мы не укажем target, размер выводимого кода будет больше, потому что по умолчанию плагины Babel группируют синтаксические функции ECMAScript в коллекцию связанных функций. Более того, с опцией меньшего размера пакета и большего прироста производительности, мы можем использовать babel/preset-modules, которые в конечном итоге будут объединены в ядро @babel/preset-env.

Настройка webpack.config.js

Теперь давайте перейдем к настройке нашего файла webpack. Создайте новый webpack.config.js в корне нашего проекта.
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default  {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'bundle.js',
    },
    experiments: {
        outputModule: true,
    },
    plugins: [
       //empty pluggins array
    ],
    module: {
         // https://webpack.js.org/loaders/babel-loader/#root
        rules: [
            {
                test: /.m?js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
            }
        ],
    },
    devtool: 'source-map'
}
В приведенном выше конфиге мы установили для поля outputModule: true, что необходимо, если мы намерены использовать webpack для компиляции публичной библиотеки, предназначенной для использования другими. babel-loader загружает код ES2015+ и транспилирует его в ES5 с помощью Babel. Как вы также можете видеть в конфигурационном файле, у нас есть свойство module, которое имеет свойство rules, содержащее массив для настройки отдельных загрузчиков, которые могут нам понадобиться для нашей конфигурации webpack. Здесь мы добавили загрузчик webpack и установили необходимые параметры в соответствии с требованиями нашего проекта. Обратите внимание, что test представляет собой регулярное выражение, соответствующее абсолютному пути к каждому файлу и проверяющее наличие расширений файлов. В приведенном выше примере мы проверяем, заканчивается ли наш файл на .mjs или .js расширение.

Использование (и неиспользование) .babelrc файл

Теперь мы можем сконфигурировать Babel, создав .babelrc , также в корне нашего проекта. Содержимое файла показано ниже:
 {
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "esmodules": true
          }
        }
      ]
    ]
  }
В случае, если мы не хотим использовать .babelrc, мы также можем добавить пресеты в объект options внутри массива rules, например:
module: {
  rules: [
    {
      test: /.m?js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env', "es2015", "es2016"],
        }
      }
    }
  ]
}

Настройка пресетов и добавление плагинов

Нам нужно установить пресеты так, чтобы функции ES2015 в нашем коде могли быть преобразованы в ES5. В массиве options мы также можем добавить плагины, которые мы хотим добавить в нашу конфигурацию. Например, мы можем добавить эту plugins: ['@babel/plugin-transform-runtime'], которая устанавливает среду выполнения Babel, которая отключает автоматическое внедрение среды выполнения для каждого файла, чтобы предотвратить раздувание. Чтобы включить это в наш код, нам нужно установить их, выполнив:
npm install -D @babel/plugin-transform-runtime
Мы также должны добавить @babel/runtime в качестве зависимости, выполнив npm install @babel/runtime. Также обратите внимание, что в объекте rules мы указываем webpack исключить файлы в папке node_modules со свойством exclude. Это сделано для того, чтобы у нас был более быстрый процесс объединения, так как мы не хотим объединять нашу node_modules папку. Также бывают случаи, когда мы хотим, чтобы webpack обрабатывал модули ES 2015. Чтобы разрешить это, нам нужно использовать плагин ModuleConcatenationPlugin. Этот плагин включает некоторую форму поведения конкатенации в webpack, называемую поднятием области, что является возможной благодаря синтаксису ESM. По умолчанию этот плагин уже включен в рабочем режиме и отключен в противном случае.

Итоги

Чтобы избежать проблем с совместимостью, нам нужно транспилировать наш код из ES2015 в ES5. Здесь на помощь приходит webpack. Webpack объединяет наш код и выводит транспилированную версию в целевой файл, как указано в конфигурационном файле. В нашем конфигурационном файле webpack правила модуля позволяют нам указывать различные загрузчики, что является простым способом отображения загрузчиков. В этом посте мы использовали только загрузчик Babel, но есть много других загрузчиков, которые мы также можем использовать в экосистеме. Что касается улучшений и изменений в последнем выпуске webpack 5, теперь есть встроенная поддержка асинхронных модулей. Как следует из названия, асинхронные модули основаны на Promise и поэтому не разрешаются синхронно. Импорт асинхронных модулей через require() теперь также будет возвращать Promise, который разрешается в их экспорт.