Skoro już mamy nasze środowisko przygotowane i wiemy co i jak z tym grunt, to pora się nim trochę pobawić :)

UWAGA: to jest przykład, nie twierdzę że najlepiej napisany, chodzi głównie o pokazanie co można zrobić.

Krok 0 – przygotowanie środowiska

W aplikacji będziemy wykorzystywać grunt jak i bower, więc warto sobie przypomnieć jak te dwie rzeczy zainstalować :)

npm install -g bower
npm install -g grunt-cli 

Krok 1 – informacje na temat aplikacji i struktury katalogów

W tym celu wykorzystamy przykładową aplikację dostępną u mnie na github. Ściągnijmy ją sobie:

git clone https://github.com/Gutek/sample-todo-app.git

Krótka notka na temat struktury katalogów:

  • build – to tutaj będzie dostępna nasza aplikacja po wykonaniu tasków w grunt
  • test – tutaj są dostępne testy naszej aplikacji
  • src – kod źródłowy naszej aplikacji
    • css – stylesheet dla aplikacji
    • images – obrazki wykorzystywane przez aplikację
    • js – miejsce gdzie są wszystkie skrypty aplikacji
      • lib – lokalizacja skryptów ściągniętych przez bower

Krok 2 – przygotowanie workspace

Skoro już sięgnęliśmy wszystko, pora zainstalować już zdefiniowane paczki:

bower install
npm install

Zainstaluje nam to wszystkie wymagane komponenty bowera w katalogu lib (plik .bowerrc to określa) oraz zainstaluje lokalną wersję grunta.

Krok 3 – pierwszy plugin do grunta – jshint

Ze względu na to, że nasza aplikacja posiada kod w JavaScript, warto przepuścić ją przez analizator.

W tym celu, musimy zainstalować paczkę grunt-contrib-jshint – wszystkie paczki jakie są dostępne, można znaleźć na stronie Grunt Plugins.

npm install grunt-contrib-jshint --save-dev

Z miejsca zainstalujmy dodatkową paczkę która spowoduje, że wyniki analizy będą “piękniejsze” – jshint-stylish :)

npm install --save-dev jshint-stylish

Jak już mamy te dwie paczki zainstalowane, otwórzmy plik gruntfile.js i nadpiszmy go:

module.exports = function (grunt) {

    grunt.initConfig({
        // full options list: 
        // https://github.com/gruntjs/grunt-contrib-jshint
        jshint: {
            options: {
                curly: true,
                eqeqeq: true,
                eqnull: true,
                browser: true,
                globals: {
                    jQuery: true,
                    ko: true
                },
                // our nice reporter
                reporter: require('jshint-stylish')
            },
            // what files should be included
            all: [
                'gruntfile.js', 
                'src/js/*.js', 
                'test/**/*.js'
            ]
        }
    });

    // load tasks
    grunt.loadNpmTasks('grunt-contrib-jshint');

    // register defaut task
    grunt.registerTask('default', [ ]);
};

Teraz, możemy odpalić nasz nowy task za pomocą:

grunt jshint

Krok 4 – auto-śledzenie zmian

Mamy nasz pierwszy task, ale co z tego, że aby go odpalić za każdym razem musimy odpalać komendę:

grunt jshint

Przecież to jest męczące. Rozwiążmy ten problem za pomocą paczki grunt-contrib-watch:

npm install grunt-contrib-watch --save-dev

I aktualizujemy nasz plik gruntfile.js:

module.exports = function (grunt) {
	
    grunt.initConfig({
        // full options list: 
        // https://github.com/gruntjs/grunt-contrib-jshint
        jshint: {
            options: {
                curly: true,
                eqeqeq: true,
                eqnull: true,
                browser: true,
                globals: {
                    jQuery: true,
                    ko: true
                },
                // our nice reporter
                reporter: require('jshint-stylish')
            },
            // what files should be included
            all: [
                'gruntfile.js', 
                'src/js/*.js', 
                'test/**/*.js'
            ]
        },
        // full options list:
        // https://github.com/gruntjs/grunt-contrib-watch
        watch: {
            all: {
                files: '<%= jshint.all %>',
                tasks: [ 'jshint:all' ],
                options: {
                    spawn: false,
                    interrupt: true
                }
            },
            configFiles: {
                files: [ 'gruntfile.js' ],
                options: {
                    reload: true
                }
            }
        }
    });

    // load tasks
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-watch');

    // register defaut task
    grunt.registerTask('default', []);
};

Teraz możemy na przykład odpalić taks:

grunt watch:all

jak tylko zapiszemy jeden z śledzonych plików, zostanie wykonany task jshint.

Dzięki temu, możemy mieć okno cmd otwarte cały czas i widzieć co gdzie źle robimy :)

Krok 5 – pozostałe paczki

Ok, nie ma sensu rozbijać każdej paczki na kolejne punkty ;) to co chcemy uzyskać to aplikację w katalogu build, która będzie z minifikowana (zarówno pliki js, css jak i html) oraz te pliki które możemy niech zostaną połączone w jeden. Dużym plusem także byłaby możliwość wykonania testów.

Zaczynamy więc z instalacją paczek:

W lini poleceń wpisujemy po kolei:

npm install grunt-contrib-qunit --save-dev
npm install grunt-contrib-uglify --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-copy --save-dev
npm install grunt-contrib-cssmin --save-dev
npm install grunt-contrib-htmlmin --save-dev
npm install grunt-usemin --save-dev

Następnie dodajmy do gruntfile.js załadowanie tych tasków:

grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-htmlmin');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-usemin');

Teraz, możemy napisać własną konfigurację dla poszczególnych tasków, lub wykorzystać usemin który wygeneruje nam taski, na podstawie komentarzy w pliku HTML (index.html), na przykład:

<!-- build:js js/app.min.js -->
<script src="js/todos.js" type="text/javascript"></script>
<script src="js/app.js" type="text/javascript"></script>
<!-- endbuild -->

Jeżeli nie potrzebujemy super dokładnej kontroli nad tym i takich rzeczy jak tworzenie pliku source map, to nie ma sensu się męczyć i tworzyć konfigurację ręcznie. Ale decyzja należy do was :)

By zrobić to co chcemy, musimy nasz plik gruntfile.js podmienić na:

module.exports = function (grunt) {

    grunt.initConfig({
        // full options list: 
        // https://github.com/gruntjs/grunt-contrib-jshint
        jshint: {
            options: {
                curly: true,
                eqeqeq: true,
                eqnull: true,
                browser: true,
                globals: {
                    jQuery: true,
                    ko: true
                },
                // our nice reporter
                reporter: require('jshint-stylish')
            },
            // what files should be included
            all: [
                'gruntfile.js', 
                'src/js/*.js', 
                'test/**/*.js'
            ],
            test: [
                'test/**/*.js'
            ],
            src: [
                'src/js/*.js'
            ]
        },
        // full options list:
        // https://github.com/gruntjs/grunt-contrib-watch
        watch: {
            all: {
                files: '<%= jshint.all %>',
                tasks: [ 'jshint:all', 'qunit' ],
                options: {
                    spawn: false,
                    interrupt: true
                }
            },
            test: {
                files: '<%= jshint.test %>',
                tasks: ['jshint:test', 'qunit' ]
            },
            configFiles: {
                files: [ 'gruntfile.js' ],
                options: {
                    reload: true
                }
            }
        },
        // we can set this task manually or we can depend on usemin
        // concat: {
        //     css: {
        //         src: [ 'src/css/*' ],
        //         dest: 'build/css/site.css'
        //     },
        //     js: {
        //         src: [ 'src/js/*.js'],
        //         dest: 'build/js/app.js'
        //     }
        // },
        // uglify: {
        //     build: {
        //         options: {
        //             sourceMap: true,
        //             sourceMapName: 'build/js/app.map'
        //         },
        //         files: {
        //             'build/js/app.min.js' : [ 'build/js/app.js' ]
        //         }
        //     }
        // },
        // usemin does not have htmlmin, so we need to run it
        // manually at the end of the process
        // otherwise usemin will not convert links
        qunit: {
            all: ['test/**/*.html']
        },
        htmlmin: {
            build: {
                options: {
                    removeComments: true,
                    collapseWhitespace: true
                },
                files: {
                    'build/index.html' : 'build/index.html'
                }
            }
        },
        // we need this task to copy files from src to desc
        // including index.html that will not be copied
        // by usemin
        copy: {
            build: {
                files: [{
                    src: 'src/index.html',
                    dest: 'build/index.html'
                }, {
                    src: 'src/images/destroy.png',
                    dest: 'build/images/destroy.png'
                }]
            }
        },
        useminPrepare: {
            options: {
                dest: 'build'
            },
            html: 'src/index.html'
        },
        usemin: {
            options: {
                dirs: ['build']
            },
            html: ['build/{,*/}*.html'],
            css: ['build/css/{,*/}*.css']
        }
    });

    // load tasks
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-htmlmin');
    grunt.loadNpmTasks('grunt-contrib-qunit');
    grunt.loadNpmTasks('grunt-usemin');

    // register defaut task
    grunt.registerTask('default', []);

    // build task
    grunt.registerTask('build', [
        // execute jshint on file
        'jshint:all',
        // unit test code
        'qunit',
        // copy images and html files
        'copy',
        // prapre concat with minify and uglify
        'useminPrepare',
        'concat',
        'cssmin',
        'uglify',
        // update index.html
        'usemin',
        // minify index.html
        'htmlmin'
    ]);
};

Teraz mamy wszystkie taski jakie chcemy, jednak jest problem – usemin, ma dwa taski – useminPrepare i task wykonywany na koniec usemin. By działał nam poprawnie i generował odpowiednie rzeczy musimy zdefiniować swój własny task – nazwijmy go build:

grunt.registerTask('build', [
    // execute jshint on file
    'jshint:all',
    // unit test code
    'qunit',
    // copy images and html files
    'copy',
    // prapre concat with minify and uglify
    'useminPrepare',
    'concat',
    'cssmin',
    'uglify',
    // update index.html
    'usemin',
    // minify index.html
    'htmlmin'
]);

Teraz wystarczy, że command line wykonamy polecenie:

grunt build

i automatycznie zostanie dla nas wykonane:

  • Analiza kodu – jeżeli wystąpi błąd, proces jest przerywany
  • Testowanie kodu – jeżeli wystąpi błąd, proces jest przerywany
  • Kopiowanie niezbędnych plików do katalogu docelowego
  • Łączenie plików w jeden i generowanie ich wersji zminifikowanych
  • Podmiana linków w HTML
  • Minifikacja pliku HTML

Moim zdaniem SUPER sprawa :)

Skąd czerpać inspiracje

Ze względu na to, że grunt jest już „dojrzałą” technologią, nie ma problemu z szukaniem przykładów w sieci. Pierwszym miejscem w które możecie zajrzeć to strona na której grunt się chwali kto z niego korzysta. Znajdziecie tam pliki grunt proste jak i bardzo skomplikowane.

Innym sposobem jest po prostu patrzenie na konkretne projekty, na przykład Angular.js korzysta z pliku gruntfile.js – jak już wiecie, że taki plik może istnieć i co on robi, to jak wejdziecie w projekt na github zobaczycie, że z dużym prawdopodobieństwem gruntfile.js będzie tam dostępny.

Jak nie to search na github i boom – ponad 200K plików. Do tego google i serio, z Gruntem nie ma problemu by znaleźć przykład, odpowiednią paczkę czy też prawie gotowy plik do wykorzystania. Plusem jest to, że przy wykorzystaniu innych narzędzi Yeoman (o czym będzie jeszcze) plik gruntfile.js zostanie dla nas wygenerowany zgodnie z szablonem który wykorzystamy.

Podsumowanie

Mam jednak sam duży problem z grunt, można w nim zrobić baaaardzo dużo, ale dla mnie jest to nieczytelne, bardzo szybko zaczynam się w pliku gruntfile.js gubić, gubię się także w konfiguracji poszczególnych tasków, gdyż jedną rzecz można wykonać na wiele sposobów.

Moim zdaniem dużym minusem grunta jest to, że plugin jeden jest odpowiedzialny za wiele rzeczy. Na przykład plugin uglify, nie tylko zminifikuje, ale także może nam połączyć pliki, i przegrać je do katalogu docelowego. No to po co wykorzystywać copy i concat? Niby nie ma po co, ale jak się z nich korzysta to przynajmniej jest „świadomość” tego, że mamy poszczególne zadania.

Tak czy siak, sami powiedzcie, czy plik docelowy gruntfile.js jest czytelny/zrozumiały? Na początku był fajny, mały, zwięzły, a ten na końcu?

Ja osobiście korzystałem z grunta może dwa razy, za pierwszym razem zrezygnowałem po godzinie konfiguracji procesu buildowania, za drugim razem, nie poddałem się, stworzyłem to co chciałem i działało, do póki modyfikacji nie musiał w nim zrobić. Wtedy powiedziałem sobie dość i przeniosłem się na alternatywę ;) ale o tym, następnym razem :)

PS.: a wy korzystanie z grunta? Jakie są wasze odczucia?