Wprowadzenie do map źródeł JavaScript

Czy kiedykolwiek chciałeś, aby kod po stronie klienta był czytelny i co ważniejsze – łatwy do debugowania nawet po połączeniu i zminifikowaniu bez wpływu na wydajność? Teraz możesz to zrobić dzięki magii map źródeł.

Mapy źródeł umożliwiają odwzorowanie połączonego lub zminifikowanego pliku w stanie nieskompilowanym. Podczas kompilacji w wersji produkcyjnej, oprócz minifikacji i połączenia plików JavaScript, generujesz mapę źródeł, która zawiera informacje o oryginalnych plikach. Gdy w wygenerowanym kodzie JavaScriptu zapytasz o określony wiersz i kolumnę, możesz wyszukać na mapie źródłowej dane, które zwracają pierwotną lokalizację. Narzędzia dla deweloperów (obecnie kompilacje WebKit nightly, Google Chrome lub Firefox 23+) mogą automatycznie analizować mapę źródłową i umieszczać ją tak, jakby pliki nie były zwinięte i połączone.

W wersji demonstracyjnej możesz kliknąć prawym przyciskiem myszy dowolne miejsce w polu tekstowym zawierającym wygenerowany kod źródłowy. Wybranie opcji „Pobierz pierwotną lokalizację” spowoduje wysłanie zapytania do mapy źródłowej z przekazaniem wygenerowanego wiersza i numeru kolumny oraz zwróci pozycję w pierwotnym kodzie. Upewnij się, że konsola jest otwarta, aby można było zobaczyć wynik.

Przykład biblioteki map źródeł JavaScriptu od Mozilli w działaniu

Rzeczywistość

Zanim obejrzysz implementację map źródeł w praktyce, upewnij się, że masz włączoną funkcję map źródeł w Chrome Canary lub WebKit nightly. Aby to zrobić, kliknij koło zębate ustawień w panelu narzędzi dla deweloperów i zaznacz opcję „Włącz mapy źródeł”.

Jak włączyć mapy źródeł w narzędziach dla programistów WebKit.

W Firefoxie 23 i nowszych mapy źródeł są domyślnie włączone w wbudowanych narzędziach dla deweloperów.

Jak włączyć mapy źródeł w narzędziach dla programistów w Firefoxie

Dlaczego warto korzystać z map źródeł?

Obecnie mapowanie źródeł działa tylko w przypadku nieskompresowanego/połączonego kodu JavaScript na skompresowany/niepołączony kod JavaScript, ale przyszłość zapowiada się świetlicie, ponieważ mówi się o kompilowaniu do JavaScripta takich języków jak CoffeeScript, a nawet o możliwości dodania obsługi preprocesorów CSS, takich jak SASS czy LESS.

W przyszłości będziemy mogli łatwo używać niemal dowolnego języka, tak jakby był on obsługiwany natywnie w przeglądarce z mapami źródłowymi:

  • CoffeeScript
  • ECMAScript 6 lub nowsza wersja
  • SASS/LESS i inne
  • Praktycznie każdy język, który kompiluje do JavaScript

Obejrzyj ten screencast, w którym pokazujemy debugowanie CoffeeScript w wersji eksperymentalnej konsoli Firefoxa:

Google Web Toolkit (GWT) obsługuje teraz mapy źródeł. Ray Cromwell z zespołu GWT przygotował świetny film, który pokazuje działanie obsługi map źródłowych.

Inny przykład, który przygotowałem, korzysta z biblioteki Google Traceur, która umożliwia pisanie kodu ES6 (ECMAScript 6 lub Next) i skompilowanie go na kod zgodny z ES3. Kompilator Traceur generuje też mapę źródłową. Dzięki mapie źródła możesz zobaczyć demonstrację atrybutów i klas ES6, które są używane tak, jakby były natywnie obsługiwane w przeglądarce.

Pole tekstowe w wersji demonstracyjnej umożliwia też pisanie kodu ES6, który zostanie skompilowany na bieżąco i wygeneruje mapę źródłową oraz odpowiedni kod ES3.

debugowanie Traceur ES6 za pomocą map źródeł.

Przykład: pisanie kodu ES6, debugowanie i mapowanie źródeł w akcji

Jak działa mapa źródłowa?

Jedynym kompilatorem/minifikatorem JavaScript, który obecnie obsługuje generowanie mapy źródłowej, jest kompilator Closure. (wyjaśnię później, jak z niego korzystać). Po połączeniu i zminifikowaniu kodu JavaScript obok niego pojawi się plik mapy źródeł.

Obecnie kompilator Closure nie dodaje specjalnego komentarza na końcu, który jest wymagany, aby poinformować narzędzia deweloperskie przeglądarki o dostępności mapy źródłowej:

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

Dzięki temu narzędzia dla deweloperów mogą mapować wywołania do ich lokalizacji w pierwotnych plikach źródłowych. Wcześniej pragma komentarza była ustawiona na //@, ale ze względu na pewne problemy z tym i z komentarzami warunkowymi w IE podjęliśmy decyzję o zmianie na //#. Obecnie nowy komentarz pragmatyczny jest obsługiwany przez Chrome Canary, WebKit Nightly i Firefox 24+. Ta zmiana składni dotyczy też parametru sourceURL.

Jeśli nie podoba Ci się pomysł dziwnego komentarza, możesz ustawić specjalny nagłówek w skompilowanym pliku JavaScript:

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

Podobnie jak komentarz, poinformuje ono użytkownika mapy źródłowej, gdzie znaleźć mapę źródłową powiązaną z plikiem JavaScript. Ten nagłówek rozwiązuje też problem odwoływania się do map źródłowych w językach, które nie obsługują komentarzy jednowierszowych.

Przykład map źródeł włączonych i wyłączonych w Narzędziach deweloperskich WebKit

Plik mapy źródłowej zostanie pobrany tylko wtedy, gdy masz włączone mapy źródłowe i otwarte narzędzia dla programistów. Musisz też przesłać oryginalne pliki, aby narzędzia dla deweloperów mogły się do nich odwoływać i wyświetlać je w razie potrzeby.

Jak wygenerować mapę źródłową?

Aby zminifikować, zconcatować i wygenerować mapę źródłową plików JavaScript, musisz użyć kompilatora Closure. Polecenie wygląda tak:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

Dwie ważne flagi poleceń to --create_source_map--source_map_format. Jest to konieczne, ponieważ domyślną wersją jest wersja 2, a my chcemy pracować tylko z wersją 3.

Budowa mapy źródeł

Aby lepiej zrozumieć mapę źródłową, omówimy krótki przykład pliku mapy źródłowej wygenerowanego przez kompilator Closure i szczegółowo wyjaśnimy, jak działa sekcja „mapowania”. Ten przykład różni się nieznacznie od przykładu specyfikacji V3.

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

Jak widać wyżej, mapa źródeł to literał obiektu zawierający wiele przydatnych informacji:

  • Numer wersji, na której oparta jest mapa źródłowa
  • Nazwa pliku wygenerowanego kodu (plik minifed lub połączony plik produkcyjny)
  • sourceRoot umożliwia dodanie struktury folderów do źródeł. Jest to też metoda na oszczędzanie miejsca.
  • sources zawiera wszystkie nazwy plików, które zostały połączone.
  • names zawiera wszystkie nazwy zmiennych i metod, które występują w Twoim kodzie.
  • Właściwość mapowania to miejsce, w którym zachodzi magia, wykorzystująca wartości VLQ w formacie Base64. To właśnie pozwala zaoszczędzić miejsce.

Base64 VLQ i utrzymywanie niewielkich rozmiarów mapy źródłowej

Początkowo specyfikacja mapy źródeł zawierała bardzo szczegółowe dane wyjściowe wszystkich mapowań, przez co rozmiar mapy źródeł był około 10 razy większy od rozmiaru wygenerowanego kodu. W wersji 2. zmniejszyliśmy ją o około 50%, a w wersji 3. jeszcze o 50%, więc w przypadku pliku o rozmiarze 133 KB otrzymujemy mapę źródłową o rozmiarze około 300 KB.

Jak udało im się zmniejszyć rozmiar, zachowując złożone mapowania?

VLQ (wartość o zmiennej długości) jest używana razem z kodowaniem wartości w formacie Base64. Właściwość mapowań to bardzo długi ciąg znaków. W tym ciągu występują średniki (;), które oznaczają numer wiersza w wygenerowanym pliku. W każdym wierszu występują przecinki (,), które oznaczają poszczególne segmenty w tym wierszu. Każdy z tych segmentów ma 1, 4 lub 5 składników w polach o zmiennej długości. Niektóre mogą wydawać się dłuższe, ale zawierają one fragmenty kontynuacji. Każdy segment opiera się na poprzednim, co pomaga zmniejszyć rozmiar pliku, ponieważ każdy bit jest powiązany z poprzednimi segmentami.

Podział segmentu w pliku JSON mapy źródłowej.

Jak wspomnieliśmy powyżej, każdy segment może mieć zmienną długość: 1, 4 lub 5. Ten diagram jest uważany za zmienną długość 4 z jednym bitem kontynuacji (g). Rozłożymy ten segment na czynniki pierwsze i pokażemy, jak mapa źródłowa określa pierwotną lokalizację.

Wyżej podane wartości to wartości po dekodowaniu w formacie Base64. Aby uzyskać ich rzeczywiste wartości, trzeba przeprowadzić jeszcze kilka operacji. Każdy segment zwykle obejmuje 5 elementów:

  • Kolumna wygenerowana
  • Pierwotny plik, w którym pojawił się błąd
  • Oryginalny numer wiersza
  • Kolumna źródłowa
  • i, jeśli jest dostępna, pierwotna nazwa.

Nie każdy segment ma nazwę, nazwę metody lub argument, więc segmenty w całości będą się zmieniać między 4 a 5 znakami zmiennej długości. Wartość g na diagramie segmentu powyżej to tzw. bit kontynuacji, który umożliwia dalszą optymalizację na etapie dekodowania VLQ Base64. Bit kontynuacji umożliwia tworzenie wartości segmentu, dzięki czemu można przechowywać duże liczby bez konieczności przechowywania dużej liczby. Jest to bardzo sprytna technika oszczędzania miejsca, która wywodzi się z formatu MIDI.

Powyższy diagram AAgBC po dalszej obróbce zwróci 0, 0, 32, 16, 1 – 32 to bit kontynuacji, który pomaga utworzyć następną wartość 16. B dekodowane w formacie Base64 to 1. Ważne wartości to 0, 0, 16, 1. Dzięki temu wiemy, że wiersz 1 (wiersze są zliczane za pomocą dwukropka) w kolumnie 0 wygenerowanego pliku jest mapowany na plik 0 (tablica plików 0 to foo.js), a wiersz 16 na kolumnę 1.

Aby pokazać, jak segmenty są dekodowane, posłużę się biblioteką JavaScriptu Map źródłowych Mozilli. Możesz też sprawdzić kod mapowania źródeł w narzędziach deweloperskich WebKit, który również jest napisany w JavaScriptzie.

Aby właściwie zrozumieć, jak z B otrzymujemy wartość 16, musimy mieć podstawową wiedzę o operatorach bitowych i o tym, jak działa specyfikacja mapowania źródeł. Poprzednia cyfra g jest oznaczana jako bit kontynuacji przez porównanie tej cyfry (32) z VLQ_CONTINUATION_BIT (w postaci binarnej 100000 lub 32) za pomocą operatora AND bitowego (&).

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

Zwraca wartość 1 w każdej pozycji bitowej, w której występują oba elementy. Dlatego wartość dekodowana z formatu Base64 33 & 32 zwróci 32, ponieważ mają tylko 32-bitową lokalizację, jak widać na diagramie powyżej. W efekcie zwiększa to wartość przesunięcia o 5 bitów w przypadku każdego poprzedzającego bitu kontynuacji. W tym przypadku przesunięcie nastąpiło tylko raz o 5, czyli przesunięcie w lewo o 5 jednostek wartości zmiennej 1 (B).

1 <<../ 5 // 32

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

Ta wartość jest następnie konwertowana z wartości VLQ ze znakiem przez przesunięcie liczby o 1 miejsce w prawo (32).

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

Oto jak zamienić 1 na 16. Może się to wydawać skomplikowane, ale gdy liczby zaczną rosnąć, wszystko stanie się jasne.

Potencjalne problemy z SSI

Specyfikacja wspomina o problemach z uwzględnianiem skryptów w witrynie, które mogą wystąpić w wyniku korzystania z mapy źródłowej. Aby temu zapobiec, zalecamy dodanie przed pierwszym wierszem mapy źródeł znaku „)]}”, aby celowo unieważnić kod JavaScript, co spowoduje wystąpienie błędu składni. Narzędzie dla programistów WebKit już to potrafi.

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

Jak widać powyżej, pierwsze 3 znaki są dzielone, aby sprawdzić, czy pasują do błędu składni w specyfikacji. Jeśli tak, usuwa wszystkie znaki prowadzące do pierwszego elementu nowego wiersza (\n).

Funkcje sourceURLdisplayName w działaniu: funkcja EVAL i funkcje anonimowe

Chociaż te 2 konwencje nie są częścią specyfikacji mapy źródłowej, znacznie ułatwiają programowanie podczas pracy z funkcjami eval i funkcjami anonimowymi.

Pierwszy pomocnik wygląda bardzo podobnie do właściwości //# sourceMappingURL i jest wymieniony w specyfikacji mapy źródeł w wersji 3. W kodzie możesz umieścić specjalny komentarz, który zostanie oceniony, dzięki czemu możesz nadać nazwy ocenie, aby wyglądała bardziej logicznie w narzędziach deweloperskich. Obejrzyj proste demo z wykorzystaniem kompilatora CoffeeScript:

Przykład: wyświetlanie kodu eval() w kształcie skryptu za pomocą adresu sourceURL

//# sourceURL=sqrt.coffee
Jak wygląda komentarz specjalny sourceURL w narzędziach dla programistów

Drugi pomocnik umożliwia nazywanie funkcji anonimowych za pomocą właściwości displayName dostępnej w bieżącym kontekście funkcji anonimowej. Aby zobaczyć działanie właściwości displayName, użyj tej wersji demonstracyjnej.

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);
Właściwość displayName w działaniu.

Podczas profilowania kodu w narzędziach dla programistów będzie widoczna usługa displayName, a nie (anonymous). Jednak wyświetlana nazwa jest praktycznie martwa i nie zostanie dodana do Chrome. Nie wszystko stracone. Zaproponowano znacznie lepszą nazwę debugName.

W momencie pisania tego artykułu nazwy eval są dostępne tylko w przeglądarkach Firefox i WebKit. Właściwość displayName jest dostępna tylko w wersjach WebKit nightlies.

Zjednoczmy siły

Obecnie trwa bardzo długa dyskusja na temat obsługi map źródeł w CoffeeScript. Sprawdź problem i podaj swój głos, aby umożliwić generowanie mapy źródeł w kompilatorze CoffeeScript. To będzie ogromny sukces dla CoffeeScript i jego wiernych fanów.

W UglifyJS występuje też problem z mapą źródeł, który warto sprawdzić.

Wiele narzędzi generuje mapy źródeł, w tym kompilator coffeescript. Uważam, że to już nie ma znaczenia.

Im więcej dostępnych narzędzi do generowania map źródłowych, tym lepiej. Dlatego poproś o dodanie obsługi map źródłowych do ulubionego projektu open source lub dodaj ją samodzielnie.

Nie jest to idealne rozwiązanie

Mapy źródeł nie obsługują obecnie wyrażeń w przypadku zegarków. Problem polega na tym, że próba sprawdzenia nazwy argumentu lub zmiennej w bieżącym kontekście wykonania nie zwróci nic, ponieważ nie istnieje ona w rzeczywistości. Wymaga to odwrotnego mapowania, aby znaleźć prawdziwą nazwę argumentu lub zmiennej, którą chcesz sprawdzić, w porównaniu z rzeczywistą nazwą argumentu lub zmiennej w skompilowanym kodzie JavaScript.

To oczywiście problem, który da się rozwiązać. Dzięki większej uwadze poświęcanej mapom źródłowym możemy zacząć wprowadzać do nich nowe funkcje i poprawiać ich stabilność.

Problemy

Niedawno w wersji jQuery 1.9 dodano obsługę map źródeł, gdy są one dostarczane przez oficjalne sieci CDN. Wykazał też dziwny błąd, który występuje, gdy komentarze do kompilacji warunkowej IE (//@cc_on) są używane przed załadowaniem jQuery. Od tego czasu wprowadzono commit, która łagodzi ten problem przez umieszczenie adresu URL źródłowego mapowania w komentarzu wielowierszowym. Wniosek: nie używaj komentarzy warunkowych.

Od tego czasu problem został rozwiązany dzięki zmianie składni na //#.

Narzędzia i zasoby

Oto dodatkowe materiały i narzędzia, które warto sprawdzić:

Mapy źródłowe to bardzo przydatne narzędzie w zestawie narzędzi dla programistów. Bardzo przydatne jest utrzymywanie aplikacji internetowej w szczupłej formie, ale z możliwością łatwego debugowania. To też bardzo przydatne narzędzie edukacyjne dla początkujących programistów, którzy mogą zobaczyć, jak doświadczeni deweloperzy tworzą i strukturyzują swoje aplikacje, bez konieczności przebijania się przez nieczytelny skompresowany kod.

Na co czekasz? Zacznij generować mapy źródłowe dla wszystkich projektów już teraz.