Вы когда-нибудь думали, как было бы здорово, если бы слитый в один файл и минифицированный яваскрипт код в 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-сайтом как с тестовым.
Пример — посмотрите в консоль на fontdragr.com
Зачем вообще нужны Source Maps?
Сейчас маппинг работает только между исходными файлами и сжатой/объединённой версией, но ведутся разговоры о том, чтобы сделать маппинг для языков, компилируемых в JavaScript (например, CoffeeScript), и даже о поддержке CSS-препроцессоров, таких как SASS и LESS.
В будущем мы могли бы легко использовать почти любой язык, как если бы он поддерживался браузером нативно:
- CoffeeScript
- ECMAScript 6 и выше
- SASS/LESS и т.п.
- Практически любой язык, который компилируется в JavaScript
Посмотрите скринкаст, в котором CoffeeScript отлаживается в экспериментальной сборке консоли Firefox:
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:
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
Можно заметить, что это обычный литерал объекта, содержащий всю нужную информацию:
- Версию маппера
- Название минифицированного/объединённого файла для 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) === ")]}") {
response = response.substring(response.indexOf('n'));
}
Как видно, первые три символа обрезаются и производится проверка их на соответствие указанному в спецификации невалидному коду и в этом случае вырезается всё до следующего символа перевода строки.
@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 для своего проекта!
Автор: bullgare