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.

Dla ubuntu 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 gulp-cli 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 czyli pliku gulpfile.js.

Zanim zaczniemy coś wpisywać spróbujmy odpalić gulpa w jakimś katalogu.
Wchodzimy do terminala w dowolne miejsce i wpisujemy gulp.
Powinno pojawić nam się polecenie „command nod found”. Rzecz oczywista.
Żeby móc korzystać z komend gulpa, musimy zainstalować gulp-cli na naszym komputerze.

Wykonujemy to według instrukcji z https://gulpjs.com/. Czyli tak naprawdę wpisujemy w konsoli polecenie

npm i gulp-cli -g

Od tej pory będziemy mogli używać klienta Gulpa na danym komputerze. Spróbujmy więc jeszcze raz wpisać gulp w terminalu. Pojawi się inny błąd, ale nie „command nod found”.

Tak naprawdę komputerów i systemów jest miliony. Dość często zdarza się że niby idziemy z instrukcjami kroczek za kroczkiem, a i tak mamy błędy. Jak coś takiego ciebie spotka, pisz w komentarzach, albo szukaj w necie. Niestety jednego 100% przepisu tutaj nie ma.

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 projektem 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 naszego 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 naszego 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": "^4.0",
  }
}

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.

Poza dodaniem wpisu do package.json stworzy się katalog node_modules, który będzie zawierał dużo plików. Naprawdę duuuuuuużo. Niby zainstalowaliśmy dopiero tylko jedną paczkę Gulpa, ale jak widać ona też korzysta z innych paczek…

Konstrukcja projektu i sposób pracy

Zanim przejdziemy do dalszej konfiguracji, 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ł (zakoszony z html5 boilerplate), 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 przyszłą konfiguracją zadań.
W głównym katalogu naszego projektu tuż obok package.json ręcznie tworzymy plik gulpgile.js. Dodajemy do niego początkową konfigurację:

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


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

Na samym początku dołączamy do pliku moduły z których będziemy korzystać. Zawsze będzie to moduł gulp, oraz wszystkie inne, których użyjemy (gulp-sass, gulp-autoprefixer, gulp-sourcemaps, 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 nazwaTaska

Zadania możemy zapisywać na 2 sposoby. Z wykorzystaniem funkcji gulp.task():

gulp.task("nazwaTaska", function() {
    console.log("wykonuję zadanie");
});

lub korzystając z nowszego zapisu – składni korzystającej z normalnej funkcji:


const nazwaTaska = function() {
    console.log("wykonuję zadanie");
}

Metoda z wykorzystaniem gulp.task() mimo, że bardzo często pojawiać się będzie w różnych instrukcjach instalowanych paczek, nie jest już oficjalnie zalecana.

W dalszej części tutoriala będę korzystał z nowszego zapisu.

Zadania w Gulpie są asynchroniczne. Jedno może trwać 1 sekundę, drugie 10 sekund, a ich operacje mogą wykonywać się w tym samym momencie.

Aby zasygnalizować zakończenie działania danego taska, zadanie musi zwrócić „stream”, „promise”, „event emitter”, „child process”, albo „observable”.

Co to oznacza? Nasze zadanie będą dzielić się na 2 typy:


//zadania które nie operują na plikach muszą zwracać funkcję zwrotną z parametru
const task1 = function(cb) {
    console.log("Jakiś tekst");
    cb();
}

//funkcje operujące na plikach zwracają stream kolejnych .pipe()
const task2 = function() {
    return gulp.src("src/scss/*.scss")
             .pipe(...)
             .pipe(...)
             .pipe(...)
             .pipe(gulp.dest("katalog_docelowy"))
}

W skrócie jeżeli zadanie nie będzie bezpośrednio operować na plikach za pomocą pipe(), będzie zwracać funkcje callback, którą podajemy w parametrze. Ten drugi typ zadań będą zwracać stram, który będzie składał się z nastu operacji pipe(). W naszym przypadku będzie to większość zadań.

Zadania operujące na plikach 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. Pobieramy plik, wykonujemy na nim kilka operacji w pamięci, a następnie wynik zapisujemy za pomocą polecenia gulp.dest() w jakieś.

Mini konfiguracja testowa

Stwórzmy przykładową mini konfigurację (zastępując poprzednią) i spróbujmy ją odpalić:


const gulp = require("gulp");

const task1 = function(cb) {
    console.log("Zadanie 1");
    cb();
}

const task1 = function(cb) {
    console.log("Zadanie 2");
    cb();
}

Gdy teraz w terminalu wpiszemy polecenie

gulp -T

naszym oczom powinna się pojawić lista zadań które możemy odpalić za pomocą polecania gulp nazwaTaska. Ale żadne zadanie nam się nie pojawi.

W starym zapisie za pomocą gulp.task() każde zadanie było domyślnie wystawione do użycia w terminalu. W nowym zapisie zadania, które chcemy wystawić do użytku w terminalu musimy wystawić za pomocą exports:


const gulp = require("gulp");

const task1 = function(cb) {
    console.log("Zadanie 1");
    cb();
}

const task1 = function(cb) {
    console.log("Zadanie 2");
    cb();
}

const task3 = function(cb) {
    console.log("Zadanie 3");
    cb();
}

//jeżeli jakiegoś zadania nie chcemy dawać do użycia w terminalu, nie wystawiamy go
exports.task1 = task1;
exports.task2 = task2;
//exports.task3 = task3;

Wpiszmy jeszcze raz w terminalu gulp -T by sprawdzić jakie taski możemy użyć, a następnie gulp task1. Powinno odpalić nam nasze zadanie.

Nasza mini konfiguracja powinna działać jak należy. Ale nie taki cel jest naszej dzisiejszej pracy. Zacznijmy coś bardziej poważnego.

SCSS / CSS

Zacznijmy od jednej z najczęstszych rzeczy robionych za pomocą Gulpa, czyli 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 zamiana scss do css.

Wchodzimy więc na stronę https://www.npmjs.com/package/gulp-sass i zgodnie z poleceniami instalujemy paczkę poleceniem:

npm i gulp-sass -D

Flaga -D to alternatywny zapis –save-dev. Oznacza ona paczki instalowane na czas tworzenia strony.
Sprawdźmy też, czy w pliku package.json została dodana odpowiednia linijka.

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


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


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


exports.css = css;

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

Domyślny task

Wyobraźmy sobie, że ściągamy jakiś projekt, instalujemy wszystko npm i, wpisujemy w terminalu gulp -T, i naszym oczom ukazuje się lista 30 tasków. Który task odpalić jako pierwszy?

Jeżeli w terminalu wpiszemy polecenie gulp nie podając nazwy taska, w terminalu wyskoczy nam błąd, że gulp próbował odpalić zadanie default, ale nie był w stanie go odnaleźć w naszym pliku.
Stwórzmy je:


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


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


exports.default = gulp.series(css);
exports.css = css;

Od tej pory powinniśmy móc odpalić nasze zadania za pomocą polecenia gulp

W powyższym tasku pojawiła nam się nowa funkcja gulp.series(). Mamy w gulpie dwie takie do użycia:

  • gulp.series(nazwaTaska1, nazwaTaska2, …) – odpala kolejne taski jeden po drugim, a na końcu zwraca wynik ich działania
  • gulp.parallel(nazwaTaska1, nazwaTaska2, …) – odpala wszystkie taski na raz nie czekając aż wcześniejsze się skończą

SourceMaps

Nasza powyższa konfiguracja sprawia, że całe wynikowe style są spakowane w 1 linijkę (ponieważ dla sass użyliśmy outputStyle ustawionego na compressed).
To bardzo dobrze, bo taki kod jest zminimalizowany, a więc i mało waży. 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ę w zasadzie niemożliwe.

My chcemy by debuger wskazywał realne miejsce – czyli konkretną linijkę w pliku SCSS. Aby to naprawić powinniśmy skorzystać z mechanizmu sourcemaps, którzy rzutuje kod źródłowy na wynikowy. Aby to zrobić, musimy zainstalować kolejną paczkę poleceniem:

npm i gulp-sourcemaps -D

I dodajemy do naszej konfiguracji:


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


const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init()) //inicjalizacja sourcemap przed zabawa na plikach
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", sass.logError)
        )
        .pipe(sourcemaps.write(".")) //po modyfikacjach na plikach zapisujemy w pamieci sourcemap
        .pipe(gulp.dest("dist/css"));
}


exports.default = gulp.series(css);
exports.css = css;

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 sourcemaps.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. Do automatyzacji dodawania prefixów służy narzędzie https://autoprefixer.github.io/.

Istnieje też wersja dla Gulpa. Wchodzimy na stronę https://www.npmjs.com/package/gulp-autoprefixer

i zgodnie z instrukcją 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");


const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", sass.logError)
        )
        .pipe(autoprefixer()) //autoprefixy https://github.com/browserslist/browserslist#queries
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}


exports.default = gulp.series(css);
exports.css = css;

Aby autoprefixer wiedział dla jakich przeglądarek musi dodać prefixy, musimy mu to jakoś powiedzieć. W poprzedniej wersji autoprefixera przeglądarki podawało się w parametrze browser tej paczki:


...
.pipe(autoprefixer({
    browsers: ["> 5%"]
}))
...

W nowej wersji autoprefixera takie ustawienie spowoduje pokazanie się błędu w terminalu:

Zostało to zmienione i teraz listę przeglądarek można podawać na kilka sposobów, o których mówi strona: https://github.com/browserslist/browserslist#readme.
Możemy więc stworzyć w głównym katalogu projektu plik .browserslistrc lub stosowną listę dodać do package.json naszego projektu (który już mamy). Ja wybiorę ta drugą opcję. W pliku package.json dodaję wpis:


"browserslist": [
    "last 1 version",
    "> 1%"
],

JSON to format, który jest bardzo restrykcyjny jeżeli chodzi o składnię, dlatego dodając do niego ręcznie wpisy pamiętaj by nie zapomnieć o żadnym nadmiarowym przecinku.

W powyższej konfiguracji autoprefixera wybrałem przeglądarki, które mają >1% na rynku amerykańskim oraz ostatnie wersje przeglądarek. Pamiętajmy, że każdy projekt musimy traktować indywidualnie. Dla jednego powyższa konfiguracja będzie prawidłowa, a dla innego nie. Możliwe zapisy znajdziemy na tej stronie: https://github.com/postcss/autoprefixer#browsers.

WATCH

Podczas pracy nad kodem ręczne odpalanie tasków po każdej zmianie w pliku było by bardzo niewygodne. Dlatego użyjemy watchera, 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");


const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", sass.logError)
        )
        .pipe(autoprefixer())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.watch = watch;

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.

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

Jeżeli w naszym kodzie SCSS będziemy mieli jakiś błąd, w terminalu pojawi się odpowiednie o tym info.

Domyślnie gulp-sass do wyświetlania takich błędów używa funkcji sass.logError (mamy ją w naszym powyższym tasku). Jeżeli chcemy bardzie te błędy wyróżnić, powinniśmy użyć własnej funkcji.


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


const showError = function(err) {
    console.log(err.toString()); //wypisze cały obiekt błędu w terminalu
}

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.watch = watch;

W powyższej funkcji wypisaliśmy cały obiekt błędu za pomocą err.toString(). Jeżeli spojrzymy na jego strukturę zobaczymy, że sama wiadomość o błędzie w sass, która do tej pory pokazywała się w terminalu to właściwość err.messageFormatted.

Żeby bardziej wyróżnić taki komunikat w gąszczu tekstu terminala, pokolorujmy go na czerwono a dodatkowo przy pojawieniu się błędu pokażmy wyskakującą notyfikację w rogu ekranu. Dzięki temu podczas pracy nie będziemy musieli patrzeć na terminal.
Do tego pierwszego musimy zainstalować paczkę 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 colors        = require("ansi-colors");
const notifier      = require("node-notifier");


const showError = function(err) {
    //console.log(err.toString());

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

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

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.watch = watch;

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 :(. Czasami też wystarczy jeszcze raz odpalić gulpa by załapało.

Rename, wait, csso

Trzy mini dodatki, które można dodać – ale niekoniecznie. Moduł gulp-rename służy do zmiany nazwy wynikowego pliku. W naszym przypadku do tej pory generowany był plik style.css (ponieważ generujemy go na bazie pliku style.scss). Jeżeli chcemy inną nazwę pliku css – np. style.min.css, użyjemy do tego paczki gulp-rename.

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 colors        = require("ansi-colors");
const notifier      = require("node-notifier");
const rename        = require("gulp-rename");


const showError = function(err) {
    //console.log(err.toString());

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

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

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({ //zamieniam wynikowy plik na style.min.css
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.default = default;
exports.watch = watch;

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 Windows 10, 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 z sasem:


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


const showError = function(err) {
    //console.log(err.toString());

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

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

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.default = default;
exports.watch = watch;

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 do 1 linii. Ale czy dodatkowo optymalizujemy? Raczej nie. Dodatek ten np. połączy właściwości background-image, background-color w jednego shorthanda background itp. Ogólnie robi całkiem fajną robotę jeżeli chodzi o optymalizację css. 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 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 showError = function(err) {
    //console.log(err.toString());

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

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

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(csso())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(sass));
}


exports.default = gulp.series(css, watch);
exports.css = css;
exports.default = default;
exports.watch = watch;

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 przejść na stronę https://www.browsersync.io/docs/gulp i zainstalować browserSynca poleceniem:

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 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 showError = function(err) {
    //console.log(err.toString());

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

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

const server = function(cb) {
    browserSync.init({
        server: {
            baseDir: "./dist"
        },
        notify: false, //reszta opcji z dokumentacji browsersync
        //host: "192.168.0.24",
        //port: 3000,
        open: true,
        //browser: "google chrome" //https://stackoverflow.com/questions/24686585/gulp-browser-sync-open-chrome-only
    });

    cb();
}

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(csso())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css"));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(css));
}


exports.default = gulp.series(css, server, watch);
exports.css = css;
exports.default = default;
exports.watch = watch;

Zadania takiego nie wystawiam za pomocą exports, ponieważ było by to bez sensu. Zadanie to tylko odpala na chwile serwer i jest zależne od reszty zadań w naszej konfiguracji.

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 adresów 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:


const gulp          = require("gulp");
const sass          = require("gulp-sass");
const sourcemaps    = require("gulp-sourcemaps");
const autoprefixer  = require("gulp-autoprefixer");
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 showError = function(err) {
    //console.log(err.toString());

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

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

const server = (cb) => {
    browserSync.init({
        server: {
            baseDir: "./dist"
        },
        notify: false,
        //host: "192.168.0.24",
        //port: 3000,
        open: true,
        //browser: "google chrome"
    });

    cb();
}

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(csso())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css")) //tu nie ma średnika!
        .pipe(browserSync.stream({match: "**/*.css"}));
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(css));
    gulp.watch("dist/**/*.html").on("change", browserSync.reload);
}


exports.default = gulp.series(css, server, watch);
exports.css = css;
exports.default = default;
exports.watch = 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 install -D babel-loader @babel/core @babel/preset-env webpack

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 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");


const showError = function(err) {
    //console.log(err.toString());

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

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

const server = function(cb) {
    browserSync.init({
        server: {
            baseDir: "./dist"
        },
        notify: false,
        //host: "192.168.0.24",
        //port: 3000,
        open: true,
        //browser: "google chrome"
    });

    cb();
}

const css = function() {
    return gulp.src("src/scss/style.scss")
        .pipe(wait(500))
        .pipe(sourcemaps.init())
        .pipe(
            sass({
                outputStyle : "compressed"
            }).on("error", showError)
        )
        .pipe(autoprefixer())
        .pipe(rename({
            suffix: ".min",
            basename: "style"
        }))
        .pipe(csso())
        .pipe(sourcemaps.write("."))
        .pipe(gulp.dest("dist/css")) //tu nie ma średnika!
        .pipe(browserSync.stream({match: "**/*.css"}));
}

const js = 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());
        browserSync.reload();
        cb();
    })
}

const watch = function() {
    gulp.watch("src/scss/**/*.scss", gulp.series(css));
    gulp.watch("src/js/**/*.js", gulp.series(js));
    gulp.watch("dist/**/*.html").on("change", browserSync.reload);
}


exports.default = gulp.series(css, js, server, watch);
exports.css = css;
exports.watch = watch;
exports.js = js;

Ż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ą konfigurację webpacka:


module.exports = {
    entry: './src/js/app.js',
    output: {
        path: `${__dirname}/dist/js`,
        filename: 'bundle.min.js'
    },
    watch: false,
    mode: 'production',
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
}

Od tej porty poleceniem gulp odpalimy nie tylko kompilację scss, ale i webpacka, który zajmie się dla nas ogarnianiem JS na starszy zapis.

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.

Podsumowanie

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.

Automatyczna Google Mapa

Poniższy tekst będzie w innej formie. Zamiast pisać typowy tutorial, w którym się wymądrzam, opiszę…
Czytaj więcej

Dynamicznie wczytywana lista

Dzisiaj zajmiemy się stworzeniem dynamicznie wczytywanej listy na stronie. Omawiana technika może być wykorzystana np…
Czytaj więcej