Wprowadzenie do map źródeł JavaScript

Ryan Seddon

Czy kiedykolwiek marzyło Ci się, aby kod po stronie klienta był czytelny, a przede wszystkim możliwy do debugowania nawet po połączeniu i minifikacji bez wpływu na wydajność? Teraz możesz dzięki magii map źródłowych.

Mapy źródłowe to sposób na mapowanie scalonego/zminimalizowanego pliku z powrotem na nieutworzony. Gdy tworzysz pliki na potrzeby wersji produkcyjnej, a także zmniejszając i łącząc pliki JavaScript, generujesz mapę źródeł, która zawiera informacje o oryginalnych plikach. Gdy wyślesz zapytanie o konkretny wiersz lub numer kolumny w wygenerowanym kodzie JavaScript, możesz wyszukać w mapie źródłowej pierwotną lokalizację. Narzędzia dla programistów (obecnie kompilacje WebKit Nightly, Google Chrome i Firefox w wersji 23 lub nowszej) mogą automatycznie analizować mapę źródłową i sprawiać wrażenie, że używasz niezminimalizowanych i niepołączonych plików.

Wersja demonstracyjna pozwala kliknąć prawym przyciskiem myszy w dowolnym miejscu w obszarze tekstowym zawierającym wygenerowane źródło. Wybranie opcji „Pobierz pierwotną lokalizację” spowoduje wysłanie zapytania do mapy źródłowej przez przekazanie wygenerowanego numeru wiersza i kolumny oraz zwrócenie pozycji w pierwotnym kodzie. Aby zobaczyć dane wyjściowe, konsola musi być otwarta.

Przykład biblioteki mapy źródłowej JavaScript Mozilla JavaScript w działaniu

Prawdziwy świat

Zanim wyświetlisz poniższą implementację Map źródłowych, sprawdź, czy co noc włączono funkcję map źródłowych w Chrome Canary lub WebKit. W tym celu kliknij ikonę koła zębatego w panelu Narzędzi dla programistów i zaznacz opcję „Włącz mapy źródeł”.

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

W przeglądarce Firefox w wersji 23 i nowszej 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ł są ważne?

Obecnie mapowanie źródeł działa tylko między nieskompresowanym/skombinowanym JavaScriptem a skompresowanym/nieskombinowanym kodem JavaScript, ale przyszłość rysuje się w sporych językach, jak CoffeeScript, takich jak CoffeeScript, czy też możliwości dodania obsługi preprocesorów CSS, takich jak SASS czy LESS.

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

  • CoffeeScript
  • ECMAScript 6 i nowsze wersje
  • SASS/LESS i inne
  • Praktycznie każdy język kompilowany do języka JavaScript

Obejrzyj ten screencast debugowany CoffeeScript w eksperymentalnej kompilacji konsoli Firefox:

W narzędziu Google Web Toolkit (GWT) dodaliśmy niedawno obsługę map źródłowych. Ray Cromwell z zespołu GWT przygotował świetny screencast pokazujący obsługę mapy źródeł w praktyce.

Inny przykład, który opracowałem, 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 również mapę źródeł. Obejrzyj tę prezentację dotyczącą cech i klas ES6 używanych w przeglądarce tak, jakby były natywnie obsługiwane w przeglądarce dzięki mapie źródeł.

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

Debugowanie usługi Traceur ES6 przy użyciu map źródeł.

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

Jak działa mapa źródeł?

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

Obecnie kompilator Closure nie dodaje na końcu specjalnego komentarza, który jest wymagany do poinformowania narzędzi deweloperskich w przeglądarkach o dostępności mapy źródłowej:

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

Dzięki temu narzędzia dla programistów mogą zmapować wywołania z powrotem na ich lokalizację w pierwotnych plikach źródłowych. Wcześniej pragma komentarzy to //@, ale ze względu na problemy z tą kompilacją i komentarzami do kompilacji warunkowej w IE podjęto decyzja o zmianie jej na //#. Obecnie Chrome Canary, WebKit Nightly i Firefox w wersji 24 lub nowszej obsługują nową pragmę komentarzy. Ta zmiana składni wpływa też na atrybut sourceURL.

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

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

Podobnie jak w przypadku komentarza wskazuje to konsumentowi mapy źródłowej, gdzie szukać mapy źródłowej powiązanej z plikiem JavaScript. W tym nagłówku uwzględniono też problem z odwoływaniem się do map źródeł w językach, które nie obsługują komentarzy jednowierszowych.

Przykład użycia map źródłowych w WebKit i wyłączeniu map źródeł.

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

Jak wygenerować mapę źródeł?

Aby minifikować, łączyć i generować mapę źródeł plików JavaScript, musisz użyć kompilatora Closure. Oto polecenie:

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 2, a chcemy pracować tylko z wersją 3.

Struktura mapy źródłowej

Aby lepiej zrozumieć mapę źródeł, wykorzystamy krótki przykład pliku mapy źródłowej, który zostałby wygenerowany przez kompilator Closure, i dokładniej omówimy działanie sekcji „mapowania”. Poniżej znajduje się przykład nieznacznie różniący się od specyfikacji wersji 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 dużo informacji:

  • Numer wersji, na której opiera się mapa źródłowa
  • Nazwa pliku wygenerowanego kodu (zminimalizowanego/połączonego pliku produkcyjnego).
  • sourceRoot umożliwia dodanie źródeł na początku struktury folderów – jest to również technika oszczędzania miejsca
  • Źródło zawiera wszystkie nazwy plików, które zostały połączone
  • Nazwa zawiera wszystkie nazwy zmiennych/metod występujące w kodzie.
  • Wreszcie właściwości mapowań służą do wykonywania magii z wykorzystaniem wartości VLQ Base64. Tutaj odbywa się rzeczywiste oszczędzanie miejsca.

VLQ w Base64 i zachowanie małej mapy źródłowej

Początkowo specyfikacja mapy źródłowej zawierała bardzo szczegółowe dane wyjściowe wszystkich mapowań, przez co mapa źródłowa miała około 10 razy większy rozmiar niż wygenerowany kod. W drugiej wersji udało się obniżyć tę liczbę o około 50%, a w wersji 3 – ponownie o kolejne 50%, więc w przypadku pliku o rozmiarze 133 KB otrzymujemy mapę źródłową o rozmiarze ok. 300 kB.

W jaki sposób udało się jej zmniejszyć rozmiar, zachowując jednocześnie złożone mapowania?

Jest używany razem z kodowaniem wartości do wartości Base64 (zmiennej długości – VLQ). Właściwość mapowania to bardzo duży ciąg znaków. W tym ciągu znaków znajdują się średniki (;) reprezentujące numer wiersza w wygenerowanym pliku. W każdym wierszu znajdują się przecinki (,), które reprezentują poszczególne segmenty. Każdy z tych segmentów ma w polach o zmiennej długości 1, 4 lub 5. Niektóre mogą wydawać się dłuższe, ale zawierają kontynuacje. Każdy segment bazuje na poprzednim, co pomaga zmniejszyć rozmiar pliku, ponieważ każdy bit jest względny w odniesieniu do poprzednich segmentów.

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

Jak już wspomnieliśmy, każdy segment może mieć 1, 4 lub 5 długości. Ten diagram jest uważany za zmienną długości 4 z 1 bitem kontynuacji (g). Podzielimy ten segment i pokażemy, jak mapa źródłowa odzwierciedla pierwotną lokalizację.

Powyższe wartości to wyłącznie wartości zdekodowane w standardzie Base64. Uzyskanie ich wartości prawdziwych wymaga trochę czasu na przetworzenie. Każdy segment zwykle działa na 5 kwestii:

  • Wygenerowana kolumna
  • Oryginalny plik, w którym pojawiło się to wydarzenie
  • Numer oryginalnego wiersza
  • Pierwotna kolumna
  • nazwę (jeśli jest dostępna),

Nie każdy segment ma nazwę, nazwę metody lub argument, więc w całym segmencie będzie widoczna wartość 4–5 zmiennych długości. Wartość g na powyższym diagramie segmentów jest nazywanym bitem kontynuacji, który umożliwia dalszą optymalizację na etapie dekodowania Base64 VLQ. Dzięki kontynuacji możesz wykorzystać wartość segmentu, aby móc przechowywać duże liczby bez konieczności zapisywania dużej liczby. Jest to bardzo sprytna technika oszczędzania miejsca, która ma swoje korzenie w formacie midi.

Powyższy wykres AAgBC po dalszej analizie zwróciłby 0, 0, 32, 16, 1, a 32 to bit kontynuacji, który pomaga utworzyć następującą wartość 16. Wartość B dekodowana w standardzie Base64 wynosi 1. Ważne wartości to 0, 0, 16, 1. Dzięki temu dowiadujemy się, że wiersz 1 (wiersze są zliczane przez półdwukropki) kolumna 0 wygenerowanego pliku jest mapowana na plik 0 (tablica plików 0 to foo.js), a wiersz 16 w kolumnie 1.

Aby pokazać, jak są dekodowane segmenty, spójrzmy na bibliotekę JavaScript mapy źródłowej Mozilli. Możesz też zapoznać się z kodem mapowania źródłowego w narzędziach WebKit, również napisanym w języku JavaScript.

Aby dokładnie zrozumieć, w jaki sposób otrzymujemy wartość 16 z B, musimy znać podstawowe informacje o operatorach bitowych i działaniu specyfikacji przy mapowaniu źródeł. Poprzednia cyfra (g) jest oznaczana jako bit kontynuacji przez porównanie cyfry (32) i VLQ_CONTINUATION_BIT (binarny 100 000 lub 32) przy użyciu operatora bitowego AND (&).

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

Zwraca wartość 1 w każdym położeniu bitu, w którym występuje oba. Zdekodowana z użyciem Base64 wartość 33 & 32 zwróciłaby wartość 32, ponieważ współdzielą one tylko lokalizację 32-bitową, jak widać na diagramie powyżej. Następnie zwiększa wartość przesunięcia bitową o 5 dla każdego poprzedzającego bitu kontynuacji. W tym przypadku widać tylko przesunięcie o 5 razy, a więc w lewo przesunięcie 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 ze znakiem VLQ przez przesunięcie w prawo liczbę (32) o jedno miejsce.

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

I gotowe. W ten sposób zmieniasz 1 na 16. Ten proces może się wydawać skomplikowany, ale gdy liczby zaczynają się zwiększać, zaczynamy mieć sens.

Potencjalne problemy z XSSI

Specyfikacja wspomina o problemach z uwzględnianiem skryptów w różnych witrynach, które mogą wynikać z korzystania z mapy źródłowej. Aby temu zaradzić, zalecamy dodanie pierwszego wiersza mapy źródłowej do „)]}”, aby celowo unieważnić kod JavaScript, co spowoduje zgłoszenie błędu składni. Narzędzia deweloperskie WebKit już to radzą sobie z tym zadaniem.

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

Jak pokazano powyżej, pierwsze 3 znaki są wycinane w celu sprawdzenia, czy pasują do błędu składni w specyfikacji, a jeśli tak, usuwa wszystkie znaki poprzedzające pierwszy wiersz nowego wiersza (\n).

Działanie sourceURL i displayName: funkcje oceniania i anonimowe

2 poniższe konwencje nie wchodzą w skład specyfikacji mapy źródłowej. Dzięki nim możesz znacznie ułatwić programowanie podczas pracy z ocenami i funkcjami anonimowymi.

Pierwszy element pomocniczy wygląda bardzo podobnie do //# sourceMappingURL i jest wspomniany w specyfikacji mapy źródłowej w wersji 3. Jeśli umieścisz w kodzie poniższy komentarz specjalny, który zostanie oceniony, możesz nazwać oceny tak, aby były wyświetlane w narzędziach dla programistów jako bardziej logiczne nazwy. Obejrzyj prostą demonstrację z kompilatorem CoffeeScript:

Demonstracja: zobacz, jak kod wyświetlany przez użytkownika eval() jest wyświetlany jako skrypt przez sourceURL

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

Drugi plik pomocniczy umożliwia nadawanie nazw funkcji anonimowych przy użyciu właściwości displayName dostępnej w bieżącym kontekście funkcji anonimowej. Profiluj tę prezentację, 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);
Wyświetlam właściwość displayName w praktyce.

Podczas profilowania kodu w narzędziach deweloperskich wyświetlana jest właściwość displayName, a nie np. (anonymous). Parametr displayName znika jednak z wody i nie trafi do Chrome. Ale nie wszystko stracone i zaproponowano znacznie lepszą propozycję nazywaną debugName.

Na chwilę obecną nazwy ocen są dostępne tylko w przeglądarkach Firefox i WebKit. Właściwość displayName jest dostępna tylko w nocnych plikach WebKit.

Zbierzmy się razem

Toczy się obecnie bardzo obszerna dyskusja na temat dodawania do CoffeeScript obsługi map źródłowych. Zapoznaj się z problemem i dodaj opcję pomocy dotyczącej generowania mapy źródeł dodanej do kompilatora CoffeeScript. To duży sukces dla CoffeeScript i jego oddanych fanów.

Na stronie UglifyJS występuje też problem z mapą źródłową, na który musisz się zwrócić.

Wiele tools do generowania map źródeł, w tym kompilatora Coffeescript. Dla mnie to teraz puenta.

Im więcej dostępnych narzędzi pozwala nam generować mapy źródeł, tym lepsze wyniki możemy osiągnąć. Zapytaj więc lub dodaj obsługę takiej mapy do swojego ulubionego projektu open source.

Nie jest idealna

Mapy źródeł nie obsługują obecnie tylko wyrażeń związanych z obserwacją. Problem polega na tym, że próba sprawdzenia nazwy argumentu lub zmiennej w bieżącym kontekście wykonania nie zwróci niczego, ponieważ faktycznie nie istnieje. Wymaga to użycia odwrotnego mapowania w celu wyszukania rzeczywistej nazwy argumentu/zmiennej, którą chcesz sprawdzić, w porównaniu z rzeczywistą nazwą argumentu/zmiennej w skompilowanym kodzie JavaScript.

Jest to oczywiście problem, który da się rozwiązać. Jeśli skupimy się bardziej na mapach źródeł, zaczniemy obserwować niesamowite funkcje i zwiększyć stabilność.

Problemy

Niedawno jQuery 1.9 dodała obsługę map źródeł w przypadku wyświetlania z oficjalnych sieci CDN. Wskazano też nietypowy błąd, który występował, gdy przed załadowaniem biblioteki jQuery były używane komentarze do kompilacji warunkowej w IE (//@cc_on). Od tamtej pory wprowadzono zobowiązanie do złagodzenia skutków takiego problemu przez umieszczenie parametru sourceMappingURL w komentarzu wielowierszowym. Lekcja, której warto się nauczyć, nie używaj komentarzy warunkowych.

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

Narzędzia i materiały

Oto dodatkowe materiały i narzędzia, z którymi warto się zapoznać:

Mapy źródeł to niezwykle przydatne narzędzie w zestawie narzędzi dla programistów. Bardzo przydatny jest dbanie o to, aby aplikacja internetowa była zwięzła, ale łatwa do debugowania. Jest to również niezwykle przydatne narzędzie do nauki dla nowych deweloperów, dzięki któremu mogą się dowiedzieć, jak doświadczeni programiści tworzą strukturę i piszą aplikacje bez konieczności zaprzątania się nieczytelnym zminimalizowanym kodem.

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