Był już wpis o kompresowaniu stylów CSS, czas więc zabrać się za kolejne optymalizacje strony. Tym razem bierzemy się za kompresowanie JavaScript.
Wpis będzie oparty o strukturę klasy stworzonej przy okazji wspomnianego kompresora CSS. Kompresja kodu zostanie natomiast wykonywana przez JavaScript’s Packer (by Dean Edwards).
Zaczynamy kodzić!
Zacznijmy od pokazania interfejsu, który został już wcześniej stworzony, a który wykorzystamy także i przy tym kompresorze.
1 2 3 4 5 6 7 8 9 10 11 | interface CodeCompressor { public function addFiles($url_files); public function addFile($url_file); public function cleanCode($code); public function compressCode($code); public function showCode(); } |
Sama klasa wygląda więc następująco:
1 2 3 4 5 | class JSCompressor implements CodeCompressor { // kod } |
Tak jak wcześniej, tak i teraz klasa zawiera szereg ustawień konfiguracyjnych:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** * Domyślne ustawienia konfiguracyjne dla klasy **/ private $config = array( 'charset' => 'utf-8', // kodowanie znaków 'compress_code' => true, // kompresja kodu 'gzip_contents' => true, // kompresja gzip 'gzip_level' => 6, // poziom kompresji gzip 'cache_enabled' => true, // buforowanie po stronie serwera 'cache_location' => 'tmp/', // folder dla cache 'use_flush_key' => true, // własnoręczne usuwanie cache, ?flush=FILE_ID 'use_cache_browser' => true, // buforowanie po stronie klienta 'time_cache_browser' => 3600 // czas trzymania w buforze (sekundy) ); |
Jak więc widać powyżej, klasa zawiera tablicę z ustawieniami konfiguracyjnymi. Przy późniejszym wywoływaniu instancji klasy możemy podać jej tablicę z własnymi ustawieniami.
W klasie zdefiniowano kilka innych właściwości przechowujących kod, adresy skompresowanych plików czy uchwyt do obiektu odpowiadającego za cache:
1 2 3 4 5 6 7 8 9 10 11 | // zmienna przechowująca złączony kod JavaScript private $js_code; // tablica z adresami plików do złączenia i optymalizacji private $files_require = array(); // tablica przechowująca adresy plików, które już przetworzono private $files_loaded = array(); // uchwyt dla klasy buforującej wynik private $cache_lite; |
Ustawienia konfiguracyjne są odpowiednio nakładane na powyższe przez konstruktor klasy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public function __construct($config=array()) { // jeśli nie zdefiniowano ustawień - pozostaw domyślne if (!is_array($config)) return false; // przypisz ustawienia konfiguracyjne do właściwości klasy foreach ($config as $name => $type) { if (in_array($name, $this->config)) { $this->config[$name] = $config[$name]; } } // uruchom buforowanie plików $options = array( 'caching' => $this->config['cache_enabled'], 'cacheDir' => $this->config['cache_location'] ); $this->cache_lite = new Cache_Lite($options); // samodzielne czyszczenie buforu // dodaj ?flush=FILE_ID do adresu, aby usunąć cache if ( $this->config['cache_enabled'] and $this->config['use_flush_key'] and !empty($_GET['flush']) ) { $this->flushCache($_GET['flush']); } } |
Jak widać wyżej, struktura jest bardzo podobna do struktury klasy CSSCompressor omawianej kilka wpisów wstecz.
Dalej definiujemy metody flushCache, addFiles, addFile, checkModifiedFiles, _lastModifiedCache, _getCacheName, showCode oraz outputHeaders. Nie opiszę ich tutaj, ponieważ nie wprowadzono do nich żadnych zmian i są jednakowe z metodami klasy CSSCompressor.
Nawiasem mówiąc, wcześniej mogłem stworzyć klasę ogólną do kompresji kodu, a następnie tylko dziedziczyć z niej i ewentualnie przeciążać wybrane metody.
Trudno, teraz już na to za późno :-)
Właściwa kompresja JavaScript
Zmiany nastąpiły w metodach odpowiadających za kompresję kodu. Tutaj zadanie jest zlecane zewnętrznej klasie, której autorem jest Dean Edwards. Nie chciałem zabierać się za to samemu, gdyż kompresja taka wymaga dokładnej analizy kodu JavaScript i trochę jest z tym roboty.
Metoda ta wygląda następująco:
1 2 3 4 5 6 7 8 9 | public function compressCode($code) { // JavaScript Compressor by Dean Edwards $packer = new JavaScriptPacker($code, 0, true, true); // zwróć skompresowany kod return $packer->pack(); } |
Klasa odpowiadająca za pobieranie kodu z plików nie uległa zbytnio zmianie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | private function getCode($files) { // zmienna przechowująca kod $full_code = null; // pobierz i zoptymalizuj kod z załączonych plików foreach ($files as $id => $url_file) { // pobierz kod źródłowy - błędy zostaną 'stłumione' i pominięte // zabezpiecz przed wielokrotnym pobieraniem tego samego kodu if ( !in_array($url_file, $this->files_loaded) and $code = @file_get_contents($url_file) ) { // odnotuj dołączenie pliku array_push($this->files_loaded, $url_file); // dodaj średnik na końcu pliku, jeśli nie istnieje if (substr($code, -1) !== ';') $code .= ';'; // wykonaj kompresję if ($this->config['compress_code']) $code = $this->compressCode($code); // dodaj kod do zwrócenia $full_code .= $code; unset($code); } } // zwróć wynik działania return $full_code; } |
Jedyną zmianą tutaj jest zaimplementowanie funkcji dodającej średnik na końcu pobranego kodu z pliku, jeśli go tam jeszcze nie ma.
Jest to zabezpieczenie przed występowaniem błędów typu:
plik1.js:
1 | document.getElementById('search-field').value = 'wpisz tekst' |
plik2.js:
1 | if (price === 32) promocja(); |
W połączeniu (bez średnika) otrzymalibyśmy następujący kod (błędny):
1 | document.getElementById('search-field').value = 'wpisz tekst'if (price === 32) promocja(); |
Dodając średnik na końcu każdego pliku ustrzeżemy się przed sytuacjami tego typu.
Kilka przykładów
Przykład 1 – kod JavaScript w zewnętrznym pliku:
Kod HTML:
1 2 3 4 5 | <input type="button" id="but1" value="button1"> <input type="button" id="but2" value="button2"> <input type="button" id="but3" value="button3"> <script type="text/javascript" src="./js1.php?load=file1,file2,file3"></script> |
Plik js1.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | require_once 'class.JSCompressor.php'; // utwórz instancję klasy $css_compress = new JSCompressor(); // dodaj pliki js do złączenia if (!isset($_GET['load'])) die('Nie wybrano żadnych plików ze skryptami.'); // przerób na tablicę $files = explode(',', $_GET['load']); foreach ($files as $id => &$file) { $file = trim($file) . '.js'; } // ustaw do kompresji $css_compress->addFiles($files); // wyświetl wynik $css_compress->showCode('infile'); |
Korzystamy tutaj z domyślnych ustawień klasy (włączona kompresja, buforowanie, cache, etc). Domyślne ustawienia moim zdaniem są optymalnymi i przeze mnie zalecanymi.
Przykład 2 – wewnętrzny kod JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?php require_once 'class.JSCompressor.php'; // konfiguracja $_config = array( 'compress_code' => true, // status kompresji kodu 'cache_enabled' => true, // buforowanie po stronie serwera 'cache_location' => 'tmp/', // folder dla cache 'use_cache_browser' => true, // buforowanie po stronie klienta 'time_cache_browser' => 3600, // czas trzymania w buforze (sekundy) 'gzip_contents' => true, // kompresja gzip 'gzip_level' => 6 // poziom kompresji gzip ); // utwórz instancję klasy $js_compress = new JSCompressor($_config); // dodaj pliki js do złączenia $js_compress->addFile('./file1.js'); $js_compress->addFile('./file2.js'); $js_compress->addFile('./file3.js'); ?> <input type="button" id="but1" value="button1"> <input type="button" id="but2" value="button2"> <input type="button" id="but3" value="button3"> <script type="text/javascript"> <?= $js_compress->showCode('inline'); ?> </script> |
W ten sposób uzyskujemy kod, który możemy wprowadzić bezpośrednio do dokumentu (który jest także buforowany i zapisany na dysku!).
Podsumowanie
No i to by było na tyle, mamy gotowy kompresor JavaScript!
Nie było to najtrudniejsze zadanie, tym bardziej, że wszystkie bardziej logiczne funkcje zlecane są zewnętrznej klasie (opracowanej przez kogoś dużo lepszego ode mnie).
Moja klasa dodaje jedynie kolejną warstwę abstrakcji do istniejącej już klasy, implementując przy tym buforowanie, cachowanie, gzip i kilka innych dodatków. Na moje potrzeby jest to w zupełni wystarczające. Mam też nadzieję, iż komuś jeszcze przyda się stworzony tutaj kod :-)
Chciałbym tutaj także wspomnieć o zaleceniu, które mówi o umieszczaniu kodu JavaScript na dole strony. Więcej info w poradach Yahoo: Best Practices for Speeding Up Your Web Site.
Bez cache’owania taka kompresja nie ma racji bytu…
Sam pozostałem przy łączeniu w jeden plik i gzipie (o czym wiesz :D)
Michał, ale tu jest i było zaimplementowane buforowanie :-)
Widzę. Z cache’owaniem – jest jak najbardziej ok, bez – średnio to widzę.
Ale to moje subiektywne blabla.
Musze powiedzieć narzędzie prawie doskonałe tak samo jak kompresor kodu css. Wielkie brawa dla autora. Proponuję jednak nieco udoskonalić kod, a mianowicie skrypt jest wykonywany za każdym razem więc za każdym razem zużywamy nieco mocy obliczeniowej a można wynik działania zapisać do pliku, ponieważ nie zmienia się kodu codziennie albo kilka razy dziennie, W przypadku kiedy przeglądarka pyta o plik my sprawdzamy jego aktualność i w nagłówkach informujemy że plik posiadany przez przeglądarkę jest aktualny względem tego co posiadamy lub nie aktualny i tu następuje pobranie pliku przez przeglądarkę… W ten sposób zyskujemy czas na wykonanie skryptu kompresji, transfer na wysyłanie i moc obliczeniową. Oczywiście to tylko moja sugestia i w zasadzie można tylko zmodyfikować to co już jest… na potrzeby tego co napisałem.
@GuruZjeb: a przeczytałeś wpis? Słowo klucz: Cache_Lite ;) w kompresorze CSS dokładniej opisałem proces keszowania – tutaj po prostu zaimplementowałem odpowiedni kod, bez większego opisu, by się nie powtarzać.
Kamilu nie zrozumieliśmy się, miałem na myśli cash dla skompresowanego już gzip’em kodu co zaoszczędziłoby czas wykonywania skryptu… To właśnie miałem na myśli, bo z tego co sprawdziłem to w folderze /tmp przechowywana jest wartość złączonego kodu kilku skryptów, ale jest to oczyszczony i złączony kod kilku plików… ale nie skompresowany gzip’em. Tak czy owak kawał dobrej roboty z Twojej strony ja pozwoliłem sobie na kilka eksperymentów z Twoim kodem i dodaniem kilku drobnych usprawnień o czym zapewne niebawem Cię poinformuje. Warto również wspomnieć że w przypadku tego skryptu kolejność podawania argumentów (plików do kompresji) ma ogromne znaczenie, to taka dygresja dla mniej zorientowanych…
Biblioteka działa w następujący sposób:
1) łączy i minifikuje kod kilku plików w jeden,
2) wygenerowany plik jest zapisywany do cache,
3) w momencie wejścia użytkownika na stronę – plik trafia do cache jego przeglądarki na time_cache_browser czasu.
Czyli plik jest gzipowany i leci do klienta przeglądarki i tam siedzi jakiś czas. Gdy czas minie to znowu odwołuje się do biblioteki – ta sprawdza czy którykolwiek z plików został zmodyfikowany i jeśli tak – generuje nowy złączony i zminifikowany plik.
Btw. z przyjemnością zobaczę zmodyfikowany/usprawniony kod :) już dawno temu sam też chciałem trochę poprawić ten kod (i dodać kilka ulepszeń), bowiem używam go w 90% swoich projektów.
[…] – doczytywane są one asynchronicznie. To nowe podejście, bowiem wcześniej najczęściej łączyło się wszystkie pliki w jeden wielki i każdorazowo go wczytywało (co też ma swoje zalety i czasem się przydaje, zwłaszcza […]