- PVSM.RU - https://www.pvsm.ru -

Приятная сборка frontend проекта

В этой статье мы подробно разберем процесс сборки фронтенд проекта, который прижился в моей повседневной работе и очень облегчил рутину.
Статья не претендует на истину в последней инстанции, так как сегодня существует большое количество различных сборщиков и подходов к сборке, и каждый выбирает по вкусу. Я лишь поделюсь своими мыслями по этой теме и покажу свой workflow.

Приятная сборка frontend проекта - 1

Мы будем использовать сборщик Gulp [1]. Соответственно у вас в системе должен быть установлен Node js. Установку ноды под конкретную платформу мы рассматривать не будем, т.к. это гуглится за пару минут.
И для начала отвечу на вопрос — почему Gulp?
Из более или менее сносных альтернатив мы имеем Grunt [2] и Brunch [3].
Когда я только начал приобщаться к сборщикам — на рынке уже были и Grunt и Gulp. Первый появился раньше и по этому имеет более большое коммьюнити и разнообразие плагинов. По данным с npm [4]:
Grunt — 11171 пакет
Gulp — 4371 пакет

Но Grunt мне показался через чур многословным. И после прочтения нескольких статей-сравнений — я предпочел Gulp за его простоту и наглядность.
Brunch — это сравнительно молодой проект, со всеми вытекающими из этого плюсами и минусами. Я с интересом наблюдаю за ним, но в работе пока не использовал.

Приступим:

Создадим папку под наш проект, например «habr». Откроем ее в консоли и выполним команду

npm init

Можно просто нажать Enter на все вопросы установщика, т.к. сейчас это не принципиально.
В итоге в папке с проектом у нас сгенерируется файл package.json, примерно такого содержания

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Немного видоизменим его под наши нужды:

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "devDependencies": {
      "gulp": "^3.8.1"
   }
}

в блоке devDependencies мы указали что нам нужен gulp и тут же будем прописывать все наши плагины.

Плагины:

gulp-autoprefixer [5] — автоматически добавляет вендорные префиксы к CSS свойствам (пару лет назад я бы убил за такую тулзу :) )
gulp-cssmin [6] — нужен для сжатия CSS кода
gulp-connect [7] — с помощью этого плагина мы можем легко развернуть локальный dev сервер с блэкджеком и livereload
gulp-imagemin [8] — для сжатия картинок
imagemin-pngquant [9] — дополнения к предыдущему плагину, для работы с PNG
gulp-uglify [10] — будет сжимать наш JS
gulp-sass [11] — для компиляции нашего SCSS кода

Не холивара ради

Я очень долгое время использовал в своей работе LESS. Мне очень импонировал этот препроцессор за его скорость и простоту изучения. Даже делал доклад [12] по нему на одном Ростовском хакатоне. И в частности в этом докладе я не очень лестно отзывался о SASS.
Но прошло время, я стал старше и мудрее :) и теперь я приобщился к этому препроцессору.
Основой моего недовольства SASS — было то что я не пишу на руби. И когда то для компиляции SASS/SCSS кода — надо было тащить в проект руби, с нужными бандлами — что меня очень огорчало.
Но все изменилось с появлением такой штуки как LibSass [13]. Это С/C++ порт компилятора для SASS. Плагин gulp-sass использует именно его. Теперь мы можем использовать SASS в нативном node окружении — что меня безгранично радует.

gulp-sourcemaps [14] — возьмем для генерации css sourscemaps, которые будут помогать нам при отладке кода
gulp-rigger [15] — это просто киллер фича. Плагин позволяет импортировать один файл в другой простой конструкцией

//= footer.html

и эта строка при компиляции будет заменена на содержимое файла footer.html
gulp-watch [16] — Будет нужен для наблюдения за изменениями файлов. Знаю что в Gulp есть встроенный watch, но у меня возникли с ним некоторые проблемы, в частности он не видел вновь созданные файлы, и приходилось его перезапускать. Этот плагин решил проблему (надеюсь в следующих версиях gulp это поправят).
opn [17] — маленькая приятность, позволяющая открыть какую-нибудь ссылку в браузере командой из node js
rimraf [18] — rm -rf для ноды

Пропишем все наши плагины в package.json

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "gulp": "^3.8.1",
    "gulp-autoprefixer": "*",
    "gulp-connect": "*",
    "gulp-cssmin": "*",
    "gulp-imagemin": "*",
    "gulp-sass": "*",
    "gulp-sourcemaps": "*",
    "gulp-rigger": "*",
    "gulp-uglify": "*",
    "gulp-watch": "*",
    "imagemin-pngquant": "*",
    "opn": "*",
    "rimraf": "*"
  }
}

и запустим в консоли команду

npm install
Bower

Я уже не мыслю своей работы без пакетного менеджера Bower [19] и надеюсь вы тоже. Если нет, то почитать о том что это и с чем его едят можно тут [20].
Давайте добавим его к нашему проекту. Для этого выполним в консоли команду

bower init

Можно так же Enter на все вопросы.
В конце мы получаем примерно такой файл bower.json

{
  "name": "habr",
  "version": "0.0.0",
  "authors": [
    "Insayt <insait.rostov@ya.ru>"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ]
}

И модифицируем его до нужного нам состояния

{
  "name": "habr",
  "version": "0.0.0",
  "authors": [
    "Insayt <insait.rostov@ya.ru>"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "normalize.css": "*",
    "jquery": "2.*"
  }
}

В блоке dependencies мы будем указывать зависимости нашего проекта. Сейчас просто для теста это normalize и jQuery (хотя я уже не помню когда начинал проект без этих вещей).
Ну и конечно установим их командой

bower i

Ну а теперь самое интересное. Создадим структуру нашего проекта и настроим сборщик.

Структура проекта:

Это очень спорный момент. Конечно проекты бывают разные, так же как и предпочтения разработчиков. Стоит только взглянуть на сайт yeoman.io [21] (кстати это очень классный инструмент, который предоставляет большое кол-во заготовленных основ для проекта со всякими плюшками. Однозначно стоит присмотреться к нему). Мы не будем ничего выдумывать и сделаем самую простую структуру.
Для начала нам понадобится 2 папки. Одна (src) в которой мы собственно будем писать код, и вторая (build), в которую сборщик будет выплевывать готовые файлы. Добавим их в проект. Текущая структура у нас выглядит так:

Приятная сборка frontend проекта - 2

В папке src создадим типичную структуру среднестатистического проекта. Сделаем main файлы в папках js/ и style/ и создадим первую html страничку такого содержания.

index.html

<!DOCTYPE html>
<html>
<head lang="ru">
    <meta charset="UTF-8">
    <title>Я собираю проекты как рок звезда</title>
</head>
<body>
    <section class="header">
        Header
    </section>
    <section class="content">
        Content
    </section>
    <section class="footer">
        Footer
    </section>
</body>
</html>

Структура папки src теперь будет выглядеть так:
Приятная сборка frontend проекта - 3
Тут все тривиально
fonts — шрифты
img — картинки
js — скрипты. В корне этой папки будет только файл main.js, который пригодится нам для сборки. Все свои js файлы — надо будет класть в папку partials
style — стили. Тут так же в корне только main.scss, а рабочие файлы в папке partials
template — тут будем хранить повторяющиеся куски html кода
Все html страницы которые мы верстаем — будут лежать в корне src/
Добавим в partilas первые js и scss файлы и напоследок — перейдем в корень нашего проекта и создадим там файл gulpfile.js. Вся папка проекта сейчас выглядит так:

Приятная сборка frontend проекта - 4
Теперь все готово к настройке нашего сборщика, так что let's rock!

Gulpfile.js

Вся магия будет заключена в этом файле. Для начала мы импортируем все наши плагины и сам gulp

gulpfile.js

'use strict';

var gulp = require('gulp'),
    watch = require('gulp-watch'),
    prefixer = require('gulp-autoprefixer'),
    uglify = require('gulp-uglify'),
    cssmin = require('gulp-cssmin'),
    sass = require('gulp-sass'),
    sourcemaps = require('gulp-sourcemaps'),
    rigger = require('gulp-rigger'),
    imagemin = require('gulp-imagemin'),
    pngquant = require('imagemin-pngquant'),
    rimraf = require('rimraf'),
    connect = require('gulp-connect'),
    opn = require('opn');

Конечно не обязательно делать это именно так. Существует плагин gulp-load-plugins [22] который позволяет не писать всю эту лапшу из require. Но мне нравится когда я четко вижу что и где подключается, и при желании могу это отключить. По этому пишу по старинке.

Так же создадим js объект в который пропишем все нужные нам пути, чтобы при необходимости легко в одном месте их редактировать

var path = {
    build: { //Тут мы укажем куда складывать готовые после сборки файлы
        html: 'build/',
        js: 'build/js/',
        css: 'build/css/',
        img: 'build/img/',
        fonts: 'build/fonts/'
    },
    src: { //Пути откуда брать исходники
        html: 'src/*.html', //Синтаксис src/*.html говорит gulp что мы хотим взять все файлы с расширением .html
        js: 'src/js/main.js',//В стилях и скриптах нам понадобятся только main файлы
        style: 'src/style/main.scss',
        img: 'src/img/**/*.*', //Синтаксис img/**/*.* означает - взять все файлы всех расширений из папки и из вложенных каталогов
        fonts: 'src/fonts/**/*.*'
    },
    watch: { //Тут мы укажем, за изменением каких файлов мы хотим наблюдать
        html: 'src/**/*.html',
        js: 'src/js/**/*.js',
        style: 'src/style/**/*.scss',
        img: 'src/img/**/*.*',
        fonts: 'src/fonts/**/*.*'
    },
    clean: './build'
};

Создадим переменную с настройками нашего dev сервера

var server = {
    host: 'localhost',
    port: '9000'
};
Собираем html

Напишем таск для сборки html:

gulp.task('html:build', function () {
    gulp.src(path.src.html) //Выберем файлы по нужному пути
        .pipe(rigger()) //Прогоним через rigger
        .pipe(gulp.dest(path.build.html)) //Выплюнем их в папку build
        .pipe(connect.reload()); //И перезагрузим наш сервер для обновлений
});

Напомню, что rigger это наш плагин, позволяющий использовать такую конструкцию для импорта файлов

//= template/footer.html

Давай те же применим его в деле!
В папке src/template/ — создадим файлы header.html и footer.html следующего содержания

header.html

<section class="header">
    Header
</section>

footer.html

<section class="header">
    Footer
</section>

а наш файл index.html изменим вот так:

<!DOCTYPE html>
<html>
<head lang="ru">
    <meta charset="UTF-8">
    <title>Я собираю проекты как рок звезда</title>
</head>
<body>
    //= template/header.html

    <section class="content">
        Content
    </section>
    
    //= template/footer.html
</body>
</html>

Осталось перейти в консоль и запустить наш таск командой

gulp html:build

После того как она отработает — идем в папку build и видим там наш файл index.html, который превратился в это:

<!DOCTYPE html>
<html>
<head lang="ru">
    <meta charset="UTF-8">
    <title>Я собираю проекты как рок звезда</title>
</head>
<body>
    <section class="header">
        Header
    </section>
    <section class="content">
        Content
    </section>
    <section class="footer">
        Footer
    </section>
</body>
</html>

Это же просто восхитительно!
Помню как много неудобств доставляло бегать по всем сверстанным страничкам и вносить изменения в какую-то повторяющуюся на них часть. Теперь это делается удобно в одном месте.

Собираем javascript

Таск по сборке скриптов будет выглядеть так:

gulp.task('js:build', function () {
    gulp.src(path.src.js) //Найдем наш main файл
        .pipe(rigger()) //Прогоним через rigger
        .pipe(sourcemaps.init()) //Инициализируем sourcemap
        .pipe(uglify()) //Сожмем наш js
        .pipe(sourcemaps.write()) //Пропишем карты
        .pipe(gulp.dest(path.build.js)) //Выплюнем готовый файл в build
        .pipe(connect.reload()); //И перезагрузим сервер
});

Помните наш файл main.js?
Вся идея тут состоит в том, чтобы с помощью rigger'a инклюдить в него все нужные нам js файлы в нужном нам порядке. Именно ради контроля над порядком подключения — я и делаю это именно так, вместо того что бы попросить gulp найти все *.js файлы и склеить их.
Часто, при поиске места ошибки я по очереди выключаю какие то файлы из сборки, что бы локализовать место проблемы. В случае если бездумно склеивать все .js — дебаг будет усложнен.

Заполним наш main.js

/*
 * Third party
 */
//= ../../bower_components/jquery/dist/jquery.js


/*
 * Custom
 */
//= partials/app.js

Именно так я делаю на боевых проектах. Вверху этого файла всегда идет подключение зависимостей, ниже подключение моих собственных скриптов.
Кстати, bower пакеты можно подключать через такой плагин как gulp-bower [23]. Но я опять же не делаю этого, потому что хочу самостоятельно определять что, где и как будет подключаться.

Осталось только запустить наш таск из консоли командой

gulp js:build  

И в папке build/js — мы увидим наш скомпилированный и сжатый файл.

Собираем стили

Напишем задачу для сборки нашего SCSS

gulp.task('style:build', function () {
    gulp.src(path.src.style) //Выберем наш main.scss
        .pipe(sourcemaps.init()) //То же самое что и с js
        .pipe(sass()) //Скомпилируем
        .pipe(prefixer()) //Добавим вендорные префиксы
        .pipe(cssmin()) //Сожмем
        .pipe(sourcemaps.write())
        .pipe(gulp.dest(path.build.css)) //И в build
        .pipe(connect.reload());
});

Здесь все просто, но вас могут заинтересовать настройки автопрификсера. По умолчанию он пишет префиксы необходимые для последних двух версий браузеров. В моем случае этого достаточно, но если вам нужны другие настройки — вы можете найти их тут [5].

Со стилями я поступаю так же как и с js, но только вместо rigger'a — использую встроенный в SCSS импорт.
Наш main.scss будет выглядеть так:

/*
* Third Party
*/
@import "CSS:../../bower_components/normalize.css/normalize.css";

/*
* Custom
*/
@import "partials/app";

Таким способом получается легко управлять порядком подключения стилей.
Проверим наш таск, запустив

gulp style:build

Собираем картинки

Таск по картинкам будет выглядеть так:

gulp.task('image:build', function () {
    gulp.src(path.src.img) //Выберем наши картинки
        .pipe(imagemin({ //Сожмем их
            progressive: true,
            svgoPlugins: [{removeViewBox: false}],
            use: [pngquant()],
            interlaced: true
        }))
        .pipe(gulp.dest(path.build.img)) //И бросим в build
        .pipe(connect.reload());
});

Я использую дефолтные настройки imagemin, за исключением interlaced. Подробнее об API этого плагина можно прочесть тут [24].
Теперь, если мы положим какую-нибудь картинку в src/img и запустим команду

gulp image:build

то увидим в build нашу оптимизированную картинку. Так же gulp любезно напишет в консоли сколько места он сэкономил для пользователей нашего сайта :)

Шрифты

Со шрифтами мне обычно не нужно проводить никаких манипуляций, но что бы не рушить парадигму «Работаем в src/ и собираем в build/» — я просто копирую файлы из src/fonts и вставляю в build/fonts. Вот таск

gulp.task('fonts:build', function() {
    gulp.src(path.src.fonts)
        .pipe(gulp.dest(path.build.fonts))
});

Теперь давайте определим таск с именем «build», который будет запускать все что мы с вами тут накодили

gulp.task('build', [
    'html:build',
    'js:build',
    'style:build',
    'fonts:build',
    'image:build'
]);

Изменения файлов

Чтобы не лазить все время в консоль давайте попросим gulp каждый раз при изменении какого то файла запускать нужную задачу. Для этого напишет такой таск:

gulp.task('watch', function(){
    watch([path.watch.html], function(event, cb) {
        gulp.start('html:build');
    });
    watch([path.watch.style], function(event, cb) {
        gulp.start('style:build');
    });
    watch([path.watch.js], function(event, cb) {
        gulp.start('js:build');
    });
    watch([path.watch.img], function(event, cb) {
        gulp.start('image:build');
    });
    watch([path.watch.fonts], function(event, cb) {
        gulp.start('fonts:build');
    });
});

С понимаем не должно возникнуть проблем. Мы просто идем по нашим путям определенным в переменной path, и в функции вызывающейся при изменении файла — просим запустить нужный нам таск.
Попробуйте запустить в консоли

gulp watch

и поменяйте разные файлы.
Ну не круто ли?)

Веб сервер

Что бы насладиться чудом livereload — нам необходимо создать себе локальный веб-сервер. Для этого напишем следующий таск:

gulp.task('webserver', function() {
    connect.server({
        host: server.host,
        port: server.port,
        livereload: true
    });
});

Тут даже нечего комментировать. Мы просто запустим сервер с livereload на хосте и порте, которые мы определили в объекте server.

Очистка

Если вы добавите какую-нибудь картинку, потом запустите задачу image:build и потом картинку удалите — она останется в папке build. Так что было бы удобно — периодически подчищать ее. Создадим для этого простой таск

gulp.task('clean', function (cb) {
    rimraf(path.clean, cb);
});

Теперь при запуске команды

gulp clean

просто будет удаляться папка build.

И напоследок маленькая милость

Этот таск не несет в себе критической функциональности, но он очень мне нравится :)

gulp.task('openbrowser', function() {
    opn( 'http://' + server.host + ':' + server.port + '/build' );
});

Когда нам будет нужно, мы запустим его — и у нас в браузере автоматически откроется вкладка с нашим проектом.
Классно же :)

Финальный аккорд

Последним делом — мы определим дефолтный таск, который будет запускать всю нашу сборку.

gulp.task('default', ['build', 'webserver', 'watch', 'openbrowser']);

Окончательно ваш gulpfile.js будет выглядеть примерно вот так [25].
Теперь выполним в консоли

gulp

И вуаля :) Заготовка для вашего проекта готова и ждет вас.

Пара слов в заключение

Эта статья задумывалась как способ еще раз освежить в памяти тонкости сборки frontend проектов, и для легкости передачи этого опыта новым разработчикам. Вам не обязательно использовать на своих проектах именно такой вариант сборки. Есть yeoman.io [26], на котором вы найдете генераторы почти под любые нужды.
Я написал этот сборщик по 2ум причинам.
— Мне нравится использовать rigger в своем html коде
— Почти во всех сборках что я встречал — используется временная папка (обычно .tmp/), для записи промежуточных результатов сборки. Мне не нравится такой подход и я хотел избавится от временных папок.
— И я хотел что бы все это было у меня из коробки :)

Мою рабочую версию сборщика вы можете скачать на моем github [27].

Надеюсь статья оказалась полезной для вас :)

P.S. обо всех ошибках, недочетах и косяках — пожалуйста пишите в личку

Автор: Insayt

Источник [28]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/82926

Ссылки в тексте:

[1] Gulp: http://gulpjs.com/

[2] Grunt: http://gruntjs.com/

[3] Brunch: http://brunch.io/

[4] npm: https://www.npmjs.com

[5] gulp-autoprefixer: https://www.npmjs.com/package/gulp-autoprefixer

[6] gulp-cssmin: https://www.npmjs.com/package/gulp-cssmin

[7] gulp-connect: https://www.npmjs.com/package/gulp-connect

[8] gulp-imagemin: https://www.npmjs.com/package/gulp-imagemin

[9] imagemin-pngquant: https://www.npmjs.com/package/imagemin-pngquant

[10] gulp-uglify: https://www.npmjs.com/package/gulp-uglify

[11] gulp-sass: https://www.npmjs.com/package/gulp-sass

[12] доклад: http://vk.com/video?section=search&z=video-54076565_169761565%2Falbum183203591

[13] LibSass: http://libsass.org/

[14] gulp-sourcemaps: https://www.npmjs.com/package/gulp-sourcemaps

[15] gulp-rigger: https://www.npmjs.com/package/gulp-rigger

[16] gulp-watch: https://www.npmjs.com/package/gulp-watch

[17] opn: https://www.npmjs.com/package/opn

[18] rimraf: https://www.npmjs.com/package/rimraf

[19] Bower: http://bower.io/

[20] тут: http://nano.sapegin.ru/all/bower

[21] yeoman.io: http://yeoman.io/generators/

[22] gulp-load-plugins: https://www.npmjs.com/package/gulp-load-plugins

[23] gulp-bower: https://www.npmjs.com/package/gulp-bower

[24] тут: https://github.com/sindresorhus/gulp-imagemin

[25] вот так: https://gist.github.com/Insayt/272c9b81936a03884768

[26] yeoman.io: http://yeoman.io/

[27] github: https://github.com/Insayt/frontend-devil

[28] Источник: http://habrahabr.ru/post/250569/