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

Original author: Ryan Seddon
  • Translation
Вы когда-нибудь думали, как было бы здорово, если бы слитый в один файл и минифицированный яваскрипт код в 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 для своего проекта!

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    +1
    Да, было бы действительно классно видеть эту поддержку во всех браузерах.

    У себя в данный момент решаем эту задачу так — в куках/локалсторейдже держим флаг, DeveloperModeOn. На определенные страницы делаем файлики лоадеров, в которых yepnope-ом, в зависимости от флага, грузим или продакшен- (скомбайненые/отминифаенные), или девелопер- файлы. Работает без сбоев.
      0
      немного не понял, т.е. в продакшн окружении для кук разработчиков грузятся отдельные файлы?
      да, удобнее, чем минифицированный файл:)
      но в этом случае тоже есть один неприятный баг — может получиться так, что возникает ошибка именно при сливании файлов в один (особенно в legacy code, где, к примеру, забыли закрывающую скобку или точку запятой в конце файла). и её ну очень трудно отследить в этом случае.
        0
        При минифайинге файлы линтуются, и js и css — так что тут все надежно. У нас порядка 200 файлов, пока проблем не было.
          0
          ну если не было — отлично.
          но в случае, когда не все файлы грузятся на каждой страничке, а минифицированный файл — один, то тут тоже могут быть проблемы.
          в общем, всё зависит от реализации, в любом случае source map сильно бы помог)
        0
        > в куках/локалсторейдже держим флаг
        Ой небезопасно. Лучше привязывать по юзеру, или по ид сессии. Или другой трудноугадываемый ид.
          0
          на предыдущем месте работы мы привязывались к ip
            0
            Зачем? Ну получит юзер вместо быстрых продакшенских файлов, поток медленных девелоперских — в чем небезопасность?
              0
              Тут зависит, чем они отличаются. Может, DeveloperModeOn включает логи или еще что-то. В общем случае — нежелательно.
                0
                Жесть. Я-то знаю ЧТО они включают, и описал выше…
          0
          а для grunt есть что-нибудь подобное?
            0
            Что вы имеете ввиду под поддержкой grunt? Все определяется плагинами, например uglify и browserify генерировать source maps умеют без проблем.
              0
              да, я имел ввиду конкретный плагин, видимо меня переклинило на мысле что uglify только минифицирует), благодарю за пояснения

          Only users with full accounts can post comments. Log in, please.