Введение в карты исходного кода JavaScript

Вам когда-нибудь хотелось, чтобы ваш клиентский код оставался читабельным и, что более важно, отлаживаемым даже после того, как вы его объединили и минимизировали, не влияя при этом на производительность? Что ж, теперь вы можете это сделать с помощью магии исходных карт .

Карты исходного кода — это способ вернуть объединенный/минимизированный файл в несобранное состояние. Когда вы создаете продукт для производства, а также минимизируете и объединяете файлы JavaScript, вы создаете карту исходного кода, содержащую информацию об исходных файлах. Когда вы запрашиваете определенный номер строки и столбца в сгенерированном JavaScript, вы можете выполнить поиск на исходной карте, который возвращает исходное местоположение. Инструменты разработчика (в настоящее время ночные сборки WebKit, Google Chrome или Firefox 23+) могут автоматически анализировать исходную карту и создавать впечатление, будто вы используете неминифицированные и необъединенные файлы.

Демо позволяет щелкнуть правой кнопкой мыши в любом месте текстовой области, содержащей сгенерированный источник. Выберите «Получить исходное местоположение», чтобы запросить исходную карту, передав сгенерированный номер строки и столбца, и вернуть позицию в исходном коде. Убедитесь, что ваша консоль открыта, чтобы вы могли видеть вывод.

Пример библиотеки исходных карт Mozilla JavaScript в действии.

Реальный мир

Прежде чем просмотреть следующую реальную реализацию исходных карт, убедитесь, что вы включили функцию исходных карт в Chrome Canary или WebKit каждую ночь, щелкая шестеренку настроек на панели инструментов разработчика и проверяя опцию «Включить исходные карты».

Как включить исходные карты в инструментах разработки WebKit.

В Firefox 23+ карты исходных кодов включены по умолчанию во встроенных инструментах разработки.

Как включить исходные карты в инструментах разработки Firefox.

Почему меня должны волновать исходные карты?

В настоящее время сопоставление исходного кода работает только между несжатым/комбинированным JavaScript и сжатым/нескомбинированным JavaScript, но будущее выглядит светлым благодаря разговорам о языках, компилируемых в JavaScript, таких как CoffeeScript, и даже о возможности добавления поддержки препроцессоров CSS, таких как SASS или LESS. .

В будущем мы сможем легко использовать практически любой язык, как если бы он изначально поддерживался в браузере с исходными картами:

  • Кофескрипт
  • ECMAScript 6 и выше
  • SASS/LESS и другие
  • Практически любой язык, который компилируется в JavaScript.

Взгляните на этот скриншот отлаживаемого CoffeeScript в экспериментальной сборке консоли Firefox:

В Google Web Toolkit (GWT) недавно добавлена ​​поддержка Source Maps . Рэй Кромвель из команды GWT сделал потрясающий скринкаст, демонстрирующий поддержку исходных карт в действии.

Другой пример, который я собрал, использует библиотеку Google Traceur , которая позволяет вам писать ES6 (ECMAScript 6 или Next) и компилировать его в код, совместимый с ES3. Компилятор Traceur также создает карту исходного кода. Взгляните на эту демонстрацию особенностей и классов ES6, которые используются так, как будто они изначально поддерживаются в браузере, благодаря карте исходного кода.

Текстовая область в демо-версии также позволяет вам написать ES6, который будет скомпилирован на лету и сгенерировать исходную карту, а также эквивалентный код ES3.

Отладка Traceur ES6 с использованием карт исходного кода.

Демо: написание ES6, его отладка, просмотр сопоставления исходного кода в действии

Как работает исходная карта?

Единственный компилятор/минификатор JavaScript, который на данный момент поддерживает генерацию исходных карт, — это компилятор Closure. (Я объясню, как его использовать позже.) После того, как вы объединили и минимизировали свой JavaScript, рядом с ним появится файл исходной карты.

В настоящее время компилятор Closure не добавляет в конце специальный комментарий, который необходим для того, чтобы указать инструментам разработки браузера, что исходная карта доступна:

//# sourceMappingURL=/path/to/file.js.map

Это позволяет инструментам разработчика сопоставлять вызовы с их местоположением в исходных файлах. Раньше прагмой комментария была //@ , но из-за некоторых проблем с ней и комментариями условной компиляции IE было принято решение изменить ее на //# . В настоящее время Chrome Canary, WebKit Nightly и Firefox 24+ поддерживают новую прагму комментариев. Это изменение синтаксиса также влияет на sourceURL.

Если вам не нравится идея странного комментария, вы можете установить специальный заголовок в скомпилированном файле JavaScript:

X-SourceMap: /path/to/file.js.map

Как и комментарий, он сообщит вашему потребителю исходной карты, где искать исходную карту, связанную с файлом JavaScript. Этот заголовок также решает проблему ссылок на исходные карты на языках, которые не поддерживают однострочные комментарии.

Пример WebKit Devtools с включенными и выключенными исходными картами.

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

Как создать исходную карту?

Вам понадобится использовать компилятор Closure для минимизации, объединения и создания исходной карты для ваших файлов 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.

Анатомия исходной карты

Чтобы лучше понять исходную карту, мы возьмем небольшой пример файла исходной карты, который будет сгенерирован компилятором Closure, и углубимся в более подробную информацию о том, как работает раздел «отображения». Следующий пример представляет собой небольшое отличие от примера спецификации V3 .

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Выше вы можете видеть, что исходная карта — это объектный литерал, содержащий много интересной информации:

  • Номер версии, на которой основана исходная карта
  • Имя файла сгенерированного кода (ваш мини-/комбинированный производственный файл)
  • sourceRoot позволяет добавлять к источникам структуру папок — это также метод экономии места.
  • источники содержат все имена файлов, которые были объединены
  • имена содержит все имена переменных/методов, которые встречаются в вашем коде.
  • Наконец, свойство сопоставлений — это то место, где происходит волшебство с использованием значений Base64 VLQ. Здесь происходит реальная экономия места.

Base64 VLQ и сохранение небольшого размера исходной карты

Первоначально спецификация исходной карты содержала очень подробный вывод всех сопоставлений, в результате чего исходная карта была примерно в 10 раз больше размера сгенерированного кода. Вторая версия уменьшила это значение примерно на 50%, а третья версия снова уменьшила его еще на 50%, поэтому для файла размером 133 КБ вы получаете исходную карту размером ~300 КБ.

Так как же им удалось уменьшить размер, сохранив при этом сложные отображения?

VLQ (количество переменной длины) используется вместе с кодированием значения в значение Base64. Свойство сопоставлений представляет собой очень большую строку. Внутри этой строки находятся точки с запятой (;), обозначающие номер строки в сгенерированном файле. Внутри каждой строки есть запятые (,), обозначающие каждый сегмент этой строки. Каждый из этих сегментов имеет значение 1, 4 или 5 в полях переменной длины. Некоторые могут выглядеть длиннее, но они содержат биты продолжения. Каждый сегмент основывается на предыдущем, что помогает уменьшить размер файла, поскольку каждый бит относится к предыдущим сегментам.

Разбивка сегмента в JSON-файле исходной карты.

Как упоминалось выше, каждый сегмент может иметь 1, 4 или 5 переменной длины. Эта диаграмма считается переменной длиной, равной четырем, с одним битом продолжения (g). Мы разобьем этот сегмент и покажем вам, как исходная карта определяет исходное местоположение.

Значения, показанные выше, являются чисто декодированными значениями Base64, для получения их истинных значений требуется дополнительная обработка. Каждый сегмент обычно решает пять задач:

  • Созданный столбец
  • Исходный файл, в котором это появилось
  • Исходный номер строки
  • Исходный столбец
  • И, если есть, оригинальное имя

Не каждый сегмент имеет имя, имя метода или аргумент, поэтому сегменты повсюду будут переключаться между четырьмя и пятью переменной длиной. Значение g на диаграмме сегментов выше — это так называемый бит продолжения, который позволяет провести дальнейшую оптимизацию на этапе декодирования Base64 VLQ. Бит продолжения позволяет вам использовать значение сегмента, чтобы вы могли хранить большие числа без необходимости хранить большое число - очень умный метод экономии места, берущий свое начало в формате MIDI.

Приведенная выше диаграмма AAgBC после дальнейшей обработки вернет 0, 0, 32, 16, 1 - 32 является битом продолжения, который помогает построить следующее значение 16. B, чисто декодированный в Base64, равен 1. Таким образом, используются важные значения: 0, 0, 16, 1. Это дает нам знать, что строка 1 (строки подсчитываются точками с запятой) столбец 0 сгенерированного файла сопоставляется с файлом 0 (массив файлов 0 — это foo.js), строка 16 в столбец 1.

Чтобы показать, как декодируются сегменты, я буду использовать JavaScript-библиотеку Source Map от Mozilla. Вы также можете посмотреть исходный код сопоставления инструментов разработки WebKit, также написанный на JavaScript.

Чтобы правильно понять, как мы получаем значение 16 из B, нам необходимо иметь базовое представление о побитовых операторах и о том, как спецификация работает для сопоставления источников. Предыдущая цифра g помечается как бит продолжения путем сравнения цифры (32) и VLQ_CONTINUATION_BIT (двоичное 100000 или 32) с помощью побитового оператора AND (&).

32 & 32 = 32
// or
100000
|
|
V
100000

Это возвращает 1 в каждой битовой позиции, где она присутствует в обоих случаях. Таким образом, декодированное значение Base64 33 & 32 вернет 32, поскольку они имеют только 32-битное расположение, как вы можете видеть на диаграмме выше. Затем это увеличивает значение битового сдвига на 5 для каждого предыдущего бита продолжения. В приведенном выше случае он сдвигается на 5 только один раз, поэтому сдвигается влево 1 (B) на 5.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

Затем это значение преобразуется из значения со знаком VLQ путем смещения числа (32) вправо на одну позицию.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

Итак, вот оно: вот как вы превращаете 1 в 16. Этот процесс может показаться слишком сложным, но как только числа начнут расти, это обретет больше смысла.

Потенциальные проблемы XSSI

В спецификации упоминаются проблемы включения межсайтовых скриптов, которые могут возникнуть в результате использования исходной карты. Чтобы смягчить это, рекомендуется добавить к первой строке исходной карты символ " )]} ", чтобы намеренно сделать JavaScript недействительным и вызвать синтаксическую ошибку. Инструменты разработки WebKit уже могут справиться с этим.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

Как показано выше, первые три символа разрезаются, чтобы проверить, соответствуют ли они синтаксической ошибке в спецификации, и если да, то удаляются все символы, ведущие к первому объекту новой строки (\n).

sourceURL и displayName в действии: Eval и анонимные функции

Хотя следующие два соглашения не являются частью спецификации карты исходного кода, они позволяют значительно упростить разработку при работе с оценками и анонимными функциями.

Первый помощник очень похож на свойство //# sourceMappingURL и фактически упоминается в спецификации исходной карты V3. Включив в свой код следующий специальный комментарий, который будет оцениваться, вы можете называть evals так, чтобы они отображались как более логичные имена в ваших инструментах разработки. Посмотрите простую демонстрацию с использованием компилятора CoffeeScript:

Демо: см. код eval() , показанный в виде сценария через sourceURL.

//# sourceURL=sqrt.coffee
Как выглядит специальный комментарий sourceURL в инструментах разработки

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

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
Показ свойства displayName в действии.

При профилировании вашего кода в инструментах разработки будет отображаться свойство displayName , а не что-то вроде (anonymous) . Однако displayName практически мертв и не появится в Chrome. Но надежда еще не потеряна, и было предложено гораздо лучшее предложение под названием debugName .

На момент написания статьи имя eval доступно только в браузерах Firefox и WebKit. Свойство displayName присутствует только в ночных версиях WebKit.

Давайте сплотимся вместе

В настоящее время идет очень продолжительное обсуждение добавления поддержки исходных карт в CoffeeScript. Изучите проблему и поддержите добавление генерации исходных карт в компилятор CoffeeScript. Это будет огромной победой для CoffeeScript и его преданных последователей.

В UglifyJS также есть проблема с исходной картой, на которую вам тоже следует обратить внимание.

Множество инструментов создают исходные карты, включая компилятор Coffeescript. Я считаю это сейчас спорным вопросом.

Чем больше нам доступно инструментов, способных генерировать исходные карты, тем лучше для нас будет, так что идите и спросите или добавьте поддержку исходных карт в свой любимый проект с открытым исходным кодом.

Это не идеально

Одна вещь, которую исходные карты сейчас не поддерживают, — это контрольные выражения. Проблема в том, что попытка проверить имя аргумента или переменной в текущем контексте выполнения ничего не вернет, поскольку на самом деле его не существует. Для этого потребуется какое-то обратное сопоставление для поиска настоящего имени аргумента/переменной, которую вы хотите проверить, по сравнению с фактическим именем аргумента/переменной в скомпилированном JavaScript.

Это, конечно, решаемая проблема, и, уделив больше внимания исходным картам, мы сможем увидеть некоторые удивительные функции и лучшую стабильность.

Проблемы

Недавно в jQuery 1.9 добавлена ​​поддержка исходных карт при обслуживании через официальные CDN. Он также указал на своеобразную ошибку , когда комментарии условной компиляции IE (//@cc_on) используются перед загрузкой jQuery. С тех пор было принято решение смягчить эту ситуацию, обернув sourceMappingURL в многострочный комментарий. Урок, который необходимо усвоить: не используйте условные комментарии.

С тех пор эта проблема была решена путем изменения синтаксиса на //# .

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

Вот еще несколько ресурсов и инструментов, которые вам стоит попробовать:

Исходные карты — очень мощная утилита в наборе инструментов разработчика. Очень полезно иметь возможность сделать ваше веб-приложение простым, но при этом легко отлаживаемым. Это также очень мощный инструмент обучения для новых разработчиков, позволяющий увидеть, как опытные разработчики структурируют и пишут свои приложения, без необходимости разбираться в нечитаемом минимизированном коде.

Чего же ты ждешь? Начните создавать исходные карты для всех проектов прямо сейчас!