Быстрая настройка Grunt для комфортной разработки

в 15:48, , рубрики: css, deployment, grunt, gruntjs, html, javascript, web-разработка, Блог компании bitCalm, Веб-разработка

Быстрая настройка Grunt для комфортной разработки

Во время разработки нашего сервиса bitcalm.com, нам потребовалось организовать автоматическую сборку проекта. Перед нами стояла цель улучшить производительность frontend-части нашего приложения, а также оптимизировать процессы разработки и развертывания на сервере.

Основными задачами, которые требовалось решить, стали:

  1. Объединение и минификация скриптов
  2. Объединение и минификация стилей
  3. Сжатие png-изображений
  4. Создание спрайтов из всех изображений (с возможностью удобного использования и с поддержкой двух видов спрайтов для девайсов с разным PPI)
  5. Построение разных версий html-документов для разработки и для продакшна

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

Вкратце расскажу, какие плагины используются для первых трёх пунктов. Для оптимизации скриптов задействованы grunt-contrib-concat и grunt-contrib-uglify. Работа со стилями осуществляется с помощью grunt-contrib-cssmin. Сжатие изображений происходит благодаря grunt-contrib-imagemin.

Стоит упомянуть одну хитрость при работе с grunt-contrib-concat. Дело в том, что если указать в параметре src просто все файлы в директории (src: ["/scripts/*.js"]), то не обязательно файлы соединятся в нужном для вас порядке. Это часто встречается, если файл какой-либо библиотеки следует после файла, в котором эта библиотека используется. Самый простой и надёжный выход в данном случае — указывать порядок обработки скриптов вручную.

Вот пример, как это делаем мы:

dist: {
    src: [
        '/scripts/lib/*.js',
        '/scripts/app/modules/*.js',
        '/scripts/app/*js'
    ],
    dest: '/dist/scripts.min.js'
}

Автоматическое создание спрайтов

Для создания спрайтов мы используем плагин grunt-spritesmith. Он берёт все изображения из определённой папки, склеивает их в один лист и создаёт css-файл для получившегося спрайта. Благодаря этому, к любому из исходных изображений можно обратиться по определённому CSS-селектору (для удобства используется `.icon-<имя_исходного_файла>`).

Небольшой пример:

Исходные файлы берутся из папки raw

├───img
│   └───raw
│       ├───img-1.png
│       ├───img-2.png
│       └───img-3.png

После выполнения таска появляются два файла: непосредственно сам спрайт spritesheet.png файл стилей spritestyles.css.

├───img
│   ├───raw
│   │   ├───img-1.png
│   │   ├───img-2.png
│   │   └───img-3.png
│   │
│   └───spritesheet.png
│
├───styles
│   └───spritestyles.css

В файле spritestyles.css содержатся стили для использования спрайта.

.icon-img-1{
	background-position: 0px 0px;
	width: 40px;
	height: 40px;
}
.icon-img-2{
	background-position: -129px 0px;
	width: 95px;
	height: 80px;
}
.icon-img-3{
	background-position: -42px 0px;
	width: 85px;
	height: 85px;
}

Кроме того, в начале этого файла описан общий стиль для использования данного спрайта:

.sprite {
	display: inline-block;
	background-image: url(../img/spritesheet.png);
	background-repeat: no-repeat;
}

В итоге мы можем использовать любое наше изображение в вёрстке с помощью пары классов:

<span class=”sprite icon-img-1”></span>

Создание спрайтов для устройств с большим PPI

Для того, чтобы наши изображения хорошо выглядели на устройствах с высокой плотностью пикселей, необходимо создать увеличенную версию спрайта. На самом деле, можно было бы легко использовать только большую версию спрайта для всех экранов. Однако такой подход является не совсем правильным, ведь большой спрайт в несколько раз «тяжелее» обычного.

Итак, как же создаются спрайты разных размеров: в папке img/raw должны лежать изображения для обычных устройств, а в папке img/raw@2x — увеличенные копии тех же изображений. Для экранов с pixel ratio ≥ 2, создаётся отдельный спрайт spritesheet@2x.png и файл стилей spritestyles@2x.css. Благодаря использованию media queries, браузер сам выбирает, какой из спрайтов ему необходимо использовать.

Вот как выглядит spritestyles@2x.css:

@media 
only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (-moz-min-device-pixel-ratio: 2), 
only screen and (-o-min-device-pixel-ratio: 2/1), 
only screen and (min-device-pixel-ratio: 2), 
only screen and (min-resolution: 192dpi), 
only screen and (min-resolution: 2dppx) {
	.sprite {
		display: inline-block;
		background-image: url(../img/spritesheet@2x.png);
		background-repeat: no-repeat;
	}

	.icon-img-1 { ... }
	.icon-img-2 { ... }
	.icon-img-3 { ... }
}

Для того, чтобы плагин grunt-spritesmith смог осуществлять всё вышеописанное, необходимо было создать собственный Mustache-шаблон или написать JS функцию, создающую CSS. Так как ни времени ни сил разбираться с первым решением не было, я решил по-быстрому написать пару функций и отложить создание шаблонов «на потом».

Код таска

sprite: {
    normal: {
        src: 'img/raw/*.png',
        destImg: 'img/spritesheet.png',
        destCSS: 'styles/spritestyles.css',
        padding: 2,
        cssTemplate: function (params) {
            var result = '.sprite {display: inline-block; background-image: url(../img/spritesheet.png); background-repeat: no-repeat;}';
            for (var i = 0, ii = params.items.length; i < ii; i += 1) {
                result += '.icon-' + params.items[i].name + '{' +
                    'background-position: ' + params.items[i].px.offset_x + ' ' + params.items[i].px.offset_y + ';' +
                    'width: ' + params.items[i].px.width + ';' +
                    'height: ' + params.items[i].px.height + ';' +
                    '}n'
            }
            return result;
        }
    },
    large: {
        src: 'img/raw@2x/*.png',
        destImg: 'img/spritesheet@2x.png',
        destCSS: 'styles/spritestyles@2x.css',
        padding: 4,
        cssTemplate: function (params) {
            var result = '.sprite {display: inline-block; background-image: url(../img/spritesheet@2x.png); background-repeat: no-repeat;}';
            result += '@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {n';
            for (var i = 0, ii = params.items.length; i < ii; i += 1) {
                result += '.icon-' + params.items[i].name + '{' +
                    'background-position: ' + params.items[i].offset_x/2 + 'px ' + params.items[i].offset_y/2 + 'px;' +
                    'background-size: ' + params.items[i].total_width/2 + 'px ' + params.items[i].total_height/2 + 'px;' +
                    'width: ' + params.items[i].width/2 + 'px;' +
                    'height: ' + params.items[i].height/2 + 'px;' +
                    '}n'
            }
            result += '}';
            return result;
        }
    }
}

Как видно из кода этого таска, создание большого спрайта отличается лишь добавлением media query, указанием свойства background-size и уменьшением всех размеров в два раза.

Настроив такую систему, можно совершенно не отвлекаться на работу с изображениями во время вёрстки. Особенно удобно использовать этот метод в сочетании с плагином для Photoshop — Retinize It.

Построение разных версий html-документов

Наверняка, многим знакома ситуация, когда для разработки в html-файле подключаются много стилей и скриптов, а для продакшна необходимо заменить их на минифицированные версии. Вручную менять файл или изобретать велосипед — сомнительное удовольствие, поэтому было решено призвать на помощь Grunt.

Непродолжительные поиски привели к grunt-processhtml. Рассмотрим основные принципы работы с этим плагином.

Обрабатываемый html-файл находится в корне. Именно в этом файле нужно производить какие-либо операции, потому что из него генерируется html-документ в папке dist.

templates
├───dist
│   └───app.html
├───app.html

Файл app.html в папке dist должен загружаться в браузер, то есть все серверные роутинги должны указывать на него, а не на файл app.html в корне. Настройка этого таска в grunt очень проста:

var templates = {
    cwd: 'templates/',
    src: ['*.html'],
    dest: 'templates/dist/',
    ext: '.html'
};

...

processhtml: {
    dev: {
        files: [templates]
    },
    dist: {
        files: [templates]
    }
}

Мы лишь указываем, в какой папке лежат исходные файлы и в какую папку складывать обработанные. Также, в самом таске есть контейнеры dev и dist, которые позволяют для разных версий сборки использовать разные файлы и настройки. С помощью этих контейнеров, происходит обработка условных конструкций внутри файлов: убираются те или иные куски кода, в зависимости от того, какой режим сборки был выбран.

<!DOCTYPE html>
<html>
<head>
	<title></title>

	<!-- build:remove:dev -->
    <link rel="stylesheet" href="dist/styles.min.css" />
    <!-- /build -->

    <!-- build:remove:dist -->
    <link rel="stylesheet" href="styles/styles-1.css"/>
    <link rel="stylesheet" href="styles/styles-2.css"/>
    <link rel="stylesheet" href="styles/styles-3.css"/>
    <!-- /build -->

</head>
<body>
	<div></div>

	<!-- build:remove:dev -->
    <script src="dist/scripts.min.js"></script>
    <!-- /build -->

    <!-- build:remove:dist -->
    <script src="scripts/scripts-1.js"></script>
    <script src="scripts/scripts-2.js"></script>
    <script src="scripts/scripts-3.js"></script>
    <!-- /build -->

</body>
</html>

Содержимое блоков build:remove:dev удаляется во время разработки, а содержимое build:remove:dist во время сборки для продакшна. Для удобства разработки можно использовать вот такие алиасы:

grunt.registerTask('dev', ['processhtml:dev']);
grunt.registerTask('default', ['processhtml:dist', ...]);

Теперь для создания продакшн-версии достаточно выполнить в консоли команду grunt, а для разработки следует запустить grunt dev.

Главное неудобство этого метода в том, что после любого изменения html-файла нужно вручную запускать grunt для того, чтобы обновился конечный файл. Решение этой проблемы очень простое: используем grunt-contrib-watch. Этот плагин отслеживает изменения файлов, а затем запускает нужный таск (в нашем случае — это dev).

watch: {
    html: {
        files: ['templates/*.html'],
        tasks: ['dev']
    }
}

Вывод

Я постарался показать, насколько просто и быстро можно начать использовать Grunt на уже работающем проекте. Данная система ускорила процесс разработки и развертывания обновлений на сервере, а также значительно оптимизировала frontend-часть нашего сервиса.

Эта третья статья из цикла про то, как мы делали сервис облачного резервного копирования серверов bitcalm.com.

Первая статья: Разработка своей системы биллинга на Django
Вторая статья: Как мы делали каркас приложения на AngularJS и Django

Автор: m_smirnov

Источник

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


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