Mendorong gruntfile Anda

Cara mengoptimalkan konfigurasi build Anda

Pengantar

Jika dunia Grunt baru bagi Anda, tempat yang ideal untuk memulai adalah artikel luar biasa Chris Coyier, “Grunt for People Who Think Things Like Grunt is Aneh and Hard”. Setelah perkenalan Chris, Anda akan menyiapkan project Grunt sendiri dan merasakan sebagian dari manfaat yang ditawarkan Grunt.

Dalam artikel ini, kita tidak akan fokus pada apa yang dilakukan oleh banyak plugin Grunt pada kode project Anda yang sebenarnya, tetapi pada proses build Grunt itu sendiri. Kami akan memberikan ide praktis tentang:

  • Cara menjaga Gruntfile Anda tetap rapi dan rapi,
  • Cara meningkatkan waktu build secara signifikan,
  • Serta cara mendapatkan notifikasi saat build terjadi.

Saatnya untuk pernyataan penyangkalan singkat: Grunt hanyalah salah satu dari banyak alat yang dapat Anda gunakan untuk menyelesaikan tugas. Jika Gulp lebih gaya Anda, bagus! Jika setelah mensurvei opsi di luar sana, Anda masih ingin membuat toolchain sendiri, itu juga tidak apa-apa. Kami memilih untuk berfokus pada Grunt untuk artikel ini karena ekosistemnya yang kuat dan basis penggunanya yang sudah lama ada.

Mengatur Gruntfile

Baik Anda menyertakan banyak plugin Grunt atau harus menulis banyak tugas manual di Gruntfile, plugin tersebut dapat menjadi sangat berat dan sulit dikelola. Untungnya, ada beberapa plugin yang berfokus pada masalah tersebut: Membuat Gruntfile Anda menjadi rapi dan rapi lagi.

Gruntfile, sebelum pengoptimalan

Berikut adalah tampilan Gruntfile sebelum melakukan pengoptimalan apa pun:

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']);

};

Jika sekarang Anda berkata "Hai! Perkiraanku jauh lebih buruk! Itu sebenarnya bisa dikelola!”, Anda mungkin benar. Demi kemudahan, kami hanya menyertakan tiga plugin tanpa banyak penyesuaian. Menggunakan Gruntfile produksi sebenarnya yang membangun project berukuran sedang akan memerlukan scrolling tanpa batas dalam artikel ini. Mari kita lihat apa yang bisa kita lakukan.

Memuat otomatis plugin Grunt

Saat menambahkan plugin Grunt baru yang ingin digunakan ke project, Anda harus menambahkan keduanya ke file package.json Anda sebagai dependensi npm, lalu memuatnya di dalam Gruntfile. Untuk plugin “grunt-contrib-concat”, plugin tersebut mungkin terlihat seperti berikut:

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

Jika sekarang Anda meng-uninstal plugin melalui npm dan memperbarui package.json, tetapi lupa memperbarui Gruntfile, build Anda akan rusak. Di sinilah plugin bagus load-grunt-tasks akan membantu.

Sementara sebelumnya, kita harus memuat plugin Grunt secara manual, seperti ini:

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

Dengan load-grunt-tasks, Anda dapat menciutkannya menjadi satu baris berikut:

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

Setelah mewajibkan plugin, plugin akan menganalisis file package.json Anda, menentukan dependensi mana yang merupakan plugin Grunt, dan memuat semuanya secara otomatis.

Memisahkan konfigurasi Grunt Anda menjadi beberapa file

load-grunt-tasks sedikit mengurangi kode dan kompleksitas Gruntfile Anda, tetapi saat Anda mengonfigurasi aplikasi yang besar, ukuran tersebut akan tetap menjadi file yang sangat besar. Di sinilah load-grunt-config berperan. load-grunt-config memungkinkan Anda membagi konfigurasi Gruntfile berdasarkan tugas. Selain itu, fungsi ini mengenkapsulasi load-grunt-tasks dan fungsinya.

Namun, penting: Memisahkan Gruntfile mungkin tidak selalu berhasil untuk setiap situasi. Jika ada banyak konfigurasi bersama di antara tugas Anda (yaitu menggunakan banyak template Grunt), Anda harus sedikit berhati-hati.

Dengan load-grunt-config, Gruntfile.js Anda akan terlihat seperti ini:

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

Ya, selesai, seluruh file! Sekarang di mana konfigurasi tugas kita berada?

Buat folder bernama grunt/ di direktori Gruntfile Anda. Secara default, plugin menyertakan file dalam folder tersebut yang cocok dengan nama tugas yang ingin Anda gunakan. Struktur direktori kita akan terlihat seperti ini:

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

Sekarang, mari kita letakkan konfigurasi tugas dari setiap tugas langsung ke file masing-masing (Anda akan melihat bahwa ini sebagian besar hanya salin dan tempel dari Gruntfile asli ke dalam struktur baru):

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/'
    }]
  }
};

Jika blok konfigurasi JavaScript bukan hal yang benar-benar Anda miliki, load-grunt-tasks bahkan memungkinkan Anda menggunakan sintaksis YAML atau CoffeeScript sebagai gantinya. Mari kita tulis file akhir yang diperlukan dalam YAML – file “aliases”. Ini adalah file khusus yang mendaftarkan alias tugas, sesuatu yang harus kita lakukan sebagai bagian dari Gruntfile sebelumnya melalui fungsi registerTask. Ini milik kami:

grunt/aliases.yaml

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

Dan selesai! Jalankan perintah berikut di terminal Anda:

$ grunt

Jika semuanya berfungsi, opsi ini sekarang akan melihat tugas "default" dan menjalankan semuanya secara berurutan. Sekarang setelah kita menghilangkan Gruntfile utama menjadi tiga baris kode yang tidak perlu kita sentuh dan mengeksternalkan setiap konfigurasi tugas, kita selesai di sini. Tapi, untuk membuat semuanya masih cukup lambat. Mari kita lihat apa yang dapat kami lakukan untuk memperbaikinya.

Meminimalkan waktu build

Meskipun performa runtime dan waktu muat aplikasi web Anda jauh lebih penting bagi bisnis daripada waktu yang diperlukan untuk menjalankan build, build yang lambat masih bermasalah. Hal ini akan membuat build otomatis sulit dijalankan dengan plugin seperti grunt-contrib-watch atau setelah Git commit cukup cepat, dan akan memberikan "penalti" untuk benar-benar menjalankan build – makin cepat waktu build, makin gesit alur kerja Anda. Jika build produksi Anda membutuhkan waktu lebih dari 10 menit untuk dijalankan, Anda hanya akan menjalankan build tersebut jika benar-benar membutuhkannya, dan Anda akan berkeliaran untuk membeli kopi saat build tersebut berjalan. Itu adalah pembunuh produktivitas. Kami ingin mempercepat prosesnya.

Hanya build file yang benar-benar berubah: lebih baru

Setelah situs mulai dibangun, Anda mungkin hanya akan menelusuri beberapa file dalam proyek saat ingin membangunnya kembali. Anggap saja dalam contoh kita, Anda mengubah gambar di direktori src/img/ – menjalankan imagemin untuk mengoptimalkan ulang gambar adalah hal yang masuk akal, tetapi hanya untuk satu gambar tersebut – dan tentu saja, menjalankan kembali concat dan uglify hanya menyia-nyiakan siklus CPU yang berharga.

Tentu saja, Anda selalu dapat menjalankan $ grunt imagemin dari terminal, bukan $ grunt, untuk hanya menjalankan tugas secara selektif, tetapi ada cara yang lebih cerdas. Namanya grunt-newer.

Grunt-newer memiliki cache lokal tempatnya menyimpan informasi tentang file apa yang sebenarnya telah berubah, dan hanya menjalankan tugas Anda untuk file yang memang benar-benar berubah. Mari kita lihat cara mengaktifkannya.

Ingat file aliases.yaml kita? Ubah dari sini:

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

menjadi:

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

Cukup tambahkan “lebih baru:” ke setiap tugas Anda akan menyalurkan file sumber dan tujuan melalui plugin yang lebih baru terlebih dahulu, yang kemudian menentukan file apa, jika ada, tugas yang harus dijalankan.

Menjalankan beberapa tugas secara paralel: serentak

grunt-concurrent adalah plugin yang sangat berguna saat Anda memiliki banyak tugas yang tidak bergantung satu sama lain dan menghabiskan banyak waktu. Layanan ini menggunakan jumlah CPU yang ada di perangkat Anda dan menjalankan beberapa tugas secara paralel.

Bagian terbaiknya, konfigurasinya sangat sederhana. Dengan asumsi Anda menggunakan load-grunt-config, buat file baru berikut:

grunt/concurrent.js

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

Kita hanya perlu menyiapkan jalur eksekusi paralel dengan nama “first” dan “second”. Tugas concat harus dijalankan terlebih dahulu, dan tidak ada lagi yang harus dijalankan dalam contoh ini. Di jalur kedua, kami menempatkan uglify dan imagemin, karena keduanya bersifat independen satu sama lain, dan keduanya memerlukan waktu yang cukup lama.

Ini sendiri belum melakukan apa pun. Kita harus mengubah alias tugas default agar mengarah ke tugas serentak, bukan tugas langsung. Berikut konten baru grunt/aliases.yaml:

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

Jika sekarang Anda menjalankan kembali build grunt, plugin serentak akan menjalankan tugas concat terlebih dahulu, lalu memunculkan dua thread pada dua core CPU yang berbeda untuk menjalankan imagemin dan uglify secara paralel. Hore!

Namun, sebuah saran: Kemungkinan besar, dalam contoh dasar kita, serempak serentak tidak akan membuat build Anda jauh lebih cepat. Alasan untuk ini adalah overhead yang dibuat dengan memunculkan berbagai instance Grunt di utas yang berbeda: Dalam kasus saya, setidaknya +300 ms pro spawn.

Berapa lama waktu yang dibutuhkan? Time-grunt

Sekarang setelah kita mengoptimalkan setiap tugas, akan sangat membantu untuk memahami berapa banyak waktu yang diperlukan untuk melaksanakan setiap tugas. Untungnya, ada juga plugin untuk itu: time-grunt.

time-grunt bukanlah plugin grunt klasik yang Anda muat sebagai tugas npm, melainkan plugin yang Anda sertakan secara langsung, mirip dengan load-grunt-config. Kita akan menambahkan kebutuhan untuk time-grunt ke Gruntfile, seperti yang kita lakukan dengan load-grunt-config. Gruntfile kita akan terlihat seperti ini sekarang:

module.exports = function(grunt) {

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

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

};

Maaf jika mengecewakan, tetapi itu saja – coba jalankan kembali Grunt dari Terminal Anda dan untuk setiap tugas (dan juga total build), Anda akan melihat panel info yang diformat dengan baik pada waktu eksekusi:

Waktu gerutu

Notifikasi sistem otomatis

Sekarang setelah Anda memiliki build Grunt yang sangat dioptimalkan dan dieksekusi dengan cepat, dan memberi Anda build otomatis dengan cara tertentu (yaitu dengan melihat file dengan grunt-contrib-watch, atau setelah commit), bukankah lebih baik jika sistem Anda dapat memberi tahu ketika build baru Anda siap digunakan, atau ketika sesuatu yang buruk terjadi? Melihat pesan grunt-notify.

Secara default, grunt-notify menyediakan notifikasi otomatis untuk semua error dan peringatan Grunt menggunakan sistem notifikasi apa pun yang tersedia di OS Anda: Growl untuk OS X atau Windows, Mountain Lion’s dan Mavericks’ Notification Center, dan Notify-send. Yang luar biasa, yang Anda butuhkan untuk mendapatkan fungsionalitas ini adalah dengan menginstal plugin dari npm dan memuatnya di Gruntfile (ingat, jika Anda menggunakan grunt-load-config di atas, langkah ini otomatis).

Bergantung pada sistem operasi Anda, tampilannya akan seperti berikut:

Notify

Selain error dan peringatan, mari kita konfigurasikan agar berjalan setelah tugas terakhir kita selesai dieksekusi. Dengan asumsi Anda menggunakan grunt-load-config untuk membagi tugas di seluruh file, berikut adalah file yang kita perlukan:

grunt/notify.js

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

Di level pertama objek konfigurasi, kunci harus cocok dengan nama tugas yang ingin dihubungkan. Contoh ini akan menyebabkan pesan muncul tepat setelah tugas imagemin dijalankan, yang merupakan tugas terakhir dalam rantai build kita.

Menyelesaikan semuanya

Jika mengikuti dari atas, sekarang Anda adalah pemilik proses build yang sangat rapi dan terorganisir, berlangsung sangat cepat karena paralelisasi dan pemrosesan selektif, serta memberi tahu Anda jika ada yang tidak beres.

Jika Anda menemukan permata lain yang meningkatkan kualitas Grunt dan pluginnya lebih lanjut, beri tahu kami. Sampai jumpa lagi!

Update (14/2/2014): Untuk mendapatkan salinan contoh project Grunt yang lengkap dan berfungsi, klik di sini.