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:
- grunt-contrib-qunit – pozwoli nam na odpalenie testów
- grunt-contrib-uglify – zminifikuje pliki JavaScript
- grunt-contrib-cssmin –zminifikuje pliki CSS
- grunt-contrib-concat – połączy pliki w jeden
- grunt-contrib-copy – kopuje pliki z jednej lokalizacji do drugiej
- grunt-contrib-htmlmin – zminifikuje pliki HTML
- grunt-usemin – umożliwia podmianę zalinkowanych zasobów w HTML na te, na które chcemy (na przykład wszystkie pliki js, zamieni na jeden tag z app.js)
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?