Webpack или Browserify & Gulp: что лучше?
Опубликовано: 2022-03-11Поскольку веб-приложения становятся все более сложными, обеспечение масштабируемости вашего веб-приложения становится крайне важным. В то время как в прошлом было достаточно написать специальный JavaScript и jQuery, в настоящее время создание веб-приложения требует гораздо большей дисциплины и формальных методов разработки программного обеспечения, таких как:
- Модульные тесты, чтобы убедиться, что изменения в вашем коде не нарушают существующую функциональность.
- Линтинг для обеспечения согласованного стиля кодирования без ошибок
- Производственные сборки, которые отличаются от сборок разработки
Интернет также предоставляет некоторые из своих уникальных проблем разработки. Например, поскольку веб-страницы делают много асинхронных запросов, производительность вашего веб-приложения может значительно снизиться из-за необходимости запрашивать сотни файлов JS и CSS, каждый из которых имеет свои собственные крошечные служебные данные (заголовки, рукопожатия и т. д.). Эту конкретную проблему часто можно решить, объединив файлы вместе, поэтому вы запрашиваете только один связанный файл JS и CSS, а не сотни отдельных.
Также довольно часто используются языковые препроцессоры, такие как SASS и JSX, которые компилируются в собственные JS и CSS, а также транспиляторы JS, такие как Babel, чтобы извлечь выгоду из кода ES6, сохраняя при этом совместимость с ES5.
Это составляет значительное количество задач, которые не имеют ничего общего с написанием логики самого веб-приложения. Здесь на помощь приходят средства выполнения задач. Цель средства выполнения задач — автоматизировать все эти задачи, чтобы вы могли извлечь выгоду из улучшенной среды разработки, сосредоточившись на написании своего приложения. После того, как средство запуска задач настроено, все, что вам нужно сделать, это вызвать одну команду в терминале.
Я буду использовать Gulp в качестве средства запуска задач, потому что он очень удобен для разработчиков, прост в освоении и понятен.
Краткое введение в Gulp
Gulp API состоит из четырех функций:
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
Вот, например, пример задачи, в которой используются три из этих четырех функций:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
Когда выполняется my-first-task
, все файлы, соответствующие шаблону glob /public/js/**/*.js
, минимизируются, а затем переносятся в папку build
.
Прелесть этого в .pipe()
. Вы берете набор входных файлов, пропускаете их через серию преобразований, а затем возвращаете выходные файлы. Чтобы сделать вещи еще более удобными, фактические преобразования конвейера, такие как minify()
, часто выполняются библиотеками NPM. В результате на практике очень редко приходится писать собственные преобразования, помимо переименования файлов в конвейере.
Следующий шаг к пониманию Gulp — понимание массива зависимостей задач.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
Здесь my-second-task
запускает функцию обратного вызова только после завершения задач lint
и bundle
. Это позволяет разделить задачи: вы создаете серию небольших задач с одной ответственностью, например, преобразование LESS
в CSS
, и создаете своего рода основную задачу, которая просто вызывает все остальные задачи через массив зависимостей задач.
Наконец, у нас есть gulp.watch
, который отслеживает изменения в шаблоне файла glob и при обнаружении изменения запускает серию задач.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
В приведенном выше примере любые изменения в файле, соответствующем /public/js/**/*.js
, вызовут задачу lint
и reload
. Обычно gulp.watch
используется для запуска перезагрузки в реальном времени в браузере, функция, настолько полезная для разработки, что вы не сможете жить без нее, как только вы ее испытаете.
И вот так вы понимаете все, что вам действительно нужно знать о gulp
.
Где вписывается Webpack?
При использовании шаблона CommonJS объединение файлов JavaScript не так просто, как их объединение. Скорее у вас есть точка входа (обычно называемая index.js
или app.js
) с рядом операторов require
или import
в верхней части файла:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
Зависимости должны быть разрешены перед оставшимся кодом в app.js
, и эти зависимости сами по себе могут иметь дополнительные зависимости для разрешения. Кроме того, вам может require
и та же зависимость в нескольких местах вашего приложения, но вы хотите разрешить эту зависимость только один раз. Как вы понимаете, если у вас есть дерево зависимостей глубиной в несколько уровней, процесс связывания вашего JavaScript становится довольно сложным. Здесь на помощь приходят такие упаковщики, как Browserify и Webpack.
Почему разработчики используют Webpack вместо Gulp?
Webpack — это упаковщик, тогда как Gulp — это средство запуска задач, поэтому вы ожидаете, что эти два инструмента будут часто использоваться вместе. Вместо этого наблюдается растущая тенденция, особенно среди сообщества React, использовать Webpack вместо Gulp. Почему это?
Проще говоря, Webpack — настолько мощный инструмент, что он уже может выполнять подавляющее большинство задач, которые вы в противном случае выполняли бы с помощью средства запуска задач. Например, Webpack уже предоставляет опции для минификации и исходных карт для вашего пакета. Кроме того, Webpack можно запускать как промежуточное ПО через пользовательский сервер под названием webpack-dev-server
, который поддерживает как динамическую перезагрузку, так и горячую перезагрузку (мы поговорим об этих функциях позже). Используя загрузчики, вы также можете добавить транспиляцию ES6 в ES5, а также пре- и постпроцессоры CSS. На самом деле это просто оставляет модульные тесты и линтинг в качестве основных задач, с которыми Webpack не может справиться самостоятельно. Учитывая, что мы сократили как минимум полдюжины потенциальных задач gulp до двух, многие разработчики предпочитают вместо этого напрямую использовать сценарии NPM, поскольку это позволяет избежать накладных расходов на добавление Gulp в проект (о чем мы также поговорим позже). .
Основным недостатком использования Webpack является то, что его довольно сложно настроить, что непривлекательно, если вы пытаетесь быстро запустить проект.
Наши 3 настройки Task Runner
Я настрою проект с тремя различными настройками запуска задач. Каждая установка будет выполнять следующие задачи:
- Настройте сервер разработки с перезагрузкой в реальном времени при отслеживаемых изменениях файла.
- Объедините наши файлы JS и CSS (включая преобразование ES6 в ES5, преобразование SASS в CSS и исходные карты) масштабируемым образом при отслеживаемых изменениях файлов.
- Запускайте модульные тесты как отдельную задачу или в режиме наблюдения.
- Запускайте анализ как отдельную задачу или в режиме наблюдения.
- Обеспечить возможность выполнения всего вышеперечисленного с помощью одной команды в терминале
- Есть еще одна команда для создания производственного пакета с минификацией и другими оптимизациями.
Нашими тремя установками будут:
- Глоток + браузер
- Глоток + веб-пакет
- Webpack + NPM-скрипты
Приложение будет использовать React для внешнего интерфейса. Изначально я хотел использовать независимый от фреймворка подход, но использование React на самом деле упрощает обязанности исполнителя задач, поскольку требуется только один HTML-файл, а React очень хорошо работает с шаблоном CommonJS.
Мы расскажем о преимуществах и недостатках каждой установки, чтобы вы могли принять обоснованное решение о том, какой тип установки лучше всего соответствует потребностям вашего проекта.
Я настроил репозиторий Git с тремя ветвями, по одной для каждого подхода (ссылка). Тестирование каждой установки так же просто, как:
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
Давайте подробно рассмотрим код в каждой ветке…
Общий код
Структура папок
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
index.html
Простой HTML-файл. Приложение React загружается в <div></div>
, и мы используем только один связанный файл JS и CSS. Фактически, в нашей настройке разработки Webpack нам даже не понадобится bundle.css
.
index.js
Это действует как точка входа JS нашего приложения. По сути, мы просто загружаем React Router в div
с идентификатором app
, о котором мы упоминали ранее.
маршруты.js
Этот файл определяет наши маршруты. URL-адреса /
, /about
и /contact
сопоставляются с компонентами HomePage
, AboutPage
и ContactPage
соответственно.
index.test.js
Это серия модульных тестов, которые проверяют нативное поведение JavaScript. В реальном качественном приложении вы должны написать модульный тест для каждого компонента React (по крайней мере, тех, которые управляют состоянием), проверяя поведение, специфичное для React. Однако для целей этого поста достаточно просто иметь функциональный модульный тест, который может работать в режиме наблюдения.
компоненты/App.js
Это можно рассматривать как контейнер для всех наших просмотров страниц. Каждая страница содержит компонент <Header/>
, а также this.props.children
, который оценивает сам просмотр страницы (например, ContactPage
, если в браузере находится /contact
).
компоненты/главная/HomePage.js
Это наш домашний вид. Я решил использовать react-bootstrap
, так как система сетки bootstrap отлично подходит для создания адаптивных страниц. При правильном использовании бутстрапа количество медиа-запросов, которые вы должны написать для небольших окон просмотра, резко сокращается.
Остальные компоненты ( Header
, AboutPage
, ContactPage
) структурированы аналогично (разметка react react-bootstrap
, никаких манипуляций с состоянием).
Теперь поговорим подробнее о стилизации.
Подход к стилю CSS
Мой предпочтительный подход к стилизации компонентов React состоит в том, чтобы иметь одну таблицу стилей для каждого компонента, чьи стили применяются только к этому конкретному компоненту. Вы заметите, что в каждом из моих компонентов React элемент div
верхнего уровня имеет имя класса, совпадающее с именем компонента. Так, например, разметка HomePage.js
заключена в:
<div className="HomePage"> ... </div>
Существует также связанный файл HomePage.scss
со следующей структурой:
@import '../../styles/variables'; .HomePage { // Content here }
Почему этот подход так полезен? Это приводит к высокомодульному CSS, в значительной степени устраняющему проблему нежелательного каскадного поведения.
Предположим, у нас есть два компонента React, Component1
и Component2
. В обоих случаях мы хотим переопределить размер шрифта h2
.
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
Размер шрифта h2
Component1
и Component2
не зависит от того, являются ли компоненты смежными или один компонент вложен в другой. В идеале это означает, что стиль компонента полностью автономен, то есть компонент будет выглядеть точно так же, независимо от того, где он находится в вашей разметке. На самом деле это не всегда так просто, но это, безусловно, огромный шаг в правильном направлении.
В дополнение к стилям для каждого компонента мне нравится иметь папку styles
, содержащую глобальную таблицу стилей global.scss
вместе с партиалами SASS, отвечающими за определенную ответственность (в данном случае _fonts.scss
и _variables.scss
для шрифтов и переменных соответственно). ). Глобальная таблица стилей позволяет нам определить общий внешний вид всего приложения, в то время как вспомогательные части могут быть импортированы таблицами стилей для каждого компонента по мере необходимости.
Теперь, когда общий код в каждой ветке был подробно изучен, давайте перенесем наше внимание на первый подход к запуску задач/упаковщику.
Gulp + Настройка браузера
gulpfile.js
Получается удивительно большой gulpfile с 22 импортами и 150 строками кода. Поэтому для краткости я подробно рассмотрю только js
, css
, server
, watch
и задачи по default
.
JS-пакет
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }
Этот подход довольно уродлив по ряду причин. Во-первых, задача разделена на три отдельные части. Во-первых, вы создаете свой объект пакета Browserify b
, передавая некоторые параметры и определяя некоторые обработчики событий. Затем у вас есть сама задача Gulp, которая должна передать именованную функцию в качестве обратного вызова вместо ее встраивания (поскольку b.on('update')
использует тот же самый обратный вызов). Едва ли это можно сравнить с элегантностью задачи Gulp, когда вы просто передаете gulp.src
и передаете некоторые изменения.
Другая проблема заключается в том, что это заставляет нас использовать разные подходы к перезагрузке html
, css
и js
в браузере. Глядя на нашу задачу watch
Gulp:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
При изменении файла html
задача html запускается повторно.
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
Последний канал вызывает livereload()
, если NODE_ENV
не является production
, что запускает обновление в браузере.
Та же логика используется для часов CSS. При изменении файла css
задача css запускается повторно, а последний канал в задаче css
вызывает livereload()
и обновляет браузер.
Однако часы js
вообще не вызывают задачу js
. Вместо этого обработчик событий b.on('update', bundle)
обрабатывает перезагрузку, используя совершенно другой подход (а именно, горячую замену модуля). Непоследовательность в этом подходе раздражает, но, к сожалению, необходима для создания инкрементных сборок. Если бы мы просто вызвали livereload()
в конце функции bundle
, это привело бы к пересборке всего пакета JS при любом изменении отдельного файла JS. Такой подход явно не масштабируется. Чем больше у вас JS-файлов, тем больше времени занимает каждый повторный сбор. Внезапно ваши повторные сборки за 500 мс начинают занимать 30 секунд, что действительно тормозит гибкую разработку.
Пакет CSS
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
Первая проблема здесь — громоздкое включение CSS поставщика. Всякий раз, когда в проект добавляется новый файл CSS поставщика, мы должны помнить об изменении нашего gulpfile, чтобы добавить элемент в массив gulp.src
, а не добавлять импорт в соответствующее место в нашем фактическом исходном коде.
Другой основной проблемой является запутанная логика в каждой трубе. Мне пришлось добавить библиотеку NPM под названием gulp-cond
только для настройки условной логики в моих каналах, и конечный результат не слишком удобочитаем (тройные скобки повсюду!).
Серверная задача
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
Эта задача очень проста. По сути, это оболочка для вызова nodemon server.js
из командной строки, которая запускает server.js
в среде узла. nodemon
используется вместо node
, так что любые изменения в файле вызывают его перезапуск. По умолчанию nodemon
перезапустит запущенный процесс при любом изменении файла JS, поэтому важно включить файл nodemon.json
, чтобы ограничить его область действия:
{ "watch": "server.js" }
Давайте рассмотрим код нашего сервера.
сервер.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
Это устанавливает базовый каталог сервера и порт на основе среды узла и создает экземпляр Express.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
Это добавляет промежуточное ПО connect-livereload
(необходимое для нашей настройки перезагрузки в реальном времени) и статическое промежуточное ПО (необходимое для обработки наших статических ресурсов).

app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
Это просто простой маршрут API. Если вы перейдете на localhost:3000/api/sample-route
в браузере, вы увидите:
{ website: "Toptal", blogPost: true }
В реальном бэкенде у вас будет целая папка, посвященная маршрутам API, отдельные файлы для установления соединений с БД и так далее. Этот пример маршрута был включен просто для того, чтобы показать, что мы можем легко создать серверную часть поверх настроенной нами внешней.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
Это универсальный маршрут, означающий, что независимо от того, какой URL-адрес вы вводите в браузере, сервер вернет нашу единственную страницу index.html
. Затем React Router отвечает за разрешение наших маршрутов на стороне клиента.
app.listen(port, () => { open(`http://localhost:${port}`); });
Это говорит нашему экспресс-экземпляру прослушивать указанный нами порт и открывать браузер в новой вкладке по указанному URL-адресу.
Пока что единственное, что мне не нравится в настройке сервера, это:
app.use(require('connect-livereload')({port: 35729}));
Учитывая, что мы уже используем gulp-livereload
в нашем файле gulp, это создает два отдельных места, где необходимо использовать livereload.
Теперь последнее, но не менее важное:
Задача по умолчанию
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
Это задача, которая запускается при простом вводе gulp
в терминал. Одна странность заключается в необходимости использовать runSequence
, чтобы задачи выполнялись последовательно. Обычно массив задач выполняется параллельно, но это не всегда желательно. Например, нам нужно запустить задачу clean
перед html
, чтобы убедиться, что наши папки назначения пусты, прежде чем перемещать в них файлы. Когда будет выпущен gulp 4, он будет изначально поддерживать методы gulp.series
и gulp.parallel
, но пока мы должны оставить эту небольшую причуду в нашей настройке.
Кроме того, это на самом деле довольно элегантно. Все создание и размещение нашего приложения выполняются одной командой, и понять любую часть рабочего процесса так же просто, как изучить отдельную задачу в последовательности выполнения. Кроме того, мы можем разбить всю последовательность на более мелкие фрагменты для более детального подхода к созданию и размещению приложения. Например, мы могли бы настроить отдельную задачу с именем validate
, которая запускает задачи lint
и test
. Или у нас может быть задача host
, которая запускает server
и watch
. Эта способность координировать задачи очень эффективна, особенно когда ваше приложение масштабируется и требует большего количества автоматизированных задач.
Разработка против производственных сборок
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
Используя библиотеку yargs
NPM, мы можем предоставить Gulp флаги командной строки. Здесь я указываю файлу gulp установить рабочую среду узла, если --prod
передан gulp
в терминале. Затем наша переменная PROD
используется в качестве условия для различения поведения разработки и производства в нашем gulpfile. Например, один из параметров, которые мы передаем в нашу конфигурацию browserify
:
plugin: PROD ? [] : [hmr, watchify]
Это говорит browserify
не использовать какие-либо плагины в рабочем режиме и использовать hmr
и watchify
в других средах.
Этот условный PROD
очень полезен, потому что он избавляет нас от необходимости писать отдельный файл gulp для производства и разработки, что в конечном итоге будет содержать много повторений кода. Вместо этого мы можем делать такие вещи, как gulp --prod
для запуска задачи по умолчанию в продакшне, или gulp html --prod
для запуска только html
задачи в продакшене. С другой стороны, мы видели ранее, что засорение наших конвейеров Gulp такими операторами, как .pipe(cond(!PROD, livereload()))
не самый читабельный. В конце концов, это вопрос предпочтений, хотите ли вы использовать подход с логическими переменными или настроить два отдельных файла gulp.
Теперь давайте посмотрим, что произойдет, если мы продолжим использовать Gulp в качестве средства запуска задач, но заменим Browserify на Webpack.
Настройка Gulp + Webpack
Внезапно наш gulpfile стал всего 99 строк с 12 импортами, что значительно меньше по сравнению с нашей предыдущей настройкой! Если мы проверим задачу по умолчанию:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
Теперь для полной настройки веб-приложения требуется всего пять задач вместо девяти, что является значительным улучшением.
Кроме того, мы устранили необходимость в livereload
. Теперь наша задача watch
просто:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
Это означает, что наш gulp watcher не запускает никакого поведения повторной сборки. В качестве дополнительного бонуса нам больше не нужно переносить index.html
из app
в dist
или build
.
Возвращаясь к сокращению задач, все наши задачи html
, css
, js
и fonts
были заменены одной задачей build
:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });
Достаточно просто. Последовательно запускайте задачи clean
и html
. Как только это будет завершено, возьмите нашу точку входа, пропустите ее через Webpack, передав файл webpack.config.js
для его настройки, и отправьте полученный пакет в наш baseDir
(либо dist
, либо build
, в зависимости от окружения узла).
Давайте посмотрим на файл конфигурации Webpack:
webpack.config.js
Это довольно большой и пугающий файл конфигурации, поэтому давайте объясним некоторые важные свойства, устанавливаемые в нашем объекте module.exports
.
devtool: PROD ? 'source-map' : 'eval-source-map',
Это устанавливает тип исходных карт, которые будет использовать Webpack. Мало того, что Webpack поддерживает исходные карты из коробки, он на самом деле поддерживает широкий спектр параметров исходной карты. Каждый вариант обеспечивает различный баланс детализации исходной карты и скорости перестроения (время, необходимое для повторного объединения изменений). Это означает, что мы можем использовать «дешевый» вариант исходной карты для разработки, чтобы добиться быстрой перезагрузки, и более дорогой вариант исходной карты в производстве.
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
Это точка входа в наш пакет. Обратите внимание, что передается массив, а это означает, что может быть несколько точек входа. В этом случае у нас есть ожидаемая точка входа app/index.js
, а также точка входа webpack-hot-middleware
, которая используется как часть нашей настройки перезагрузки горячего модуля.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
Здесь будет выводиться скомпилированный пакет. Самый запутанный вариант — это publicPath
. Он устанавливает базовый URL-адрес для размещения вашего пакета на сервере. Так, например, если ваш publicPath
— /public/assets
, тогда пакет появится в /public/assets/bundle.js
на сервере.
devServer: { contentBase: PROD ? './build' : './app' }
Это сообщает серверу, какую папку в вашем проекте использовать в качестве корневого каталога сервера.
Если вы когда-нибудь запутались в том, как Webpack сопоставляет созданный пакет в вашем проекте с пакетом на сервере, просто запомните следующее:
-
path
+filename
: точное расположение пакета в исходном коде вашего проекта. -
contentBase
(как корень,/
) +publicPath
: расположение пакета на сервере.
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],
Это плагины, которые каким-то образом улучшают функциональность Webpack. Например, за webpack.optimize.UglifyJsPlugin
отвечает webpack.optimize.UglifyJsPlugin.
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]
Это грузчики. По сути, они предварительно обрабатывают файлы, которые загружаются с помощью операторов require()
. Они чем-то похожи на каналы Gulp в том, что вы можете соединять загрузчики вместе.
Давайте рассмотрим один из наших объектов загрузчика:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
Свойство test
сообщает Webpack, что данный загрузчик применяется, если файл соответствует предоставленному шаблону регулярного выражения, в данном случае /\.scss$/
. Свойство loader
соответствует действию, которое выполняет загрузчик. Здесь мы соединяем вместе загрузчики style
, css
, resolve-url
и sass
, которые выполняются в обратном порядке.
Я должен признать, что синтаксис loader3!loader2!loader1
не кажется мне очень элегантным. В конце концов, когда вам когда-нибудь приходилось что-то читать в программе справа налево? Несмотря на это, загрузчики — очень мощная функция webpack. Фактически, только что упомянутый загрузчик позволяет нам импортировать файлы SASS прямо в наш JavaScript! Например, мы можем импортировать таблицы стилей поставщиков и глобальных стилей в наш файл точки входа:
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));
Точно так же в нашем компоненте Header мы можем добавить import './Header.scss'
для импорта таблицы стилей, связанной с компонентом. Это также относится ко всем другим нашим компонентам.
На мой взгляд, это почти можно считать революционным изменением в мире разработки JavaScript. Нет необходимости беспокоиться о связывании CSS, минимизации или исходных картах, поскольку наш загрузчик обрабатывает все это за нас. Даже горячая перезагрузка модуля работает для наших файлов CSS. Затем возможность обработки импорта JS и CSS в одном файле делает разработку концептуально проще: больше согласованности, меньше переключения контекста и проще рассуждать.
Кратко о том, как работает эта функция: Webpack встраивает CSS в наш пакет JS. На самом деле, Webpack может делать это и для изображений, и для шрифтов:
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}
Загрузчик URL-адресов инструктирует Webpack встраивать наши изображения и шрифты в качестве URL-адресов данных, если они меньше 100 КБ, в противном случае использовать их как отдельные файлы. Конечно, мы также можем настроить размер отсечки на другое значение, например 10 КБ.
И это конфигурация Webpack в двух словах. Я признаю, что настроек достаточно много, но преимущества их использования просто феноменальны. Хотя у Browserify есть плагины и преобразования, они просто не могут сравниться с загрузчиками Webpack с точки зрения дополнительной функциональности.
Настройка Webpack + NPM скриптов
В этой настройке мы используем сценарии npm напрямую, вместо того, чтобы полагаться на gulpfile для автоматизации наших задач.
пакет.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }
Чтобы запустить сборки для разработки и производства, введите npm start
и npm run start:prod
соответственно.
Это, безусловно, более компактно, чем наш файл gulp, учитывая, что мы сократили от 99 до 150 строк кода до 19 сценариев NPM или 12, если исключить рабочие сценарии (большинство из которых просто отражают сценарии разработки с установленной средой узла для рабочей среды). ). Недостатком является то, что эти команды несколько загадочны по сравнению с нашими аналогами задач Gulp и не так выразительны. Например, нет способа (по крайней мере, насколько мне известно) заставить один скрипт npm запускать определенные команды последовательно, а другие — параллельно. Это либо одно, либо другое.
Однако в таком подходе есть огромное преимущество. Используя библиотеки NPM, такие как mocha
, непосредственно из командной строки, вам не нужно устанавливать эквивалентную оболочку Gulp для каждой (в данном случае gulp-mocha
).
Вместо установки NPM
- глоток-эслинт
- глоток мокко
- gulp-nodemon
- так далее
Устанавливаем следующие пакеты:
- эслинт
- мокко
- нодмон
- так далее
Цитируя сообщение Кори Хауса « Почему я оставил Gulp и Grunt для сценариев NPM» :
Я был большим поклонником Gulp. Но в моем последнем проекте у меня были сотни строк в файле gulp и около дюжины плагинов Gulp. Я изо всех сил пытался интегрировать Webpack, Browsersync, горячую перезагрузку, Mocha и многое другое с помощью Gulp. Почему? Ну, у некоторых плагинов было недостаточно документации для моего варианта использования. Некоторые плагины раскрывали только часть нужного мне API. У одного была странная ошибка, из-за которой он просматривал только небольшое количество файлов. Еще раздели цвета при выводе в командную строку.
Он выделяет три основные проблемы с Gulp:
- Зависимость от авторов плагинов
- Разочарование для отладки
- Разрозненная документация
Я склонен согласиться со всем этим.
1. Зависимость от авторов плагинов
Всякий раз, когда обновляется такая библиотека, как eslint
, соответствующей библиотеке gulp-eslint
требуется соответствующее обновление. Если сопровождающий библиотеки теряет интерес, gulp-версия библиотеки перестает синхронизироваться с нативной. То же самое происходит при создании новой библиотеки. Если кто-то создаст библиотеку xyz
и она приживется, то вдруг вам понадобится соответствующая библиотека gulp-xyz
, чтобы использовать ее в своих задачах gulp.
В некотором смысле этот подход просто не масштабируется. В идеале нам нужен такой подход, как Gulp, который может использовать нативные библиотеки.
2. Разочаровывает отладку
Хотя такие библиотеки, как gulp-plumber
, помогают значительно облегчить эту проблему, тем не менее верно, что сообщения об ошибках в gulp
просто не очень полезны. Если даже один канал генерирует необработанное исключение, вы получаете трассировку стека для проблемы, которая кажется совершенно не связанной с тем, что вызывает проблему в вашем исходном коде. В некоторых случаях это может превратить отладку в кошмар. Никакие поиски в Google или Stack Overflow не помогут вам, если ошибка достаточно загадочна или вводит в заблуждение.
3. Разрозненная документация
Часто я обнаруживаю, что небольшие библиотеки gulp
имеют очень ограниченную документацию. Я подозреваю, что это потому, что автор обычно создает библиотеку в первую очередь для собственного использования. Кроме того, часто приходится просматривать документацию как к плагину Gulp, так и к самой нативной библиотеке, что означает много переключений контекста и вдвое больше чтения.
Заключение
Мне кажется совершенно очевидным, что Webpack предпочтительнее Browserify, а скрипты NPM предпочтительнее Gulp, хотя у каждого варианта есть свои преимущества и недостатки. Gulp, безусловно, более выразителен и удобен в использовании, чем скрипты NPM, но вы платите цену за всю добавленную абстракцию.
Не каждая комбинация может быть идеальной для вашего приложения, но если вы хотите избежать огромного количества зависимостей при разработке и разочаровывающей отладки, Webpack со сценариями NPM — это то, что вам нужно. Я надеюсь, что вы найдете эту статью полезной при выборе правильных инструментов для вашего следующего проекта.
- Сохраняйте контроль: руководство по Webpack и React, Pt. 1
- Gulp под капотом: создание инструмента автоматизации задач на основе потоков