Frontender Magazine

Пакуем как боги

Webpack — это крутая новая утилита для сборки бандлов и оптимизации модулей JavaScript и других ресурсов для фронтенда. Если вы уже пользовались RequireJS или Browserify, то есть все шансы, что вы полюбите webpack так же, как и я. Эта статья — подробная инструкция для вас.

Логотип webpack

Чтобы продемонстрировать магию webpack, я в этой статье буду использовать в качестве примеров очень простой, даже противоестественно примитивный, код. Он хранится вот тут, на GitHub.

Представьте, что вам что-то взбрело в голову и захотелось написать код, который изменяет цвет блока текста на розовый, и код, который делает начертание блока текста полужирным по нажатию на кнопку. Непросто, ага. Вот как бы вы такое чудо сделали?

Если бы мы пилили JavaScript как в старые добрые беззаботные времена, то вы бы создали такие файлы:

function Pinkyfier(id) { // Орозовитель
    this.element = document.getElementById(id);
}

Pinkyfier.prototype.pink = function () {
    this.element.style.backgroundColor = "mistyrose";
    this.element.style.color = "hotpink";
}

js/Pinkyfier.js

function Fattyfier(id) { // Ожирнитель
    this.element = document.getElementById(id);
}

Fattyfier.prototype.fat = function () {
    this.element.style.fontWeight = "bold";
}

js/Fattyfier.js

var pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

И подключили эти три файла при помощи тегов script, вот как-то так:

<!DOCTYPE html>
<html lang="ru">
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <p id="text">
            Родился на улице Герцена, в гастрономе № 22. Известный экономист,
            по призванию своему — библиотекарь. В народе — колхозник.
            В магазине — продавец. В экономике, так сказать, необходим.
        </p>
        <button id="fat" type="button">Ожирнить</button>

        <script src="js/Fattyfier.js"></script>
        <script src="js/Pinkyfier.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>

index.html

Теперь открывайте их в своём браузере (или смотрите на демо тут), вы любуетесь розовым цветом, щёлкаете по кнопке, чтобы сделать его полужирным, и вы довольны. Что ещё нужно для счастья?

Ну, все функции и переменные, которые вы определили в этих трёх файлах, находятся в глобальной области видимости. Как известно каждому программисту, глобальные переменные — полная лажа. Что, если бы у вас было более сложное приложение со, скажем, сотней файлов JS вместо трёх? Что, если бы код в этих файлах зависел от других файлов сильнее, и изменение порядка подключения скриптов ломало бы страницу?

Если бы вы продолжали делать так, в итоге получилась бы здоровенная неуклюжая каша из кода. Вам нужна система модулей.

Способ получше: модули AMD

Прежде чем рассматривать магию webpack и то, как он всё делает, лучше давайте взглянем на самые известные системы модулей: AMD, CommonJS и, в ближайшем будущем, ES6.

Самая распространённая реализация модулей AMD — это RequireJS. Посмотрим, как мы можем сделать текст на страничке розовым и полужирным при помощи RequireJS.

Вместо того, чтобы вываливать классы Pinkyfier и Fattyfier прямо в глобальную область, мы обернём их в клёвую конструкцию define:

define(function () {
    function Pinkyfier(id) {
        this.element = document.getElementById(id);
    }

    Pinkyfier.prototype.pink = function () {
        this.element.style.backgroundColor = "mistyrose";
        this.element.style.color = "hotpink";
    }

    return Pinkyfier;
});

js/Pinkyfier.js

define(function () {
    function Fattyfier(id) {
        this.element = document.getElementById(id);
    }

    Fattyfier.prototype.fat = function () {
        this.element.style.fontWeight = "bold";
    }

    return Fattyfier;
});

js/Fattyfier.js

Вы теперь можете в своём main.js запросить модули, которые мы только что определили:

require([ "Fattyfier", "Pinkyfier" ], function (Fattyfier, Pinkyfier) {

    var pinkyfier = new Pinkyfier("text"),
        fattyfier = new Fattyfier("text");

    pinkyfier.pink();

    document.getElementById("fat").onclick = function () {
        fattyfier.fat();
    }
});

js/main.js

А в коде HTML вместо трёх тегов script, как в примере ранее, вы просто пишете один тег script, который загружает RequireJS, с data-атрибутом указывающим на точку входа в ваше приложение, main.js:

<script data-main="js/main.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.17/require.min.js"></script>

index.html

Вот так с лёгкостью можно строить более сложные приложения, и что самое приятное, модули подгружаются асинхронно — блокирует загрузку страницы только загрузка самой библиотеки RequireJS, ваши же скрипты её не блокируют.

Посмотрите на результат вживую тут.

Как в Node.js: модули CommonJS

С появлением серверного JavaScript на основе Node.js или io.js стала популярной другая система модулей JavaScript, CommonJS.

Хотя она используется в основном для npm-модулей и приложений, выполняющихся на серверах, при помощи Browserify ей можно также пользоваться в коде на клиентской стороне. Это очень круто, потому что в ваше распоряжение попадает всё изобилие модулей npm, а также это позволяет использовать один и тот же код как на бэкенде, так и на фронтенде.

Вот наш маленький примерчик в виде модуля CommonJS:

function Pinkyfier(id) {
    this.element = document.getElementById(id);
}

Pinkyfier.prototype.pink = function () {
    this.element.style.backgroundColor = "mistyrose";
    this.element.style.color = "hotpink";
}

module.exports = Pinkyfier;

js/Pinkyfier.js

function Fattyfier(id) {
    this.element = document.getElementById(id);
}

Fattyfier.prototype.fat = function () {
    this.element.style.fontWeight = "bold";
}

module.exports = Fattyfier;

js/Fattyfier.js

Как вы могли заметить, единственное отличие от вышеупомянутой «наївной» реализации в последних строчках каждого файла указывающих, какие части кода следует отдавать клиенту при подгрузке модулей, а именно, классы Pinkyfier и Fattyfier. В этом отношении модули CommonJS проще и менее навязчивы, чем RequireJS.

Чтобы использовать эти модули, добавьте вызов require в main.js:

var Pinkyfier = require("./Pinkyfier"),
    Fattyfier = require("./Fattyfier"),

    pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

А как этим пользоваться в браузере? Browserify работает иначе, чем RequireJS. Вместо того, чтобы подгружать библиотеку, которая всё делает сама, мы при помощи утилиты на node создаём бандл, файл, содержащий весь ваш код на JavaScript.

Чтобы это сделать, установите Browserify из npm (Я полагаю, что node и npm у вас установлены):

npm install -g browserify

А теперь перейдите в папку, где хранятся ваши файлы JS и создайте бандл такой командой:

browserify main.js > bundle.js

Теперь достаточно просто подключить его в код HTML тегом script:

<script src="js/bundle.js"></script>

index.html

Посмотрите на результат вживую.

Будущее: модули ES6

Сейчас, когда я пишу эти строки, новый стандарт JavaScript лишь только на подходе и должен быть выпущен в июне 2015.

ES6 привносит нативную поддержку модулей JavaScript, делая AMD и CommonJS устаревшими.

Давайте взглянем, как орозовить и ожирнить наш рыбный текст с помощью ES6:

class Pinkyfier {

    constructor(id) {
        this.element = document.getElementById(id);
    }

    pink() {
        this.element.style.backgroundColor = "mistyrose";
        this.element.style.color = "hotpink";
    }
}

export default Pinkyfier;

js/Pinkyfier.js

class Fattyfier {

    constructor(id) {
        this.element = document.getElementById(id);
    }

    fat() {
        this.element.style.fontWeight = "bold";
    }
}

export default Fattyfier;

js/Fattyfier.js

Ух ты, а что это было? Ну, в ES6, как вы видите, классы определяются совсем по-другому, нежели в том старом JavaScript, который мы знаем и любим. Но речь тут даже не об этом, вся соль в последних строчках с ключевыми словами export, которые выносят определённые классы из файла в клиентский модуль. Выглядит похоже на пример с CommonJS, не правда ли?

А вот так мы используем модули в ES6:

import Pinkyfier from "./Pinkyfier";
import Fattyfier from "./Fattyfier";

let pinkyfier = new Pinkyfier("text"),
    fattyfier = new Fattyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    fattyfier.fat();
}

js/main.js

В ES6 export и import — части языка и браузеры (из будущего) их понимают, и могут подгружать модули без участия библиотек вроде RequireJS или сборщиков вроде Browserify. Вы можете просто подключить файл main.js через тег script.

Вы можете посмотреть на результат работы вживую тут. Хотя нет, не можете, ну разве только что вы не гость из будущего. В настоящем мой браузер жалуется: «модули пока не реализованы».

Но webpack может это исправить, и далее в этой статье мы увидим, как.

Прошу любить и жаловать, webpack

Итак, давайте уже наконец перейдём к webpack. Начнём с примера с AMD/RequireJS и «вебпакифицируем» его. Webpack поддерживает модули AMD прямо из коробки, так что мы просто используем модули AMD из примера выше: Pinkyfier.js, Fattyfier.js и main.js.

В использовании он схож с Browserify, вы устанавливаете утилиту на node через npm и пользуетесь ей, чтобы собрать один или несколько бандлов.

Установка webpack:

npm install -g webpack

Чтобы настроить webpack, создайте файл настроек под именем webpack.config.js. В этом простом варианте там будет находиться только код настроек, который указывает webpack путь, где он должен искать модули (modulesDirectories), где у приложения точка входа (entry), как назвать и куда положить файл бандла на выходе (output).

var webpack = require("webpack");

module.exports = {
    entry: "./main",
    resolve: {
        modulesDirectories: [
            "."
        ]
    },
    output: {
        publicPath: "js/",
        filename: "bundle.js"
    }
};

js/webpack.config.js

После того, как мы создали в нужном месте файл, мы можем просто набрать «webpack» в командной строке:

webpack

Это создаст два файла, bundle.js и 1.bundle.js.

«А почему два?» — спросите вы. Что ж, это из-за того, что мы используем модули AMD, которые подгружаются асинхронно. bundle.js содержит код из main.js, а в 1.bundle.js — код из Pinkyfier.js и Fattyfier.js, который грузится асинхронно. Если бы у нас были модули CommonJS, которые всегда подгружаются синхронно, на выходе был бы всего один файл. Вы уже начинаете понимать, насколько дьявольски умная эта утилита?

Далее мы подключаем bundle.js через тег script в HTML (подключать второй бандл не нужно).

И смотрим на то, что получилось.

Интересный момент

Это всё хорошо, но в чём тут преимущество по сравнению с использованием RequreJS? С RequireJS идёт в комплекте оптимизатор (r.js), который тоже может создавать бандлы…

Тут начинается самое интересное: помните, что я говорил про Browserify, что он позволяет использовать модули npm и на бэкенде, и на фронтенде, а преимущество RequireJS в асинхронной загрузке? Так вот, с webpack вы можете взять лучшее от обоих миров. webpack поддерживает и модули AMD, и модули CommonJS одновременно.

Попробуйте сами, вы можете заменить Pinkifier.js в формате AMD на версию с CommonJS. Запустите команду webpack ещё раз.

Посмотрите на пример и результат — всё работает точно так же.

Заметьте, не требуется никакой дополнительной настройки, не нужно говорить webpack: «Эй, я использую оба формата модулей». Webpack достаточно умён, чтобы понять это самостоятельно.

Назад в будущее

Вернёмся к нашему примеру с ES6, который я, к сожалению, не могу запустить на своём браузере образца апреля 2015 года. Может ли нам помочь webpack? Легко! В webpack есть понятие загрузчиков, дополнительных модулей, которые добавляются в конфигурацию, чтобы загружать файлы, соответствующие какому-то признаку. Есть целая огромная куча загрузчиков для самых различных вещей, не только для JavaScript, а даже для CSS или изображений.

Мы настроим загрузчик Babel для всех файлов JavaScript, добавив такой блок в webpack.config.js:

module: {
        loaders: [
            {
                test: /\.js$/,
                loader: "babel-loader"
            }
        ]
    }

js/webpack.config.js

Теперь webpack будет загружать все файлы JS через загрузчик, который использует Babel чтобы транскомпилировать код на ES6 в код старой версии JavaScript, который смогут понять нынешние браузеры.

Загрузчик Babel не является частью webpack по умолчанию, это лишь дополнение, поэтому придётся установить его в проект через npm.

Я добавил package.json, так что можно просто запустить npm install в командной строке (из папки проекта), и он установится.

После запуска webpack мы получим единственный бандл (ничего асинхронного тут не происходит), который можно подключить через тег script.

Посмотрите на этот пример вживую.

Хорошенькая, чистенькая асинхронность

Вернёмся к примеру с AMD + webpack, я писал ранее, что webpack автоматически создаёт несколько бандлов когда ему попадаются модули AMD. Это приятно, но зачем это может пригодиться? Мы загружаем орозовитель и ожирнитель асинхронно, но вся эта асинхронная загрузка происходит сразу после загрузки страницы. Не очень-то большой выигрыш в скорости загрузки страницы по сравнению с одним большим бандлом со всем кодом, подключённым через тег script перед закрывающим тегом body, приёмом, который многими признан полезным.

Но действительно ли нам нужен модуль Fattifier? Вообще-то, он нам нужен только когда пользователь щёлкнет по кнопке «ожирнить». Разве не было бы круто использовать асинхронную загрузку как преимущество и загружать код ожирнителя только когда он нам нужен, т.е., когда кнопка была нажата?

С webpack этого очень легко добиться. Мы изменим код main.js, как-то так:

var Pinkyfier = require("./Pinkyfier"),
    pinkyfier = new Pinkyfier("text");

pinkyfier.pink();

document.getElementById("fat").onclick = function () {
    require(["./Fattyfier"], function (Fattyfier) {
        var fattyfier = new Fattyfier("text");
        fattyfier.fat();
    });
}

js/main.js

Что тут происходит? Я смешал модули в стилях CommonJS и AMD в одном файле: require в стиле CommonJS на строке 1 отвечает за загрузку модуля Pinkyfier синхронно. require в стиле AMD на строке 7 загружает модуль Fattyfier асинхронно.

Запустив команду webpack, я получаю на выходе файлы: bundle.js с кодом из main.js и Pinkyfier.js и 1.bundle.js, с кодом из Fattyfier.js.

Когда я открою страницу в браузере, загрузится только bundle.js. И только после того, как я нажму на кнопку, подгрузится другой бандл.

Посмотрите на этот пример.

Это хороший приём для уменьшения времени загрузки страницы и увеличения скорости работы сайта. Мы используем его на странице поиска mobile.de, когда я щёлкаю по кнопке «расширенный поиск» сверху слева, появляется большая старая форма для поиска. Весь код JavaScript для этой формы, даже код шаблона, который рендерится на клиентской стороне шаблонизатором Soy, загружается асинхронно, только после нажатия кнопки.

Заключение

Итак, теперь у вас должно было появиться понимание того, что такое webpack, и как он работает.

Очевидно, это только начало.

Как я уже упоминал, к вашим услугам имеется огромное количество модулей-загрузчиков и плагинов для различных задач вроде минификации или компиляции Sass или LESS в CSS. Вы можете сказать webpack генерировать карты кода для более удобной отладки JavaScript в браузере. Можете запустить webpack как сервер для разработки, и он будет отслеживать изменения в коде и сразу же обновлять сгенерированные файлы. Интегрировать webpack в Grunt или Gulp и генерировать хэши содержимого (также известные как отпечатки пальцев) для оптимизации кэширования в браузере. Использовать различные бандлы для различных страниц вашего приложения и позволить webpack автоматически организовать общие модули для этих страниц в общие бандлы. Можете даже написать собственный модуль-загрузчик, что, кстати говоря, очень легко.

Развлекайтесь, исследуя возможности!

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Patrick Hund
Автор:
Patrick Hund
GitHub:
pahund
Twitter:
@wiekatz
Антон Хлыновский
Переводчик:
Антон Хлыновский
GitHub:
subzey
Twitter:
@subzey

Комментарии (31 комментарий, если быть точным)

Автар пользователя
dehimer

Хм, у меня всё равно пока не возникло желания слезать с require.js. По крайней мере переписывать всю кучу кода точно.

Автар пользователя
SilentImp

Переписывать ничего точно смысла не имеет. А вот попробовать в новом проекте — вероятно, да.

Автар пользователя
SilentImp

Я правда не очень уверен в осмысленности прикручивания его к тому же gulp в качестве задачи. Он сам по себе для небольших проектов вполне заменяет этот самый gulp.

Автар пользователя
mr47

Тут есть небольшая проблема, автор описывает webpack как чудо вещь но забывает упомянуть что когда фронтенд разрастается и его обьем увеличивается бандла в разы.

Представьте себе бандл размером 1 - 2МБ и динамическими модулями, гугл картами и momentjs каким то с локалями и получается все 2-3МБ. А такие бандлы хранить в кеше не эффективно, почему ?

Это тема целой статьи ...

Решение пока это requirejs и частичные бандлы. А лучше это babel или systemjs или stealjs (очень крутая штука между прочим).

Мне нравится из прощупанного это systemjs отличная штука

Автар пользователя
iamstarkov

разве systemjs решает покрывает тот же спектр задач как и webpack?

Автар пользователя
mr47

@iamstarkov более чем там особый врапер для модулей поддерживается стандартный amd/commonjs + es6(import и System.import) + другие фичи (там поддержка babel и многое другое) подробнее тут. Что сразу дает нам поддержку по стандарту, т.е в будущем просто заменяем наш systemjs на найтивные модули и все.

Автар пользователя
iamstarkov

а require('./component.css') работает? кастомные лоадеры? hot-swap?

Автар пользователя
mr47

javascript System.import('some/data.css!').then(function(css){})

Что соответствует стандарту es6 там есть плагины не только для css (json,handlebars,etc). Т.е есть возможность писать кастомные лоадеры. Есть live-reload. hot-swap не слышал пока, возможно есть в stealjs там что то подобное проскакивало.

Автар пользователя
iamstarkov

live-reload это hot-swap из палеолита

Автар пользователя
vporoshok

Вот сколько не смотрю на все эти webpack’и, browserify’и и system’ы, а роднее require.js, ведь кто кроме него может грузить скрипт с cdn с fallback’ом на локальную версию?

Автар пользователя
mr47

@vporoshok с этим справится stealjs %) Кроме того es6 на дворе пора уже задумываться об альтернативах.

Автар пользователя
vporoshok

@mr47 вот это отличная новость! Надо почитать. Спасибо =)

Автар пользователя
artyomtrityak

Я люблю require.js, но webpack выглядит действительно хорошо. Я использовал require.js на протяжении 3х лет, сейчас в пет проектах использую webpack. Хот свап имхо не очень удобен, а вот кеширующий сервер + поддержка es6 очень даже клево

Автар пользователя
mr47

Посмотрел я все таки webpack, а то руки не доходили очень даже занятная вещь, хотя есть свои минусы например отсутствие System.import но я думаю с помощью плагинов можно исправить ситуацию. Из минусов могу причислить те же плагины их очень много, а много не === качественно. Webpack явно стоит того чтобы использовать уже сейчас.

Да я немного был не прав насчет частичных бандлов в комментарии выше, все можно делать просто нужно было читать документацию wepack'a.

Автар пользователя
whitecolor

@iamstarkov Stealjs добавлено недавно hot-swap: http://blog.bitovi.com/hot-module-replacement-comes-to-stealjs/

Я не пробовал, использую собстбственный hot reloader написанный для systemjs/stealjs (там довольно простая работа с модулями удаление, переопределение, перезагрузка).

Systemjs надо по нормальному использовать с jspm, но там своя кухня с зависимостями.

Stealjs добавляет удобство при работе с npm модулями (в приципе как и с bower, если нужно) - не надо ничего конфигурировать.

Webpack я смотрел, хорошая штука, опять же своя проприетарная немецкая кухня (т.е. создатель из германии). Решили остаться на stealjs, особенно после того как вышла новая версия. http://blog.bitovi.com/introducing-stealjs/

Автар пользователя
iamstarkov

круто, что ввели

ps. назвать эту фичу live-reload как-то неправильно (это из блогпоста)

Автар пользователя
Tom910

В конце статьи говорится о "хэши содержимого", где можно прочитать подробнее о вариантах интеграции в cms? к примеру в WP?

Автар пользователя
iamstarkov

А как вы планируете интегрировать?

Автар пользователя
Tom910

Хороший вопрос, хочется сделать автоматическое обновление файлов на сервере, что бы я, когда комитил в продуктовую ветку гита, файлы обновлялись и на сервере. Обновление файлов не было проблемой, но появилась проблема в обновлении кэша у клиентов, приходилось в ручную менять. Какие есть решения ? Хотя когда писал это, я понял, что не рационально хранить в гите скомпилированные файлы, так как они сильно будут увеличивать размер гита.

Автар пользователя
iamstarkov

вы правы, также справедливо, что этим не должен заниматься вебпак. Инвалидация кеша одна из двух самых главных проблем программирования =)

Автар пользователя
whitecolor

@Tom910 мы строим production bundles на сервере при деплое, там же пакуется хешируя имена бандлов.

Автар пользователя
huston007

@mr47

Представьте себе бандл размером 1 - 2МБ и динамическими модулями, гугл картами и momentjs каким то с локалями и получается все 2-3МБ. А такие бандлы хранить в кеше не эффективно, почему ?

Не правда, есть встроенна кухня для разбиения бандлов. http://webpack.github.io/docs/code-splitting.html

Автар пользователя
mr47

@huston007 я посмотрел webpack, и поправил сам себя ниже в комментариях :)

Автар пользователя
huston007

@mr47 В таком случае извиняюсь, не слишком внимательно пролистал.

Автар пользователя
mrsum

Честно сказать webpack действительно хорош. Вот немного цифр из реального приложения: - количество модулей: ~340 - используется jade, stylus, svgstore + svgmin - доступны amd, различные плагины для сбора ассетов и манифестов - всякие разные ништячки вроде ауторелоада - все это в конфиге в 80 строк - aliases – вот это действительно крутая вещь - map files из коробки - uglify + minify

Все это укладывается легко и непринужденно. Стоит сказать что конфиг gulp с аналогичным функционалом выходил далеко за 150 строк + увесистый багаж разных плагинов. Все быстро и действительно четко

Автар пользователя
iamstarkov

а что такое aliases?

Автар пользователя
huston007

@iamstarkov Можно прописывать произвольные aliases для произвольных модулей. Чтобы, например, require('foo') превращалось в require('foo/lib/lab/bar.js)`. Кажется, что это ненужная вещь, но ровно до тех пор, пока не захочется подсунуть несуществующий модуль или тому подобную функциональность (бывает при сложной сборке множества проектов разом)

http://webpack.github.io/docs/configuration.html#resolve-alias http://webpack.github.io/docs/resolving.html#aliasing

Автар пользователя
mrsum

@huston007 Зря вы так. Если aliases использовать лишь для определения неймспейсов, то очень помогает избегать головняков вида javascript require('../../../../foo/bar');

Автар пользователя
huston007

@mrsum Зависит от ситуации. Оборотная сторона медали - это довольно неявно, читая такие require-ы не легко сразу понять, что это за зависимость. Это ещё нужно вспомнить, что где-то прописаны aliases, пойти в webpack.config, посмотреть их.

require('../../../../foo/bar'); - это больно да. К счастью, большинство npm-пакетов сами описывают свой main-файл, и этим пользоваться не приходит.

Если же нужно часто подключать кусочки какого-то пакета, то лучше добавить его корень в modules directories или в resolve fallback

Автар пользователя
BaNdErOzZz

Горите в аду, люди, которые делают статьи без указания даты.

Автар пользователя
myadzel

@BaNdErOzZz, ну, не все так плохо:

<meta property="article:published_time" content="2015-05-12">