Wprowadzenie do map źródeł JavaScript

Czy kiedykolwiek chcielibyście mieć możliwość zachowania czytelnego i co ważniejszego debugowania kodu po stronie klienta nawet po połączeniu i zminimalizacji go bez wpływu na wydajność? Teraz możesz dzięki magii map źródłowych.

Mapy źródłowe to sposób mapowania połączonego/zminifikowanego pliku z powrotem na stan nieukompilowany. Gdy tworzysz kod pod kątem środowiska produkcyjnego, a także zmniejszasz i łączysz pliki JavaScript, generujesz mapę źródeł, która zawiera informacje o oryginalnych plikach. Jeśli w wygenerowanym kodzie JavaScript wyślesz zapytanie o określony wiersz lub numer kolumny, możesz przeszukać mapę źródłową, która zwróci pierwotną lokalizację. Narzędzia dla programistów (obecnie kompilacje WebKit Nightly, Google Chrome oraz Firefox w wersji 23 lub nowszej) mogą automatycznie analizować mapę źródłową, przez co wygląda na to, że używasz niezminifikowanych i niepołączonych plików.

Tryb demonstracyjny umożliwia kliknięcie prawym przyciskiem myszy w dowolnym miejscu w obszarze tekstowym, w którym znajduje się wygenerowane źródło. Wybranie opcji „Pobierz pierwotną lokalizację” spowoduje przesłanie zapytania do mapy źródłowej przez przekazanie wygenerowanego wiersza i numeru kolumny oraz zwrócenie pozycji w oryginalnym kodzie. Upewnij się, że konsola jest otwarta, aby można było zobaczyć dane wyjściowe.

Przykład użycia biblioteki mapy źródłowej w języku Mozilla JavaScript.

Prawdziwy świat

Przed wyświetleniem poniższej rzeczywistej implementacji Map źródłowych włącz funkcję map źródłowych w Chrome Canary lub WebKit co noc. W tym celu kliknij ikonę koła zębatego w panelu narzędzi dla programistów i zaznacz opcję „Włącz mapy źródłowe”.

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

W przeglądarce Firefox w wersji 23 i nowszych mapy źródeł są domyślnie włączone we wbudowanych narzędziach dla programistów.

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

Dlaczego mapy źródeł powinny mi pomóc?

Obecnie mapowanie źródeł działa tylko z nieskompresowanym/nieskompresowanym JavaScriptem na skompresowany/nieskompresowany JavaScript, ale przyszłość jest jasna z rozmowami o językach skompilowanych na JavaScript, takich jak CoffeeScript, a nawet o możliwości dodania obsługi preprocesorów CSS, takich jak SASS lub LESS.

W przyszłości moglibyśmy z łatwością używać niemal każdego języka, tak jakby był on natywnie obsługiwany w przeglądarce z mapami źródeł:

  • CoffeeScript
  • ECMAScript 6 i nowsze
  • SASS/LESS i inne
  • Prawie każdy język kompilujący się do JavaScriptu

Spójrz na ten screencast z debugowanymi CoffeeScript w ramach eksperymentalnej kompilacji konsoli Firefox:

Do pakietu narzędzi Google Web Toolkit (GWT) dodaliśmy ostatnio obsługę map źródłowych. Ray Cromwell z zespołu GWT przygotował świetny screencast pokazujący, jak działa pomoc mapy źródeł.

Inny przykład, który udało mi się znaleźć, korzysta z biblioteki Google Traceur, która umożliwia napisanie kodu ES6 (ECMAScript 6 lub Next) i skompilowanie go do kodu zgodnego z ES3. Kompilator Traceur generuje też mapę źródeł. Skorzystaj z tej wersji demonstracyjnej cech i klas ES6 używanych tak, jakby były obsługiwane bezpośrednio w przeglądarce dzięki mapie źródeł.

Obszar tekstowy w wersji demonstracyjnej umożliwia też napisanie kodu ES6, który zostanie skompilowany na bieżąco i wygeneruje mapę źródła z odpowiednim kodem w wersji ES3.

Debugowanie narzędzia Traceur ES6 za pomocą map źródłowych.

Demonstracja: pisanie kodu ES6, debugowanie, wyświetlanie mapowania źródła w praktyce

Jak działa mapa źródeł?

Jedynym kompilatorem i minifikatorem JavaScriptu, który obsługuje generowanie mapy źródeł, jest obecnie kompilator Closure. (objaśnię ją później). Gdy połączysz i zminifikujesz kod JavaScript, obok niego pojawi się plik mapy źródła.

Obecnie kompilator Closure nie dodaje na końcu specjalnego komentarza, który jest wymagany, aby poinformować narzędzia dla programistów przeglądarek, że mapa źródłowa jest dostępna:

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

Dzięki temu narzędzia dla programistów mogą mapować wywołania z powrotem na ich lokalizację w oryginalnych plikach źródłowych. Wcześniej pragma komentarza wynosiła //@, ale ze względu na pewne problemy i komentarze kompilacji warunkowej IE podjęliśmy decyzję o jej zmianie na //#. Obecnie Chrome Canary, WebKit Nightly oraz Firefox w wersji 24 lub nowszej obsługują nowe pragmy dotyczące komentarzy. Ta zmiana składni ma też wpływ na adres sourceURL.

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

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

Polub ten komentarz, aby konsument mapy źródłowej wiedział, gdzie znaleźć mapę źródłową powiązaną z plikiem JavaScript. Ten nagłówek rozwiązuje również problem odniesień do map źródłowych w językach, w których komentarze jednowierszowe nie są obsługiwane.

Przykład korzystania z map źródłowych i map źródłowych w narzędziach WebKit Devtools

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 także przesłać oryginalne pliki, aby narzędzia deweloperskie mogły się do nich odwoływać i wyświetlać je w razie potrzeby.

Jak wygenerować mapę źródeł?

Aby zmniejszyć, połączyć i wygenerować mapę źródeł plików JavaScript, musisz użyć kompilatora Closure. Polecenie jest następujące:

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 i --source_map_format. Jest to wymagane, ponieważ domyślna wersja to V2, a my chcemy pracować tylko z wersją 3.

Struktura mapy źródłowej

Aby lepiej zrozumieć mapę źródeł, wykorzystamy mały przykład pliku mapy źródła, który zostałby wygenerowany przez kompilator Closure, i szczegółowo omówimy, jak działa sekcja „mapowania”. Poniższy przykład nieznacznie różni się 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"
}

Powyżej widać, że mapa źródła to literał obiektu zawierający wiele soczystych informacji:

  • Numer wersji, na której opiera się mapa źródłowa
  • Nazwa pliku wygenerowanego kodu (Twój minifederowany/połączony plik produkcyjny)
  • sourceRoot umożliwia dołączanie źródeł do źródeł za pomocą struktury folderów, co pozwala również zaoszczędzić miejsce
  • source zawiera wszystkie nazwy plików, które zostały połączone
  • nazwy zawierają wszystkie nazwy zmiennych/metod, które występują w kodzie.
  • Właściwość mapowania to miejsce, w którym dzieje się to za pomocą wartości VLQ Base64. Tutaj odbywa się prawdziwa oszczędność miejsca.

VLQ w Base64 i niewielka mapa źródłowa

Początkowo specyfikacja mapy źródłowej zawierała bardzo dokładne dane wyjściowe wszystkich mapowań, dzięki czemu mapa źródła była około 10 razy większa niż wygenerowany kod. W drugiej wersji udało się to obniżyć o około 50%, a w wersji trzeciej o kolejne 50%, więc w przypadku pliku 133 kB otrzymujemy mapę źródeł o rozmiarze ok. 300 kB.

W jaki sposób firma zmniejszyła rozmiar, zachowując jednocześnie złożone mapowania?

VLQ (o zmiennej długości) jest używany razem z kodowaniem wartości w formacie Base64. Właściwość mapowania to bardzo duży ciąg znaków. Zawiera on średniki (;) reprezentujące numer wiersza w wygenerowanym pliku. W każdej linii znajdują się przecinki (,), które odpowiadają poszczególnym segmentom w danym wierszu. Każdy z tych segmentów ma 1, 4 lub 5 pól o zmiennej długości. Niektóre mogą być dłuższe, ale zawierają kontynuacje. Każdy segment bazuje na poprzednim, co pomaga zmniejszyć rozmiar pliku w zależności od tego, czy każdy bit jest porównywany z poprzednimi segmentami.

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

Jak wspomnieliśmy powyżej, każdy segment może mieć 1, 4 lub 5 długości. Uznaje się, że ten diagram ma zmienną długość 4 z 1 bitem kontynuacji (g). Podzielimy ten segment i pokażemy Ci, jak mapa źródła działa w pierwotnej lokalizacji.

Wartości widoczne powyżej to wartości wyłącznie zdekodowane za pomocą Base64. Uzyskanie ich prawdziwych wartości wymaga trochę czasu. Każdy segment obejmuje zwykle pięć elementów:

  • Wygenerowana kolumna
  • Oryginalny plik, w którym ten plik się pojawił
  • Numer oryginalnego wiersza
  • Kolumna oryginalna
  • oraz, jeśli jest dostępna, pierwotna nazwa

Nie każdy segment ma nazwę, nazwę metody lub argument, więc fragmenty w każdym segmencie będą miały zmienną długością od czterech do pięciu. Wartość g na diagramie segmentu powyżej jest nazywana bitem kontynuacji, co pozwala na dalszą optymalizację na etapie dekodowania Base64 VLQ. Opcja kontynuacji pozwala wykorzystywać wartość segmentu, dzięki czemu możesz przechowywać duże liczby bez konieczności przechowywania dużej liczby. Ta sprytna technika oszczędzania przestrzeni opiera się na formacie midi.

Po przeprowadzeniu dalszego przetwarzania powyższego diagramu AAgBC zwracałby on wartości 0, 0, 32, 16, 1 – przy czym 32 to bit kontynuacji, który pomaga uzyskać podaną wartość 16. Wartość B zdekodowana w formacie Base64 to 1. Zastosowane ważne wartości to 0, 0, 16, 1. Dzięki temu wiemy, że wiersz 1 (wiersze są liczone średnikami) jest zmapowany na plik 0 (tablica plików 0 to foo.js) i wiersz 16 w kolumnie 1.

Aby pokazać, jak są dekodowane segmenty, użyję biblioteki JavaScript Mapa źródła Mozilli. Możesz też zapoznać się z kodem mapowania źródłowego w narzędziach WebKit dla programistów, również w języku JavaScript.

Aby właściwie zrozumieć, skąd pochodzi wartość 16 z wartości B, musimy znać podstawowe operatory bitowe oraz to, jak specyfikacja działa w przypadku mapowania źródeł. Poprzednia cyfra g zostaje oznaczona jako kontynuacja przez porównanie cyfry (32) i VLQ_CONTINUATION_BIT (binarne 100000 lub 32) za pomocą operatora bitowego AND (&).

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

Zwraca ono 1 w każdej pozycji bitowej, w której występuje obie. Zatem zdekodowana w formacie Base64 wartość 33 & 32 zwracałaby 32, ponieważ współdzielą tylko 32-bitową lokalizację, jak widać na powyższym diagramie. Następnie zwiększa wartość przesunięcia bitu o 5 dla każdego poprzedzającego bitu kontynuacji. W powyższym przypadku wartość jest przesunięta o 5 razy, więc przesunięcie w lewo o 1 (B) o 5.

1 <<../ 5 // 32

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

Wartość ta jest następnie konwertowana z wartości podpisanej VLQ przez przesunięcie liczby (32) w prawo o jedną pozycję.

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

Tak to wygląda z 1 na 16. Może to wydawać się zbyt skomplikowany proces, ale gdy te liczby zaczną rosnąć, nabiera sensu.

Potencjalne problemy z XSSI

Specyfikacja wspomina problemy z uwzględnieniem skryptów z innych witryn, które mogą powstać w wyniku użycia mapy źródłowej. Aby temu zaradzić, zalecamy dodanie do pierwszego wiersza mapy źródłowej ciągu „)]}”. Pozwoli to celowo unieważnić JavaScript, co spowoduje zgłoszenie błędu składni. Narzędzia dla programistów WebKit już sobie z tym radzą.

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

Jak pokazano powyżej, pierwsze 3 znaki są dzielone w celu sprawdzenia, czy pasują do błędu składni w specyfikacji, a jeśli tak, to zostaną usunięte wszystkie znaki poprzedzające pierwszą pozycję nowego wiersza (\n).

sourceURL i displayName w praktyce: funkcje oceny i anonimowe

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

Pierwszy element pomocniczy wygląda bardzo podobnie do właściwości //# sourceMappingURL i jest wspomniany w specyfikacji mapy źródłowej w wersji 3. Dodając do kodu poniższy komentarz specjalny, który zostanie oceniony, możesz nazwać oceny, tak aby wyglądały one w narzędziach deweloperskich jako bardziej logiczne nazwy. Obejrzyj prostą prezentację za pomocą kompilatora CoffeeScript:

Demonstracja: patrz kod źródłowy eval() jako skrypt na stronie sourceURL

//# sourceURL=sqrt.coffee
Jak wygląda specjalny komentarz 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. Przejdź na profil tej demonstracji, aby zobaczyć, jak działa właściwość 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);
Pokazuję działanie właściwości displayName.

Podczas profilowania kodu w narzędziach dla programistów widoczna będzie właściwość displayName, a nie (anonymous). Parametr displayName jest jednak w zasadzie gotowy do użycia i nie trafi do Chrome. Nie tracimy jednak nadziei i proponowano znacznie lepszą propozycję o nazwie debugName.

Nazewnictwo oceny jest dostępne tylko w przeglądarkach Firefox i WebKit. Właściwość displayName jest dostępna tylko w przypadku wieczorów WebKit.

Połączmy siły

Obecnie toczy się bardzo długa dyskusja na temat dodania obsługi mapy źródłowej do CoffeeScript. Sprawdź problem i dodaj pomoc w generowaniu mapy źródeł dodanej do kompilatora CoffeeScript. To będzie ogromna korzyść dla CoffeeScript i jego oddanych fanów.

W UglifyJS występuje też problem z mapą źródłową, którym też warto się zająć.

Dostępnych jest wiele tools do generowania map źródeł, w tym kompilator CoffeeScript. Traktuję to teraz za drobiazg.

Im więcej dostępnych narzędzi jest w stanie wygenerować mapy źródeł, tym lepiej się staje, więc możesz poprosić o ich obsługę lub dodać ją do swojego ulubionego projektu open source.

Funkcja nie jest idealna

Jedną z rzeczy, których mapy źródłowe nie są obecnie dostępne, to wyrażenia dotyczące obserwacji. Problem polega na tym, że próba sprawdzenia nazwy argumentu lub zmiennej w bieżącym kontekście wykonania nie zwróci niczego, ponieważ ten argument nie istnieje. Wymaga to jakiegoś odwrotnego mapowania w celu wyszukania rzeczywistej nazwy argumentu lub zmiennej, którą chcesz sprawdzić, w porównaniu z rzeczywistą nazwą argumentu lub zmiennej w skompilowanym kodzie JavaScript.

Jest to oczywiście problem, który można rozwiązać, a przy poświęcaniu większej uwagi mapom źródeł możemy zaobserwować nowe niesamowite funkcje i większą stabilność.

Problemy

W nowej wersji jQuery 1.9 dodaliśmy obsługę map źródłowych wyświetlanych z oficjalnych sieci CDN. Wskazano również typowy błąd, który występował, gdy komentarze do warunkowej kompilacji IE (//@cc_on) są używane przed załadowaniem biblioteki jQuery. Od tamtej pory wypracowano zobowiązanie, aby zniwelować ten problem przez opakowanie parametru sourceMappingURL w komentarzu wielowierszowym. Nie używaj komentarzy warunkowych, czego się nauczysz.

Ten problem został rozwiązany przez zmianę składni na //#.

Narzędzia i zasoby

Oto dodatkowe zasoby i narzędzia, z którymi warto skorzystać:

Mapy źródeł to bardzo przydatne narzędzie w zestawie narzędzi dla programistów. Bardzo ważne jest, aby aplikacja internetowa była uporządkowana, ale łatwa do debugowania. Jest to również bardzo zaawansowane narzędzie edukacyjne dla nowych deweloperów, które pozwala im zobaczyć, jak doświadczeni deweloperzy tworzą aplikacje i nie muszą sięgać po zminifikowany, nieczytelny kod.

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