Gulp

W poniższym tekście zajmiemy się Gulpem. Będzie to jeden z kilku tekstów dotyczących ustawiania środowiska pracy jakie sam często stosuję w pracy. Przyda się w jednym z kolejnych artykułów o wordpressie, gdzie będziemy korzystać z takich technik.

Gulp jest menadżerem tasków, który po odpaleniu wykonuje stworzone przez nas zadania. Łączenie oddzielnych plików ze skryptami w jeden plik (dla zmniejszenia requestów), minimalizacja skryptów, przetwarzanie html, kompilacja scss, kopiowanie plików, optymalizacja obrazków itp – czego dusza zapragnie, a właściwie czego nam w danej chwili potrzeba. Dodatkowo możemy odpalić w nim śledzenie zmian plików. Dzięki niemu gulp będzie pracował w tle, a jeżeli wykryje zmiany na danych plikach, wykona odpowiednie dla danego typu plików zadanie (np. po zmianie w scss skompiluje je do css, po zmianie grafiki zoptymalizuje ją itp.).

Instalacja Node JS

Gulp działa pod Node JS, dlatego musimy je wcześniej zainstalować. Wchodzimy na stronę https://nodejs.org/en/ i pobieramy odpowiednią dla naszego systemu wersję. Dla Windows i OsX z instalacją raczej nie ma problemów.

Dla Ubuntu instrukcja instalacji opisana jest na stronie https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions.

Często gęsto taka instalacja może jednak powodować później problemy z instalacją globalnych modułów (np. gulp-cli w poniższym artykule), ponieważ są one instalowane w katalogu, do którego instalacja danego modułu nie ma uprawnień, co zmusza nas do nadużywania polecenia sudo.

Pomaga tutaj skorzystanie z podpowiedzi ze strony https://docs.npmjs.com/getting-started/fixing-npm-permissions (szczególnie pierwsza opisana metoda, która korzysta z nvm). Zostało to też opisane w wątku na StackOverlow.

Wchodzimy więc na stronę https://github.com/creationix/nvm#installation i korzystając z wget instalujemy nvm. Restartujemy terminal (zamykamy i otwieramy ponownie). Następnie instalujemy node poleceniem nvm install node. Od tej pory nie powinno być problemów z instalacją paczek.

Instalacja Gulpa

Sama instalacja Gulpa składa się z trzech kroków: instalacji konsoli gulpa globalnie na danym komputerze (coś jak bash dla gita w windowsie), dodaniu modułów gulpa do danego projektu i stworzeniu konfiguracji dla gulpa w projekcie.

Pierwszy krok wykonujemy według instrukcji z https://gulpjs.com/. Czyli tak naprawdę wpisujemy w konsoli polecenie

npm i gulp-cli -D

Gdzie i jest alternatywą dla install, a -D jest alternatywą dla --save-dev.

Od tej pory będziemy mogli używać klienta Gulpa na danym komputerze.

Dodanie Gulpa do projektu

Powyżej zainstalowaliśmy rzeczy globalnie na cały komputer. Podobnie zresztą jak w przypadku gita – którego też przecież się instaluje na komputerze. Od tej pory – podobnie jak w przypadku git – zaczynamy działać per projekt.

Jeżeli chcemy używać gita w danym projekcie, powinniśmy go w nim zainicjować poleceniem „git init”. Podobnie jest z node i npm. Jeżeli chcemy ich używać w naszym projekcie, powinniśmy zainicjować je w głównym katalogu naszego projektu, co utworzy w nim plik package.json.

Robimy to poleceniem npm init w konsoli będąc w głównym katalogu projektu. Ważne by nasz katalog nie zawierał w nazwie spacji czy dużych liter. Odpowiadamy na kolejne pytania naciskając kilka razy enter (chyba że naprawdę chcemy to wypełniać…). Alternatywą jest wpisanie npm init -y, które to polecenie spowoduje, że nie będziemy musieli na nic odpowiadać. Zostanie utworzony plik packages.json, który będzie zawierał informacje, które właśnie podaliśmy. Spokojnie możemy go edytować ręcznie…
Uwaga. Podczas tego procesu czytaj komunikaty – bardzo często proces staje w miejscu, bo nazwa projektu nie może zawierać dużych liter, a jest automatycznie brana z nazwy katalogu, który ma dużą literę w nazwie – wystarczy to ręcznie poprawić wpisując cokolwiek

Aby dodać Gulpa do naszeg projektu, dalej będąc w głównym katalogu projektu wpisujemy w terminalu polecenie

npm i gulp -D

Polecenie to zainstaluje gulpa w katalogu node_modules, oraz doda do pliku packages.json wpis o gulpie. Warto do tego pliku zaglądać by mieć pewność, że wszystko ładnie się dodaje do naszego projektu. Podobnie będziemy do niego dodawać wszystkie moduły, które będziemy wykorzystywać przy pracy z gulpem.

Plik packages.json poza podstawową konfiguracją zawiera informacje o tym, co w danym projekcie jest zainstalowane (sekcja devDependencies w powyższym przykładzie). Jeżeli instalujemy jakiś moduł np
npm i gulp -D, to zostanie od dodany właśnie do tej sekcji.

{
  "name": "nasz-projekt",
  "version": "0.0.1",
  "devDependencies": {
    "gulp": "^3.9.1",
  }
}

Takich paczek a co za tym idzie wpisów może być wiele. Jeżeli teraz inna osoba ściągnie nasz projekt z plikiem packages.json nie będzie musiała się przebijać przez to co my. Zamiast tego wystarczy, że użyje polecenia npm i, co automatycznie zainstaluje u niej wszystkie moduły wypisane w tym pliku.

Konstrukcja projektu i sposób pracy

Zanim napiszemy nasze pierwsze zadania, spójrzmy na strukturę naszego projektu i sposób pracy.

src - tutaj trzymamy nasze pliki źródłowe
   scss
      _inny_partial.scss
      _inny_partial.scss
      _mixiny-dziewczyny.scss
      _ititaki-inne-raki.scss
      style.scss
   js
      inny_skrypt.js
      inny_skrypt.js
      scripts.js

dist - tutaj trzymamy finalną wersję strony
   css - tutaj trafią skompilowane css
   js - tutaj trafią transpilowane js
   images - tutaj trafi grafika
   index.html - ten plik będzie naszą stroną

W naszej pracy wszystkie pliki źródłowe będziemy trzymać w katalogu src. Za pomocą gulpa będziemy je kompilować do katalogu dist w odpowiednie miejsca. W katalogu dist będzie więc nasza wynikowa strona. Spokojnie możemy to zmienić i na przykład mieć tylko katalog src, z którego będziemy wrzucać wynik do katalogu głównego. Ja osobiście lubię powyższy podział, bo dzięki niemu jeżeli zajdzie potrzeba szybkiego przerzucenia końcowej strony na serwer wiem, że znajdę ją w katalogu dist, wiec nie muszę odznaczać katalogów z plikami źródłowymi.

Spojrzenie na plik gulpfile.js

Poza dodaniem Gula do projektu, musimy ręcznie stworzyć plik z konfiguracją zadań, czyli ręcznie utworzyć plik gulpgile.js w głównym katalogu projektu. Tworzymy więc ten plik i dodajemy do niego kod:

//dołączamy do konfiguracji moduły
const gulp = require("gulp");
...
...

//opis, konfiguracja tasków
gulp.task("nazwa_taska", function() {
    //tutaj jakies czynnosci taska np.:
    console.log("Tekst który pojawi się w konsoli");
});

//głowny task odpalany gdy w konsoli użyjemy polecenia "gulp" bez podania nazwy tasku
//odpalamy w nim to co chcemy odpalać domyślnie
gulp.task("default", ["nazwa_taska", "inna_nazwa_taska"]);

Na samym początku dołączamy do pliku moduły z których będziemy korzystać. Zawsze będzie do moduł gulp, oraz wszystkie inne, których użyjemy (gulp-concat, gulp-sass, gulp-uglify, gulp-autoprefixer, browser-sync itp).

W dalszej kolejności tworzymy już właściwe zadania. Takie zadania będziemy potem odpalać z konsoli za pomocą polecenia

gulp nazwa_taska

W większości przypadków wygląd tasków będzie miał postać:

gulp.task("nazwa_taska", function() {
    return gulp.src("pliki_zrodlowe")
        .pipe(...)
        .pipe(...)
        .pipe(gulp.dest("katalog_docelowy"))
});

W pierwszym kroku zadania najczęściej będą pobierać pliki na których będą przeprowadzać operacje. Służy do tego funkcja gulp.src. Funkcja ta wymaga podania plików źródłowych, które mogą być przedstawione za pomocą jednego „wyrażenia”, lub tablicy wyrażeń.

Poniżej kilka przykładowych zapisów

gulp.src("src/scss/style.scss"); //pobierz konkretny plik
gulp.src("src/scss/*.scss"); //pobierz wszystkie pliki .scss z tego katalogu
gulp.src("src/scss/**/*.scss"); //pobierz wszystkie pliki .scss z tego katalogu i podkatalogów
gulp.src("src/scss/*.+(scss|sass)"); //pobierz wszystkie pliki .scss i .sass z tego katalogu
gulp.src(["src/scss/style.scss", "src/scss/style2.scss"]); //pobierz 2 pliki
gulp.src(["src/js/**/*.js", "!src/js/test.js"]); //pobierz wszystkie pliki js oprócz pliku test.js
gulp.src(["*.js", "!temp*.js", "tempest.js"]); //pobierz wszystkie pliki js, oprócz tych zaczynających się na temp, ale pobierz tempest.js

Następnie za pomocą poleceń .pipe() będą wykonywać kolejne operacje na pobranych plikach. Takie operacje wykonywane są w streamie. Pobieramy plik, wykonujemy na nim kilka operacji, a następnie go zapisujemy za pomocą polecenia gulp.dest().

Czasami niektóre zadania będą też odpalać inne za pomocą polecenia gulp.start(). Pokazane jest to w końcowej części powyższego przykładu.

Tak naprawdę więcej nam nie potrzebna, bo powyższy przykład opisuje większość przypadków. Wszystko wyjdzie w dalszej pracy.

SCSS / CSS

Zacznijmy od zadania służącego do kompilacji SCSS na CSS. Do tego celu wykorzystamy moduł https://www.npmjs.com/package/gulp-sass, którego zadaniem jest kompilujacja scss do css.

Instalujemy go poleceniem ze strony:

npm i gulp-sass -D

A następnie napiszmy nasz pierwszy task służący do kompilacji scss na css:


const gulp          = require("gulp");
const sass          = require("gulp-sass");


gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(
            sass({
                outputStyle : "compressed" //styl kodu - extended, nested, copressed, compact - i tak chcemy compressed
            })
        )
        .pipe(gulp.dest("dist/css"));
});


gulp.task("default", ["sass"]);

I tyle. W tej chwili gdy w konsoli terminala odpalimy polecenie gulp sass lub gulp odpali się task który skompiluje plik src/scss/style.scss do dist/css/style.css.

Co do ostatniego tasku. Alternatywnie można go napisać tak:


gulp.task("default", function() {
    console.log("======================= start =======================");
    gulp.start(["sass"]);
});

Co daje nam dodatkowe możliwości upiększania startu pracy. W interenecie piszą że metoda start() umyślnie nie widnieje w dokumentacji by ludzie jej nie nadużywali. No ok. Działać działa. My w listingach zostaniemy przy mniej pięknym ale zalecanym podejściu. Wiąże się to też ze sprawami migracji na gulp 4. W nowej wersji pojawią się 2 sposoby odpalania tasków – gulp.series i gulp.parallel. Stosując pierwszy zapis taka migracja będzie o wiele łatwiejsza, bo nic się praktycznie nie zmieni. Na chwilę obecną jest to jeszcze pieśń przyszłości. Niby 4 wersji używać można, ale wciąż tam coś mącą, a i dokumentacja woła o pomstę do nieba. Poczekamy zobaczymy…

SourceMaps

Nasz powyższa konfiguracja sprawia, że całe wynikowe style są spakowane w 1 linijkę. To bardzo dobrze, bo wielkość takiego pliku jest mała. To bardzo źle, bo badając w debugerze dowolny element na stronie debuger zawsze pokaże „1 linijka w style.css”, przez co debugowanie staje się niemożliwe.
My chcemy by debuger wksazywał realne miejsce – czyli konkretną linijkę w SCSS. Aby to naprawić powinniśmy skorzystać z mechanizmu sourcemaps, którzy rzutuje kod źródłowy na wynikowy. Cała sprawa dzieje się za naszymi plecami więc wystarczy go dodać do naszej konfiguracji. Instalujemy go poleceniem:

npm i gulp-sourcemaps -D

I dodajemy do powyższego zadania:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init()) //inicjalizacja sourcemap przed zabawa na plikach
        .pipe(
            sass({
                outputStyle : "compressed" //styl kodu - extended, nested, copressed, compact - i tak chcemy compressed
            })
        )
        .pipe(sourcemaps.write(".")) //po modyfikacjach na plikach zapisujemy w pamieci sourcemap
        .pipe(gulp.dest("dist/css"));
});


gulp.task("default", ["sass"]);

Normalnie sourcemaps rzutowanie kodu zapisuje w postaci komentarza w samym pliku css (na jego końcu). Jeżeli nam to przeszkadza i takie rzutowanie chcemy mieć w osobnym pliku, wskazujemy relatywne miejsce w pierwszym parametarze metody write – zwróć uwagę w powyższym kodzie na pojedynczą kropkę. Dzięki temu tuż obok style.css pojawi się plik style.css.map.

Autoprefixer

No dobrze – jedziemy dalej.
Do naszej kompilacji dodajmy 2 rzeczy. Po pierwsze nie chcemy myśleć o prefixach (-webkit-, -moz- itp). Nasz kod powinniśmy pisać ładnie bez śmiecenia, a odpowiednie prefixy powinny być dodawane w wynikowych css za naszymi plecami.

Wchodzimy na stronę https://www.npmjs.com/package/gulp-autoprefixer

i instalujemy autoprefixera poleceniem:

npm i gulp-autoprefixer -D

a następnie dodajemy go do naszego zadania:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init()) //inicjalizacja sourcemap przed zabawa na plikach
        .pipe(
            sass({
                outputStyle : "compressed" //styl kodu - extended, nested, copressed, compact - i tak chcemy compressed
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        })) //autoprefixy https://github.com/browserslist/browserslist#queries
        .pipe(sourcemaps.write(".")) //po modyfikacjach na plikach zapisujemy w pamieci sourcemap
        .pipe(gulp.dest("dist/css"));
});


gulp.task("default", ["sass"]);

W powyższej konfiguracji autoprefixera wybrałem przeglądarki, które mają >5% na rynku amerykańskim. Możliwe zapisy znajdziesz na tej stronie: https://github.com/postcss/autoprefixer#browsers.

Na Ubuntu gulp-autoprefixer może zwracać błąd array coś tam w terminalu. Aby to naprawić, trzeba zaktualizować node poleceniami:

sudo npm cache clean -f
sudo npm install -g n
sudo n 6.11.0

sudo npm rebuild node-sass --force

WATCH

Podczas pracy nad kodem ręczne odpalanie tasków po każdej zmianie w pliku było by bardzo niewygodne. Dlatego użyjemy watch, który jest domyślnie dostępny w gulpie.

Po odpaleniu, watch działa sobie w tle obserwując zmiany na plikach. Gdy takie wykryje (czyli coś nich zmienimy i zapiszemy zmiany), odpali odpowiednie taski, które wcześniej zdefiniowaliśmy, a które mu przekażemy w konfiguracji:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init()) //inicjalizacja sourcemap przed zabawa na plikach
        .pipe(
            sass({
                outputStyle : "compressed" //styl kodu - extended, nested, copressed, compact - i tak chcemy compressed
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        })) //autoprefixy https://github.com/browserslist/browserslist#queries
        .pipe(sourcemaps.write(".")) //po modyfikacjach na plikach zapisujemy w pamieci sourcemap
        .pipe(gulp.dest("dist/css"));
});

gulp.task("watch", function() {
    gulp.watch("src/scss/**/*.scss", ["sass"]);
});

gulp.task("default", ["sass", "watch"]);
Istnieje też oddzielny moduł gulp-watch.
Domyślny gulp watch nie u wszystkich się sprawdza.
Żeby go używać, wystarczy go zainstalować poleceniem npm i gulp-watch -D, a potem dołączyć go do gulpa tak samo jak resztę modułów const watch = require("gulp-watch");.
Samo użycie gulp watch jest identyczne do tego domyślnego, więc reszty kodu nie musimy ruszać.

Obsługa błędów i piękne komunikaty

Gdy teraz odpalimy naszą konfigurację poleceniem gulp, powinna się rozpocząć w terminalu początkowa kompilacja sass na css, a następnie gulp powinien nasłuchiwać zmian w plikach. Można to poznać po mrugającym kursorze w terminalu.

Żeby teraz przerwać działanie obserwowania, w terminalu naciśnij kilka razy Ctrl + C.

Problem pojawi się, gdy podczas odpalenia naszej konfiguracji i pisania kodu sass zrobimy jakiś błąd. A to użyjemy zmiennej której nie ma, a to nie dodamy średnika itp. Node-sass wyrzuci w terminalu bardzo ładny błąd, ale też zakończy nasłuchiwanie.

Błąd przerywający nasłuchiwanie wynika z tego, że napotykając na błąd node-sass nie informuje node.js o zakończeniu procesu, a dokładniej nie emituje zdarzenia „end”. Powoduje to, że gulp nie wykonuje zadania przypisanego do kolejnego pipe. Żeby to naprawić, powinniśmy użyj jednej z kilku technik do naprawy błędów. Ja osobiście najbardziej polecam użycie hydraulika. Jak w życiu. Jak masz problem z rurkami wołaj po hydraulika.

Przechodzimy na stronę https://www.npmjs.com/package/gulp-plumber gdzie widnieje nędzna dokumentacja tego modułu (to standard w przypadku nodowych modułów) i instalujemy tą paczkę poleceniem:

npm i gulp-plumber -D

Po instalacji idąc zgodnie z instrukcją dodajemy plumbera do naszego projektu.


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");

function showError(err) {
    console.log(colors.red(err.messageFormatted));
    this.emmit("end");
}

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(plumber({ //zawsze na początku - przed pierwszą rurką
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
});

...

Do obsługi błędów przez plumbera dodaliśmy osobną funkcję, która emituje wspomniane zakończenie danego pipe. Żeby upiększyć pokazywanie takich błędów, zróbmy jeszcze 2 rzeczy: pokolorujmy je na czerwono i dodatkowo pokazujmy notyfikację w prawym dolnym rogu ekranu (a to zależy od systemu).

Do tego pierwszego musimy zainstalować moduł ansi-colors, a do tego drugiego moduł node-notifier:


npm i ansi-colors node-notifier -D

Dodajemy je do konfiguracji:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier");

function showError(err) {
    notifier.notify({
        title: "Error in sass",
        message: err.messageFormatted
      });

    console.log(colors.red("==============================="));
    console.log(colors.red(err.messageFormatted));
    console.log(colors.red("==============================="));
    this.emit("end");
}

gulp.task("sass", function() {
    ...
});

...

Od tej pory podczas kompilacji gdy zrobimy błąd pojawi się on na czerwono w terminalu oraz wyskoczy nam okienko ze stosowną informacją. Nie każdemu – wszystko zależy jak mamy ustawiony nasz system i jakiego terminala używamy. Niektóre systemy nie mają notyfikacji, a niektóre terminale nie używają kolorowania.

Rename, wait, csso

Trzy mini dodatki, które można dodać. Moduł gulp-rename służy do zmiany nazwy wynikowego pliku. W naszym przypadku do tej pory generowany będzie plik style.css, a my może chcemy dostać style.min.css.

Instalujemy tą paczkę i dodajemy do gulpa:


npm i gulp-rename -D

const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");

function showError(err) {
    ...
}

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(plumber({
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        }))
        .pipe(rename({ //zamieniam wynikowy plik na style.min.css
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
});

...

Drugi dodatek tyczy się niektórych systemów. Podczas pracy może się zdarzyć, że w terminalu pojawi się dziwny komunikat, że node-sass nie mógł uzyskać dostępu do pliku. Ja tak miałem po n-tej aktualizacji Windows10, ale widziałem u znajomych na innych systemach podobny problem. W tym przypadku pomaga zainstalowanie modułu gulp-wait i odpalenie go na początku zadania:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");
const wait          = require("gulp-wait");

function showError(err) {
    ...
}

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(plumber({
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        }))
        .pipe(rename({ //zamieniam wynikowy plik na style.min.css
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
});

...

Trzeci – ostatni dodatek to https://github.com/css/csso. Moduł csso służy do większej optymalizacji styli. My na razie minimalizujemy nasze style. Ale czy dodatkowo optymalizujemy? Raczej nie. Nas to nic nie kosztuje, więc dodajmy dodatkową rzecz, która przeleci przez nasz wynikowy plik i trochę go uporządkuje.

Wchodzimy więc na stronę https://github.com/css/csso i instalujemy ten moduł poleceniem:

npm i gulp-csso -D

a następnie dodajemy go do naszego zadania:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");
const wait          = require("gulp-wait");
const csso          = require("gulp-csso");

function showError(err) {
    ...
}

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(plumber({
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        }))
        .pipe(rename({ //zamieniam wynikowy plik na style.min.css
            suffix: ".min",
            basename: "style"
        }))
        .pipe(csso())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
});

...

I w zasadzie tyle. Stwórz sobie odpowiednią strukturę plików którą pokazałem na początku (możesz też olać to i ściągnąć gotową paczkę z końca tekstu) i spróbuj odpalić powyższą konfigurację (poleceniem gulp). Jeżeli wszystko zadziała, powinien ci się w katalogu dist/css pojawić nowy plik style.min.css, w którym będą zapisane skompilowane, zminimalizowane i zoptymalizowane style. Oczywiście jeżeli cokolwiek napisałeś w src/scss/style.scss. Jeżeli coś pójdzie nie tak daj znać w komentarzach, a tymczasem jedziemy dalej.

BrowserSync

BrowserSync umożliwia automatyczne odświeżanie strony po wykryciu zmian. Czyli my piszemy w edytorze i zwyczajnie olewamy odświeżanie przeglądarki po każdej zmianie w kodzie. Możemy więc na jednym ekranie mieć otwarty edytor, na drugim okno przeglądarki i automatycznie widzieć co się zmienia. Dodatkowo po uruchomieniu browsersync udostępnia on nam adres, na który możemy wejść dowolnym urządzeniem w danej sieci i synchronicznie przeglądać naszą stronę. Synchronicznie czyli jeżeli na danym urządzeniu ciut przewinę stronę, przewinie się ona na wszystkich urządzeniach. Jeżeli rozwinę menu, kliknę w link itp – zrobię to na wszystkich urządzeniach równocześnie.

Aby skorzystać z tych cudów musimy zainstalować browserSynca:

npm i browser-sync -D

BrowserSync działa w „2 trybach”. Jednym z nich jest automatyczne przeładowanie strony (reload). Drugim trybem jest wstrzykiwanie na stronę zmienionych styli bez przeładowywania strony (bardzo pomocne, gdy pracujemy np nad ajaxem). To wstrzykiwanie wykonujemy za pomocą stream.

Aby BrowserSync mógł w ogóle działać, musimy dopisać task, który nam uruchomi server BrowserSynca:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");
const wait          = require("gulp-wait");
const csso          = require("gulp-csso");
const browserSync   = require("browser-sync").create();
...

gulp.task("browseSync", function() {
    browserSync.init({
        server: "./dist", //gdzie postawić serwer
        notify: false, //czy pokazywać notyfikacje o przeładowaniu
        //host: "192.168.0.20", //gdy domyślny adres będzie blokowany przez zaporę, zmień go tutaj na swój
        //port: 3000,
        open: true, //czy automatycznie otwierać stronę w przeglądarce
        //browser: "google chrome" //jaka przeglądarka ma być otwierana - zależnie od systemu - https://stackoverflow.com/questions/24686585/gulp-browser-sync-open-chrome-only
    });
});

...

gulp.task("default", ["sass", "browseSync", "watch"]);

Domyślne na serwerach pierwszym plikiem odpalanym po wejściu na stronę jest index.html. W powyższej konfiguracji wskazujemy, że serwer powinien startować w katalogu dist, dlatego powinien tam się taki plik znaleźć i to właśnie on będzie domyślnie otwierany przez BrowserSync.

Po odpaleniu gulpa (gulp), w terminalu zostaną udostępnione 4 adresy, a nasza strona powinna zostać odpalona w przeglądarce.

Pierwszy z tych adresow to adres na który powinieneś otworzyć (albo przeglądarka sama sobie go otworzyła). Drugi to adres na urządzeń zewnętrznych. Dwa kolejne to UI służące do konfiguracji zachowania się BrowserSynca. Można tam wyłączyć symulację klikania, przewijania itp.

Od razu mówię – BrowserSync nie zawsze zadziała. Zdarza się, że system blokuje dane porty, w tle działa antywirus, który blokuje działanie BrowserSynca. Czasami trzeba powalczyć z antywirusami czy zaporami, czasami trzeba sprawdzić jaki BrowserSync generuje ip (polecenie ipconfig i podobne), czy np. spróbować zmienić domyślny port. Niestety są to bardzo indywidualne sprawy i nie ma ogólnego przepisu.

Powyżej odpaliliśmy serwer BrowserSync. Żeby teraz aktualizować html i css musimy do naszych zadań dodać odświeżanie:


...

gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(plumber({
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            })
        )
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        }))
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css")) //tutaj średnika już nie ma
        .pipe(browserSync.stream({match: "**/*.css"}));
});

gulp.task("watch", function() {
    gulp.watch("src/scss/**/*.scss", ["sass"]);
    gulp.watch("dist/**/*.html").on("change", browserSync.reload);
});

gulp.task("default", ["sass", "browseSync", "watch"]);

Po odpaleniu tych tasków w naszej przeglądarce zostanie załadowana strona z katalogu dist. Każda zmiana w plikach tej strony spowoduje odświeżenie strony w przeglądarce. Fajnie.

JS

Na chwilę obecną jednym z lepszych narzędzi do ogarniania JS jest webpack. Narzędzie to bez problemu możemy połączyć z naszym gulpem, dzięki czemu wszystko odpalimy jednym poleceniem (gulp). Instalujemy więc 2 rzeczy: webpack i webpack-cli. Ten drugi został dodany do webpacka i musimy go doinstalować, bo inaczej webpack będzie krzyczał w terminalu (możliwe, że pozazdrościli gulpowi ich klienta).

W naszym przypadku webpacka chcemy użyć nie tylko do minimalizacji kodu, ale też jego transpilacji na ES5. Żeby to zrobić skorzystamy z babel. Musimy więc nie tylko zainstalować webpack i webpack-cli, ale też moduły babel, babel-preset-env i babel-core

npm i webpack webpack-cli babel babel-core babel-preset-env -D

O samym webpacku i presetach do babela pisałem w artykule http://kursjs.pl/kurs/es6/webpack.php, ale tak naprawdę na necie jest multum tekstów o tych rzeczach.

Po instalacji paczek dodajemy zadanie dla JS do naszego pliku konfiguracyjnego:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");
const wait          = require("gulp-wait");
const csso          = require("gulp-csso");
const browserSync   = require("browser-sync").create();
const webpack       = require("webpack");

...

gulp.task("sass", function() {
    ...
});

gulp.task("es6", function(cb) { //https://github.com/webpack/docs/wiki/usage-with-gulp#normal-compilation
    return webpack(require("./webpack.config.js"), function(err, stats) {
        if (err) throw err;
        console.log(stats.toString());
        cb();
        browserSync.reload();
    })
})

gulp.task("watch", function() {
    gulp.watch("src/scss/**/*.scss", ["sass"]);
    gulp.watch("src/js/**/*.js", ["es6"]);
    gulp.watch("dist/**/*.html").on("change", browserSync.reload);
});

gulp.task("default", ["sass", "js", "browseSync", "watch"]);

Żeby powyższa konfiguracja zadziałała, musimy do naszego projektu dodać dodatkowy plik konfiguracji dla samego webpacka. Tworzymy więc obok pliku gulpfile.js plik webpack.config.js i dodajemy w nim podstawową konigurację webpacka:


module.exports = {
    entry: "./src/js/app.js",
    output: {
        path: `${__dirname}/dist/js`,
        filename: "script.min.js"
    },
    watch: true,
    mode: "production",
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [["env", {
                            targets: {
                                browsers: ["> 1%"]
                            }
                        }]]
                    }
                }
            }
        ]
    }
}

Od tej porty poleceniem gulp odpalimy nie tylko komplację scss, ale i webpacka, który zajmie się dla nas transpilacją JS.

Optymalizacja grafiki

Osobiście nigdy nie tworzę tasków do optymalizacji grafiki. Czemu? Raz, że optymalizacja grafiki nie jest procesem cyklicznym i takie zadania nie są potrzebne non stop. Przygotowując grafiki do cięcia robimy to tylko raz. Po drugie wolę używać bardziej „specjalnych” narzędzi.

Jednym z najbardziej lubianym przeze mnie jest Riot, który umożliwia podgląd optymalizowanej grafiki, dzięki czemu od razu widzę ile stracę na takiej optymalizacji i czy dobrana metoda i format są optymalne. Polecam pobrać wersję portable z powyższej strony.

Drugim częstokroć używanym przeze mnie programem jest https://psydk.org/pngoptimizer, który jest bardzo wygodnym batchem (wspomniany powyżej Riot też ma opcję masowej optymalizacji).

No ale nie każdemu może pasować moje podejście. Wystarczy więc skorzystać z https://www.npmjs.com/package/gulp-image-optimization lub https://www.npmjs.com/package/gulp-imagemin

Praca z taskami

Po napisaniu naszej konfiguracji czas ją wypróbować. W 99% procentach zaczynam pracę poleceniem gulp, który skompiluje js, scss, odpali browserSync i zacznie nasłuchiwać plików.
W niektórych przypadkach nie chcę odpalać watcha i podglądu strony – wtedy odpalam gulp sass, lub gulp js. To wszystko.

Gotowy plik Gulpa

Cały kod wygląda teraz tak:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
const plumber       = require("gulp-plumber");
const colors        = require("ansi-colors");
const notifier      = require("node-notifier")
const rename        = require("gulp-rename");
const wait          = require("gulp-wait");
const csso          = require("gulp-csso");
const browserSync     = require("browser-sync").create();
const webpack         = require("webpack");

function showError(err) {
    notifier.notify({
        title: "Error in sass",
        message: err.messageFormatted
      });

    console.log(colors.red("==============================="));
    console.log(colors.red(err.messageFormatted));
    console.log(colors.red("==============================="));
    this.emit("end");
}


gulp.task("browseSync", function() {
    browserSync.init({
        server: "./dist",
        notify: false,
        //host: "192.168.0.24",
        //port: 3000,
        open: true,
        //browser: "google chrome" //https://stackoverflow.com/questions/24686585/gulp-browser-sync-open-chrome-only
    });
});


gulp.task("sass", function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(plumber({
            errorHandler: showError
        }))
        .pipe(sourcemaps.init())
        .pipe(sass({
            outputStyle: "compressed" //nested, expanded, compact, compressed
        }))
        .pipe(autoprefixer({
            browsers: ["> 5%"]
        })) //autoprefixy https://github.com/browserslist/browserslist#queries
        .pipe(csso())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"))
        .pipe(browserSync.stream({match: "**/*.css"}));
});


gulp.task("es6", function(cb) { //https://github.com/webpack/docs/wiki/usage-with-gulp#normal-compilation
    return webpack(require("./webpack.config.js"), function(err, stats) {
        if (err) throw err;
        console.log(stats.toString());
        cb();
        browserSync.reload();
    })
})


gulp.task("watch", function() {
    gulp.watch("src/scss/**/*.scss", ["sass"]);
    gulp.watch("src/js/**/*.js", ["es6"]);
    gulp.watch("dist/**/*.html").on("change", browserSync.reload);
});


gulp.task("default", ["sass", "es6", "browseSync", "watch"]);

Jak widzicie, powyższa konfiguracja nie jest skomplikowana. Staram się unikać przekombinowania w swoich projektach, bo potem każdorazowo zamiast ruszać pędem z projektem, muszę skupiać się na walce z narzędziami (a przecież nie o to chodzi…).

Powyższa konfiguracja rzecz jasna nie wyczerpuje tematu. Na necie są miliony paczek dla gulpa. Tutaj znajdziesz fajną listę takich rzeczy. W moim skromnym odczuciu nie warto zbyt mocno kombinować. Bardzo często zdarza się, że paczki są porzucane, nagle się zmieniają bez kompatybilności wstecznej, ich dokumentacja pozostawia wiele do życzenia. I weź to człowieku potem nadganiaj. Ot – uroki dzisiejszego frontendu.

Gotową paczkę znajdziesz tutaj. Wystarczy ją ściągnąć, rozpakować do naszego katalogu, zainstalować poleceniem npm i i odpalić poleceniem gulp.

Komentarze