Привет!
Отгремели фанфары, прошел звон в ушах от истязаний «евангелистов», модников в сфере фронтенд разработки. Кто-то ушел на sprockets, кто-то пустился во все тяжкие и стал писать свои велосипеды или расширять функционал gulp или grunt. Но статей по поводу популярных инструментов автоматизации процесса сборки – стало существенно меньше и это факт! Пора бы заполнить освободившееся пространство чем-то существенно иным.
Уверен многие слышали о webpack. Кто-то подумал «он слишком много на себя берет» и не стал дочитывать даже вводную статью на оффициальном сайте проекта. Некоторые решили попробовать, но столкнувшись с небольшим количеством примеров настройки — отверг инструмент решив подождать пару лет. Но в целом, разговоров «вокруг» ходит масса. В этой статье — хочу развенчать некоторые сомнения. Может быть, кто-то заинтересуется и перейдет на «светлую сторону». Вообщем желающие под кат.
Честно сказать, активно работать с этим мощным инструментом, я начал в начале этого года. Сначала конечно же “wow” эффект. Который достаточно быстро сменился болью от жутко неудобной документации. Но пересилив этот этап – честно забыл о многих типичных проблемах, был поражен скоростью и удобством. Не буду утомлять, разберемся с…
Механика
Логика работы очень простая, но эффективная
Webpack принимает один или несколько файлов, так называемых (entry points) используя пути из конфигурационного файла, далее загружает его.
Сборщик – во время обработки, встречая знакомую ему функцию require() – оборачивает содержимое вызываемого файла в функцию. Которая возвращает его контекст.
При этом не нужно заботиться о «гонке загрузки». Все что вы потребуете, будет доставлено в нужной последовательности!
Важно отметить, что во время разработки, когда запущен webpack-dev-server – промежуточные файлы (chunks) попадают в оперативную память RAM. Браузер же, получает их по протоколу webpack:// прямо из контейнера.
Так же dev-server – поднимает простейший веб сервер на порт 8080, к которому можно достучаться по адресу localhost:8080
Этот подход управления содержимым, как Вы понимаете ускоряет время промежуточной сборки, на значимое количество секунд. Что в рамках рабочего дня – экономит уйму Вашего времени.
Что касается окончательной сборки, то webpack используя тот же конфиг выполняет лишь этап, когда файлы раскладываются по папкам в файловой системе, готовые работать на Ваше благо.
source: jlongster.com/Backend-Apps-with-Webpack--Part-III
Подготовка
Тут все достаточно тривиально. Все что нам нужно это node.js и npm. Далее просто следуйте простым командам:
$ mkdir app
$ cd $_
$ npm init
& npm i webpack webpack-dev-server --save-dev # важно поставить именно в dev dependencies
Как бы это не звучало, но больше половины Вы уже сделали. Давайте перейдем к конфигурации проекта.
Для этого нужно определить что именно Вы хотите видеть и какие инструменты использовать. Условимся на этом:
- Common JS
- Stylus
- Jade
- Autoprefixer
Остальное поставим по необходимости.
Настройка
По умолчанию, webpack принимает конфигурационный файл выполненный в формате JSON. Находиться он должен в директории проекта и называться webpack.config.js
Для более удобной работы с запуском задач, а их будет две:
- Сборка готового проекта
- Режим разработки
Воспользуемся script секцией package.json файла, добавив следующие строчки:
"scripts": {
"dev": "NODE_ENV=development webpack-dev-server --progress",
"build": "NODE_ENV=production webpack --progress"
}
После этого, находясь в директории проекта, Вам будут доступны следующие команды:
$ npm run dev # режим разработки http://localhost:8080
$ npm run build # сборка готовых ассетов и файлов в ./dist
Создадим файлы конфигурации терминальной магией:
$ touch webpack.config.js # корневой файл конфигурации обрабатываемый webpack
$ mkdir config # директория с конфигурациями
$ cd $_ # перейдем
$ touch global.js # общие настройки инструментов, загрузчиков и плагинов
$ mkdir env && cd $_
$ touch development.js # файл с 'тонкой' настройкой webpack для режима разработки
$ touch production.js # аналогичный файл для конечной сборки
Перейдем к настройке окружения, для этого откроем файл ./webpack.config.js и заполним следующим содержимым:
'use strict';
var _ = require('lodash');
var _configs = {
global: require(__dirname + '/config/global'),
production: require(__dirname + '/config/env/production'),
development: require(__dirname + '/config/env/development')
};
var _load = function(environment) {
// Проверяем окружение
if (!environment) throw 'Can\'t find local environment variable via process.env.NODE_ENV';
if (!_configs[environment]) throw 'Can\'t find environments see _config object';
// load config file by environment
return _configs && _.merge(
_configs[environment](__dirname),
_configs['global'](__dirname)
);
};
/**
* Export WebPack config
* @type {[type]}
*/
module.exports = _load(process.env.NODE_ENV);
Как вы заметили lodash, исправим его отсутствие выполнением следующей команды:
$ npm i lodash --save-dev
Немного схитрив, мы сможем используя метод merge – библитеки lodash, 'склеить' нужную нам исходную конфигурацию, используя для этого 2 файла. В качестве аргумента функции принимая process.env.
./config/global.js
Файл содержит
'use strict'
var path = require('path');
var webpack = require('webpack');
var Manifest = require('manifest-revision-webpack-plugin');
var TextPlugin = require('extract-text-webpack-plugin');
var HtmlPlugin = require('html-webpack-plugin');
module.exports = function(_path) {
//define local variables
var dependencies = Object.keys(require(_path + '/package').dependencies);
var rootAssetPath = _path + 'app';
return {
// точки входа
entry: {
application: _path + '/app/app.js',
vendors: dependencies
},
// то, что получим на выходе
output: {
path: path.join(_path, 'dist'),
filename: path.join('assets', 'js', '[name].bundle.[chunkhash].js'),
chunkFilename: '[id].bundle.[chunkhash].js',
publicPath: '/'
},
// Небольшие настройки связанные с тем, где искать сторонние библиотеки
resolve: {
extensions: ['', '.js'],
modulesDirectories: ['node_modules'],
// Алиасы - очень важный инструмент для определения области видимости ex. require('_modules/test/index')
alias: {
_svg: path.join(_path, 'app', 'assets', 'svg'),
_data: path.join(_path, 'app', 'data'),
_fonts: path.join(_path, 'app', 'assets', 'fonts'),
_modules: path.join(_path, 'app', 'modules'),
_images: path.join(_path, 'app', 'assets', 'images'),
_stylesheets: path.join(_path, 'app', 'assets', 'stylesheets'),
_templates: path.join(_path, 'app', 'assets', 'templates')
}
},
// Настройка загрузчиков, они выполняют роль обработчика исходного файла в конечный
module: {
loaders: [
{ test: /\.jade$/, loader: 'jade-loader' },
{ test: /\.(css|ttf|eot|woff|woff2|png|ico|jpg|jpeg|gif|svg)$/i, loaders: ['file?context=' + rootAssetPath + '&name=assets/static/[ext]/[name].[hash].[ext]'] },
{ test: /\.styl$/, loader: TextPlugin.extract('style-loader', 'css-loader!autoprefixer-loader?browsers=last 5 version!stylus-loader') }
]
},
// загружаем плагины
plugins: [
new webpack.optimize.CommonsChunkPlugin('vendors', 'assets/js/vendors.[hash].js'),
new TextPlugin('assets/css/[name].[hash].css'),
new Manifest(path.join(_path + '/config', 'manifest.json'), {
rootAssetPath: rootAssetPath
}),
// Этот файл будет являться "корневым" index.html
new HtmlPlugin({
title: 'Test APP',
chunks: ['application', 'vendors'],
filename: 'index.html',
template: path.join(_path, 'app', 'index.html')
})
]
}
};
Аттеншен господа!
Появились новые зависимости – которые нужно доставить в проект следующей командой:
$ npm i path manifest-revision-webpack-plugin extract-text-webpack-plugin html-webpack-plugin --save-dev
Разработка
Методом проб и ошибок, оптимальный для меня конфиг режима разработки – стал выглядеть следующим образом:
'use strict';
/**
* Development config
*/
module.exports = function(_path) {
return {
context: _path,
debug: true,
devtool: 'eval',
devServer: {
contentBase: './dist',
info: true,
hot: false,
inline: true
}
}
};
Окончательная сборка
А вот настройка конечное сборки все еще в стадии «изменений». Хотя работает на 5+
'use strict';
/**
* Production config
*/
module.exports = function(_path) {
return {
context: _path,
debug: false,
devtool: 'cheap-source-map',
output: {
publicPath: '/'
}
}
}
index.html
Этот шаблон, мы положим в папку ./app/index.html – именно он будет отдавать правильные пути до хешированной статики, после конечной сборки.
<!DOCTYPE html>
<html{% if(o.htmlWebpackPlugin.files.manifest) { %} manifest="{%= o.htmlWebpackPlugin.files.manifest %}"{% } %}>
<head>
<meta charset="UTF-8">
<title>{%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}</title>
{% for (var css in o.htmlWebpackPlugin.files.css) { %}
<link href="{%=o.htmlWebpackPlugin.files.css[css] %}" rel="stylesheet">
{% } %}
</head>
<body>
{% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %}
<script src="{%=o.htmlWebpackPlugin.files.chunks[chunk].entry %}"></script>
{% } %}
</body>
</html>
В заключении
Хотелось бы поблагодарить разработчиков webpack за столь мощный инструмент.
Скорость его работы действительно поражает, даже на больших объемах исходных файлов.
Ах, да. Для того чтобы в своем проекте использовать например underscore достаточно установить его привычной npm командой
$ npm i underscore --save
После выполнения, библиотека попадет в dependencies – следовательно webpack, поместит его содержимое в файл vendors.[hash].js при этом захеширует название файла полученное от md5 размера исходников + время последнего их изменения.
Для чего это надо, объяснять не буду.
На этом все, пробуйте, пишите комменты.
Спасибо.
Ссылки
Тут можно посмотреть код приведенный в статье
А тут ознакомиться с документацией по проекту
Ну и тут статья, которая поможет разложить все еще раз.
Delphinum
Как то начал его пользовать, но закончил тем, что не смог решить на нем одну простую задачу, а именно:
необходимо было разделить весь JS на несколько файлов так, чтобы основная логика была в одном файле, а несколько придаточных логик в других. При этом в зависимости от разрешений текущего пользователя, файлы нескольких придаточных логик загружать не должны были (ну или вместо них должны были загружаться заглушки).
Может здесь подскажут, как это реализуется?
mrsum
Да конечно, это реализуется очень просто. Для этого Вам необходимо в файл конфигурации добавить еще один entrypoint. Рассмотрим ситуацию, когда есть скрипты основного приложения и скрипт формы авторизации:
Вот и вся магия.
Delphinum
А как решается, когда подключать придаточную логику, а когда заглушку?
mrsum
Подразумевается что вызов require() в каждом entry point — подтянет только ту логику, которая будет там использована.
Отвечая на Ваш вопрос – самостоятельно =)
Delphinum
Так задача не в том, подтягивать ли логику или не подтягивать в зависимости от необходимости. Ограничения такие: если заходит пользователь с
currentUser.role != 'admin'
, то ему загружается в качестве модуля 'admin' файл 'public/js/mock/admin.js', а если пользователь сcurrentUser.role == 'admin'
, то ему загружается файл 'public/js/admin.js'. Более того, если пользователь не является админом и попробует открывать все файлы js, которые найдет, он не должен найти адрес файла 'public/js/admin.js'. То есть для «не админа» в результирующем js файле (файлах) не должно быть ссылок на 'public/js/admin.js'.mrsum
Если не секрет, почему Вы эту логику не хотите реализовать через ACL?
Delphinum
Так речь об обычном web-сайте. Вы предлагаете прикрутить ACL к nginx?
geakstr
del
cyber_ua
А как вы решаете проблему с круговыми зависимостями?
mrsum
Мне кажется или это проблема архитектуры конкретного приложения?
auine
Циклическими*
Нет, это не проблема приложения, просто такое бывает необходимо :))
Я вижу сотни любителей вебпака, но ни один мне не может ответим, чем же этот толстый вебпак, лучше, удобней того же require.js, который поддерживает все тоже самое, и даже больше. Хоть не из коробки
Delphinum
Так require.js и webpack это два совершенно разных инструмента. Второй умеет собирать проект в статичный файл, оптимизировать его, добавлять хэши и тому подобное. Webpack лучше уж сравнивать с Grunt, чем с requireJS.
auine
require так же умеет собирать, в том числе и html.
requirejs.org/docs/optimization.html.
Grunt'ом можно рулить вебпакт, но не сравнивать :)
Delphinum
Так это же просто оптимизация, а для сборки может потребоваться еще много чего, разве нет?
auine
Прочитайте цели, который там написаны. Прежде чем задавать еще один вопрос :))
Так же повторюсь html и все, что вам угодно. При этом с require можно не билдить в дев окружении
Delphinum
Возможно я плохо задал вопрос и вы таки меня тыкните носом, чему я буду очень признателен, но где предварительная компиляция Sass, CoffeeScript, добавление хэшей к измененным файла, дробление на куски и подобный функционал, необходимый любому инструменту сборки? В целях, которые там написаны, я вижу только оптимизацию JS и CSS.
auine
Хм, вебпак так же использует сторонние модули для работы с такими вещами?
В данном случае я отдаю такое напрямую в траспилиры и пре(пост)процессоры. С помощью таск ранера, например gulp или grunt. Собственно и сам require оптимизатор запускается галпом.
На этом моменте можно начать сравнивать у кого конфиг меньше и что более популярно :))
webpack — 7к звезд, галп — 15к
Delphinum
В том то и разница, что Webpack — это инструмент, который объединяет несколько инструментов в один конвеер, который после выполняется над проектом и мы получаем результирующие js'ы, css'ы и html'ы. Он под это заточен, как и Grunt. С другой стороны, RequireJS это больше инструмент для подключения модулей. Ему даже не обязательно уметь оптимизировать и склеивать JS файлы (я считаю это лишним функционалом).
Я тоже (пользую Grunt), но вопрос не в этом. Не совсем верно сравнивать системы сборки проекта (таск ранеры, как вы их называете) и RequireJS, который по сути является инструментом загрузки зависимостей.
auine
Во, гору спасибок вам, так понятней) Теперь можно холиварить webpack vs gulp плагинами :)
Ну тут как и browserify, ему нужен свой механик, что бы собрать все + html темлпейты в стиле AMD модулей, тоже я пока негде не видел)
Delphinum
Да что тут холиварить. Хотите программируемые инструменты, пользуйте Gulp, хотите непрограммируемые, пользуйте Webpack.
RusSuckOFF
Есть хорошая страничка со сравнением разных сборщиков webpack.github.io/docs/comparison.html
GrizliK1988
В том и дело, что не из коробки. Будучи абсолютным мастером в обращении с require.js нет необходимости использовать webpack. Но вся штука как раз в том, что вебпак не требует большого количества времени на изучение/кофигурирование. Сам недавно перевел ~500 js файлов на webpack.
Циклические зависимости так же вполне можно решить.
Delphinum
Эмм… А что нужно такого знать про require.js? Элементарный инструмент ведь.
mrsum
Webpack это:
– СommonJs
– компонентность
– быстрая скорость сборки
– 100% доставка компонентов системы по первому вызову
– Пре- и пост- процессоры
– Минификация
– Сборка *.map файлов
– Хеширование конечных файлов
– и еще десятки пунктов…
Из коробки. Вот чем webpack хорош
auine
Абсолютно тоже самое из коробки делает и умеет require.js. Увы месье — не удивили :)
Так же CommonJs Modules, а то не понятно, что вы ввиду имеете. Стандарт сверверного js или стиль подключения модулей.
dfuse
RequireJS не умеет подключать файлы без явного указания плагина (css!./path/to.css).
RequireJS не умеет без диких плясок с бубном работать с TypeScript/ES6/Coffee/JADE/SASS, ему надо либо прекомпиляци, либо опять же плагины, а в вебпаке это делается за минуту.
Я создал две одинаковых системы для дичайше огромного сайта и пришел к выводу, что Webpack удобнее, т.к. позволяет на клиенте видеть и при разработке и после билда максимально идентичное окружение. Кол-во багов, которое вылезало из-за разницы в работе на живую и после сборки, было немного удручающим…
Я согласен, что эти два инструмента отличаются буквально в мелочах, но на больших проектах с множеством команд, эти мелочи выливаются в часы и дни.
gbezyuk
А hot reloading require.js умеет?
auine
То есть подключив левый плагин, в терминах webpack — модуль, настроив hot релоад уже означает, что я точно так же не могу подключить его к галпу? Или об этом в туториалах не пишут? :)
gbezyuk
я просто спросил. webpack это умеет из коробки dev-server'ом, и этим активно пользуется, например, Dan Abramov в своём redux-devtools.
Eternalko
Webpack решает их так же как и node.js
cyber_ua
Можно пример?
Например, у меня есть файл
a.js
x.js
я знаю что на прямую могу подключить b, но можно ли как то решить такую ситуацию не меняя код?
Eternalko
Вопроса не понял. А в чем проблема? Вы пробовали это запустить?
Если коротко, то я не вижу тут проблем. Если у вас логические зависимости — исполнение кода одного модуля зависит от исполнения кода другого модуля и обратно, то у вас есть 2 выбора:
— Поменять структуру
— Использовать паттерн Message Broker.
Советую второе.
Eternalko
В любом месте:
cyber_ua
Насчет ensure, как для ensure указать ссылку на cdn ??
Я пробывал просто вставить
Eternalko
А вы чего ожидаете? Что он соберет chunk во время компиляции или в во время рантайма только вещи из URL будет скачивать?
Правильное решение такого рода проблем:
cyber_ua
Я хочу динамически подключать скрипт если нужно, ensure и так подключает скрипт динамически, интересно можно ли подключить скрипт по ссылке например.
cyber_ua
del
Delphinum
А заглушку то как подгрузить и скрыть адрес «secret» файла в путях?
Eternalko
Не понял вопроса.
Delphinum
Я объясню задачу еще раз и постараюсь более точно.
Предположим у нас есть кучка JS файлов, которые после сборки превращатся в 'core.js' файл. В этом файле активно используется модуль 'admin' (файл 'admin.js'). Используется этот модуль как и положено, через объявление зависимостей. Файл это ('admin.js') так же собирается из некоторой кучки файлов.
Для обеспечение безопасности, определены следующие два ограничения:
1. Если пользователь, который посетил страницу, не является админом, ему под модулем 'admin' должен загружаться модуль заглушка (он в файле 'mock/admin.js')
2. Если пользователь, который посетил страницу, не является админом, открыв файлы 'core.js' через браузер (исходники), или файл 'mock/admin.js', он не должен найти в этих файлах каких либо ссылок или чего либо, что позволило бы ему найти файл 'admin.js' и посмотреть его исходники
Eternalko
Если валидация происходит на стороне JS то это невозможно сделать. В теории.
Вы прячете URL в стоге сена. А у меня есть большой магнит.
Разве сервер может шаманить и раздавать разный файл в зависимости от сессии.
Если это просто Security through obscurity то пройдитесь обсфукатором и делов то.
Delphinum
На стороне сервера тоже можно, это не принципиально. Проблема в том, что webpack не может сложить проект так, чтобы в нем не осталось ссылок на подключаемые модули (либо я не понял, как это сделать).
Kongo
Это выглядит как задача, которую webpack решать не призван. Он отделил модуль, который используется только в определенных сценариях (admin.js), а вот ограничить доступ к нему можно с помощью других инструментов. Например, используя сервер приложения и x-accel-redirect для nginx.
Delphinum
Ну когда я выбираю систему сборки, меня мало волнует, что некоторый инструмент, называемый системой сборки, не призван решать мои задачи, согласитесь? )
А можно просто на стороне сервера заменить один файл другим при подключении JS к странице, и никакой больше магии.
Kongo
Динамическое ограничение доступа к файлам не выглядит как задача для системы сборки (webpack только частный случай), на мой взгляд.
Подмена файла больше похожа на магию, чем использование рассчитанных на это инструментов. Впрочем, как её предлагается устроить? Я предлагал делать проверку статуса на сервере приложения с последующим запросом к реверс-прокси на перенаправление к нужному скрипту, что несколько похоже на «подмену» файла.
В любом случае, у меня создалось впечатление, что вы рассматриваете сборку, как нечто динамическое. Вот пришел авторизованный клиент, собираем для него админский набор. Иначе — собираем другой. Но сборка-то едина и статична для всех сценариев.
Delphinum
Не совсем так. Просто у webpack проблема в том, что его пытаются сделать «слишком умным». С одной стороны это удобно, так как не нужно ничего настраивать руками, все есть из коробки, но когда сталкиваешься с подобными задачами понимаешь, что это не панацея.
Проблема здесь не в том, что нужно что то делать в зависимости от ситуации, а в том, что нельзя скрыть от пользователя пути к JS файлам (если эти файлы не нужны пользователю) используя webpack.
Eternalko
Это отделяет возможную к решению задачу от невозможной даже в теории.
Delphinum
Не принципиально, так как проблема, которую я пытаюсь описать, не связана с этим.
Eternalko
webpack может.
И 2 раза собрать с FeatureFlags где один раз будет заглушка а второй раз нормальный файл.
А дальше 10 строчек колдовства на сервере и все.
webpack тупой как пробка. Не надо вводить людей в заблуждение.
Delphinum
А можно не собирать 2 раза, так как таких модулей, как 'admin', может быть больше чем 1 )
Тупой как пробла Grunt, его нужно всему учить, а webpack вполне ученый.
Eternalko
Можно. Только дальше уже сами.
Delphinum
Потому и не стал пользовать webpack.
Eternalko
Зачем тогда спрашивали?
Delphinum
Я же писал выше — мне было интересно, это я такой глупый, или webpack не может.
Eternalko
Ага. Ну хорошо что все разъяснилось.