webpack: 7 бед — один ответ

в 10:27, , рубрики: javascript, node.js, React, webpack, Веб-разработка

После моего недавнего выступления на MoscowJS #17 с одноимённым докладом у многих возник интерес к этому инструменту. В рамках 11-го выпуска RadioJS, Миша Башкиров bashmish рассказал, что решился попробовать его в своём новом проекте, об успешном опыте и множестве положительных эмоций. Но были озвучены вопросы и возникла дискуссия, в результате которой я решил написать эту статью, чтобы раскрыть основные тезисы с доклада и рассказать о том, что тогда не успел.
Статья ориентирована, как на профессионалов, так и на тех, кто с похожими технологиями ещё не сталкивался.
Итак, начнём.

7 бед

Современный мир веб-разработки открывает перед нами, как невероятные возможности, так и проблемы. По масштабу их можно разделить на два:

  • Глобальные проблемы отрасли веб-разработки.
  • Локальные проблемы наших проектов.

Глобальные проблемы

  1. Разрозненность решений. HTML5 и JavaScript окончательно развязали нам руки. Новые и революционные решения создаются чуть ли не каждый день. У каждого своя специфика, преимущества и недостатки. При этом все они созданы для решения небольшого, по сути, круга задач:
    • jQuery, Knockout, Angular, Marionette, React — если оттолкнуться от деталей, всё это создано для решения одних и тех же задач.
    • Underscore, lodash, Lazy, ES6 — во многом, для работы с данными.
    • MVC, MVVM, Flux — даже архитектурные паттерны созданы для решения одной задачи.
  2. Разнообразие версии этих решений на порядок увеличивает масштаб озвученной проблемы:
    • Один плагин требует jQuery 1.8, второй — jQuery 2.1. Что делать?
    • Angular 1.3 и Angular 2 — почти полностью разные.
    • Bootstrap 2.3 и Bootstrap 3.1 — компоненты, созданные для одной версии не встанут из коробки для другой.
  3. Форматы для пре-процессинга (сюда же контейнеры) например:
    • LESS, SCSS, SASS, Stylus — для стилей.
    • Handlebars, jade, EJS — для шаблонов.
    • JSON, JSON5, PJSON, XML — для данных.
    • CoffeeScript, JSX, ES6 — скрипты и т.д.

Как это отражается в реальной работе?

Локальные проблемы

  1. Сложность роста проекта:
    • Функциональная — создать прототип, добавить раздел, сделать форму, связать разделы между собой и т.д.
    • Технологическая — перейти на Bootstrap, подключить Leaflet, внедрить React и т.д.
  2. Управление зависимостями. Когда в проекте появляются хотя бы 3 скрипта, начинается жонглирование — постоянно приходится задумываться:
    • Загружены ли библиотеки?
    • А необходимые стили?
    • А шаблоны?
    • Если это jQuery-плагин, загружен ли для него jQuery?
    • А необходимые стили?
    • А шаблоны?
    • А какие ещё необходимы ему библиотеки?
  3. Управление версиями (суть проблемы описана выше).

Какое решение?

Наиболее оптимальное решение — разбивать код на изолированные модули. Исторически для этого сложилось два подхода — 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>
      

Загрузчики

Благодаря этой точке расширения 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

Позволяет видеть обновления на странице без пересборки проекта. Просто, как магия.
Как это работает:

  1. создаётся веб-сервер на основе вашей assets-директории;
  2. при загрузке файла сборки, устанавливается соединение через socket.io;
  3. как только вы что-то изменили — автоматически обновляется открытая страница

Запуск через консоль.

npm install webpack-dev-server -g
webpack-dev-server --content-base assets/

Запуск через скрипт

Установка:

npm install webpack-dev-server —save-dev

Использование:

  1. Конфигурацию выносим в webpack.config.js, оставляя в builds.js лишь:
    var webpack = require('webpack');
    var config = require('./webpack.config');
    var compiler = webpack(config);
    compiler.run(function () {
    // stay silent
    });
    

  2. Создаём 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');
    

  3. Запускаем:
    node dev-server
    

  4. Открываем: 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:

  1. Вы открываете на одном мониторе — IDE, на втором — браузер.
  2. В окне IDE изменяете код своего React-компонента, сохраняете.
  3. В это время на страницу через открытое socket-соединение передаётся информация только об изменённой части.
  4. Происходит “горячая” замена компонента (unmount + mount).
  5. На экране автоматические обновляется изменённый компонент.

Подробней об этом можно почитать на странице разработчика расширения.
Или посмотреть на этом видео:

Невероятно, ведь так?

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.

Резюме

  1. Современный мир веб-разработки открывает перед нами, как невероятные возможности, так и проблемы.
  2. Основная проблема — постоянная эволюция (рост количества и качества как решений, так и проектов).
  3. Это ставит перед нами задачи — быть гибкими, открытыми, быстрыми и эффективными.
  4. Изолированные модули и единый интерфейс их взаимодействия — это то, без чего невозможно развитие и то, что делает из JavaScript полноценную экосистему.
  5. CommonJS стал стандартом де-факто в организации модулей (npm, Bower).
  6. На сегодняшний день, webpack — самая мощная платформа для сборки и оптимизации, которая учитывает весь опыт предыдущего поколения сборщиков (Require.js, Browserify) и реализует его наиболее эффективным способом. webpack может легко работать как с таскерами вроде Grunt и gulp, так и во многих случаях заменить их.
  7. 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js