После моего недавнего выступления на MoscowJS #17 с одноимённым докладом у многих возник интерес к этому инструменту. В рамках 11-го выпуска RadioJS, Миша Башкиров bashmish рассказал, что решился попробовать его в своём новом проекте, об успешном опыте и множестве положительных эмоций. Но были озвучены вопросы и возникла дискуссия, в результате которой я решил написать эту статью, чтобы раскрыть основные тезисы с доклада и рассказать о том, что тогда не успел.
Статья ориентирована, как на профессионалов, так и на тех, кто с похожими технологиями ещё не сталкивался.
Итак, начнём.
7 бед
Современный мир веб-разработки открывает перед нами, как невероятные возможности, так и проблемы. По масштабу их можно разделить на два:
- Глобальные проблемы отрасли веб-разработки.
- Локальные проблемы наших проектов.
Глобальные проблемы
- Разрозненность решений. HTML5 и JavaScript окончательно развязали нам руки. Новые и революционные решения создаются чуть ли не каждый день. У каждого своя специфика, преимущества и недостатки. При этом все они созданы для решения небольшого, по сути, круга задач:
- jQuery, Knockout, Angular, Marionette, React — если оттолкнуться от деталей, всё это создано для решения одних и тех же задач.
- Underscore, lodash, Lazy, ES6 — во многом, для работы с данными.
- MVC, MVVM, Flux — даже архитектурные паттерны созданы для решения одной задачи.
- Разнообразие версии этих решений на порядок увеличивает масштаб озвученной проблемы:
- Один плагин требует jQuery 1.8, второй — jQuery 2.1. Что делать?
- Angular 1.3 и Angular 2 — почти полностью разные.
- Bootstrap 2.3 и Bootstrap 3.1 — компоненты, созданные для одной версии не встанут из коробки для другой.
- Форматы для пре-процессинга (сюда же контейнеры) например:
- LESS, SCSS, SASS, Stylus — для стилей.
- Handlebars, jade, EJS — для шаблонов.
- JSON, JSON5, PJSON, XML — для данных.
- CoffeeScript, JSX, ES6 — скрипты и т.д.
Как это отражается в реальной работе?
Локальные проблемы
- Сложность роста проекта:
- Функциональная — создать прототип, добавить раздел, сделать форму, связать разделы между собой и т.д.
- Технологическая — перейти на Bootstrap, подключить Leaflet, внедрить React и т.д.
- Управление зависимостями. Когда в проекте появляются хотя бы 3 скрипта, начинается жонглирование — постоянно приходится задумываться:
- Загружены ли библиотеки?
- А необходимые стили?
- А шаблоны?
- Если это jQuery-плагин, загружен ли для него jQuery?
- А необходимые стили?
- А шаблоны?
- А какие ещё необходимы ему библиотеки?
- Управление версиями (суть проблемы описана выше).
Какое решение?
Наиболее оптимальное решение — разбивать код на изолированные модули. Исторически для этого сложилось два подхода — AMD и CommonJS. О них уже многое писали и говори, но приведу краткий обзор (кто знаком — может пропустить).
AMD (Asynchronous Module Definition)
Сводится к определению модуля через define() и вызову через require():
define(['jquery', 'products'], function ($, products) {
return {
show: function () {
products.forEach(function (item) {
$('.items').append(item.html);
});
}
};
});
Достаточно удобно. Но при росте зависимостей это превращается в спагетти-код. Поэтому разработчики добавили CommonJS-обёртку:
define(function (require, module, exports) {
var $ = require('jquery');
var products = require('products');
module.exports = {
show: function () {
products.forEach(function (item) {
$('.items').append(item.html);
});
}
};
});
Подробней об этом хорошо изложено в статье от clslrns.
CommonJS
Нативно реализован на серверном JavaScript в Node.js/Rhino.
Сводится к определению модуля через глобальную переменную и вызову через require():
var $ = require('jquery');
var products = require('products');
module.exports = {
show: function () {
products.forEach(function (item) {
$('.items').append(item.html);
});
}
};
Основные преимущества перед AMD:
- Повышенная читаемость.
- Упрощённая написание.
- Изоморфность (один код, как для браузера, так и для сервера):
var _ = require('lodash'); var data = require('./stock.json'); module.exports = _.where(data, function (item) { return item['count'] > 0; });
Подробней об этом рассказывал в своём докладе Антон Шувалов из Rambler.
Оба эти подхода позволяют:
- Создавать и использовать изолированные модули.
- Не думать, о порядке загрузки.
- Безопасно подключать сторонние решения.
- Использовать разные версии библиотек.
- Собирать из нескольких модулей один JS-файл.
Так что же теперь? Что воплотит наши фантазии в жизнь? Что лучшее мы имеем на сегодняшний день?
Встречайте — webpack
Вы только представьте. Любая логика. Любые форматы. Ваш проект быстро собирается. Ваш проект быстро загружается. Вы имеете самые развитые инструменты разработки. А теперь давайте взглянем подробней.
Быстрое начало
Для эксперимента, создадим директорию src и в ней простой скрипт index.js:
console.log('Hello Habrahabr!');
В директории assets будет наши выходные файлы. Это те самые файлы, которые мы можем выложить на наш веб-сервер, CDN и т.д.
Глобально есть две стратегии использования webpack:
- через консоль (подходит для небольших проектов);
- через скрипт в качестве Node.js-модуля.
Использование через консоль.
npm install webpack -g
webpack src/index.js assets/bundle.js
Использование в качестве модуля.
Если ещё не сделали это ранее — самое время начать: устанавливаем Node.js и npm. После этого в директории проекта создаём package.json. Это можно сделать командой:
npm init
Теперь, когда у нас есть npm, добавляем к проекту webpack:
npm install webpack —save-dev
Для сборки проекта создаём Node.js-скрипт. Например, build.js:
var path = require('path');
var webpack = require('webpack');
var config = {
context: path.join(__dirname, 'src'), // исходная директория
entry: './index', // файл для сборки, если несколько - указываем hash (entry name => filename)
output: {
path: path.join(__dirname, 'assets') // выходая директория
}
};
var compiler = webpack(config);
compiler.run(function (err, stats) {
console.log(stats.toJson()); // по завершению, выводим всю статистику
});
Запускаем сборку:
node build
В обоих случаях мы получаем директорию assets и в ней bundle.js — это наш собранный файл, где будет сам index.js и все подключаемые им зависимости. В примере выше, он у меня был размером 1528 байт.
Для его использования нам не нужен никакой дополнительный загрузчик, поэтому достаточно подключить только этот файл:
<!doctype html>
<html>
<body>
<script src="assets/bundle.js"></script>
</body>
</html>
Вот всё и заработало. Полноценно webpack может раскрыться, только через конфигурацию, поэтому далее я не буду приводить примеров для консоли, однако, открыть для себя всю магию консоли Вы без проблем можете в документации.
Плагины
Одна из главных точек расширения webpack — это плагины. Они позволяют менять 'на лету' логику сборки, алгоритм поиска модулей, иными словами, залезать в самое сердце процесса сборки.
Подключение происходит через добавление секции plugins в передаваемых настройках.
Примеры плагинов:
- webpack.optimize.UglifyJsPlugin — минификации кода с использованием UglifyJS.
Настройка (build.js):
... plugins: [ new webpack.optimize.UglifyJsPlugin(), ...
После добавления этих строк, файл bundle.js уменьшается с 1528 до 246 байт.
- webpack.optimize.DedupePlugin — удаление дубликатов модулей. Если Вы, например, используете сторонние Node.js-библиотеки, то с большой вероятностью некоторые используемые ими модули могут быть дублированны. Этот плагин находит дубликаты модулей и удаляет их. Это не влияет на стабильность кода, но существенно может сократить размер собранного файла.
- webpack.DefinePlugin — определение констант и выражений внутрь кода. Как в старом добром C++.
Настройка (build.js):
... plugins: [ DefinePlugin({ 'NODE_ENV': JSON.stringify('production') }), ...
Использование (index.js):
if (NODE_ENV === 'production') { console.log('There is Production mode'); } else { console.log('There is Development mode'); }
При сборке этот код будет собран в следующий вид:
if (true) { console.log('There is Production mode'); } else { console.log('There is Development mode'); }
А если включена минификация:
console.log('There is Production mode');
Это достаточно удобный функционал для того, чтобы разделять код на слои и делать продакшн-код ещё более чистым и быстрым.
- BowerWebpackPlugin — сторонний плагин, который позволяет осуществить прозрачное подключение bower-пакетов.
Установка:
npm install bower-webpack-plugin --save-dev
Настройка (build.js):
... plugins: [ new BowerWebpackPlugin({ modulesDirectories: ['bower_components'], manifestFiles: ['bower.json', '.bower.json'], includes: /.*/, excludes: /.*.less$/ }), ...
Использование:
bower install jquery
index.js:
var $ = require('jquery'); if (NODE_ENV === 'production') { $('body').html('There is Production mode.'); } else { $('body').html('There is Development mode.'); }
- ExtractTextPlugin позволяет извлеч содержимое всех подключаемых CSS-файлов в один отдельный CSS-файл. Это позволяет ускорить загрузку, поскольку CSS загружается асинхронно (параллельно JS-файлам). Рекомендуется использовать при большом количестве стилей.
Установка:
npm install css-loader style-loader extract-text-webpack-plugin—save-dev
Примечание: пакеты css-loader и style-loader — это загрузчики для загрузки и подключения в DOM стилей. Подробней о них речь пойдёт дальше.
Настройка (build.js):
... module: { loaders: [ { test: /.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') }, ... ], }, plugins: [ new ExtractTextPlugin('style.css'), ...
Использование:
- src/header.css:
h1 { color: #036; }
- src/header.js:
var $ = require('jquery'); require('./header.css'); $('body').prepend('<h1>Hello, Habrahabr</h1>');
- src/index.css:
body { background: #eee; }
- src/index.js:
var $ = require('jquery'); require('./index.css'); require('./header'); if (NODE_ENV === 'production') { $('body').append('There is Production mode.'); } else { $('body').append('There is Development mode.'); }
- index.html:
<!doctype html> <html> <head> <link rel="stylesheet" href="assets/styles.css" /> </head> <body> <script src="assets/bundle.js"></script> </body> </html>
- src/header.css:
Загрузчики
Благодаря этой точке расширения webpack обеспечивает прозрачное подключение любой статики — CSS, LESS, SASS, Stylus, CoffeeScript, JSX, JSON, шрифтов, графики, шаблонов и т.д. Вы просто указываете имя файла в require(), а загрузчики сами обеспечивают все необходимые операции для его подключения.
Подключаете CSS? Загрузчики позаботятся обо всём сами — загрузят CSS-данные, а в момент исполнения добавят в DOM элемент с этими стилями.
Пишете модули в LESS, CoffeeScript или JSX? Загрузчики сами выполнят весь пре-процессинг при сборке — Вам достаточно просто указать имя файла.
Пример использования загрузчиков
Установка:
npm install style-loader css-loader json-loader handlebars-loader url-loader --save-dev
Настройка (build.js):
...
module: {
loaders: [
{test: /.css$/, loader: 'style-loader!css-loader'},
{test: /.json$/, loader: 'json-loader'},
{test: /.hbs$/, loader: 'handlebars-loader'},
{
test: /.(eot|woff|ttf|svg|png|jpg)$/,
loader: 'url-loader?limit=30000&name=[name]-[hash].[ext]'
},
...
Загрузчики указываются в порядке справа налево, т.е. для CSS — сначала используется css-loader, а его результат передаётся в style-loader, который помещает загруженные данные, как блок <style />.
url-loader — особенный загрузчик. В данном примере, если файлы графики и шрифтов имеют размер до 30кб, он вставляет их в виде data:uri. Если же они превышают этот объем, то он сохраняет их в выходную директорию с заданным шаблоном имени, где hash — уникальное значение для файла. Такой подход позволяет с одной стороны — избежать дополнительных обращений к серверу (даже при Keep-Alive, это дорогостоящая операция), с другой — избежать проблем с кэшированием одноимённых файлов (этот подход известен, как static freeze).
Использование:
- src/customer.json:
{"name": "Habrahabr"}
- src/header.hbs:
<h1>Hello, dear {{name}}</h1>
- src/header.js:
var $ = require('jquery'); // загружаем данные из JSON-файла в объект: var customer = require('./customer.json'); // загружаем и компилируем шаблон: var Header = require('./header.hbs'); require('./header.css'); // отдаём данные в шаблон и выводим полученный HTML $('body').prepend(Header(customer));
Интеграция с React JSX
webpack отлично дружит с React и позже станет ясно почему, а пока просто приведу пример подключения JSX-скриптов.
Установка:
npm install react jsx-loader —save-dev
Настройка (build.js):
...
resolve: {
extensions: ['', '.js', '.jsx'],
},
module: {
loaders: [
{test: /.jsx$/, loader: 'jsx-loader'},
...
Использование:
- src/toolbar.jsx:
var React = require('react'); module.exports = React.createClass({displayName: 'Toolbar', render: function () { return ( <div> <button>Button 1</button> </div> ); } });
- src/index.js:
var $ = require('jquery'); var React = require('react'); var Toolbar = require('./toolbar'); require('./index.css'); require('./header'); React.render( React.createElement(Toolbar), document.getElementById('toolbar') ); if (NODE_ENV === 'production') { $('body').append('There is Production mode.'); } else { $('body').append('There is Development mode.'); }
- index.html:
<!doctype html> <html> <body> <div id="toolbar"></div> <script src="assets/bundle.js"></script> </body> </html>
Инструмент webpack-dev-server
Позволяет видеть обновления на странице без пересборки проекта. Просто, как магия.
Как это работает:
- создаётся веб-сервер на основе вашей assets-директории;
- при загрузке файла сборки, устанавливается соединение через socket.io;
- как только вы что-то изменили — автоматически обновляется открытая страница
Запуск через консоль.
npm install webpack-dev-server -g
webpack-dev-server --content-base assets/
Запуск через скрипт
Установка:
npm install webpack-dev-server —save-dev
Использование:
- Конфигурацию выносим в webpack.config.js, оставляя в builds.js лишь:
var webpack = require('webpack'); var config = require('./webpack.config'); var compiler = webpack(config); compiler.run(function () { // stay silent });
- Создаём dev-server.js:
var WebpackDevServer = require('webpack-dev-server'); var config = require('./webpack.config.js'); var devServer = new WebpackDevServer( webpack(config), { contentBase: __dirname, publicPath: '/assets/' } ).listen(8088, 'localhost');
- Запускаем:
node dev-server
- Открываем: http://localhost:8088/webpack-dev-server/
Перед нами появится index.html со строкой статуса в шапке: “App ready”. Если сейчам мы что-то изменим в коде, страница автоматически обновится.
Hot Module Replacement — чистая магия
Позволяет обновлять, удалять и добавлять модули в реальном режиме, т.е. даже без перезагрузки страницы (тем самым сохранив её состояние). Мало того, что это безумно весело, это позволяет на порядок быстрей прототипировать веб-приложения и разрабатывать сложные Single Page Applications.
Развёрнутый ответ автора на вопрос What exactly is Hot Module Replacement in Webpack?
Как это работает в связке с React:
- Вы открываете на одном мониторе — IDE, на втором — браузер.
- В окне IDE изменяете код своего React-компонента, сохраняете.
- В это время на страницу через открытое socket-соединение передаётся информация только об изменённой части.
- Происходит “горячая” замена компонента (unmount + mount).
- На экране автоматические обновляется изменённый компонент.
Подробней об этом можно почитать на странице разработчика расширения.
Или посмотреть на этом видео:
Невероятно, ведь так?
Chunks
webpack позволяет разбивать собранный код на части. Например, Вы можете выделить общий код для всех страниц в assets/common.js, а для каждой отдельной страницы делать свой assets/feed.js, assets/products.js и т.д. Таким образом, при первой загрузке, common.js будет закеширован, а для каждой из страниц Вашего проекта будет достаточно догрузить небольшой файл с нужным для неё чанком. Забегая вперёд, Facebook использует порядка 50 чанков на странице выдачи, в то время, как Instagram в среднем по два, например — common.js и Feed.js.
Производительность и анализ
По моим личным наблюдениям и наблюдением коллег производительность сборки у webpack на порядок выше аналогов. Во многом за счёт применения “умной” сборки и AST-парсинга.
Для тонкой и более эффективной оптимизации webpack предлагает развитую статистику по результату сборки Вашего проекта и инструменты для визуального анализа.
Подведём итоги
Итак, мы рассмотрели:
- Быстрый старт
- Плагины и их примеры:
- webpack.optimize.UglifyJsPlugin
- webpack.optimize.DedupePlugin
- webpack.DefinePlugin
- BowerWebpackPlugin
- ExtractTextPlugin
- Загрузчики:
- Работа со статикой
- Подключение React JSX
- webpack-dev-server
- Hot Module Replacement
- Chunks
- Производительность и анализ
Миграция со старых сборщиков
Помимо CommonJS, из коробки поддерживается и AMD — это позволяет быстро и безболезненно перебраться с Require.js.
Миграция с Browserify происходит также легко и волшебно, как и всё остальное — для этого в документации даже есть раздел webpack for Browserify users.
Как насчёт поддержки?
По личному опыту — на вопросы в github ответили в течении дня. Разработчики очень открытые и активные. Делал pull-request'ы — автор принял их на следующий день. Динамика каммитов на github впечатляет.
Так значит можно доверять?
Безусловно. Например, команда Facebook использует webpack для веб-интерфейса Instagram. Если быть честным, то делая реверс-инжиниринг этого проекта я и наткнулся на webpack.
Кроме этого, Twitter использует webpack для своих проектов, о чём на конференции Fronteers 2014 рассказал Nicholas Gallagher.
Резюме
- Современный мир веб-разработки открывает перед нами, как невероятные возможности, так и проблемы.
- Основная проблема — постоянная эволюция (рост количества и качества как решений, так и проектов).
- Это ставит перед нами задачи — быть гибкими, открытыми, быстрыми и эффективными.
- Изолированные модули и единый интерфейс их взаимодействия — это то, без чего невозможно развитие и то, что делает из JavaScript полноценную экосистему.
- CommonJS стал стандартом де-факто в организации модулей (npm, Bower).
- На сегодняшний день, webpack — самая мощная платформа для сборки и оптимизации, которая учитывает весь опыт предыдущего поколения сборщиков (Require.js, Browserify) и реализует его наиболее эффективным способом. webpack может легко работать как с таскерами вроде Grunt и gulp, так и во многих случаях заменить их.
- webpack открывает перед нами мир npm (112 393 пакетов) и bower (20 694 пакетов), делая их использование таким же простым и прозрачным, как и использование своих модулей.
Призываю всех нас держать руку на пульсе. Мыслить глобально. Всегда развиваться и видеть, что творится в мире. Быть смелей, активней, экспериментировать. Изучать, как работают успешные проекты. Не держаться, а делиться и обмениваться своими открытиями, результатами и решениями. Это сделает каждого из нас быстрей, умней и эффективней.
Благодарю за внимание.
Ссылки
Документация по webpack — http://webpack.github.io/docs/
Скачать и попробовать — https://github.com/webpack/webpack
Пример из статьи — https://github.com/DenisIzmaylov/hh-webpack
Автор: DenisIzmaylov