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ę. Dla ubuntu zamiast ściągać wersję ze strony wpiszemy w konsoli


sudo apt-get update
sudo apt-get install node-legacy    

Po instalacji powinniśmy móc odpalić node z terminala (w windows cmd). Możemy to sprawdzić odpalając w terminalu komendę

node -v
//lub
nodejs -v

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 restart (lub 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).

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://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

Install może być zapisane jako i, a –save-dev jako -D czyli powyższe polecenie możemy zapisać jako:

npm i gulp -D

Ta sama zasada będzie tyczyć się innych modułów.

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. 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 autoamtycznie brana z nazwy katalogu, który ma dużą literę w nazwie – wystarczy to ręcznie poprawić wpisując siu-bziu”
{
  "name": "nasz-projekt",
  "version": "0.0.1",
  "devDependencies": {
  }
}
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. Jeżeli teraz inna osoba ściągnie nasz projekt z plikiem packages.json, 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 taski, 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
      _mixyny-dziewczyny.scss
      style.scss
   js
      inny_skrypt.js
      inny_skrypt.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. Jasne – spokojnie możemy to zmienić i na przykład mieć tylko katalog src, z którego będziemy wrzucać wynik do katalogu głównego.

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 katalogu projektu. Tworzymy więc ten plik i dodajemy do niego kod:

//dolączamy do konfiguracji moduły
var 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 taski. Takie taski 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 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 i gulp gulp-concat gulp-uglify gulp-sourcemaps gulp-plumber gulp-size gulp-notify gulp-rename browser-sync -D

a następnie piszemy kod naszych tasków:

var gulp = require('gulp');
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
//============================================    
    gulp.task('js', function() {
        var s = size();
        return gulp.src('src/js/*.js')
            .pipe(plumber({ //dodaje obsługę błędów
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init()) //przed operacjami inicjalizujemy sourcemap
            .pipe(concat('scripts.js')) //laczymy wszystkie pliki js
            .pipe(uglify()) //minimalizujemy powyzszy polaczony kod
            .pipe(s) //pobieramy wielkosc tego kodu
            .pipe(rename({ //zmieniamy nazwę tego kodu na scripts.min.js
                suffix: '.min'
            }))
            .pipe(sourcemaps.write('.')) //wpisujemy sourcemap
            .pipe(gulp.dest('dist/js')) //wszystko powyzsze zapisujemy w katalogu
            .pipe(browserSync.stream({match: '**/*.js'})) //wstrzykujemy nowe pliki
            .pipe(notify({ //i informujemy o tym siebie samych
                onLast: true,
                message: function () {
                    return 'Total JS size ' + s.prettySize;
                }
            }))
    });


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

Ż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 (no ok – nie zawsze – czasami trzeba szperać w necie – takie życie :|).

Gulp działa strumieniowo, czyli w taki sposób, że wykonuje po kolei kolejne polecenia pipe (tak jak woda leci po rurkach). Jeżeli któryś z pipe zakończy się błędem, kolejne pipe nie zostaną wykonane. Aby to obejść używam pluginu plumber (hydraulik), który służy do naprawy obsługi błędów. Wymaga on dopisania dodatkowej funkcji, która będzie emitować 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
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). Jeżeli takie informacje będą cię denerwowały, usuć ostatnie pipe z notify…

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', function() {
        var s = size();
        return gulp.src('src/scss/style.scss')
            .pipe(plumber({
                errorHandler: handleError
            }))
            .pipe(sourcemaps.init())
            .pipe(
                sass({
                    outputStyle : 'compressed' //styl kodu - extended, nested, copressed, compact - i tak chcemy 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('default', ['js', 'sass']);        

Konfigurację autoprefixera, który automatycznie będzie nam dopisywać prefixy (-webkit-, -moz- itp) znajdziesz 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 watch. Od razu go zainstalujmy:
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. 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:

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

//============================================
//Watch tasks
//============================================
    gulp.task('watch', function() {
        gulp.watch('src/js/*.js', ['js']); //obserwuj wszystkie pliki .js w katalogu src/js i w razie czego odpal task js
        gulp.watch('src/scss/**/*.scss', ['sass']); //obserwuj wszystkie pliki .scss w katalogu src/scss i podkatalogach i w razie czego odpal task sass
    });

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

BrowserSync

BrowserSync umożliwia automatyczne odświeżanie strony po wykryciu zmian. Czyli my piszemy w edytorze i mamy gdzieś każdorazowe naciskanie Ctrl + R po wejściu na przeglądarkę. Możemy więc na jednym ekranie mieć otwarty edytor, na drugim okno przeglądarki i automatycznie widzieć co się zmienia. Dodatkowo daje nam możliwość wstrzykiwania zaktualizowanych styli i skryptów bez przeładowania stron oraz po uruchomieniu udostępnia adresy, na który możemy się połączyć różnymi urządzeniami (pod tym adresem będzie wyświetlana strona z aktualnym stanem – np z hoverem na przycisku – we wszystkich urządzeniach równocześnie!)

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

$ npm install browser-sync --save-dev

(zwróć uwagę, że ten moduł nie ma żadnej końcówki gulp).

BrowserSync działa w „2 trybach”. Jednym z nich jest automatyczne przeładowanie strony (reload). Drugim trybem jest wstrzykiwanie na stronę zmienionych styli i css bez przeładowywania strony (bardzo pomocne, gdy pracujemy np nad ajaxem). To wskrzykiwanie wykonujemy za pomocą stream.
Aby BrowserSync mógł w ogóle działać, musimy dopisać task, który nam uruchomi server BrowserSynca:

...
var browserSync  = require('browser-sync').create();
...

gulp.task('browseSync', function() {
    browserSync.init({
        server: "./dist",
        notify: false
    });    
});

gulp.task('sass', function() {
    return gulp.src('src/scss/style.scss')
        ...
        .pipe(gulp.dest("dist/css")) //i calosc zapisujemy w dest
        .pipe(browserSync.stream({match: '**/*.css'}));        
});

gulp.task('js', function() {
    return gulp.src('src/js/**/*.js')
        ...
        .pipe(gulp.dest('dist/js'))
        .pipe(browserSync.stream({match: '**/*.js'}));            
});

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

gulp.task('default', ['js', '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.

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. Nie ma co kombinować :) To wszystko.

Gotowy plik Gulpa

Jak widzicie, powyższa konfiguracja nie jest skomplikowana. Staram się unikać przekombinowania w swoich projektach (zobaczysz to też w 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 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, package.json i package-lock.json

Komentarze

  • kraczo

    Super ! dzięki

  • DarV

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