[Из песочницы] webpack: 7 бед — один ответ
После моего недавнего выступления на 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: Подробней об этом рассказывал в своём докладе Антон Шувалов из 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 байт.Для его использования нам не нужен никакой дополнительный загрузчик, поэтому достаточно подключить только этот файл:
Вот всё и заработало. Полноценно 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 ('
Hello, Habrahabr
'); 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: Загрузчики Благодаря этой точке расширения 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, который помещает загруженные данные, как блок .url-loader — особенный загрузчик. В данном примере, если файлы графики и шрифтов имеют размер до 30 кб, он вставляет их в виде data: uri. Если же они превышают этот объем, то он сохраняет их в выходную директорию с заданным шаблоном имени, где hash — уникальное значение для файла. Такой подход позволяет с одной стороны — избежать дополнительных обращений к серверу (даже при Keep-Alive, это дорогостоящая операция), с другой — избежать проблем с кэшированием одноимённых файлов (этот подход известен, как static freeze).
Использование:
src/customer.json: {«name»: «Habrahabr»} src/header.hbs:
Hello, dear {{name}}
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 (
Hot Module Replacement — чистая магия Позволяет обновлять, удалять и добавлять модули в реальном режиме, т.е. даже без перезагрузки страницы (тем самым сохранив её состояние). Мало того, что это безумно весело, это позволяет на порядок быстрей прототипировать веб-приложения и разрабатывать сложные Single Page Applications.Развёрнутый ответ автора на вопрос What exactly is Hot Module Replacement in Webpack?
Как это работает в связке с React:
Вы открываете на одном мониторе — IDE, на втором — браузер. В окне IDE изменяете код своего React-компонента, сохраняете. В это время на страницу через открытое socket-соединение передаётся информация только об изменённой части. Происходит «горячая» замена компонента (unmount + mount). На экране автоматические обновляется изменённый компонент. Подробней об этом можно почитать на странице разработчика расширения.Или посмотреть на этом видео:[embedded content]
Невероятно, ведь так?
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