Wzbogacanie pliku gruntfile

Jak w pełni wykorzystać konfigurację kompilacji

Wstęp

Jeśli dopiero poznajesz świat Grunt, najlepiej zacząć od świetnego artykułu Chrisa Coyiera „Grunt for People Who Think Things Like Grunt are Weird and Hard”. Po wprowadzeniu Chrisa możesz stworzyć własny projekt Grunt i spróbować świeżej energii.

W tym artykule nie będziemy skupiać się na tym, jak liczne wtyczki Grunt robią z rzeczywistym kodem projektu, ale na samym procesie kompilacji Grunt. Przedstawimy Ci praktyczne koncepcje dotyczące:

  • Jak zachować porządek w Gruntfile?
  • Jak znacznie skrócić czas kompilacji,
  • oraz o tym, jak otrzymywać powiadomienia o kompilacji.

Czas na krótką zastrzeżenie: Grunt to tylko jedno z wielu narzędzi, które przydadzą się do wykonania tego zadania. Jeśli Gulp to Twój styl, to świetnie. Jeśli po zapoznaniu się z dostępnymi opcjami nadal zechcesz utworzyć własny łańcuch narzędzi, nie szkodzi. W tym artykule skupiliśmy się na Grunt ze względu na rozbudowany ekosystem i wieloletnią bazę użytkowników.

Porządkowanie pliku Gruntfile

W pliku Gruntfile jest wiele wtyczek Grunt i konieczne jest pisanie wielu ręcznych zadań. Taki plik szybko staje się nieporęczny i trudny w utrzymaniu. Na szczęście jest sporo wtyczek, które koncentrują się właśnie na tym problemie: utrzymaniu porządku w pliku Gruntfile.

Plik Gruntfile przed optymalizacją

Tak wygląda plik Gruntfile, zanim przeprowadziliśmy optymalizację:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

Jeśli teraz mówisz „Hej! Spodziewałem się znacznie gorszego! Można to łatwo utrzymać!”, zapewne masz rację. Dla uproszczenia dołączyliśmy tylko 3 wtyczki, które nie wymagają dużych zmian. Użycie rzeczywistego pliku Gruntfile do utworzenia umiarkowanego projektu wymagałoby nieskończonego przewijania w tym artykule. Zobaczmy, co potrafimy.

Automatycznie ładuj wtyczki Grunt

Gdy dodajesz nową wtyczkę Grunt, której chcesz użyć w projekcie, musisz dodać ją do pliku package.json jako zależność npm i wczytać ją w Gruntfile. Wtyczka „grunt-contrib-concat” może wyglądać tak:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

Jeśli teraz odinstalujesz wtyczkę przez npm i zaktualizujesz plik package.json, ale zapomnisz zaktualizować plik Gruntfile, kompilacja przestanie działać. Z pomocą przychodzi Ci do tego sprytna wtyczka load-grunt-tasks.

Wcześniej musieliśmy ręcznie wczytywać wtyczki Grunt w ten sposób:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

Możesz je zwinąć za pomocą polecenia load-grunt-tasks:

require('load-grunt-tasks')(grunt);

Po włączeniu tej wtyczki system przeanalizuje plik package.json, określi, które z zależności są wtyczkami Grunt, i automatycznie je wczyta.

Dzielenie konfiguracji Grunt na różne pliki

Zastosowanie metody load-grunt-tasks pozwoliło nieznacznie zmniejszyć plik Gruntfile i pod względem złożoności kodu, ale kiedy skonfigurujesz dużą aplikację, stanie się ona bardzo dużym plikiem. W tym miejscu wkracza do gry load-grunt-config. load-grunt-config pozwala podzielić konfigurację Gruntfile według zadań. Obejmuje też funkcję load-grunt-tasks i jej funkcje.

Ważne jest jednak, że podział pliku Gruntfile nie zawsze się sprawdza w każdej sytuacji. Jeśli między zadaniami jest wiele wspólnych konfiguracji (tj. używasz wielu szablonów Grunt), zachowaj ostrożność.

Po zastosowaniu pliku load-grunt-config plik Gruntfile.js będzie wyglądać tak:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

Tak, to wszystko. Gdzie znajdują się teraz nasze konfiguracje zadań?

W katalogu pliku Gruntfile utwórz folder o nazwie grunt/. Domyślnie wtyczka uwzględnia w folderze pliki o nazwie zadania, którego chcesz użyć. Struktura katalogów powinna wyglądać tak:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

Umieśćmy teraz konfigurację poszczególnych zadań bezpośrednio w odpowiednich plikach (zobaczysz, że w większości przypadków są to po prostu skopiowanie z pierwotnego pliku Gruntfile i wklejenie ich do nowej struktury):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

Jeśli bloki konfiguracji JavaScriptu Ci nie odpowiadają, możesz użyć polecenia load-grunt-tasks na użycie zamiast nich składni YAML lub CoffeeScript. Zapiszmy nasz ostatni wymagany plik w formacie YAML, czyli „aliases”. To specjalny plik rejestrujący aliasy zadań. Wcześniej musieliśmy to zrobić w ramach Gruntfile przy użyciu funkcji registerTask. Oto nasze:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

Proste, prawda? Wykonaj w terminalu to polecenie:

$ grunt

Jeśli wszystko zadziałało, zostanie wyświetlone zadanie „domyślne” i uruchomione wszystko po kolei. Po skróceniu głównego pliku Gruntfile do 3 wierszy kodu nie musimy już nigdy ingerować w każdą konfigurację zadań i nadal korzystać z nich na zewnątrz. Ale ja nadal mamy dość wolnego czasu. Zobaczmy, jak możemy to poprawić.

Minimalizowanie czasu kompilacji

Choć wydajność środowiska wykonawczego i czasu wczytywania aplikacji internetowej ma większe znaczenie dla firmy niż czas potrzebny na wykonanie kompilacji, powolna kompilacja i tak stanowi problem. Utrudni to wykonywanie automatycznych kompilacji za pomocą wtyczek takich jak grunt-contrib-watch lub po dostatecznie szybkim zatwierdzeniu Git. Wprowadzi też „karę”, która doprowadzi do faktycznego uruchomienia kompilacji – im krótszy czas kompilacji, tym bardziej elastyczniejszy przepływ pracy. Jeśli kompilacja produkcyjna trwa dłużej niż 10 minut, uruchomisz ją tylko wtedy, kiedy jest to absolutnie konieczne, i wyjdziesz po kawę w trakcie trwania procesu. To prawdziwa rewolucja. Mamy coś, co możemy przyspieszyć.

Kompiluj tylko te pliki, które faktycznie się zmieniły: grunt-newer

Gdy już po wstępnej kompilacji strony wrócisz do tworzenia, może się zdarzyć, że będziesz mieć tylko kilka plików w projekcie. Załóżmy, że w naszym przykładzie zmienisz obraz w katalogu src/img/ – uruchomienie imagemin w celu ponownej optymalizacji obrazów ma sens, ale tylko w przypadku tego pojedynczego obrazu. No i oczywiście ponowne uruchomienie concat i uglify tylko marnuje cenne cykle procesora.

Oczywiście możesz użyć polecenia $ grunt imagemin w terminalu zamiast $ grunt, aby wykonać tylko selektywne wykonywanie zadania, ale jest na to sposób. To grunt-newer.

Grunt-newer ma lokalną pamięć podręczną, w której przechowuje informacje o zmienionych plikach. Wykonuje zadania tylko dla tych plików, które faktycznie uległy zmianie. Zobaczmy, jak go aktywować.

Pamiętasz plik aliases.yaml? Zmień go z tego:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

na:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

Po prostu na początku przed którymkolwiek z zadań przetwarza się pliki źródłowe i docelowe przez wtyczkę grunt-newer. Następnie ta wtyczka określa, które pliki (jeśli w ogóle) mają zostać uruchomione.

Równoległe uruchamianie wielu zadań: grunt-concurrent

Wtyczka grunt-concurrent bardzo przydaje się w sytuacjach, gdy masz dużo niezależnych od siebie zadań, które pochłaniają dużo czasu. Wykorzystuje liczbę procesorów w urządzeniu i wykonuje wiele zadań równolegle.

A co najważniejsze, konfiguracja tej funkcji jest bardzo prosta. Zakładając, że używasz load-grunt-config, utwórz nowy plik:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

Po prostu ustawiamy równoległe ścieżki wykonania o nazwach „first” i „second”. W naszym przykładzie musi zostać uruchomione zadanie concat. W drugiej ścieżce umieszczamy zarówno wartości uglify, jak i imagemin, ponieważ są one od siebie niezależne i zajmują sporo czasu.

To samoczynnie jeszcze nic nie daje. Musimy zmienić domyślny alias zadań, aby wskazywał zadania równoległe, a nie bezpośrednie. Oto nowa zawartość pliku grunt/aliases.yaml:

default:
  - 'concurrent:first'
  - 'concurrent:second'

Jeśli teraz uruchomisz ponownie kompilację grunt, wtyczka równoczesna najpierw uruchomi zadanie concat, a potem utworzy 2 wątki na 2 różnych rdzeniach procesora, aby uruchomić równolegle funkcje imagemin i uglify. Hura!

Jedna rada: w naszym podstawowym przykładzie może się okazać, że chrapanie współbieżne nie przyspieszy kompilacji. Dzieje się tak w przypadku narzutu powodowanego przez wywoływanie różnych wystąpień Grunt w różnych wątkach. W moim przypadku pojawia się co najmniej 300 ms protezy.

Ile to zajęło?

Teraz, gdy optymalizujemy wszystkie zadania, może mi pomóc Pan/Pani dowiedzieć się, ile czasu trzeba poświęcić na wykonanie poszczególnych zadań. Na szczęście możesz skorzystać z dostępnej do tego wtyczki: time-grunt.

Time-grunt nie jest klasyczną wtyczkę grunt, którą wczytujesz jako zadanie npm. Jest to raczej wtyczka dołączona bezpośrednio, podobnie jak w przypadku load-grunt-config. Tak jak w przypadku load-grunt-config, dodamy do pliku Gruntfile wymóg ograniczenia czasu. Nasz plik Gruntfile powinien teraz wyglądać tak:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

Przykro mi, ale to wszystko. Uruchom ponownie Grunt w terminalu, a w przypadku każdego zadania (i całej kompilacji) powinien pojawić się ładnie sformatowany panel informacyjny o czasie wykonywania:

Czas brutalności

Automatyczne powiadomienia systemowe

Masz już mocno zoptymalizowaną kompilację Grunt, która szybko się uruchamia i w jakiś sposób automatycznie buduje (np.przez oglądanie plików przy użyciu polecenia grunt-contrib-watch lub po zatwierdzeniach). Czy nie byłoby wspaniale, gdyby system informował Cię, gdy nowa kompilacja jest gotowa do użycia lub gdy coś się stało? Meet – grunt-notify.

Domyślnie funkcja grunt-notify dostarcza automatyczne powiadomienia o wszystkich błędach i ostrzeżeniach Grunt, korzystając z dowolnego systemu powiadomień dostępnego w Twoim systemie operacyjnym: Growl na OS X lub Windows, Centrum powiadomień Mountain Lion i Mavericks oraz Notify-send. Niesamowite, aby uzyskać dostęp do tej funkcji, wystarczy zainstalować wtyczkę z npm i wczytać ją w pliku Gruntfile (pamiętaj, że jeśli korzystasz z grunt-load-config powyżej, ten krok jest automatyczny).

Tak będzie to wyglądać w zależności od systemu operacyjnego:

Powiadom

Oprócz błędów i ostrzeżeń skonfigurujmy je tak, aby uruchamiało się po zakończeniu wykonywania ostatniego zadania. Zakładając, że używasz polecenia grunt-load-config do podziału zadań na pliki, potrzebujemy tego pliku:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

Na pierwszym poziomie obiektu konfiguracyjnego klucz musi odpowiadać nazwie zadania, z którym chcesz go połączyć. W tym przykładzie komunikat pojawi się tuż po wykonaniu zadania imagemin, czyli ostatniego w łańcuchu kompilacji.

Podsumowanie

Teraz jesteś dumnym właścicielem procesu tworzenia, który jest wyjątkowo uporządkowany i niezwykle szybki, dzięki równoległemu i selektywnemu przetwarzaniu oraz powiadamia o problemach.

Jeśli znajdziesz inny klejnot, który jeszcze bardziej usprawni Grunt i jego wtyczki, daj nam znać. Do tego czasu – miłego chrapania!

Aktualizacja (14.02.2014): aby pobrać kopię pełnego, działającego projektu Grunt, kliknij tutaj.