Polub bloga na fejsie!

Hashowanie plików CSS za pomocą Gulp

17

To już kolejny wpis gościnny na tym blogu! Tym razem jego autorem jest mój kolega – Kacper Tylenda – który jest właśnie na etapie nauki programowania (i całkiem nieźle mu to idzie). Aktualnie Kacper poznaje tajniki pracy front-end developera, w tym takie narzędzia jak Gulp. Stąd też wziął się pomysł na wpis – hashowanie plików CSS – przedstawiający całkiem realny problem z jakim spotkał się ten „młody padawan”… Zapraszam do lektury!

Na irytujące problemy przy zarządzaniu projektem lekarz zalecił mi regularne korzystanie z Gulpa!

Wyobraźcie sobie sytuację – zaczynacie swoją przygodę z webdevelopmentem, google pracuje na pełnych obrotach, stackoverflow ledwo wytrzymuje Waszą ilość zapytań do bazy danych. Chcecie sprawdzić jaki jest rezultat Waszej walki w Chrome i….nic nie działa. Odświeżacie stronę. Dalej nic. I tak mijają Wam kolejne godziny… Okazuje się jednak, że od samego początku Wasz kod był dobry. Pliki js i css trafiły do cache i widzieliście starą wersję Waszego projektu.

Uwierzycie, bądź nie ale kiedy pierwszy raz spotkała mnie taka sytuacja straciłem 1,5 godziny żeby znaleźć błąd, który nigdy nie istniał! Wtedy dowiadujecie się, że zawsze powinniśmy używać trybu incognito. Problem wygląda na rozwiązany dopóki nie spotka nas ponownie taka sytuacja i tryb incognito w żaden sposób tutaj nie pomaga. Dzisiaj wytłumaczę jak pozbyć się tego problemu raz na zawsze używając kilku pluginów z Gulpa.

Rozwiązanie

Nasze zadanie – za pomocą wtyczek dostępnych w Gulpie pozbyć się problemu cache’owania plików CSS naszego projektu w przeglądarce. Jednak sposób, którego użyjemy może nam się przydać w sytuacji kiedy chcemy aby goście na naszej stronie zawsze widzieli wersję, którą chcemy im pokazać a nie tę zapamiętaną przez Chrome.

Pomijam w tym poradniku sposób instalacji gulpa w naszym projekcie. Początkującym polecam jeden z wielu tutoriali znajdujących się na YouTube [na tym blogu też kiedyś na ten temat pisałem – przyp. red.].

bierzemy się do pracy!

Skoro więc mamy już Gulpa zainstalowanego w naszym projekcie przejdźmy do rzeczy!

Najłatwiejszy sposób jaki udało mi się wymyślić to dodawanie hashów do naszych plików CSS i JavaScript. Jest kilka pluginów, które wykonają to za nas i każdy z tych pluginów tworzy kolejne problemy, które będziemy musieli rozwiązać po drodze.

W tym poradniku przedstawię jak hashować pliki css i postaram się wytłumaczyć w jaki sposób możemy dalej rozwijać nasz gulpfile.js by robił to czego od niego oczekujemy.

Plugin gulp-rev

Do hashowania plików będzie nam potrzebny plugin o nazwie gulp-rev. Zainstalujmy go w naszym projekcie używając terminala/konsoli:

Utwórzmy teraz nasz gulpfile.js i zaimportujmy biblioteki gulp oraz gulp-rev do pliku:

A także dodajmy podstawowe zadanie Gulp:

Przygotujmy teraz zadanie, które doda hashe do naszych plików CSS:

POLUB BLOGA NA FACEBOOKU!

Chcesz być na bieżąco informowany o nowościach na blogu oraz innych ciekawych treściach? Polub fanpage bloga na Facebooku!

Całość zaczyna się standardowo – tworzymy zadanie o nazwie cssHash i w funkcji jako źródło podajemy plik z naszymi stylami. Jeśli posiadamy więcej plików możemy oczywiście w gulp.src podać ./css/*.css i wszystkie pliki znajdujące się w tym katalogu otrzymają swoje „hashe”. W linii 3 podajemy ścieżkę docelową naszych plików. Trzy ostatnie linie określają gdzie ma wylądować raport z tego zadania (tutaj ten sam folder co hashowane pliki CSS). Do czego będzie potrzebny nam ten raport? Do tego dojdziemy już za chwilkę.

Plugin gulp-inject:

Przede wszystkim plugin gulp-rev jedynie dodaje hash do plików i w tym momencie pojawia się problem – w jaki sposób wstawić automatycznie plik CSS do sekcji head w pliku HTML. Dobrym rozwiązaniem będzie tutaj skorzystanie z kolejnej wtyczki Gulpa o nazwie gulp-inject.

Zainstalujmy ją zatem standardowo:

I dodajmy do naszego projektu:

Setup tej wtyczki będzie odrobinę bardziej skomplikowany. Najpierw do naszego pliku html dodajmy, pomiędzy znacznikami head, taki oto kod:

To w tym miejscu plugin gulp-inject będzie „wstrzykiwać” odwołania do plików. Teraz wracamy do naszego gulpfile.js i tworzymy nowe zadanie, które nazwiemy inject:

Ustalamy źródło na nasz folder css/dist/ do którego mają trafiać nasze hashowane pliki. Bardzo ważną kwestią jest użycie { relative: true }. Bez tego ścieżki do naszych plików będą tworzone niepoprawnie. Aby zrozumieć problemy jakie powstały po dodaniu tego pluginu musimy cofnąć się do podstaw samego Gulpa i jego zalet, które w tej chwili będą dla nas wadami

Gulp jest bardzo szybkim narzędziem dzięki wykonywaniu zadań asynchronicznie. W naszej sytuacji zadanie nie może wykonywać się asynchronicznie dlatego, że jeśli task cssHash zakończy się po tasku inject do naszego pliku HTML trafi stara wersja pliku css! Aby uporać się z tym problemem musimy dodać task cssHash jako zależność w zadaniu inject. Pierwsza linia po modyfikacjach wygląda w ten sposób:

Wróćmy jednak do pierwotnego zapytania – po co nam raport z wykonania zadania cssHash? Otóż wtyczka ta działa w ten sposób, że po zmianie w pliku/plikach CSS zmienia rownież ich hash, wynik zapisuje do katalogu docelowego, a w pliku raportu rev-manifest.json umieszcza nazwę najnowszego zhashowanego pliku. Niestety, przy każdej operacji hashowania tworzony jest nowy plik, a stare nie są usuwane (tym zajmiemy się później). Jak więc widzisz, informacje z raportu będą nam bardzo potrzebne bo nie chcemy przecież żeby inject wstawiał nam setki plików CSS do pliku index.html. W tym celu musimy wczytać plik rev-manifest.json w pliku gulpfile.

Dynamiczne ładowanie raportu

W tym momencie moglibyśmy użyć standardowego require() aby załadować zawartość pliku do zmiennej, jednak w ten sposób przy uruchomieniu watchera będziemy musieli za każdym razem kasować cache, w którym zawartość tego pliku jest przechowywana. Z pomocą przyjdzie nam tutaj funkcja fs.readFileSync(), która będzie odczytywać zawartość pliku raportu po każdym wywołaniu zadania przez watcher.

Aby to zrobić, najpierw na początku pliku gulpfile.js zaimportujmy odpowiedni pakiet oraz zadeklarujmy zmienną jsonCss:

Następnie zmodyfikujemy zadanie inject w poniższy sposób:

Jak widać najpierw wczytujemy zawartość pliku rev-manifest.json, a następnie parsujemy go do zmiennej jsonCss.

W tym momencie, gdy w konsoli wpiszemy gulp inject to wszystko zacznie działać 
- do pliku *.css jest dodawany hash, a do pliku index.html dodawana jest poprawna ścieżka do tego pliku.

Sprzątanie – plugin gulp-clean

Główny cel został osiągnięty ale teraz musimy po sobie posprzątać. Jak już wspomniałem, a Ty też zapewne zauważyłeś, w folderze dist/ będą powstawać nowe pliki z hashami. Stare niestety w nim pozostaną… Po całym dniu pracy odnalezienie się w tym bałaganie będzie bardzo trudne. Jak zatem poradzić sobie z tą sytuacją? Nie wątpię, że już się domyślasz. Za pomocą kolejnego pluginu Gulp!

Najpierw go zainstalujmy:

A następnie dodajmy do pliku gulpfile.js:

Plugin ten będzie czyścił folder dist z niepotrzebnych (starych) plików. W tym momencie kluczowy będzie timing – plugin gulp-clean może sprzątać dopiero po utworzeniu nowego pliku z hashem, a także po wstawieniu go do naszego index.html: Otwórzmy więc nowe zadanie o nazwie delete:

W tym momencie wywołując zadanie delete (np. poprzez gulp delete w konsoli) wywołujemy najpierw taska inject, a ten z kolei cofa się aż do zadania cssHash. Kolejność zachowana jednak to nie wszystko. Musimy ponownie pobierać zawartość rev-manifest.json żeby wykluczyć znajdujący się w nim plik z czyszczenia. Dodajmy odpowiednią implementację do zadania delete:

Jak widzisz, pierwsze co robimy to, jak zwykle, podajemy Gulpowi źródła plików, na których ma pracować. W pierwszym elemencie przekazanej mu tablicy podajemy ścieżkę do wszystkich plików *.css znajdujących się w katalogu dist. W drugim parametrze, poprzez dodanie przed ścieżką znaku ! informujemy Gulpa jaki plik ma wykluczyć. Jest to oczywiście aktualny, zhashowany plik CSS, którego nazwę pobieramy z rev-manifest.json.

Na koniec dodajmy teraz nasze zadanie delete jako zależność taska default:

Przydałoby się jeszcze skonfigurować watchera, który będzie nam to wszystko wykonywać także w trakcie naszej pracy, po zmianie któregoś z plików CSS:

I to wszystko! Hashowanie plików CSS przy użyciu Gulp skonfigurowane i gotowe do pracy!

Hashowanie plików CSS – podsumowanie

Jak więc widzisz hashowanie plików CSS w Gulpie jest nie dość, że proste to jeszcze bardzo przydatne! Po wykonaniu powyższych kroków mamy pewność, że Chrome nigdy nas nie zaskoczy swoimi nieprzewidywalnymi zachowaniami!

Oczywiście to tylko podstawowa wersja – powinniśmy jeszcze dodać te same zadania dla plików *.js (w taskach możemy dodawać więcej niż jedno zadanie jako zależność, np. gulp.task('inject', ['cssHash', ‘jsHash’], function() { ... } itd.). A jeśli używacie gulp-ruby-sass uwzględnijcie to w kolejce. Mam nadzieję, że pomogłem choć kilku osobom…

P.S. Jeśli chcesz zobaczyć cały skrypt to stworzyłem odpowiedniego „gista” na GitHubie.

REACT, REDUX, REACT-ROUTER - KURSY ON-LINE

Chcesz od podstaw poznać tajniki React, Redux oraz react-router? Zapraszam do moich szkoleń on-line:

Przejdź do szkoleń

Uwaga! Obecnie trwa przedsprzedaż kursów - premiera 1 sierpnia 2017!

  • Kamil Rogala

    fajne nie powiem- dojście do takiego rozwiązania to nie lada wyczyn jak na początkującego 🙂

    warto obadać jeszcze taki myk dla zasobów https://www.npmjs.com/package/gulp-css-assets-hash

    • Kacper Tylenda

      Dzieki 🙂
      Samych pluginow do hashowania jest kilka, nie badalem jak one dzialaja ale ich efekt jest w zasadzie za kazdym razem taki sam – dalej trzeba injectowac pliki do html samemu i to samo z usuwaniem – zaden nie robi porzadku. Przynajmniej ja nie moglem nic takiego znalezc.

  • Piotr Jańczak

    Sposób drugi, inny, mający swoje wady, ale i zalety:
    http://imgur.com/a/fgFMB

    • oczywiście, można i tak – ale hashowanie i tak się przydaje: chociażby przy wysyłaniu plików na produkcję mamy pewność, że wszystkim użytkownikom wyświetli się nowa wersja

  • Fajne.
    Chrome też można wyłączyć cachowanie 😉 Dev tools -> Network -> Disable cache
    Edit
    O, nie zauważyłem, że poniżej w screenie jest to samo 😀

  • Ja u siebie, w Gruncie, załatwiłem to tak: https://github.com/Comandeer/comandeers-homepage/blob/master/grunt/less.js 😉

    • Kacper Tylenda

      Nigdy nic nie robilem Gruncie. Czy tutaj po prostu nie dodajesz timestampu zamiast losowego hashu? W pluginie gulp-rev mozna ustalic jak ma wygladac hash i jest tam opcja timestamp. A jak to potem wrzucasz do html?

      • Tak, timestamp – co sprawia, że w sumie to jest losowy (unikalny) hash 😉

        Myk jest prosty – każdy build ma inny timestamp, który jest używany globalnie we wszystkich zadaniach – stąd wiadomo, jakie ścieżki do plików otrzymamy.

  • Łukasz Korowicki

    Wydaje mi się, że jednym z najprostszych, a zarazem najlepszych sposobów, jest zwykły timestamp:
    https://css-tricks.com/can-we-prevent-css-caching/

  • Sebastian Wojdyga

    Dużo prościej jest użyć funkcji filemtime – timestamp ostatniej modyfikacji pliku – wtedy na produkcji obsługujemy cache przeglądarki, bo wartość ta się zmienia dopiero, kiedy wprowadzimy jakieś zmiany w plikach.

    PS. Do odświeżania strony polecam skrót Ctrl + F5 na Windowsach, na Macach Cmd + Shift + R – wtedy odświeżamy stronę wraz z cachem.

    • hash tez się zmienia tylko kiedy dokonamy zmiany w plikach…

    • Kacper Tylenda

      korzystam zazwyczaj z gulp-livereload do odświeżania w chrome co bez hashów prowadziło często do szewskiej pasji 😉 i tak jak Bartek pisał – hash zmienia się tylko w przypadku zmiany zawartości pliku. Co mnie zaskoczyło przy powrocie do wcześniejszej wersji pliku wracał również hash tej wersji.
      pozdrawiam

  • Mokry

    A nie prościej jest po prostu w kodzie do nazwy pliku dokleić md5(filemtime($filename))?
    Mamy wtedy unikalne hash’e dla plików, które zmieniają się tylko w przypadku modyfikacji plików CSS/JS. Hash’owanie ostatniej daty modyfikacji pliku daje nam przy okazji ukrywanie ostatniej aktualizacji systemu aplikacji web’owej.

    • a co w przypadku gdy pracujesz nad statyczną stroną HTML? Twoje rozwiązanie wymaga kodu wykonywanego po stronie serwera, nie mówiąc już o tym, że funkcja md5 musiałaby się wykonywać przy każdym requeście – pytanie po co, skoro można to zrobić raz podczas edycji pliku

      • Mokry

        Co do strony w czystym HTML masz rację, choć trzeba sobie zadać pytanie, które strony dzisiaj nie są już stawiane z użyciem jakiegokolwiek interpretatora po stronie serwera?
        Co do narzutu na funkcję md5() to bez przesady… Nie robimy hash’a z zawartości całego pliku tylko z daty jego ostatniej modyfikacji. W tym przypadku wpływ na wydajność jest praktycznie zerowy.

        Innym rozwiązaniem jest jeszcze korzystanie z systemów dostarczania treści (CDN). W takim przypadku to mechanizmy CDN’a zadbają o to aby zawsze dostarczać i wymuszać najnowsze wersje plików (po nagłówkach). Dodatkowo zyskujemy oszczędność transferu 😉

        • > Co do strony w czystym HTML masz rację, choć trzeba sobie zadać pytanie, które strony dzisiaj nie są już stawiane z użyciem jakiegokolwiek interpretatora po stronie serwera?

          Nawet Smashing Magazine próbuje przejść na Jekylla, więc prawdopodobnie trend odrzucania backendu na rzecz generatorów statycznych stron będzie się pogłębiał.

Google Analytics Alternative