Пособие по webpack

https://github.com/shekhargulati/52-technologies-in-2016/blob/master/36-webpack/README.md
  • Перевод
  • Tutorial


Давайте сначала разберемся, зачем нужен вебпак (webpack), и какие проблемы он пытается решить, а потом научимся работать с ним. Webpack позволяет избавиться от bower и gulp/grunt в приложении, и заменить их одним инструментом. Вместо bower'а для установки и управления клиентскими зависимостями, можно использовать стандартный Node Package Manager (npm) для установки и управления всеми фронтэнд-зависимостями. Вебпак также может выполнять большинство задач grunt/gulp'а.


Bower это пакетный менеджер для клиентской части. Его можно использовать для поиска, установки, удаления компонентов на JavaScript, HTML и CSS. GruntJS это JavaScript-утилита командной строки, помогающая разработчикам автоматизировать повторяющиеся задачи. Можно считать его JavaScript-альтернативой Make или Ant. Он занимается задачами вроде минификации, компиляции, юнит-тестирования, линтинга и пр.

Допустим, мы пишем простую страницу профиля пользователя в веб-приложении. Там используется jQuery и библиотеки underscore. Один из способов — включить оба файла в HTML:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>User Profile</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" media="screen">
    <link rel="stylesheet" href="/css/style.css" media="screen">
  </head>
  <body>
    <div class="container">
      <div class="page-header">
        <h1 id="timeline"></h1>
      </div>
      <ul class="timeline">
      </ul>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script src="js/profile.js"></script>
  </body>
</html>

Это простой HTML с Бутстрапом. Мы подключили jQuery и underscore с помощью тега script.


Давайте рассмотрим файл profile.js, который использует подключенные нами библиотеки. Наш код лежит внутри анонимного замыкания, которое хранит бизнес-логику приложения. Если не замыкать код в функцию, то переменные будут находиться в глобальном окружении, и это плохо.


(function(){
  var user = {
    name : "Shekhar Gulati",
    messages : [
      "hello",
      "bye",
      "good night"
    ]
  };

  $("#timeline").text(user.name+ " Timeline");

  _.each(user.messages, function(msg){
    var html = "<li><div class='timeline-heading'><h4 class='timeline-title'>"+msg+"</h4></div></li>";
    $(".timeline").append(html);
  });

}());

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



У этого кода две задачи:


  1. получить информацию о пользователе
  2. настроить таймлайн.

Известно, что смешивать понятия — плохая практика, так что нужно написать отдельные модули, отвечающие за определенные задачи. В файле profile.js мы использовали анонимное замыкание для хранения всего кода. В JavaScript есть способы делать модули получше. Два популярных способа это CommonJS и AMD.


  • Модуль CommonJS это грубо говоря кусок повторно используемого кода, который экспортирует определенные объекты, и они становятся доступными другим модулям с помощью require.
  • Asynchronous Module Definition (AMD) позволяет загружать модули асинхронно.

Если хотите узнать о модулях в JavaScript больше, то советую прочитать статью JavaScript Modules: A Beginner’s Guide.


А в этой статье мы будем писать модули на CommonJS. Давайте напишем модуль timeline с методами для установки хедера и таймлайна. В CommonJS можно импортировать зависимости с помощью функции require. Таймлайн зависит от jquery и underscore.


var $ = require('jquery');
var _ = require('underscore');

function timeline(user){
  this.setHeader = function(){
      $("#timeline").text(user.name+ " Timeline");
  }

  this.setTimeline = function(){
    _.each(user.messages, function(msg){
      var html = "<li><div class='timeline-heading'><h4 class='timeline-title'>"+msg+"</h4></div></li>";
      $(".timeline").append(html);
    });
  }
}

module.exports = timeline;

Этот код создает новый модуль timeline. Есть две функции: setHeader и setTimeline. Мы используем специальный объект module и добавляем ссылку на нее в module.exports. Таким образом мы сообщаем модульной системе CommonJS, что хотим позволить другим функциям использовать модуль.


Теперь обновим profile.js, он должен использовать модуль timeline. Можно создать новый модуль, который будет загружать информацию о пользователе, но пока давайте ограничимся одним модулем.


var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

Если загрузить index.html в браузере, то отобразится пустая страница. В консоли (в developer tools) можно обнаружить ошибку:


profile.js:1 Uncaught ReferenceError: require is not defined

Проблема в том, что браузеры не понимают модульную систему вроде CommonJS. Нужно предоставить браузеру формат, который он ожидает.


Бандлеры модулей идут на помощь


Веб-браузеры не понимают эти хорошо описанные модули. Нужно или добавить весь JavaScript-код в один файл и импортировать его, или нужно добавить все файлы вручную на страницу с помощью тега script. Используем бандлер модулей (module bundler) для решения этой проблемы. Бандлер модулей комбинируют разные модули и их зависимости в один файл в правильном порядке. Он может парсить код, написанный с использованием разных модульных систем, и комбинировать в один формат, понятный браузеру. Два популярных бандлера модулей это:


  1. browserify: пакует npm-модули, чтобы потом использовать их в браузере. В случае с browserify приходится дополнительно подключать Grunt или Gulp для линтинга, запуска тестов и пр. Это значит, что нужно тратить время на работу с несколькими инструментами и интеграцией.
  2. webpack: система сборки, которая предоставляет не только бандлинг (компоновку) модулей, но и может выполнять задачи, которыми занимаются Gulp/Grunt. К тому же, вебпак не ограничивается JavsScript-файлами, он может работать с другой статикой вроде CSS, картинок, html-компонентов и др. Вебпак также поддерживает очень полезную фичу — code splitting (разбиение кода). Большое приложение можно разбить на куски, которые загружаются по мере необходимости.

Что такое вебпак?


Официальное определение звучит так:


webpack берет модули с зависимостями и генерирует статические ресурсы, которые представляют эти модули

Это определение теперь имеет смысл, когда понятна решаемая проблема. Вебпак берет набор ресурсов и трансформирует их в наборы (бандлы).



Трансформацией ресурсов занимаются загрузчики, которые являются сердцем вебпака.


Вебпак в действии


Для установки вебпака нужен node. Можно скачать node с официального сайта.


Теперь можно установить вебпак глобально:


$ npm install -g webpack

Создайте новый модуль командой npm init. Она создаст файл package.json. Установите зависимости с помощью npm.


$ npm install -S jquery
$ npm install -S underscore

В дополнение, нужно установить вебпак как зависимость.


$ npm install -S webpack

Замените index.html следующим кодом. Как видите, мы удалили все теги script для jquery и underscore. Также, вместо импорта js/profile.js импортируется dist/bundle.js.


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>User Profile</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" media="screen">
    <link rel="stylesheet" href="/css/style.css" media="screen" title="no title">

  </head>
  <body>

    <div class="container">
      <div class="page-header">
        <h1 id="timeline"></h1>
      </div>
      <ul class="timeline">
      </ul>

    </div>

    <script src="dist/bundle.js" charset="utf-8"></script>
  </body>
</html>

Давайте создадим бандл.


$ webpack js/profile.js dist/bundle.js

Hash: 6d83c7db8ae0939be3d0
Version: webpack 1.13.2
Time: 350ms
    Asset    Size  Chunks             Chunk Names
bundle.js  329 kB       0  [emitted]  main
   [0] ./js/profile.js 252 bytes {0} [built]
   [1] ./js/timeline.js 427 bytes {0} [built]
    + 2 hidden modules

Теперь страница работает нормально.


Можно сделать так, чтобы вебпак следил за изменениями и генерировал бандл автоматически. Для этого нужно запустить его с таким флагом:


$ webpack -w js/profile.js dist/bundle.js

Теперь вебпак не будет завершаться сам. При изменении файлов будет генерироваться новый бандл. Нужно лишь перезагрузить страницу в браузере. Давайте изменим profile.js:


var user = {
  name : "Shekhar Gulati!!!",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

Файл bundle.js, сгенерированный вебпаком, содержит много кода, относящегося к самому вебпаку, а ваш код там будет в измененном виде. Будет очень неудобно отлаживать приложение в браузере, в инструментах разработчика, например. Чтобы упростить себе жизнь, можно запустить вебпак с флагом devtools.


$ webpack -w --devtool source-map js/profile.js dist/bundle.js

Вебпак сгенерирует source map для файла bundle.js. Source map связывает минимизированный и собранный в один файл код с исходным, несобранным кодом. Для тестирования можно добавить строчку debugger в profile.js


var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

debugger;
var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

Перезагрузите страницу, и приложение остановится на этой строке.



Добавление CSS


В HTML выше видно, что мы загружаем /css/style.css. Вебпак умеет работать не только с JavaScript, но и с другой статикой, в том числе CSS. Удалите строку с /css/style.css из index.html. Мы будем подключать стили в profile.js таким образом:


require('../css/style.css');

var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

вебпак перезагрузит изменения, и мы увидим сообщение об ошибке в консоли:


ERROR in ./css/style.css
Module parse failed: /Users/shekhargulati/dev/52-technologies-in-2016/36-webpack/code/css/style.css Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

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


$ npm install style-loader css-loader --save-dev

Вебпак использует загрузчики для трансформации текста в нужный формат. Теперь нужно обновить require:


require('style!css!../css/style.css');

Синтаксис style!css! означает, что сначала нужно применить трансформацию css для конвертации текста из style.css в CSS, а потом применить стиль к странице с помощью трансформации style.


Запустите вебпак снова.


$ webpack -w --devtool source-map js/profile.js dist/bundle.js

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


Чтобы не указывать все опции в командной строке, можно создать конфигурационный файл webpack.config.js в корне приложения:


module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  }
}

Теперь можно запускать вебпак простой командой webpack -w.


Когда мы добавили style!css! в profile.js, мы смешали продакшен-код с конфигурацией вебпака. Можно перенести эту опцию в файл конфигурации.


После изменений конфигурации нужно перезапускать вебпак.


var webpack = require('webpack');

module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  module:{
    loaders: [
      {test : /\.css$/, loader: 'style!css!'}
    ]
  }
}

Самая интересная секция тут это декларация модулей. Тут мы указали, что если файл заканчивается на .css, то нужно применять трансформацию style!css!.


Горячая перезагрузка


Для горячей перезагрузки (hot reloading) нужен webpack-dev-server. Установите его так:


$ npm install -g webpack-dev-server

Теперь можно запускать сервер командой webpack-dev-server.


Мы запустим сервер по адресу http://localhost:8080/webpack-dev-server/ с конфигурацией из webpack.config.js.


Порт можно изменить опцией --port.


$ webpack-dev-server --port 10000

 http://localhost:10000/webpack-dev-server

Конфигурацию webpack-dev-server также можно указать в файле webpack.config.js, в секции devServer.


module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  module:{
    loaders: [
      {test : /\.css$/, loader: 'style!css!'}
    ]
  },
  devServer: {
    inline:true,
    port: 10000
  },
}



На сегодня все. Узнать больше о вебпаке можно из документации.

Поделиться публикацией

Комментарии 13

    +1
    Всё-таки авторитет, известность автора статьи/перевода имеет значение.
    Если бы я эту статью сам в оригинале нашел, только лишь пробежал бы глазами «ага, можно будет потом вернуться, когда-нибудь, наконец, разобраться».
    Из ваших же рук, материал уже отобранный, прошёл фильтрацию, одобрен и рекомендован к изучению читателю.
    Спасибо, годный тьюториал. А то с Grunt'ом как-то сходу не пошло.
      +4
      Шутка в тему:
      — А как в этот Webpack'e подключить стили?
      — Ну тут все просто, ставишь precss, postcss-loader, postcss-import, css-loader, style-loader, file-loader, normalizr, normalizr.css, autoprefixer
      — Поставил все как вы сказали, но картинки не загружаются.
      — Очевидно вы забыли установить image-webpack-loader
        0

        "hot reloading" — нет такого понятия.


        // "Live Reloading" и "Hot Module Replacement", соответственно: 
        $ webpack-dev-server  --inline --hot
          +1
          Мне понравился этот скринкаст скринкаст по webpack, может кому-то поможет разобраться с ним.
            +1
            Удивительно насколько понятным стал для меня webpack, для человека вчера познакомившимся с nodejs, и не изучавший ни разу gulp/grunt. Спасибо за статью, написано очень понятным языком, что даже бегиннеры поймут с чем есть вебпак.
              +1
              Спасибо за статью!
              Допустим, мы пишем простую страницу...

              Мне кажется это основная проблема подобных туториалов.
              Всё упомянутые инструменты, будь то Gulp, Grunt, bower, browsery или webpack, хороши на простых примерах.
              Но как их использовать на реальных проектах, в которых десятки модулей, специфичные требования, какие-нибудь не стандартные библиотеки?

              В частности, про webpack мне было бы интересно почитать про:
              — как настраивать различные окружения (например, dev для локального запуска, а prod с минификацией и ссылками на реальные бэкенды);
              — как собирать готовый билд, который потом можно выкатывать в продукцию (не просто js-бандл, а отдельная директория / zip / jar со всем необходимым);
              — как интегрировать всё это с процессами бэкенда (аля, написал «gradlew build» и на выходе получил и фронт и бэк).

              У меня есть смутные предчувствия что одним webpack-ом тут не обойтись.
                +1

                Часть ответов есть здесь: https://habrahabr.ru/company/plarium/blog/309230/.

                  0

                  Отдельная директория с отдельными ассетами (вместо бандла) получается при использовании extract-text-webpack-plugin.
                  zip (а уж из него можно же сделать jar) делается zip-it-loader.
                  Интеграция с gradle делается как и везде: cd frontend/dir && npm run build.

                    0
                    — как настраивать различные окружения (например, dev для локального запуска, а prod с минификацией и ссылками на реальные бэкенды);

                    Элементарно, на основе переданных переменных окружения, у вас ведь вся инфраструктура Node.js в руках. В деталях реализация может быть разной, например используя webpam-merge модуль, ограничений нет.
                    — как собирать готовый билд, который потом можно выкатывать в продукцию (не просто js-бандл, а отдельная директория / zip / jar со всем необходимым);

                    Как вам более удобно так и собирать, у меня допустим Maven билдит Java и вместе с этим весь фронт, включая и Webpack и Gulp и прочий зоопарк.
                    — как интегрировать всё это с процессами бэкенда (аля, написал «gradlew build» и на выходе получил и фронт и бэк).

                    Как вам более удобно так и интегрировать. Webpack — винтик в общем стеке и нет проблем его интегрировать куда-либо.
                    +2
                    Webpack позволяет избавиться от bower и gulp/grunt

                    Это заявление не правда и новичками может быть понято не верно, т.к. раскрыто не полностью.

                    Вместо bower'а для установки и управления клиентскими зависимостями, можно использовать стандартный Node Package Manager (npm)

                    Да, вместо bower-а можно использовать npm, но Webpack вас этому не обязывает и вообще ему до этого нет дела.

                    Вебпак также может выполнять большинство задач grunt/gulp'а

                    grunt/gulp и webpack не взаимозаменяемые инструменты, т.к. их область применения совершенно разная. grunt и gulp могут быть полностью заменены webpack-ом только в том случае, если их задачами была только сборка проекта.

                    Знаю, что это перевод, уточнения, скорее, для читателя.
                      0
                      grunt и gulp могут быть полностью заменены webpack-ом только в том случае, если их задачами была только сборка проекта.

                      Пару примеров где webpack не справится учитывая написание кастомных плагинов/лоадеров? В целом конечно же grunt и gulp это раннеры задач. считай скрипты, что х-очешь то и делай, а webpack гораздо более деакларативная вещь которая свою задачу выполняет лучше простых скриптов тк она специализирована. Никто не мешает использовать webpack в связке с gulp при необходимости, это не взаимоисключающие вещи. В большинстве случаев webpack заменяет собой gulp/grunt.
                        +1
                        У grunt\gulp и webpack разная философия.

                        Первые запускают задачи, из частых — скомпилить стили (sass, less), минифицировать и склеить js, почистить папки. При чем так же умеют их запускать по отдельности и\или комбинировать.

                        Webpack же в свою очередь комплексное решение для сборки проекта, где каждый файл\сущность это модуль. Он не может отдельно собрать css или js, он делает все сразу (нет, ну конечно вы можете наплодить файлы конфигураций и запускать webpack указывая эти файлы, но зачем?).

                        Я уже отмечал, что эти инструменты могут существовать вместе, выполняя свои задачи. Тот же gulp может служить «запускатором» задачи сборки проекта webpack-ом. Пример из жизни: был проект, состоящий из подпроектов и gulp использовался, чтобы запускать сборку каждого из них. А сборщикам были где кто — то gulp, то webpack.

                        На gulp/grunt можно навесить задачи деплоя, миграций и многое другое, что не касается сборки проекта. Поэтому идеей моего негодования было именно то, что webpack может заменить gulp/grunt не в большинстве случаев, а именно тогда, когда на эти инструменты возлагались задачи сборки проекта и ни для чего более они не использовались.
                          0
                          Я ведь то же самое выше написал :) grunt/gulp раннеры, по сути голый JS облагороженные некоторой не слишком жесткой структурой, поэтому очевидно свобода полная, а webpack декларативная вещь. Это как например сравнить Ant c Maven в Java мире, при этом их тоже можно использовать вместе.

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

                      Самое читаемое