Jedną z podstawowych zasad w pracy każdego programisty jest reguła DRY (don’t repeat yourself – nie powtarzaj się)  Ścisłe jej respektowanie pozwala nie tylko tworzyć lepsze oprogramowanie, ale również zaoszczędzić czas,  który możemy przeznaczyć na ciekawsze rzeczy, niż wykonywanie nudnych, powtarzalnych czynności. W dzisiejszym artykule pokażę wam, jak ten czas zaoszczędzić z pomocą Grunt.

Słowem wstępu – czym jest Grunt?

Grunt jest systemem automatyzacji pracy, w którym możemy stworzyć listę (własnych lub stworzonych przez kogoś) zadań, które zostaną wykonane po odpaleniu odpowiedniego polecenia. Pozwala nam to oszczędzić sporo czasu, zautomatyzować pracę i pozbyć się czasem nudnego i żmudnego wykonywania ciągle tych samych zadań. Przykładowymi zadaniami, jakie możemy zlecić Gruntowi to: komplikacja Sass bądź Less do CSS, minifikacja CSS, generowanie tzw. sprite’ów, sprawdzenie błędów w plikach JavaScript i wiele więcej.

Sama instalacja i konfiguracja jest dziecinnie prosta, choć wymaga trochę czasu. Na początek przygotujmy nasze środowisko. Potrzebujemy:

  1. Node.js – instalujemy najnowszą wersję, w systemie Windows instalator automatycznie doda nam do PATH ścieżkę instalacyjną do Node.js, co z pozwoli używać w konsoli polecenia npm.
  2. Grunt – po instalacji Node.js, w konsoli wykonujemy polecenie npm install -g grunt-cli, co z kolei pozwolić używać w konsoli polecenia grunt.
  3. Zadań, które możemy sobie zautomatyzować.

Zaczynamy zabawę

Podzielimy zadania na dwa środowiska – developerskie i produkcyjne. W pierwszym znajdą się zadania wykonywane przy każdej zmianie w pliku, w produkcyjnym natomiast zadbamy o generowanie plików dla serwera produkcyjnego.

Środowisko developerskie

Na początek ułatwmy sobie prace z Sass. Przyjrzyjmy się strukturze pliku Gruntfile.js, w którym umieszczamy zadania. Może przerażać, ale bez obawy – całość jest dziecinnie prosta!

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        taskName: {          //nazwa zadania
            options: {       //opcje

            },
            operations: {    //operacje

            }
        }
    });

    grunt.loadNpmTasks('grunt-module');                  // w taki sposób ładujemy nasze moduły
    grunt.registerTask('default', ['taskName']);         // określany jakie zadania wykonują się po wpisaniu w konsoli grunt
};

W tym miejscu wypada od razu wyjaśnić, z czego jest zbudowany plik Gruntfile.js. Struktura pliku zazwyczaj jest taka sama, różni się zadaniami. Polecenie grunt.file.readJSON('package.json'), odnosi się do pliku package.json – zawiera on informację o tym, z jakich modułów korzystamy. Na razie się nim nie przejmujmy.

Zajmijmy się kompilacją pliku z kodem Sass do pliku z kodem CSS. Należy w tym celu pobrać zadanie o nazwie grunt-libsass, które się tym zajmie. Wydajemy w konsoli polecenie: npm install grunt-libsass --save-dev. Gdy zadanie zostanie pobrane i zainstalowane, możemy przejść do jego konfiguracji:

libsass: {                       //zadanie główne
    files: {
        expand: true,
        src: ['css/style.scss'], // pliki na których domyślnie ma być wykonane zadanie
        dest: '',                // gdzie mają się zapisać skompilowane pliki CSS (w tym przypadku zapisze się w lokalizacji pliku Sass)
        ext: '.css'              // rozszerzenie po skompilowaniu
    }
}

Powyższy listing wyjaśnia cel użycia poszczególnych opcji w zadaniu. Pełna lista opcji zazwyczaj znajduje się na stronie pakietu na stronie npmjs.com bądź na stronie jego autora.

Teraz należy załadować zadanie, dopisując do pliku Gruntfile.js linię: grunt.loadNpmTasks('grunt-libsass');,  Jeśli wszystko zrobiliśmy dobrze, plik Gruntfile.js powinien wyglądać tak, jak ten poniżej, a po wpisaniu polecenia grunt libsass w konsolę, powinna rozpocząć się kompilacja pliku.

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        libsass: {
            files: {
                expand: true,
                src: ['css/style.scss'],
                dest: '',
                ext: '.css'
            }
        }
    });

    grunt.loadNpmTasks('grunt-libsass');
};

Jednak wpisywanie polecenia za każdym razem, kiedy dokonamy zmian w pliku Sass, to sporo monotonnej pracy. Dlaczego nie nakazać Gruntowi kompilacji pliku natychmiast po wykryciu dokonanych w nim zmian?  Z pomocą przychodzi nam obowiązkowe zadanie watch. Instalujemy je wpisując w konsolę npm install grunt-contrib-watch --save-dev i konfigurujemy następująco:

watch: {
    scss: {                     // możemy określić dowolną nazwę
        files: ['css/*.scss'],  // określamy na jakich plikach pracujemy
        tasks: ['libsass']      // wybieramy zadania jakie na nich wykonujemy, po kolei
    }
}

Ładujemy zadanie za pomocą kodu grunt.loadNpmTasks('grunt-contrib-watch'); i dodajemy watch do domyślnych zadań (wykonywanych po odpaleniu polecenia gruntgrunt.registerTask('default', ['watch']);. Zapisujemy plik Gruntfile.js i odpalamy w konsoli polecenie grunt Od teraz każda modyfikacja pliku Sass w folderze css będzie powodować automatyczne kompilowanie. Należy jednak pamiętać, że jeżeli zamkniemy konsolę, zamkniemy również zadanie i kompilacja automatyczna zatrzyma się.

Sprite – wszystkie ikony w jednym obrazku

Aby nieco zmniejszyć liczbę zapytań do serwera i przyspieszyć działanie strony, możemy łatwo scalić ikony w jeden obrazek i dla każdej ikony zmieniać background-position. Bez obaw – nic nie musimy pisać sami! Wykorzystamy zadanie grunt-spritesmith. Instalujemy je za pomocą polecenia: npm install grunt-spritesmith --save-dev.

Pozostało jeszcze skonfigurować to zadanie:

sprite: {
    all: {
        src: 'images/icons/*.png',            // folder z naszymi ikonami, każda zmiana w nim będzie podować wygenerowanie sprita
        dest: 'images/spritesheet.png',       // wynikowy plik z ikonami
        destCss: 'css/icons.scss',            // plik gdzie wygeneruje nam mixin bądź css
        padding: 10,                          // odstęp pomiędzy ikonami
        cssOpts: {
            cssClass: function (item) {
                return '.icon-' + item.name;  // przedrostek klasy
            }
        }
    }
}

Po wykonaniu wyżej wymienionych kroków, rzut okiem na gotowy plik Gruntfile.js:

module.exports = function (grunt) {
    grunt.initConfig({
        sprite: {
            all: {
                src: 'images/icons/*.png',
                dest: 'images/spritesheet.png',
                destCss: 'css/icons.scss',
                padding: 10,
                cssOpts: {
                    cssClass: function (item) {
                        return '.icon-' + item.name;
                    }
                }
            }
        },
        libsass: {
            files: {
                expand: true,
                src: ['css/style.scss'],
                dest: '',
                ext: '.css'
            }
        },
        watch: {
            libsass: {
                files: ['css/*.scss', 'css/**/*.scss'],
                tasks: ['libsass']
            },
            sprite: {
              files: ['images/icons/*.png'],
              tasks: ['sprite']
          }
        }
    });

    // ładowanie wybranych modułów dla Grunt.js
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-libsass');
    grunt.loadNpmTasks('grunt-spritesmith');

    // rejestrowanie domyślnego zestawu zadań dla Grunt
    grunt.registerTask('default', ['watch']);
};

Możemy również definiować własną grupę zadań do uruchomienia korzystając z metody grunt.registerTask('zadanie', ['inne', 'zadania', 'ktore', 'wykonujemy']);. Podobnie jest z wykonaniem pojedynczego zadania i podzadania: grunt libsass wykonana nam wszystko z libsass, ale już grunt libsass:files tylko to co znajduje się w files.

Polecam zadanie grunt-notify, którego zadaniem jest informowanie o błędach podczas wykonywania zadania. Nie zawsze przecież musimy patrzeć na konsolę.

Generowanie środowiska produkcyjnego

Przy generowaniu środowiska produkcyjnego powinno nam zależeć na możliwie największym zmniejszeniu wagi plików – kosztem ich czytelności. Jednym poleceniem rozwiązujemy problem zminifikowania plików CSS, JavaScript i HTML. Służy do tego zadanie grunt-contrib-compressor (instalacja: npm install grunt-contrib-compressor --save-dev). Całość – oczyszczona i odchudzona – będzie generowana do folderu dist.  Konfiguracja tego zadania wygląda następująco:

compressor: {
    css:{
        files: {
            'dist/css/style.css': ['css/style.css']
        }
    },
    js: {
        options: {
            mangle: true
        },
        files:grunt.file.expandMapping(['js/*.js','js/*/*.js'], '', {
            rename: function(base,file) {
                return 'dist/'+file;
            }
        })
    },
    html:{
        options:{
            removeComments: true, // usunie komentarze
            collapseWhitespace: true
        },
        files:{
            'dist/index.html': ['index.html']
        }
    }
}

Nasz Gruntfile.js finalnie powinien wyglądać tak:

module.exports = function (grunt) {
    grunt.initConfig({
        sprite: {
            all: {
                src: 'images/icons/*.png',
                dest: 'images/spritesheet.png',
                destCss: 'css/icons.scss',
                padding: 20,
                cssOpts: {
                    cssClass: function (item) {
                        return '.icon-' + item.name;
                    }
                },
            }
        },
        libsass: {
            files: {
                expand: true,
                src: ['css/style.scss', 'css/icons.scss', 'css/modules/**/*.scss'],
                dest: '',
                ext: '.css'
            }
        },
        compressor:{
            css: {
                files: {
                    'dist/css/style.css': ['css/style.css']
                }
            },
            js: {
                options: {
                    mangle: true
                },
                files:grunt.file.expandMapping(['js/*.js','js/*/*.js'], '', {
                    rename: function(base,file) {
                        return 'dist/'+file;
                    }
                })
            },
            html:{
                options:{
                    removeComments: true
                },
                files:{
                    'dist/index.html': ['index.html']
                }
            }
        },
        watch: {
            libsass: {
                files: ['css/*.scss', 'css/**/*.scss'],
                tasks: ['libsass', 'notify']
            },
            sprite: {
                files: ['images/icons/*.png'],
                tasks: ['sprite']
            }
        }
    });
    
    // ładowanie wybranych rozszerzeń dla Grunt.js
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-compressor');
    grunt.loadNpmTasks('grunt-libsass');
    grunt.loadNpmTasks('grunt-spritesmith');

    // rejestrowanie domyślnego zestawu zadań dla Grunt.js
    grunt.registerTask('default', ['watch', 'notify']);
    grunt.registerTask('prod', ['compressor', 'notify']);
};

Na koniec

Mam nadzieję, że ten artykuł zainteresuje was możliwością wykorzystania systemu automatyzacji pracy, jakim jest Grunt, w codziennej pracy. Warto jeszcze wymienić kilka zadań, o których nie wspomniałem w tym artykule, a które mogą okazać się niezwykle pomocne:

  • grunt-contrib-clean – usuwa wskazane pliki i foldery
  • grunt-contrib-copy – pozwala na kopiowanie pliku z miejsca na miejsce
  • grunt-contrib-concat – pozwala na łączenie wielu plików w jeden
  • grunt-contrib-less – kompiluje kod Less do CSS
  • grunt-contrib-imagemin – zmniejsza wagę obrazków

A może wykorzystujecie już Grunt? Chętnie przeczytam o Waszych zastosowaniach.

Tomasz Piątek
autor artykułu

Od kilku lat zajmuje się szeroko pojętą webmasterką, pasjonują go wszelkie nowiki branżowe. Od 1,5 roku pracuje jako front-end developer rozszerzając swoje umiejętności z zakresu HTML, CSS i JS, a gdy potrzeba również coś zaprojektuje. Poza tym fan e-sportu, technologii mobilnych i siatkówki.

Ilustracja w nagłówku artykułu: © Myst
Wszystkie prawa zastrzeżone

Wszystkie prawa zastrzeżone. Wszelkie prawa do artykułu są własnością jego autora. Kopiowanie, modyfikowanie, cytowanie artykułu bez jego zgodny jest zabronione.