esbuild — это быстрый сборщик, который может оптимизировать код JavaScript, TypeScript, JSX и CSS. Эта статья поможет вам быстро освоиться с esbuild и покажет, как создать собственную систему сборки без других зависимостей.
Как работает esbuild?
Фреймворки, такие как Vite, приняли esbuild, но вы можете использовать esbuild как отдельный инструмент в своих собственных проектах.
- esbuild упаковывает код JavaScript в один файл аналогично сборщикам, таким как Rollup. Это основная функция esbuild, и она разрешает модули, сообщает о проблемах синтаксиса, «встряхивает дерево» для удаления неиспользуемых функций, стирает операторы ведения журнала и отладчика, минимизирует код и предоставляет исходные карты.
- esbuild объединяет код CSS в один файл. Это не полная замена препроцессорам, таким как Sass или PostCSS, но esbuild может обрабатывать частичные, синтаксические проблемы, вложенность, встроенное кодирование ресурсов, исходные карты, автоматическое префиксирование и минимизацию. Это может быть все, что вам нужно.
- esbuild также предоставляет локальный сервер разработки с автоматическим связыванием и горячей перезагрузкой, поэтому нет необходимости в обновлении. Он не обладает всеми функциями, предлагаемыми Browsersync, но в большинстве случаев его достаточно.
Приведенный ниже код поможет вам понять концепции esbuild, чтобы вы могли исследовать дополнительные возможности конфигурации для своих проектов.
Почему Bundle?
Объединение кода в один файл дает различные преимущества. Вот некоторые из них:
- вы можете разрабатывать небольшие автономные исходные файлы, которые легче поддерживать
- вы можете анализировать, претифицировать и проверять синтаксис кода в процессе сборки
- упаковщик может удалять неиспользуемые функции — это называется встряхиванием дерева
- вы можете объединять альтернативные версии одного и того же кода и создавать цели для старых браузеров, Node.js, Deno и т. д.
- отдельные файлы загружаются быстрее, чем несколько файлов, и браузеру не требуется поддержка модуля ES.
- объединение на производственном уровне может повысить производительность за счет минимизации кода и удаления операторов ведения журнала и отладки.
Зачем использовать esbuild?
В отличие от сборщиков JavaScript, esbuild представляет собой скомпилированный исполняемый файл Go, который реализует тяжелую параллельную обработку. Это быстро и до ста раз быстрее, чем Rollup, Parcel или Webpack. Это может сэкономить недели времени разработки в течение всего жизненного цикла проекта.
Кроме того, esbuild также предлагает:
- встроенная сборка и компиляция для JavaScript, TypeScript, JSX и CSS
- командная строка, JavaScript и API конфигурации Go
- поддержка модулей ES и CommonJS
- локальный сервер разработки с режимом просмотра и перезагрузкой в реальном времени
- плагины для добавления дополнительных функций
- исчерпывающая документация и онлайн-инструмент для экспериментов
Почему следует избегать esbuild?
На момент написания esbuild достиг версии 0.18. Это надежный, но все еще бета-продукт.
esbuild часто обновляется, и параметры могут меняться между версиями. Документация рекомендует придерживаться определенной версии. Вы можете обновить его, но вам может потребоваться перенести файлы конфигурации и изучить новую документацию, чтобы обнаружить критические изменения.
Также обратите внимание, что esbuild не выполняет проверку типов TypeScript, поэтому вам все равно нужно запустить tsc -noEmit.
Супер-быстрый старт
При необходимости создайте новый проект Node.js с помощью npm init, затем установите esbuild локально в качестве зависимости разработки:
npm install esbuild --save-dev --save-exact
Для установки требуется около 9MB. Убедитесь, что это работает, выполнив эту команду, чтобы увидеть установленную версию:
./node_modules/.bin/esbuild --version
Или запустите эту команду, чтобы просмотреть справку CLI:
./node_modules/.bin/esbuild --help
Используйте CLI API для объединения сценария ввода ( myapp.js) и всех его импортированных модулей в один файл с именем bundle.js. esbuild выведет файл, используя формат по умолчанию, предназначенный для браузера, с немедленным вызовом функционального выражения (IIFE):
./node_modules/.bin/esbuild myapp.js --bundle --outfile=bundle.js
Вы можете установить esbuild другими способами, если вы не используете Node.js.
Пример проекта
Загрузите файлы примеров и конфигурацию esbuild с Github. Это проект Node.js, поэтому установите единственную зависимость esbuild с помощью:
npm install
Соберите исходные файлы в srcкаталог buildи запустите сервер разработки с помощью:
npm start
Теперь перейдите в localhost:8000браузере, чтобы просмотреть веб-страницу с часами реального времени. Когда вы обновляете какой-либо файл CSS в src/css/или src/css/partials, esbuild повторно объединяет код и перезагружает стили в реальном времени.
Нажмите Ctrl|Cmd+ Ctrl|Cmd, чтобы остановить сервер.
Создайте производственную сборку для развертывания, используя:
npm run build
Изучите файлы CSS и JavaScript в buildкаталоге, чтобы увидеть уменьшенные версии без исходных карт.
Обзор проекта
Страница часов реального времени создается в buildкаталоге с использованием исходных файлов из src.
Файл package.jsonопределяет пять npmсценариев. Первый удаляет buildкаталог:
"clean": "rm -rf ./build",
Прежде чем произойдет какое-либо связывание, initзапускается скрипт clean, создает новый buildкаталог и копирует:
- статический HTML-файл от src/html/index.htmlдоbuild/index.html
- статические изображения от src/images/доbuild/images/
"init": "npm run clean && mkdir ./build && cp ./src/html/* ./build/ && cp -r ./src/images ./build",
Файл esbuild.config.jsуправляет процессом сборки esbuild с помощью JavaScript API. Это проще в управлении, чем передача параметров в CLI API, что может стать громоздким. npm bundleЗапускается скрипт, за initкоторым следует node./esbuild.config.js:
"bundle": "npm run init && node ./esbuild.config.js",
Последние два npmскрипта запускаются bundleс параметром productionили, developmentпереданным./esbuild.config.jsдля управления сборкой:
"build": "npm run bundle -- production", "start": "npm run bundle -- development"
При./esbuild.config.jsзапуске он определяет, следует ли создавать мини- productionфайлы (по умолчанию) или developmentфайлы с автоматическими обновлениями, исходными картами и сервером с перезагрузкой в реальном времени. В обоих случаях esbuild пакеты:
- входной файл CSS src/css/main.cssдляbuild/css/main.css
- входной файл JavaScript scr/js/main.jsдляbuild/js/main.js
Настройка esbuild
package.jsonимеет «type»так «module»что все.jsфайлы могут использовать модули ES. Сценарий esbuild.config.jsимпортирует esbuildи устанавливает productionModeпри trueкомплектации для производства или falseпри комплектации для разработки:
import { argv } from 'node:process'; import * as esbuild from 'esbuild'; const productionMode = ('development' !== (argv[2] || process.env.NODE_ENV)), target = 'chrome100,firefox100,safari15'.split(','); console.log(`${ productionMode ? 'production' : 'development' } build`);
Объединить цель
Обратите внимание, что целевая переменная определяет массив браузеров и номеров версий для использования в конфигурации. Это влияет на объединенный вывод и изменяет синтаксис для поддержки определенных платформ. Например, esbuild может:
- расширить нативную вложенность CSS в полные селекторы (вложенность осталась бы, если бы «Chrome115″была единственной целью)
- добавьте свойства с префиксом поставщика CSS, где это необходимо
- polyfill ??нулевой оператор объединения
- удалить #из полей закрытого класса
Помимо браузеров, вы также можете настроить таргетинг nodeна esтакие версии, как es2020и esnext(последние функции JS и CSS).
Объединение JavaScript
Самый простой API для создания бандла:
await esbuild.build({ entryPoints: ['myapp.js'], bundle: true outfile: 'bundle.js' });
Это повторяет команду CLI, использованную выше:
./node_modules/.bin/esbuild myapp.js --bundle --outfile=bundle.js
В примере проекта используются более продвинутые параметры, такие как просмотр файлов. Для этого требуется долгосрочный контекст сборки, который устанавливает конфигурацию:
// bundle JS const buildJS = await esbuild.context({ entryPoints: [ './src/js/main.js' ], format: 'esm', bundle: true, target, drop: productionMode ? ['debugger', 'console'] : [], logLevel: productionMode ? 'error' : 'info', minify: productionMode, sourcemap: !productionMode && 'linked', outdir: './build/js' });
esbuild предлагает десятки вариантов конфигурации. Вот краткое изложение тех, которые используются здесь:
- entryPointsопределяет массив точек входа файла для объединения. В примере проекта есть один скрипт по адресу./src/js/main.js.
- formatустанавливает выходной формат. В примере используется esm, но при желании вы можете установить iifeдля старых браузеров или commonjsдля Node.js.
- bundleустановить trueимпортированные модули в выходной файл.
- target— это массив целевых браузеров, определенный выше.
- dropпредставляет собой массив операторов consoleи/или debuggerдля удаления. В этом случае рабочие сборки удаляют оба, а разрабатываемые сборки сохраняют их.
- logLevelопределяет подробность ведения журнала. В приведенном выше примере показаны ошибки во время рабочих сборок и более подробные информационные сообщения во время сборок для разработки.
- minifyуменьшает размер кода, удаляя комментарии и пробелы и переименовывая переменные и функции, где это возможно. Пример проекта минимизируется во время производственных сборок, но улучшает код во время сборок для разработки.
- sourcemapустановлен в linked(только в режиме разработки) создает связанную исходную карту в файле,.mapпоэтому исходный исходный файл и строка доступны в инструментах разработчика браузера. Вы также можете настроить inlineвключение исходной карты в связанный файл, bothсоздать и то, и другое или externalсоздать.mapфайл без ссылки из связанного JavaScript.
- outdirопределяет выходной каталог связанного файла.
Вызовите метод объекта контекста rebuild(), чтобы запустить сборку один раз — обычно для рабочей сборки:
await buildJS.rebuild(); buildJS.dispose(); // free up resources
Вызовите метод объекта контекста watch(), чтобы он продолжал работать и автоматически перестраивался при изменении отслеживаемых файлов:
await buildJS.watch();
Объект контекста обеспечивает пошаговую обработку последующих сборок и повторное использование результатов предыдущих сборок для повышения производительности.
Входные и выходные файлы JavaScript
Входной src/js/main.jsфайл импортирует dom.jsи time.jsмодули из libподпапки. Он находит все элементы с классом clockи устанавливает их текстовое содержимое в текущее время каждую секунду:
import * as dom from './lib/dom.js'; import { formatHMS } from './lib/time.js'; // get clock element const clock = dom.getAll('.clock'); if (clock.length) { console.log('initializing clock'); setInterval(() => { clock.forEach(c => c.textContent = formatHMS()); }, 1000); }
dom.jsэкспортирует две функции. main.jsимпортирует оба, но использует только getAll():
// DOM libary // fetch first node from selector export function get(selector, doc = document) { return doc.querySelector(selector); } // fetch all nodes from selector export function getAll(selector, doc = document) { return Array.from(doc.querySelectorAll(selector)); }
time.jsэкспортирует две функции. main.jsimports formatHMS(), но при этом используются другие функции модуля:
// time library // return 2-digit value function timePad(n) { return String(n).padStart(2, '0'); } // return time in HH:MM format export function formatHM(d = new Date()) { return timePad(d.getHours()) + ':' + timePad(d.getMinutes()); } // return time in HH:MM:SS format export function formatHMS(d = new Date()) { return formatHM(d) + ':' + timePad(d.getSeconds()); }
Полученный пакет разработки удаляет (tree Shakes), get()но dom.jsвключает все time.jsфункции. Также генерируется исходная карта:
// src/js/lib/dom.js function getAll(selector, doc = document) { return Array.from(doc.querySelectorAll(selector)); } // src/js/lib/time.js function timePad(n) { return String(n).padStart(2, "0"); } function formatHM(d = new Date()) { return timePad(d.getHours()) + ":" + timePad(d.getMinutes()); } function formatHMS(d = new Date()) { return formatHM(d) + ":" + timePad(d.getSeconds()); } // src/js/main.js var clock = getAll(".clock"); if (clock.length) { console.log("initializing clock"); setInterval(() => { clock.forEach((c) => c.textContent = formatHMS()); }, 1e3); } //# sourceMappingURL=main.js.map
(Обратите внимание, что esbuild может переписать let и constto varдля корректности и скорости.)
Полученный производственный пакет минимизирует код до 322 символов:
function o(t,c=document){return Array.from(c.querySelectorAll(t))}function e(t){return String(t).padStart(2,"0")}function l(t=new Date){return e(t.getHours())+":"+e(t.getMinutes())}function r(t=new Date){return l(t)+":"+e(t.getSeconds())}var n=o(".clock");n.length&&setInterval(()=>{n.forEach(t=>t.textContent=r())},1e3);
Связка CSS
Связывание CSS в примере проекта использует объект контекста, аналогичный приведенному выше JavaScript:
// bundle CSS const buildCSS = await esbuild.context({ entryPoints: [ './src/css/main.css' ], bundle: true, target, external: ['/images/*'], loader: { '.png': 'file', '.jpg': 'file', '.svg': 'dataurl' }, logLevel: productionMode ? 'error' : 'info', minify: productionMode, sourcemap: !productionMode && 'linked', outdir: './build/css' });
Он определяет externalпараметр как массив файлов и путей, которые нужно исключить из сборки. В примере проекта файлы из src/images/каталога копируются в buildкаталог, чтобы HTML, CSS или JavaScript могли ссылаться на них напрямую. Если бы это не было установлено, esbuild копировал бы файлы в выходной build/css/каталог при их использовании в background-imageили подобных свойствах.
Параметр loaderизменяет способ обработки esbuild импортированного файла, на который не ссылаются как на externalресурс. В этом примере:
- Изображения SVG встраиваются как URI данных.
- Изображения PNG и JPG копируются в build/css/каталог и ссылаются на них как на файлы.
Входные и выходные файлы CSS
Файл записи src/css/main.cssимпортируется variables.cssи elements.cssиз partialsподпапки:
/* import */ @import './partials/variables.css'; @import './partials/elements.css';
variables.cssопределяет пользовательские свойства по умолчанию:
/* primary variables */ :root { --font-body: sans-serif; --color-fore: #fff; --color-back: #112; }
elements.cssопределяет все стили. Примечание:
- имеет bodyфоновое изображение, загруженное из внешнего imagesкаталога
- вложен h1внутриheader
- имеет h1фон SVG, который будет встроен
- целевые браузеры не требуют префиксов поставщиков
/* element styling */ *, *::before, ::after { box-sizing: border-box; font-weight: normal; padding: 0; margin: 0; } body { font-family: var(--font-body); color: var(--color-fore); background: var(--color-back) url(/images/web.png) repeat; margin: 1em; } /* nested elements with inline icon */ header { & h1 { font-size: 2em; padding-left: 1.5em; margin: 0.5em 0; background: url(../../icons/clock.svg) no-repeat; } } .clock { display: block; font-size: 5em; text-align: center; font-variant-numeric: tabular-nums; }
Получившийся пакет разработки расширяет вложенный синтаксис, встраивает SVG и генерирует исходную карту:
/* src/css/partials/variables.css */ :root { --font-body: sans-serif; --color-fore: #fff; --color-back: #112; } /* src/css/partials/elements.css */ *, *::before, ::after { box-sizing: border-box; font-weight: normal; padding: 0; margin: 0; } body { font-family: var(--font-body); color: var(--color-fore); background: var(--color-back) url(/images/web.png) repeat; margin: 1em; } header h1 { font-size: 2em; padding-left: 1.5em; margin: 0.5em 0; background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>*{fill:none;stroke:%23fff;stroke-width:1.5;stroke-miterlimit:10}<\/style></defs><circle cx="12" cy="12" r="10.5"></circle><circle cx="12" cy="12" r="0.95"></circle><polyline points="12 4.36 12 12 16.77 16.77"></polyline></svg>') no-repeat; } .clock { display: block; font-size: 5em; text-align: center; font-variant-numeric: tabular-nums; } /* src/css/main.css */ /*# sourceMappingURL=main.css.map */
Получившийся производственный пакет уменьшит код до 764 символов (здесь опущен SVG):
:root{--font-body: sans-serif;--color-fore: #fff;--color-back: #112}*,*:before,:after{box-sizing:border-box;font-weight:400;padding:0;margin:0}body{font-family:var(--font-body);color:var(--color-fore);background:var(--color-back) url(/images/web.png) repeat;margin:1em}header h1{font-size:2em;padding-left:1.5em;margin:.5em 0;background:url('data:image/svg+xml,<svg...></svg>') no-repeat}.clock{display:block;font-size:5em;text-align:center;font-variant-numeric:tabular-nums}
Наблюдение, восстановление и служение
Оставшаяся часть esbuild.config.jsскрипта объединяется один раз для производственных сборок перед завершением:
if (productionMode) { // single production build await buildCSS.rebuild(); buildCSS.dispose(); await buildJS.rebuild(); buildJS.dispose(); }
Во время сборок разработки скрипт продолжает работать, отслеживает изменения файлов и снова автоматически объединяется. Контекст buildCSSзапускает веб-сервер разработки с build/корневым каталогом:
else { // watch for file changes await buildCSS.watch(); await buildJS.watch(); // development server await buildCSS.serve({ servedir: './build' }); }
Запустите сборку разработки с помощью:
npm start
Затем перейдите к localhost:8000 для просмотра страницы.
В отличие от Browsersync, вам потребуется добавить собственный код на страницы разработки для перезагрузки в реальном времени. Когда происходят изменения, esbuild отправляет информацию об обновлении через событие, отправленное сервером. Самый простой вариант — полностью перезагрузить страницу при любых изменениях:
new EventSource('/esbuild').addEventListener('change', () => location.reload());
В примере проекта объект контекста CSS используется для создания сервера. Это потому, что я предпочитаю вручную обновлять изменения JavaScript — и потому, что я не смог найти способ, с помощью которого esbuild может отправлять события как для обновлений CSS, так и для JS! HTML-страница включает следующий сценарий для замены обновленных файлов CSS без полного обновления страницы (горячая перезагрузка):
<script type="module"> // esbuild server-sent event - live reload CSS new EventSource('/esbuild').addEventListener('change', e => { const { added, removed, updated } = JSON.parse(e.data); // reload when CSS files are added or removed if (added.length || removed.length) { location.reload(); return; } // replace updated CSS files Array.from(document.getElementsByTagName('link')).forEach(link => { const url = new URL(link.href), path = url.pathname; if (updated.includes(path) && url.host === location.host) { const css = link.cloneNode(); css.onload = () => link.remove(); css.href = `${ path }?${ +new Date() }`; link.after(css); } }) });
Обратите внимание, что esbuild в настоящее время не поддерживает горячую перезагрузку JavaScript — не то чтобы я все равно ему доверял!
Заключение
С небольшой настройкой esbuild может быть достаточно, чтобы справиться со всеми требованиями вашего проекта к разработке и производственной сборке.
Существует полный набор плагинов, если вам требуется более продвинутая функциональность. Имейте в виду, что они часто включают Sass, PostCSS или аналогичные инструменты сборки, поэтому они эффективно используют esbuild в качестве средства запуска задач. Вы всегда можете создать свои собственные плагины, если вам нужны более легкие настраиваемые параметры.
Пользуюсь esbuild год. Скорость поразительна по сравнению с аналогичными сборщиками, и часто появляются новые функции. Единственным незначительным недостатком являются критические изменения, требующие обслуживания.