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 plikach źródłowych, wykona odpowiednie dla danego typu plików zadanie (np. po zmianie w scss skompiluje je do css).

Instalacja Gulpa

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ę. Po instalacji powinniśmy móc odpalić node z terminala (w windows cmd). Możemy to sprawdzić odpalając w terminalu komendę

node --version

W Win7 64bit mogą pojawić się czasami problemy. Może to wynikać z tego, że zainstalowaliśmy niewłaściwą wersję (np. pod system 32 bit), lub z jakiś względów ścieżka do node nie została dodana do PATH. Czasami pomoże ponowne zalogowanie, czasami ręczne dodanie tej ścieżki (http://superuser.com/questions/834319/node-js-wont-run-from-cmd-exe-in-windows-7-x64), a czasami wystarczy zamiast z cmd korzystać z dołączonego do node „Node Command prompt” (powinien się znajdować w pasku zadań w folderze wraz z zainstalowanym NODE).

Sama instalacja Gulpa składa się z trzech kroków: instalacji konsoli gulpa na danym komputerze, dodaniu modułów gulpa do projektu i stworzeniu konfiguracji dla gulpa w projekcie.

Pierwszy krok wykonujemy według instrukcji z https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md. Czyli tak naprawdę wpisujemy w konsoli polecenie

npm install --global gulp-cli

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

Dodanie modułu Gulpa do projektu

Aby dodać Gulpa do konkretnego projektu, przechodzimy do głównego katalogu w projekcie i wpisujemy polecenie

npm install gulp --save-dev

Polecenie to zainstaluje gulpa w katalogu node_modules, oraz doda do pliku packages.json wpis o gulpie. Podobnie będziemy do niego dodawać wszystkie moduły, które będziemy wykorzystywać przy pracy z gulpem.

Jeżeli takiego pliku nie znaleźliśmy, tworzymy go poleceniem npm init w konsoli będąc w głównym katalogu projektu. Będziemy musieli odpowiedzieć na kilka prostych pytań (lub je pominąć enterem). 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…
{
  "name": "nasz-projekt",
  "version": "0.0.1",
  "devDependencies": {
  }
}
Plik packages.json poza podstawową konfiguracją zawiera informacje o tym, co ma być zainstalowane (sekcja devDependencies w powyższym przykładzie), jeżeli użyjemy polecenia npm install. Jeżeli teraz inna osoba ściągnie nasz projekt z plikiem packages.json, wystarczy, że użyje polecenia npm install, co automatycznie zainstaluje u niej wszystkie moduły wypisane w tym pliku

Konstrukcja projektu i sposób pracy

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

src - tutaj trzymamy nasze pliki źródłowe
   scss
      _inne_pliki.scss
      style.scss
   js
      inne_pliki.js
      scripts.js

dist - tutaj trzymamy finalną wersję strony
   css
   js
   images
   index.html

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.

Podczas pracy będziemy działać w dwóch trybach – developerskim i produkcyjnym. Wszystkie taski developerskie nie będą minimalizować kodu, a tylko łączyć pliki w jeden wspólny. Dzięki temu wynikowe pliki będą łatwiejsze do czytania. Czasami mimo wspaniałości dzisiejszych narzędzi zachodzi taka potrzeba. Po zakończonej pracy developerskiej wystarczy, że odpalimy taski produkcyjne, co da nam ten sam efekt co developerskie, z tym, że kod będzie zminimalizowany/zoptymalizowany.

Niektórzy tworzą taski w jednej wersji, a ich przebieg uzależniają od jakiejś zmiennej, która określa czy jest to środowisko testowe czy produkcyjne. Ta zmienna jest albo zakodowana na sztywno w samym pliku konfiguracyjnym gulpa, albo brana np. z html. My stworzymy nasze taski w dwóch wersjach – produkcyjnej i developerskiej. Dzięki temu w każdym momencie każdy task będziemy mogli odpalić w odpowiedniej wersji. Dodatkowo stworzymy taski ogólne (dev i prod), które będą odpalać zbiorczo bardziej konkretne zadania jak taski dotyczące js, czy scss. To właśnie z tych zbiorczych tasków będziemy najczęściej korzystać.

Spojrzenie na plik gulpfile.js

Poza dodaniem modułu Gula do projektu, musimy ręcznie stworzyć plik z konfiguracją tasków, czyli ręcznie utworzyć plik gulpgile.js w głównym katalogugu projektu. Tworzymy go więc i dodajemy do niego kod:

//dolączamy do konfiguracji moduły
var gulp = require('gulp');
...
...

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

//poniższy task "prod" po uruchomieniu odpali task "task_name"
gulp.task('prod', function() {
    gulp.start('task_name');
});

//głowny task odpalany gdy w konsoli użyjemy polecenia 'gulp' bez podania nazwy tasku
gulp.task('default', ['task_name']);

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 taski. Takie taski będziemy potem odpalać z konsoli za pomocą polecenia

gulp task_name

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

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

W pierwszym kroku taski 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 stramie. Pobieramy plik, wykonujemy na nim kilka operacji, a następnie go zapisujemy za pomocą polecenia gulp.dest().

Czasami niektóre taski będą też odpalać inne taski 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.

Obsługa JS

Zaczynamy pisać nasze taski. Na początku zajmiemy się Javascriptem.
Dla tasków obsługujących nasze skrypty skorzystamy z modułów:

Instalujemy powyższe biblioteki poleceniem

npm install gulp gulp-concat gulp-uglify gulp-jshint gulp-sourcemaps gulp-plumber gulp-size gulp-notify gulp-rename browser-sync --save-dev

a następnie piszemy kod naszych tasków:

var gulp = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var browserSync = require('browser-sync');
var plumber = require('gulp-plumber');
var rename = require('gulp-rename');
var size = require('gulp-size');
var notify = require('gulp-notify');


var handleError = function(err) {
    console.log(err);
    this.emit('end');
}


//============================================
//JS tasks
//============================================    
    // Concatenate JS
    gulp.task('js:dev', function() {
        var s = size();
        return gulp.src('src/js/*.js')
            .pipe(plumber({ //dodaje obsługę błędów
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init()) //odpalam generowanie sourcemapy
            .pipe(concat('scripts.js')) //łaczę pliki
            .pipe(s) //pobieram rozmiar plików w stramie
            .pipe(rename({suffix: '.min'})) //zmieniam nazwę
            .pipe(sourcemaps.write('.')) //tworzę sourcemapę
            .pipe(gulp.dest('dist/js')) //wszystko zapisuję w dist/js
            .pipe(browserSync.stream()) //odpalam browserSync
            .pipe(notify({ //i wypisuję komunikat
                onLast: true,
                message: function () {
                    return 'Total JS size ' + s.prettySize;
                }
            }))
    });

    // Minify JS
    gulp.task('js:prod', function() {
        var s = size();
        return gulp.src('src/js/*.js')
            .pipe(plumber({ //dodaje obsługę błędów
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init())
            .pipe(concat('scripts.js'))
            .pipe(uglify())
            .pipe(s)
            .pipe(rename({suffix: '.min'}))
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('dist/js'))
            .pipe(browserSync.stream())
            .pipe(notify({
                onLast: true,
                message: function () {
                    return 'Total JS size ' + s.prettySize;
                }
            }))
    });

    // Lint Task
    gulp.task('js-lint', function() {
        return gulp.src('src/js/*.js')
            .pipe(plumber({
                errorHandler: handleError
            }))
            .pipe(jshint())
            .pipe(jshint.reporter('default'));
    });

Żeby zrozumieć sens dzialania poszczególnych tasków wystarczy przeanalizować kolejne pipe linijka po linijce. Część
konfiguracji wynika ze specyfiki danego modułu i zawsze opisana jest na stronie danego modułu.

Gulp działa strumieniowo, czyli w taki sposób, że wykonuje po kolei kolejne polecenia pipe. Jeżeli któryś z pipe zakończy się błędem, kolejne pipe nie zostaną wykonane. Aby to obejść używam pluginu plumber, który służy do obsługi błędów. Wymaga on dopisania dodatkowej funkcji, która będzie emotować end. Wrzuciłem ją na sam początek kodu.

Od tej pory będziemy mogli odpalać nasze taski z konsoli. Sprawdzimy ich listę wpisując w konsoli polecenie

gulp --tasks

Task gulp js:dev odpali developerską kompilację skryptów w jeden plik wynikowy (bez minimalizacji kodu), gulp js:prod odpali kompilację i minimalizowanie kodu do jednego pliku. Każdorazowo zmiany zostaną odświeżone na stronie. Ostatni task gulp js-lint sprawdzi nasze skrypty i wyświetli w konsoli podpowiedzi do naszych skryptów.

Jeżeli chcesz aby w twoim systemie wyskakiwały okienka z informacjami wypluwanymi przez gulp_notify, zainstaluj program Growl (lub podobny, króty służy do pokazywania notyfikacji)

SCSS / CSS

Do kompilacji SCSS wykorzystamy moduły:

Część modułów zainstalowaliśmy już poprzednio. Instalujemy brakujące poleceniem:

npm install gulp-sass gulp-autoprefixer gulp-clean-css

A następnie rozbudowujemy naszą dotychczasową konfigurację o poniższy kod:

...
var sass = require('gulp-sass');
var autoprefixer = require('gulp-autoprefixer');
var sourcemaps = require('gulp-sourcemaps');
var cssmin = require('gulp-cssmin');
var browserSync = require('browser-sync');
var cleanCSS = require('gulp-clean-css');
var plumber = require('gulp-plumber');
var rename = require('gulp-rename');
var size = require('gulp-size');
var notify = require('gulp-notify');
...

//============================================
//Sass tasks
//============================================
    gulp.task('sass:prod', function() {
        var s = size();
        return gulp.src('src/scss/style.scss')
            .pipe(plumber({
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init())
            .pipe(
                sass({
                    outputStyle : 'compressed'
                })
            )
            .pipe(autoprefixer({browsers: ["> 1%"]}))
            .pipe(cleanCSS())
            .pipe(s)
            .pipe(rename({suffix: '.min'}))
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('dist/css'))
            .pipe(browserSync.stream({match: '**/*.css'}))
            .pipe(notify({
                onLast: true,
                message: function () {
                    return 'Total CSS size: ' + s.prettySize;
                }
            }))
    });

    gulp.task('sass:dev', function() {
        var s = size();
        return gulp.src('src/scss/style.scss')
            .pipe(plumber({
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init())
            .pipe(
                sass({
                    outputStyle : 'expanded'
                })
            )
            .pipe(autoprefixer({browsers: ["> 1%"]}))
            .pipe(rename({suffix: '.min'}))
            .pipe(s)
            .pipe(sourcemaps.write('.'))
            .pipe(gulp.dest('dist/css'))
            .pipe(browserSync.stream({match: '**/*.css'}))
            .pipe(notify({
                onLast: true,
                message: function () {
                    return 'Total CSS size ' + s.prettySize;
                }
            }))
    });

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
https://www.npmjs.com/package/gulp-watch

npm install gulp-watch --save-dev

Po odpaleniu, watch działa sobie w tle obserwując zmiany na plikach (tak samo jak np. watch w sass ruby). Gdy takie wykryje, odpali odpowiednie taski, które wcześniej zdefiniowaliśmy, a które mu przekażemy w konfiguracji:

...
var watch = require('gulp-watch');

...

//============================================
//Watch tasks
//============================================
    gulp.task('watch:prod', function() {
        gulp.watch('src/js/*.js', ['js-lint', 'js:prod']);
        gulp.watch('src/scss/**/*.scss', ['sass:prod']);
    });

    gulp.task('watch:dev', function() {
        gulp.watch('src/js/*.js', ['js-lint', 'js:dev']);
        gulp.watch('src/scss/**/*.scss', ['sass:dev']);
    });

PHP

Jeżeli tak jak ja lubicie używać PHP do cięcia stron, przyda wam się task do włączenia serwera php.
Skorzystamy tutaj z modułu https://www.npmjs.com/package/gulp-connect-php oraz wcześniej zainstalowanego browserSync.


    npm install --save-dev gulp-connect browser-sync

Aby ten task mógł działać dodatkowo musimy mieć na komputerze zainstalowane php (np. za pomocą xampp czy innego serwera lokalnego)

...
var browserSync = require('browser-sync');
var connect = require('gulp-connect-php');

...

//============================================
//Server tasks
//============================================    
    //php server
    gulp.task('browser-sync-php', function() {
        connect.server({
            base : './dist'
        }, function() {
            browserSync({
                proxy: '127.0.0.1:8000',
                notify: false            
            });
        });

        gulp.watch('**/*.php').on('change', function () {
            browserSync.reload();
        });
    });

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

Taski zbiorcze

W zasadzie wszystkie taski mamy już przygotowane. Pozostaje nam napisanie tasków zbiorczych, które po odpaleniu
odpalą odpowiednie taski za nas oraz poinformują nas o rozpoczętej pracy.
W większości przypadków będziemy korzystać właśnie z tasków zbiorczych, wywołując ręcznie specyficzne taski tylko w nielicznych sytuacjach.

Aby wyróżnić te taski w konsoli użyjemy modułu https://www.npmjs.com/package/gulp-color który umożliwia używanie kolorów w konsoli:


    npm install --save-dev gulp-color
...

//============================================
//Global tasks
//============================================
    gulp.task('compile:dev', function() {
        console.log(color('-------------------------------------------', 'YELLOW'));
        console.log(color('Kompiluję scss i łączę js', 'YELLOW'));
        console.log(color('-------------------------------------------', 'YELLOW'));
        gulp.start('sass:dev', 'js-lint', 'js:dev');
    });

    gulp.task('compile:prod', function() {
        console.log(color('-------------------------------------------', 'YELLOW'));
        console.log(color('Kompiluję scss i js', 'YELLOW'));
        console.log(color('-------------------------------------------', 'YELLOW'));
        gulp.start('sass:prod', 'js-lint', 'js:prod');
    });

    gulp.task('dev', function() {
        gulp.start('sass:dev', 'js-lint', 'js:dev', 'watch:dev', 'browser-sync-php');
        console.log(color('-------------------------------------------', 'YELLOW'));
        console.log(color('Rozpoczynamy pracę milordzie (DEV)', 'YELLOW'));
        console.log(color('-------------------------------------------', 'YELLOW'));
    });

    gulp.task('prod', function() {
        gulp.start('sass:prod', 'js-lint', 'js:prod', 'watch:prod', 'browser-sync-php');
        console.log(color('-------------------------------------------', 'YELLOW'));
        console.log(color('Rozpoczynamy pracę milordzie (PROD)', 'YELLOW'));
        console.log(color('-------------------------------------------', 'YELLOW'));
    });

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

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 powyższymi taskami

Po napisaniu naszej konfiguracji czas ją wypróbować. W 99% procentach zaczynam pracę poleceniem gulp lub gulp dev. Gulp automatycznie odpala wtedy podgląd strony w przeglądarce, przebudowuje js i scss, oraz włącza nasłuchiwanie zmian na plikach. Po zakończonej pracy odpalam task gulp prod by kod skryptów i styli był zminimalizowany.
W niektórych przypadkach nie chcę odpalać watcha i podglądu strony – wtedy odpalam gulp compile:dev, lub gulp compile:prod. To wszystko.

Gotowy plik Gulpa

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

W ostatecznym kodzie można spokojnie dodać kilka tasków jak te od optymalizacji grafiki. Możemy też podstawić urle pod zmienne, dzięki czemu ich przyszła podmiana będzie szybsza. Ja tego nie robiłem, bo o dziwo w tak prostym pliku o powyższy zapis był dla mnie wystarczająco czytelny. Jednak dla chcącego nic trudnego. Pozostawiam to wam jako zadanie domowe.

Gotowe pliki znajdziecie tutaj: gulpfile.js i package.json

A tutaj możesz znaleźć inną wersję powyższych konfiguracji, która jest prostsza, ale i ma w sobie kompilację scss wraz z autoprefixerem i browsersyncem, oraz laczenie i minimalizowanie js: package.json, package-lock.json i gulpfile.js.
Plik package-lock.json jest tworzony w nowych wersjach node (gdy powstawał ten artykuł pliki te nie były jeszcze tworzone…)

Komentarze

  • kraczo

    Super ! dzięki

  • DarV

    Świetny artykuł , btw. chyba wkradła się literówka w „utworzyć plik gulpgile.js w głównym „