Kompilacja zaawansowana

Omówienie

Korzystanie z kompilatora Closure z wartością compilation_level wynoszącą ADVANCED_OPTIMIZATIONS zapewnia lepszą kompresję niż kompilacja SIMPLE_OPTIMIZATIONS lub WHITESPACE_ONLY. Kompilacja ADVANCED_OPTIMIZATIONS zapewnia dodatkową kompresję, bardziej agresywną transformację kodu oraz zmianę nazwy symboli. Jednak bardziej agresywne podejście wymaga zachowania większej ostrożności podczas korzystania z ADVANCED_OPTIMIZATIONS, aby mieć pewność, że kod wyjściowy działa tak samo jak kod wejściowy.

W tym samouczku pokazujemy, jak wygląda kompilacja ADVANCED_OPTIMIZATIONS i co możesz zrobić, aby kod działał po kompilacji z ADVANCED_OPTIMIZATIONS. Przedstawiamy również koncepcję extern – symbol, który jest zdefiniowany w kodzie poza kodem przetwarzanym przez kompilator.

Zanim zapoznasz się z tym samouczkiem, zapoznaj się z procesem kompilowania kodu JavaScript za pomocą jednego z narzędzi kompilatora Closure Compiler (interfejsu usługi kompilacji, interfejsu API usługi kompilacji lub aplikacji kompilatora).

Uwaga dotycząca terminologii: flaga wiersza poleceń --compilation_level obsługuje najczęściej używane skróty ADVANCED i SIMPLE, a także bardziej precyzyjne ADVANCED_OPTIMIZATIONS i SIMPLE_OPTIMIZATIONS. W tym dokumencie użyto dłuższego dokumentu, ale nazwy w wierszu poleceń mogą być używane wymienne.

  1. Nawet lepsza kompresja
  2. Jak włączyć ADVANCED_OPTIMIZATIONS
  3. Na co zwracać uwagę, jeśli korzystasz z ADVANCED_OPTIMIZATIONS
    1. Usuwanie kodu, który chcesz zachować
    2. Niespójne nazwy usługi
    3. Osobne dzielenie części kodu
    4. Uszkodzone odwołania między skompilowanym a nieskompilowanym kodem

Jeszcze lepsza kompresja

Domyślny kompilacja SIMPLE_OPTIMIZATIONS sprawia, że kompilacja kodu Closure Compiler zmniejsza rozmiar JavaScriptu przez zmianę nazw lokalnych zmiennych. Istnieją jednak symbole inne niż zmienne lokalne, które można skrócić. Dostępne są również sposoby zmniejszenia kodu oprócz zmiany ich symboli. Kompilacja z ADVANCED_OPTIMIZATIONS wykorzystuje pełny zakres możliwości zmniejszania kodu.

Porównaj dane wyjściowe SIMPLE_OPTIMIZATIONS i ADVANCED_OPTIMIZATIONS tego kodu:

function unusedFunction(note) {
  alert(note['text']);
}

function displayNoteTitle(note) {
  alert(note['title']);
}

var flowerNote = {};
flowerNote['title'] = "Flowers";
displayNoteTitle(flowerNote);

Kompilacja tagu SIMPLE_OPTIMIZATIONS skraca kod do tego:

function unusedFunction(a){alert(a.text)}function displayNoteTitle(a){alert(a.title)}var flowerNote={};flowerNote.title="Flowers";displayNoteTitle(flowerNote);

Kompilacja tagu ADVANCED_OPTIMIZATIONS całkowicie skraca ten kod:

alert("Flowers");

Oba skrypty generują alert dotyczący odczytywania tekstu "Flowers", ale drugi jest znacznie mniejszy.

Poziom ADVANCED_OPTIMIZATIONS znacznie wykracza poza proste skracanie nazw zmiennych na kilka sposobów, m.in.:

  • bardziej agresywną zmianę nazwy:

    Kompilacja SIMPLE_OPTIMIZATIONS wymaga jedynie zmiany parametrów note funkcji displayNoteTitle() i unusedFunction(), ponieważ są to jedyne zmienne w skrypcie, które są lokalne dla funkcji. ADVANCED_OPTIMIZATIONS także zmienia nazwę zmiennej globalnej flowerNote.

  • usunięcie kodu martwego:

    Kompilacja metody ADVANCED_OPTIMIZATIONS całkowicie usuwa funkcję unusedFunction(), bo nigdy nie jest wywoływana w kodzie.

  • wbudowane funkcje:

    Kompilacja metody ADVANCED_OPTIMIZATIONS zamienia wywołanie displayNoteTitle() na pojedynczy element alert(), który tworzy treść funkcji. To zamiana wywołania funkcji na treść funkcji jest nazywana „inline”. Jeśli funkcja jest dłuższa lub bardziej złożona, może to spowodować zmianę sposobu działania kodu, ale kompilacja Closure Compiler ustali, że wbudowany element jest bezpieczny i zaoszczędzi miejsce. Kompilacja z parametrem ADVANCED_OPTIMIZATIONS wymaga też wbudowanych stałych i niektórych zmiennych.

Ta lista zawiera tylko przykładowe przekształcenia zmniejszające rozmiar, które może wykonać kompilacja ADVANCED_OPTIMIZATIONS.

Jak włączyć ADVANCED_OPTIMIZATIONS

Interfejs usługi Closure Compiler, interfejs API usługi i aplikacja mają różne metody ustawiania właściwości compilation_level na ADVANCED_OPTIMIZATIONS.

Jak włączyć ADVANCED_OPTIMIZATIONS w interfejsie usługi kompilatora zamkniętych

Aby włączyć interfejs ADVANCED_OPTIMIZATIONS w interfejsie usługi kompilatora zamkniętych, kliknij przycisk „Zaawansowane”.

Jak włączyć interfejs ADVANCED_OPTIMIZATIONS w interfejsie Closure Compiler API.

Aby włączyć ADVANCED_OPTIMIZATIONS w interfejsie API usługi Closure Compiler, dołącz parametr żądania o nazwie compilation_level z wartością ADVANCED_OPTIMIZATIONS, jak w tym programie w Pythonie:

#!/usr/bin/python2.4

import httplib, urllib, sys

params = urllib.urlencode([
    ('code_url', sys.argv[1]),
    ('compilation_level', 'ADVANCED_OPTIMIZATIONS'),
    ('output_format', 'text'),
    ('output_info', 'compiled_code'),
  ])

headers = { "Content-type": "application/x-www-form-urlencoded" }
conn = httplib.HTTPSConnection('closure-compiler.appspot.com')
conn.request('POST', '/compile', params, headers)
response = conn.getresponse()
data = response.read()
print data
conn.close()

Jak włączyć interfejs ADVANCED_OPTIMIZATIONS w aplikacji Closure Compiler

Aby włączyć funkcję ADVANCED_OPTIMIZATIONS w aplikacji Compute Compiler, dołącz flagę wiersza poleceń --compilation_level ADVANCED_OPTIMIZATIONS w następujący sposób:

java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js hello.js

Na co należy uważać przy korzystaniu z ADVANCED_OPTIMIZATIONS

Poniżej omawiamy kilka najczęstszych, niezamierzonych rezultatów działania ADVANCED_OPTIMIZATIONS, a także kroki, jakie możesz podjąć, aby ich uniknąć.

Usunięcie kodu, który chcesz zachować

Jeśli skompilujesz tylko poniższą funkcję z użyciem funkcji ADVANCED_OPTIMIZATIONS, funkcja Closure Compiler wygeneruje puste dane wyjściowe:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}

Ponieważ funkcja nigdy nie jest wywoływana w JavaScripcie przesyłanym do kompilatora, Closure Compiler zakłada, że ten kod nie jest potrzebny.

W wielu przypadkach jest to zgodne z Twoimi oczekiwaniami. Jeśli na przykład skompilujesz kod razem z dużą biblioteką, Closure Compiler może określić, które funkcje z danej biblioteki faktycznie są używane, i odrzucić te, których nie używasz.

Jeśli jednak skompilujesz funkcje, które chcesz zachować, możesz usunąć je na 2 sposoby:

  • Przenieś wywołania funkcji do kodu przetwarzanego przez Closure Compiler.
  • Dodaj rozszerzenia do funkcji, które chcesz udostępnić.

W kolejnych sekcjach szczegółowo omówiono każdą z opcji.

Rozwiązanie: przenieś wywołania funkcji do kodu przetwarzanego przez kompilator zamknięcia

Możesz napotkać niepożądane usuwanie kodu, jeśli skompilujesz tylko część kodu za pomocą kompilatora Closure. Możesz mieć na przykład plik biblioteki, który zawiera tylko definicje funkcji, oraz plik HTML, który zawiera bibliotekę i zawiera kod wywołujący te funkcje. W takim przypadku, gdy skompilujesz plik biblioteki z zasadą ADVANCED_OPTIMIZATIONS, Closure Compiler usunie wszystkie funkcje biblioteki.

Najprostszym rozwiązaniem tego problemu jest połączenie funkcji z częścią programu, która je wywołuje. Na przykład kompilator Closure nie usunie kodu displayNoteTitle(), gdy skompiluje ten program:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
displayNoteTitle({'myTitle': 'Flowers'});

Funkcja displayNoteTitle() nie jest w tym przypadku usuwana, ponieważ funkcja Closure Compiler wywołuje ją.

Inaczej mówiąc, możesz zapobiec niepożądanemu usuwaniu kodu, dodając punkt wejścia programu w kodzie przekazywanym do kompilatora. Punkt wejścia programu to miejsce w kodzie, w którym program zaczyna się uruchamiać. Na przykład w programie dotyczącym kwiatów z poprzedniej sekcji ostatnie 3 wiersze są wykonywane natychmiast po wczytaniu kodu JavaScript do przeglądarki. Oto punkt wejścia tego programu. Aby określić, jaki kod należy zachować, program Closure Compiler rozpoczyna się w tym punkcie wejścia i od tego momentu śledzi przepływ kontroli programu.

Rozwiązanie: uwzględnij zewnętrzne funkcje, które chcesz ujawnić

Więcej informacji o tym rozwiązaniu znajdziesz poniżej oraz na stronie o rozszerzeniach i eksportach.

Niespójne nazwy właściwości

Kompilacja Closure Compiler nigdy nie zmienia literału ciągu w Twoim kodzie, niezależnie od używanego poziomu kompilacji. Oznacza to, że kompilacja za pomocą tagu ADVANCED_OPTIMIZATIONS traktuje właściwości w różny sposób zależnie od tego, czy Twój kod uzyskuje do nich dostęp za pomocą ciągu znaków. Jeśli w przypadku mieszania odwołań do ciągów odwołuje się właściwość z odwołaniami do składni kropki, kompilator Closure zmienia nazwy niektórych odwołań do tej właściwości, a innych nie. W związku z tym kod prawdopodobnie nie będzie działał poprawnie.

Weźmy na przykład ten kod:

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
var flowerNote = {};
flowerNote.myTitle = 'Flowers';

alert(flowerNote.myTitle);
displayNoteTitle(flowerNote);

Ostatnie 2 instrukcje w tym kodzie źródłowym działają dokładnie tak samo. Jednak po skompresowaniu kodu za pomocą polecenia ADVANCED_OPTIMIZATIONS dostajesz taki wynik:

var a={};a.a="Flowers";alert(a.a);alert(a.myTitle);

Ostatni kod w skompresowanym kodzie generuje błąd. Nazwa bezpośredniego odniesienia do właściwości myTitle została zmieniona na a, ale cytowane odwołanie do myTitle w funkcji displayNoteTitle nie zostało zmienione. W związku z tym ostatnie wyrażenie odwołuje się do właściwości myTitle, która już nie istnieje.

Rozwiązanie: zachowaj spójność nazw usług

To rozwiązanie jest dość proste. W przypadku każdego typu obiektu lub obiektu używaj wyłącznie syntezy kropek lub cudzysłowów. Nie używaj różnych składni, zwłaszcza w odniesieniu do tej samej właściwości.

W miarę możliwości używaj też kropki, ponieważ obsługuje ona dokładniejsze optymalizacje. Korzystaj z dostępu do właściwości ciągu znaków w cudzysłowie tylko wtedy, gdy nie chcesz, aby kompilacja Closure Compuler miała zmieniać nazwę (na przykład gdy nazwa pochodzi ze źródła zewnętrznego, np. zdekodowanego pliku JSON).

Łączenie osobnych fragmentów kodu

Jeśli dzielisz aplikację na różne fragmenty kodu, możesz skompilować fragmenty osobno. Jeśli jednak niektóre fragmenty kodu w ogóle zachodzą na coś innego, może to spowodować trudności. Nawet jeśli operacja się powiedzie, dane wyjściowe 2 kompilacji Closure Compiler będą niezgodne.

Załóżmy na przykład, że aplikacja jest podzielona na 2 części: część pobierającą dane i część wyświetlającą dane.

Oto kod do pobrania danych:

function getData() {
  // In an actual project, this data would be retrieved from the server.
  return {title: 'Flower Care', text: 'Flowers need water.'};
}

Oto kod służący do wyświetlania danych:

var displayElement = document.getElementById('display');
function displayData(parent, data) {
  var textElement = document.createTextNode(data.text);
  parent.appendChild(textElement);
}
displayData(displayElement, getData());

Jeśli spróbujesz skompilować te dwa fragmenty kodu oddzielnie, napotkasz kilka problemów. Najpierw kompilator zamknięty usuwa funkcję getData() z powodów opisanych w sekcji Usuwanie kodu, który chcesz zachować. Po drugie, podczas tworzenia kodu, który wyświetla dane, kompilator zamknięty zawiera błąd krytyczny.

input:6: ERROR - variable getData is undefined
displayData(displayElement, getData());

Kompilator nie ma dostępu do funkcji getData() podczas kompilowania kodu, który wyświetla dane, więc traktuje getData jako niezdefiniowane.

Rozwiązanie: skompiluj cały kod strony razem

Aby zapewnić prawidłowe kompilowanie kodu, skompiluj cały kod strony w jednym kompilacji. Kompresor Closure może przyjmować jako dane wejściowe wiele plików JavaScript i ciągów JavaScript, dzięki czemu możesz przekazać kod biblioteki oraz inny kod razem w jednym żądaniu kompilacji.

Uwaga: ta metoda nie zadziała, jeśli musisz łączyć skompilowany i nieskompilowany kod. Wskazówki dotyczące postępowania w tej sytuacji znajdziesz w sekcji Zepsute odwołania między skompilowanymi a nieskompilowanymi kodami.

Uszkodzone odwołania między skompilowanym a nieskompilowanym kodem

Zmiana nazwy symbolu w ADVANCED_OPTIMIZATIONS spowoduje przerwanie komunikacji między kodem przetwarzanym przez kompilację Closure, a dowolnym innym kodem. Kompilacja zmieni nazwy funkcji zdefiniowanych w kodzie źródłowym. Każdy kod zewnętrzny, który wywołuje Twoje funkcje, przestanie działać po skompilowaniu, bo nadal odwołuje się do starej nazwy funkcji. Odwołania w skompilowanym kodzie do symboli zdefiniowanych zewnętrznie mogą być jeszcze zmienione przez kompilator zamknięty.

Pamiętaj, że „nieskompilowany kod” zawiera dowolny kod przekazywany do funkcji eval() jako ciąg znaków. Kompilacja Closure Compiler nigdy nie zmienia literałów ciągów w kodzie, więc kompilator Closure nie zmienia ciągów przekazywanych na instrukcje eval().

Pamiętaj o tych związanych i odrębnych kwestiach: utrzymaniu skompilowanej komunikacji zewnętrznej i utrzymaniu takiej komunikacji. Te odrębne problemy mają częste rozwiązanie, ale z każdej strony występują niuanse. Aby w pełni wykorzystać kompilator zakłóceń, trzeba wiedzieć, co to jest.

Zanim przejdziesz dalej, zapoznaj się z rozszerzeniami i eksportami.

Rozwiązanie do wywoływania kodu zewnętrznego z poziomu skompilowanego kodu: kompilowanie za pomocą kodów zewnętrznych

Jeśli używasz kodu udostępnionego na stronie przez inny skrypt, upewnij się, że kompilator Closure nie zmienia nazw odwołań do symboli zdefiniowanych w tej bibliotece zewnętrznej. Aby to zrobić, dodaj do kompilacji plik zawierający rozszerzenia zewnętrzne biblioteki zewnętrznej. Spowoduje to przekazanie informacji o tym, których nazw nie kontrolujesz, w związku z czym nie będzie można ich zmienić. Kod musi mieć te same nazwy co plik zewnętrzny.

Typowe przykłady to interfejsy API OpenSocial API i Google Maps API. Jeśli na przykład Twój kod wywołuje funkcję OpenSocial opensocial.newDataRequest() bez odpowiednich rozszerzeń, Closure Compiler przekształci to wywołanie w funkcję a.b().

Rozwiązanie do wywoływania skompilowanego kodu z kodu zewnętrznego: implementacja zewnętrznych

Jeśli używasz kodu JavaScript, którego używasz ponownie jako biblioteki, możesz użyć narzędzia Closure Compiler, aby zmniejszyć tylko bibliotekę, a jednocześnie umożliwić nieskompilowanym kodom wywoływanie funkcji w bibliotece.

Rozwiązaniem w takiej sytuacji będzie wdrożenie zestawu zewnętrznych narzędzi definiujących publiczny interfejs API biblioteki. Twój kod poda definicje symboli zadeklarowanych w tych rozszerzeniach. To oznacza dowolne klasy lub funkcje wspomniane przez zewnętrznych użytkowników. Może też oznaczać, że klasy implementują interfejsy deklarowane w rozszerzeniach.

Te rozszerzenia są przydatne nie tylko dla Ciebie. Klienci, którzy korzystają z Twojej biblioteki, muszą go uwzględnić, gdy kompilują kod, ponieważ Twoja biblioteka reprezentuje zewnętrzny skrypt z perspektywy. To tak jak z kontrahentami – umowa między Tobą a klientami.

Dlatego pamiętaj, aby podczas kompilacji kodu uwzględnić w nim kompilatory. Może się to wydawać dziwne, ponieważ często postrzegamy terenów jako „przechodzących z innego miejsca”, ale tak naprawdę musimy wyjaśnić, z którymi symbolami masz do czynienia, aby ich nazwa nie została zmieniona.

Jedną z ważnych kwestii jest to, że możesz otrzymać diagnostykę „zduplikowanej” definicji kodu zewnętrznego. Kompilator zamknięty zakłada, że dowolny symbol w rozszerzeniach jest dostarczany przez bibliotekę zewnętrzną i obecnie nie może zrozumieć, czy celowo podajesz definicję. Takie diagnostyki można bezpiecznie pominąć. Możesz je traktować jako potwierdzenie, że rzeczywiście wykonujesz interfejs API.

Dodatkowo Closure Compiler może sprawdzać, czy Twoje definicje odpowiadają typom deklaracji zewnętrznych. Jest to dodatkowe potwierdzenie, że definicje są poprawne.