[Из песочницы] Source Maps: быстро и понятно

bu7xh3edrmihv6ard4tpx2ryhvk.jpeg

Механизм 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-файле, который может выглядеть, например, так:

{
    "version":3,
    "file":"index.js",
    "sourceRoot":"",
    "sources":["../src/index.ts"],
    "names":[],
    "mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,SAAS,SAAS;IACd,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;",
    "sourcesContent": []
}


Обычно, имя 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.

hu2gwyeavv5pu9gsxibohury_cs.png

Декодируем число 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 весьма полезна одноименная библиотека.

© Habrahabr.ru