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:

npm install –save-dev gulp-rev

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

var gulp = require('gulp');
var rev = require('gulp-rev');

A także dodajmy podstawowe zadanie Gulp:

gulp.task('default');

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

gulp.task('cssHash', function() {
  return gulp.src('./css/styles.css')
    .pipe(gulp.dest('./css/dist'))
    .pipe(rev())
    .pipe(gulp.dest('./css/dist'))
    .pipe(rev.manifest())
    .pipe(gulp.dest('./css/dist/'));
});

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:

npm install –-save-dev gulp-inject

I dodajmy do naszego projektu:

var inject = require('gulp-inject');

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

<!-- inject:css -->
<!-- endinject -->

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:

gulp.task('inject', function() {
  var target = gulp.src('./index.html');
  var source = gulp.src('./css/dist/*.css', { read: false });

  return target.pipe(inject(source, { relative: true }))
    .pipe(gulp.dest(''));
});

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:

gulp.task('inject', ['cssHash'], function() { ... };

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:

var fs = require('fs');
var jsonCss;

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

gulp.task('inject', ['cssHash'], function() {
  jsonCss = JSON.parse(fs.readFileSync('./css/dist/rev-manifest.json'));
  var target = gulp.src('./index.html');
  var source = gulp.src('./css/dist/' + jsonCss['styles.css'], {read: false});

  return target.pipe(inject(source, {relative: true}))
    .pipe(gulp.dest(''));
});

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:

npm install –save-dev gulp-clean

A następnie dodajmy do pliku gulpfile.js:

var clean = require('gulp-clean');

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:

gulp.task('delete', ['inject'], function () {
  ...
}

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:

return gulp.src(['css/dist/*.css', '!css/dist/' +jsonCss['styles.css']])
  .pipe(clean());

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:

gulp.task('default', ['delete']);

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:

gulp.task('watch', function() {
  gulp.watch('css/styles.css', ['delete']);
});

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.