#3 — Autoprefixer и Source Maps

Некоторые CSS3 свойства некорректно отображаются в различных браузерах. Чтобы их исправить придумали специальные префиксы для свойств. Gulp обладает плагином Autoprefixer, что позволяет не прописывать префиксы к свойствам каждый раз вручную, а добавлять их в автоматическом режиме. В уроке вы научитесь работать с плагином Autoprefixer, а также познакомитесь с Source Mapping.
Видеоурок
Некоторые CSS3 свойства, например transition , могут некорректно отображаться в не особо популярных браузерах. Для исправления ошибки были придуманы специальные префиксы к свойствам.
Пример:
Прописывать префиксы под каждое CSS3 свойство очень неудобно и это занимает множество времени.
Для решения проблемы придумали плагин Autoprefixer. Подключив плагин и указав какие CSS или SCSS файлы вы отслеживаете, вы можете в автоматическом режиме добавлять все необходимые префиксы к свойствам и ваш сайт всегда будет отображаться корректно.
Source Maps
Source Maps — плагин, позволяющий указать исходный файл со стилями. Указав такой файл, вам намного проще будет отслеживать ошибки и проверять код через консоль разработчика в Google Chrome, Mozilla Firefox и в любых других веб браузерах.
Source Maps: быстро и понятно

Механизм Source Maps используется для отображения исходных текстов программы на сгенерированные на их основе скрипты. Несмотря на то, что тема не нова и по ней уже написан ряд статей (например эта, эта и эта) некоторые аспекты все же нуждаются в прояснении. Представляемая статья представляет собой попытку упорядочить и систематизировать все, что известно по данной теме в краткой и доступной форме.
В статье Source Maps рассматриваются применительно к клиентской разработке в среде популярных браузеров (на примере, DevTools Google Chrome), хотя область их применения не привязана к какому-либо конкретному языку или среде. Главным источникам по Source Maps является, конечно, стандарт, хотя он до сих пор не принят (статус — proposal), но, тем не менее, широко поддерживается браузерами.
Работа над Source Maps была начата в конце нулевых, первая версия была создана для плагина Firebug Closure Inspector. Вторая версия вышла в 2010 и содержала изменения в части сокращения размера map-файла. Третья версия разработана в рамках сотрудничества Google и Mozilla и предложена в 2011 (последняя редакция в 2013).
В настоящее время в среде клиентской разработки сложилась ситуация, когда исходный код почти никогда не интегрируется на веб-страницу непосредственно, но проходит перед этим различные стадии обработки: минификацию, оптимизацию, конкатенацию, более того, сам исходный код может быть написан на языках требующих транспиляции. В таком случае, для целей отладки необходим механизм позволяющий наблюдать в дебаггере именно исходный, человекочитаемый код.
Для работы Source Maps необходимы следующие файлы:
- собственно сгенерированный JavaScript-файл
- набор файлов с исходным кодом использовавшийся для его создания
- map-файл отображающий их друг на друга
Map-файл
Вся работа Source Maps основана на map-файле, который может выглядеть, например, так:
Обычно, имя map-файла складывается из имени скрипта, к которому он относится, с добавлением расширения «.map», bundle.js — bundle.js.map. Это обычный json-файл со следующими полями:
- «version» — версия Source Maps;
- «file» — (опционально) имя сгенерированного файла, к которому относится текущий map-файл;
- «sourceRoot» — (опционально) префикс для путей к файлам-исходникам;
- «sources» — список путей к файлам-исходникам (разрешаются аналогично адресам src тега script, можно использовать file://.);
- «names» — список имен переменных и функций, которые подверглись изменению в сгенерированном файле;
- «mappings» — координаты отображения переменных и функций исходных файлов на сгенерированный файл в формате Base64 VLQ;
- «sourcesContent» — (опционально) в случае self-contained map-файла список строк, каждая из которых содержит исходный текст файла из sources;
Загрузка Source Maps
Для того, чтобы браузер загрузил map-файл может быть использован один из следующих способов:
- JavaScript-файл пришел с HTTP-заголовком: SourceMap: (ранее использовался ныне устаревший X-SourceMap: )
- в сгенерированном JavaScript-файле есть особый комментарий вида:
//# sourceMappingURL= (для CSS /*# sourceMappingURL= */)
Таким образом, загрузив map-файл браузер подтянет и исходники из поля «sources» и с помощью данных в поле «mappings» отобразит их на сгенерированный скрипт. Во вкладке Sources DevTools можно будет найти оба варианта.
Для указания пути может использоваться пседопротокол file://. Также, в может быть включено все содержимое map-файла в кодировке Base64. В терминологии Webpack подобные Source Maps названы inline source maps.
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
Ошибки загрузки Source Maps
Следует заметить, что map-файлы не являются частью веб-страницы, поэтому вы не увидите информации об их загрузке во вкладке Network DevTools. Тем не менее, если в сгенерированном файле находится ссылка на несуществующий map-файл, в Console DevTools будет предупреждение вида: «DevTools failed to load SourceMap: . ». Также при наличии ссылки на несуществующий исходник, вместо него будет сообщение вида: «Could not load content for . ».
Self-contained map-файлы
Код файлов-исходников можно включить непосредственно в map-файл в поле «sourcesContent», при наличии этого поля необходимость в их отдельной загрузке отпадает. В этом случае названия файлов в «sources» не отражают их реального адреса и могут быть совершенно произвольными. Именно поэтому, вы можете видеть во вкладке Sources DevTools такие странные «протоколы»: webpack://, ng:// и т.д
Mappings
Сущность механизма отображения состоит в том, что координаты (строка/столбец) имен переменных и функций в сгенерированном файле отображаются на координаты в соотвествующем файле исходного кода. Для работы механизма отображения необходима следующая информация:
(#1) номер строки в сгенерированном файле;
(#2) номер столбца в сгенерированном файле;
(#3) индекс исходника в «sources»;
(#4) номер строки исходника;
(#5) номер столбца исходника;
Все эти данные находятся в поле «mappings», значение которого — длинная строка с особой структурой и значениями закодированными в Base64 VLQ.
Строка разделена точками с запятой (;) на разделы, соответствующие строкам в сгенерированном файле (#1).
Каждый раздел разделен запятыми (,) на сегменты, каждый из которых может содержать 1,4 или 5 значений:
- номер столбца в сгенерированном файле (#2);
- индекс исходника в «sources» (#3);
- номер строки исходника (#4);
- номер столбца исходника (#5);
- индекс имени переменной/функции из списка «names»;
Каждое значение представляет собой число в формате Base64 VLQ. VLQ (Variable-length quantity) представляет собой принцип кодирования сколь угодно большого числа с помощью произвольного числа двоичных блоков фиксированной длины.
В Source Maps используются шестибитные блоки, которые следуют в порядке от младшей части числа к старшей. Старший 6-й бит каждого блока (continuation bit) зарезервирован, если он установлен, то за текущим следует следующий блок относящийся к этому же числу, если сброшен — последовательность завершена.
Поскольку в Source Maps значение должно иметь знак, для него также зарезервирован младший 1-бит (sign bit), но только в первом блоке последовательности. Как и ожидается, установленный sign бит означает отрицательно число.
Таким образом, если число можно закодировать единственным блоком, оно не может быть по модулю больше 15 (11112), так как в первом шестибитном блоке последовательности два бита зарезервированы: continuation бит всегда будет сброшен, sign бит будет установлен в зависимости от знака числа.
Шестибитные блоки VLQ отображаются на кодировку Base64, где каждой шестибитной последовательности соответствует определенный символ ASCII.

Декодируем число mE. Инверсируем порядок, младшая часть последняя — Em. Декодируем числа из Base64: E — 000100, m — 100110. В первом отбрасываем старший continuation бит и два лидирующих нуля — 100. Во втором отбрасываем старший continuation и младший sign биты (sign бит сброшен — число положительное) — 0011. В итоге получаем 100 00112, что соответствует десятичному 67.
Можно и в обратную сторону, закодируем 41. Его двоичный код 1010012, разбиваем на два блока: старшая часть — 10, младшая часть (всегда 4-битная) — 1001. К старшей части добавляем старший continuation бит (сброшен) и три лидирующих нуля — 000010. К младшей части добавляем старший continuation бит (установлен) и младший sign бит (сброшен — число положительное) — 110010. Кодируем числа в Base64: 000010 — C, 110010 — y. Инверсируем порядок и, в итоге, получаем yC.
Для работы с VLQ весьма полезна одноименная библиотека.
Введение в Javascript Source Maps
Вы когда-нибудь думали, как было бы здорово, если бы слитый в один файл и минифицированный яваскрипт код в production-окружении можно было удобно читать и даже отлаживать без ущерба производительности? Теперь это возможно, если использовать штуку под названием source maps.
Если коротко, то это способ связать минифицированный/объединённый файл с файлами, из которых он получился. Во время сборки для боевого окружения помимо минификации и объединения файлов также генерируется файл-маппер, который содержит информацию об исходных файлах. Когда производится обращение к конкретному месту в минифицированном файле, то производится поиск в маппере, по которому вычисляется строка и символ в исходном файле. Developer Tools (WebKit nightly builds или Google Chrome Canary) умеет парсить этот файл автоматически и прозрачно подменять файлы, как будто ведётся работа с исходными файлами. На момент написания (оригинальной статьи — прим. перев.) Firefox заблокировал развитие поддержки Source Map. Подробнее — на MozillaWiki Source Map.
Пример — правильное определение места в исходном коде
В этом примере можно ткнуть в любом месте textarea правой кнопкой и выбрать пункт «Get original location». При этом будет произведено обращение к файлу-мапперу с передачей строки и номера символа в минифицированном коде, и будет показан соответствующий кусок кода из исходного файла. В консоль будут выведены номер строки и номер символа в исходном файле и другая интересная информация.

Реальное использование

Прежде чем смотреть следующий пример, нужно активировать просмотр source maps в Chrome Canary или WebKit nightly, для этого в свойствах активировать пункт «Enable source maps» (см. скриншот)
Продолжим. Предыдущий пример был интересным, но как это можно использовать? Зайдите на dev.fontdragr.com настроенным браузером Google Chrome и вы увидите, что яваскрипты на странице не скомпилированы и можно смотреть отдельные js-файлы. Это всё благодаря использованию маппера, а на самом деле код на странице скомпилирован. Все ошибки, выводы в лог и точки останова будут маппиться на исходный код, и отлаживать код будет очень удобно. В итоге можно работать с production-сайтом как с тестовым.
Зачем вообще нужны Source Maps?
- CoffeeScript
- ECMAScript 6 и выше
- SASS/LESS и т.п.
- Практически любой язык, который компилируется в JavaScript
Google Web Toolkit (GWT) недавно добавил поддержку Source Maps и Ray Cromwell из GWT сделал отличный скринкаст, показывающий работу Source Map в действии.

Другой пример использует библиотеку Google Traceur, которая позволяет писать на ES6 (ECMAScript 6) и компилировать в ES3-совместимый код. Компилятор Traceur также генерирует source map. Посмотрите на пример использования особенностей ES6 (классов и traits), как если бы они поддерживались браузером нативно. Textarea в примере также позволяет писать ES6-код, который будет компилироваться на лету в ES3 и также будет создаваться файл-маппер.
Пример — можно написать код на ES6 и сразу посмотреть в отладчике
Как это работает?
Единственный пока компилятор/минификатор с поддержкой Source Map — Closure compiler (как при компиляции сгенерировать маппер — написано ниже). При минификации JavaScript будет создан и файл-маппер. Пока Closure compiler не добавляет в конец файла специальный комментарий для Google Chrome Canary dev tools о том, что доступен файл-маппер:
//@ sourceMappingURL=/path/to/file.js.map
Такой комментарий позволяет браузеру искать нужное место в исходном файле, используя файл-маппер. Если идея использовать странные комментарии вам не нравится, то можно добавить к скомпилированному файлу специальный заголовок:
X-SourceMap: /path/to/file.js.map

Как и комментарий, это скажет клиенту, где искать маппер для этого файла. Использование заголовка также позволяет работать с языками, которые не поддерживают однострочные комментарии.
Файл-маппер будет скачан только если включено свойство и открыта консоль. Ну и конечно нужно будет залить исходные файлы, чтобы они были доступны по указанным в маппере путям.
Как сгенерировать файл-маппер?
Как уже говорилось выше, нужен будет Closure compiler для минификаци, склейки и генерации файла-маппера для нужных JavaScript-файлов. Для этого нужно выполнить команду:
java -jar compiler.jar \ --js script.js \ --create_source_map ./script-min.js.map \ --source_map_format=V3 \ --js_output_file script-min.js
Нужные флаги — это —create_source_map и —source_map_format . Последний нужен, т.к. по умолчанию маппер создаётся в формате V2, а нам нужен V3.
Внутреннее устройство Source Map
Чтобы лучше понять Source Map, возьмём для примера небольшой файл-маппер и подробно разберём, как устроена «адресация». Ниже приведён немного модифицированный пример из V3 spec:
- Версию маппера
- Название минифицированного/объединённого файла для production
- sourceRoot позволяет дописывать префикс в путь к исходным файлам
- sources содержит названия исходных файлов
- names содержит все настоящие названия переменных/функций из полученного файла
- а mappings — это соответствующие минифицированные названия
BASE64 VLQ или как сделать Source Map маленьким

Изначально в спецификации был описан очень подробный вывод всех зависимостей, что делало файл-маппер в 10 раз больше размером, чем сгенерированный файл. Вторая версия уменьшила размер файла вполовину, а третья версия — уменьшила ещё раз вполовину. Теперь для 133kB файла генерируется ~300kB файл-маппер. Как же удалось добиться такого уменьшения и при этом уметь отслеживать сложные зависимости?
Используется VLQ (Variable Length Quantity) и Base64-кодирование. Свойство mappings — это одна очень большая строка. Внутри этой строки точки с запятой (;) отделяют номера строк в сгенерированном файле. Внутри получившейся строки используются запятые для отделения сегментов кода. Каждый из сегментов представляет собой 1, 4 или 5 VLQ-полей. Некоторые могут быть длиннее за счёт бита продолжения. Каждый сегмент строится на основе предыдущего, что помогает уменьшить размер файла.
Как говорилось раньше, каждый сегмент может быть 1, 4 или пятью VLQ. На диаграмме показаны 4 VLQ с одним битом продолжения. Разберём её подробнее и покажем, как маппер вычисляет положение в исходном файле. Сегмент состоит из пяти вещей:
- Номер символа в сгенерированном файле
- Исходный файл
- Номер строки в исходном файле
- Номер символа в исходном файле
- Исходное название (если есть)
(прим. перев.: не осилил до конца перевести эту часть статьи, полностью можно прочесть в оригинале; если есть желающие помочь — пишите, буду благодарен)
Потенциальные проблемы с XSSI
В спецификации говорится о возможных проблемах с внедрением XSS при использовании Source Map. Избавиться от неё можно, написав в начале своего map-файла » )]> «, чтобы сделать это js-файл невалидным и вызвать ошибку. WebKit dev tools уже умеет её забарывать:
if (response.slice(0, 3) === ")]>")
Как видно, первые три символа обрезаются и производится проверка их на соответствие указанному в спецификации невалидному коду и в этом случае вырезается всё до следующего символа перевода строки.
@sourceURL и displayName в действии: eval и анонимные функции
Эти два соглашения хотя пока и не входят в спецификацию Source Map, но позволяют серьёзно упростить работу с eval и анонимными функциями.
Первый хелпер очень похож на свойство //@ sourceMappingURL и вообще-то в спецификации (V3) упоминается. Включив этот специальный комментарий в код, который потом будет выполнен через eval , можно назвать eval -ы, что даст им более логичные имена при работе в консоли. Ниже приведён простой пример с использованием компилятора CoffeeScript:

Пример — пропущенный через eval код со сгенерированным именем
Другой хелпер позволяет давать имена анонимным функциям при помощи свойства displayName , указанного в контексте этой функции. Попрофилируйте этот пример, чтобы увидеть displayName в действии.

Пример — названия для анонимных функций через displayName (только WebKit NIghtly)
При профилировании будут показываться красивые названия вместо (anonymous function) . Но скорее всего displayName не будет включён в финальную сборку Google Chrome. Хотя надежды ещё остаются, предлагают также переименовать свойство в debugName.
К моменту написания статьи присваивание названий коду, выполненному через eval , поддерживают только Firefox и Google Chrome. Свойство displayName доступно только в ночных сборках Google Chrome.
Вливайтесь
Есть очень длинное обсуждение по поддержке Source Map в CoffeeScript.
У UglifyJS также есть тикет про поддержку Source Map.
Вы можете помочь, если примете участие в обсуждении и выскажете мнение по поводу нужности поддержки Source Map. Чем больше будет инструментов, поддерживающих эту технологию, тем будет проще работать, так что требуйте её поддержки в вашем любимом OpenSource-проекте.
Source Map не идеален
Есть одна неприятность с использованием Source Map для нормальной отладки. Проблема заключается в том, что при попытке проверить значение аргумента или переменной, определённой в контексте исходного файла, контекст ничего не вернёт, т.к. он на самом деле не существует. Нужен какой-то обратный маппинг, чтобы проверить значение соответствующей переменной/аргумента в минифицированном коде и сопоставить его исходному коду.
Проблема решаемая, а при должном внимании к Source Map могут появиться ещё более интересные его применения.
Инструменты и ресурсы
- Nick Fitzgerald сделал форк UglifyJS с поддержкой Source Map
- Paul Irish сделал простое демо Source Map
- Conrad Irwin написал удобный Source Map gem для Ruby-разработчиков
- Что ещё почитать про именование eval и свойство displayName
- Можно посмотреть исходный код Closure Compiler создания Source Map
- Несколько скриншотов и разговор о поддержке GWT source maps
Source Map — мощный инструмент для разработчика. Он позволяет держать production-код максимально сжатым, но при этом позволяет его отлаживать. Так же полезен для начинающих разработчиков, чтобы посмотреть код, написанный опытными разработчиками, чтобы поучиться правильному структурированию и написанию своего кода без необходимости продираться сквозь минифицированный код. Так чего же вы ждёте? Сгенерируйте Source Map для своего проекта!
Создание и использование Source Maps для css и js в GULP 4
Хотелось бы в двух словах рассказать, как настроить Source Maps для GULP 4, но не получится вырвать из контекста просто пару строк кода, поэтому разобью статью на 2 части. Первая будет с базовым кодом для Source Maps, а во второй части будет куча кода из рабочего проекта.
Source Maps нужны для проектов, где происходит сборка нескольких файлов в один, например, компиляция файлов scss в один файл main.min.css или конкатенация js-файлов в один scripts.min.js . Карты источников нужны для того, чтобы быстро вносить правки, и не искать в каком файле задано определенное правило или функция, особенно когда для сборки используются десятки файлов или различных библиотек.

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

Так будет выглядеть код с использованием Source Maps:
И по итогу, если нам нужно увеличить отступ снизу у элемента .icon , то нам не нужно искать по всем файлам scss где задано это правило, а с помощью карты источников видим, что данное правило указано в файле _general.scss на 44 строке.
Если вам это не нужно, то дальше можно не читать.
1. Базовый пример вывода Source Maps для js-файла
var gulp = require('gulp'); var plugin1 = require('gulp-plugin1'); var plugin2 = require('gulp-plugin2'); var sourcemaps = require('gulp-sourcemaps'); gulp.task('javascript', function() < gulp.src('src/**/*.js') .pipe(sourcemaps.init()) .pipe(plugin1()) .pipe(plugin2()) .pipe(sourcemaps.write()) .pipe(gulp.dest('dist')); >);
Где — plugin1() и plugin2() — это какие-то образные плагины, которые занимаются минификацией и конкатенацией js-файлов, например.
Дальше будет много кода из рабочего примера, будет использоваться scss, сборка, минификация, конкатенация и т.д.
2. Кастомизированный пример подключения Source Maps в js-файлах и в css-файлах
var syntax = 'scss', gulp = require('gulp'), sass = require('gulp-sass'), cleancss = require('gulp-clean-css'), autoprefixer = require('gulp-autoprefixer'), concat = require('gulp-concat'), uglify = require('gulp-uglify'), rename = require('gulp-rename'), sourcemaps = require('gulp-sourcemaps'), notify = require('gulp-notify'),
Задача для компиляции файла main.min.css с Source Maps
// Compile .scss to *.min.css gulp.task('styles', function() < return gulp.src('app/'+syntax+'/**/*.'+syntax+'') // берем все файлы scss .pipe(sourcemaps.init()) // инициализируем создание Source Maps .pipe(sass(< outputStyle: 'compressed' >).on("error", notify.onError())) // компилируем сжатый файл .css .pipe(rename(< suffix: '.min', prefix : '' >)) // переименовываем файл в .min.css .pipe(autoprefixer(['last 15 versions'])) // добавляем вендорные префиксы .pipe(cleancss( > >)) // удаляем все комментарии из кода .pipe(sourcemaps.write()) // пути для записи SourceMaps - в данном случае карта SourceMaps будет добавлена прям в данный файл main.min.css в самом конце .pipe(gulp.dest('app/css')) // перемещение скомпилированного файла main.min.css в папку app/css >);
Задача для конкатенации файла scripts.min.js с Source Maps
// Concatenate all .js from /libs/ to libs.min.js gulp.task('scripts', function() < return gulp.src([ 'app/libs/jquery/dist/jquery.min.js', // указываем пути всех подключаемых библиотек и файлов JS 'app/js/common.js', // основной файл функций подключаем в самом конце ]) .pipe(sourcemaps.init()) // инициализируем создание Source Maps .pipe(concat('scripts.min.js')) // объединяем все вышеперечисленные файлы в один scripts.min.js .pipe(uglify()) // минифицируем и удаляем комментарии из файла scripts.min.js .pipe(sourcemaps.write()) // пути для записи SourceMaps - в данном случае карта SourceMaps будет добавлена прям в данный файл scripts.min.js в самом конце в формате комментария .pipe(gulp.dest('app/js')) // перемещаем полученный файл scripts.min.js в директорию app/js >);
Все классно, но есть одна проблема.
sourcemaps.write() — не указаны пути для файла *.map, значит карта источников будет записана внутри файла в самый конец, тем самым увеличивая вес файлов более чем в два раза. Это не круто, но зато карты источников будут работать и в Chrome, и в Firefox.
sourcemaps.write(‘.’) — в таком формате source map запишется отдельным файлом в той же папке, что и основной файл, в формате main.min.css.map и scripts.min.js.map.
Проблема с лишним содержимым в виде карты путей решаема.
Source Maps нужны нам только во время разработки, поэтому при билде продашена просто удалим все комментарии из файлов .css и .js — а карта источников записана именно в виде комментария.
Это часть кода из моей таски ‘build’:
var buildCss = gulp.src('app/css/**/*.css') .pipe(cleancss( > >)) // удаляем все комментарии из css .pipe(gulp.dest('dist/css')); // отправляем полученный файл на продакшен var buildJs = gulp.src('app/js/**/*.js') .pipe(uglify()) // удаляем все комментарии из js .pipe(gulp.dest('dist/js')); // отправляем полученный файл на продакшен
По итогу, на продакшене будут чистые минифицированные файлы без лишнего кода и комментариев.
Для оптимизации запросов на сервер можно объединить иконки и небольшие картинки, которые используются в css,…
В общем, перепробовал десятки разных плагинов и настроек, но .jpg совсем не оптимизировались, а иногда…
Бесконечный анимированный фон с эффектом параллакса создан на основе варианта 2 из этой статьи HTML…
Изменение внешнего вида для чекбокса с помощью CSS HTML для чекбокса или радио-кнопок CSS для…