Введение в Javascript Source Maps

в 8:15, , рубрики: Closure Compiler, html5, javascript, minification, sourceMappingURL, переводы

Вы когда-нибудь думали, как было бы здорово, если бы слитый в один файл и минифицированный яваскрипт код в production-окружении можено было удобно читать и даже отлаживать без ущерба производительности? Теперь это возможно, если использовать штуку под названием source maps.

Если коротко, то это способ связать минифицированный/объединённый файл с файлами, из которых он получился. Во время сборки для боевого окружения помимо минификации и объединения файлов также генерируется файл-маппер, который содержит информацию об исходных файлах. Когда производится обращение к конкретному месту в минифицированном файле, то производится поиск в маппере, по которому вычисляется строка и символ в исходном файле. Developer Tools (WebKit nightly builds или Google Chrome Canary) умеет парсить этот файл автоматически и прозрачно подменять файлы, как будто ведётся работа с исходными файлами. На момент написания (оригинальной статьи — прим. перев.) Firefox заблокировал развитие поддержки Source Map. Подробнее — на MozillaWiki Source Map.

Пример — правильное определение места в исходном коде

В этом примере можно ткнуть в любом месте textarea правой кнопкой и выбрать пункт «Get original location». При этом будет произведено обращение к файлу-мапперу с передачей строки и номера символа в минифицированном коде, и будет показан соответствующий кусок кода из исходного файла. В консоль будут выведены номер строки и номер символа в исходном файле и другая интересная информация.

image

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

Прежде чем смотреть следующий пример, нужно активировать просмотр source maps в Chrome Canary или WebKit nightly, для этого в свойствах активировать пункт «Enable source maps» (см. скриншот)
image

Продолжим. Предыдущий пример был интересным, но как это можно использовать? Зайдите на 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 и также будет создаваться файл-маппер.
image
Пример — можно написать код на 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

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

Как сгенерировать файл-маппер?

Как уже говорилось выше, нужен будет 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-полей. Некоторые могут быть длиннее за счёт бита продолжения. Каждый сегмент строится на основе предыдущего, что помогает уменьшить размер файла.
image
Как говорилось раньше, каждый сегмент может быть 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 код со сгенерированным именем
image

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

Пример — названия для анонимных функций через displayName (только WebKit NIghtly)
image
При профилировании будут показываться красивые названия вместо (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 могут появиться ещё более интересные его применения.

Инструменты и ресурсы

Source Map — мощный инструмент для разработчика. Он позволяет держать production-код максимально сжатым, но при этом позволяет его отлаживать. Так же полезен для начинающих разработчиков, чтобы посмотреть код, написанный опытными разработчиками, чтобы поучиться правильному структурированию и написанию своего кода без необходимости продираться сквозь минифицированный код. Так чего же вы ждёте? Сгенерируйте Source Map для своего проекта!

Автор: bullgare


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


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