Lassen Sie uns Grunt-Tasks der Marie Kondo Organisationsbehandlung unterziehen

Avatar of Serj Lavrin
Serj Lavrin am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wir leben in einer Ära von webpack und npm-Skripten. Ob gut oder schlecht, sie haben die Führung für Bundling und Task-Ausführung übernommen, zusammen mit Teilen von Rollup, JSPM und Gulp. Aber seien wir ehrlich. Einige Ihrer älteren Projekte verwenden immer noch das gute alte Grunt. Auch wenn es nicht mehr so glänzt, erledigt es seine Arbeit gut, sodass es wenig Grund gibt, es anzurühren.

Doch ab und zu fragen Sie sich, ob es einen Weg gibt, diese Projekte besser zu machen, richtig? Dann beginnen Sie mit dem Artikel „Organizing Your Grunt Tasks“ und kommen Sie zurück. Das wird die Bühne für diesen Beitrag bereiten, und dann werden wir gemeinsam weitermachen, um eine solide Organisation von Grunt-Tasks zu schaffen.

Automatisches Laden von Speed-Daemon-Tasks

Es macht keinen Spaß, Ladedeklarationen für jeden Task zu schreiben, so wie hier

grunt.loadNpmTasks('grunt-contrib-clean')
grunt.loadNpmTasks('grunt-contrib-watch')
grunt.loadNpmTasks('grunt-csso')
grunt.loadNpmTasks('grunt-postcss')
grunt.loadNpmTasks('grunt-sass')
grunt.loadNpmTasks('grunt-uncss')

grunt.initConfig({})

Es ist üblich, load-grunt-tasks zu verwenden, um alle Tasks automatisch zu laden. Aber was, wenn ich Ihnen sage, dass es einen schnelleren Weg gibt?

Probieren Sie jit-grunt! Ähnlich wie load-grunt-tasks, aber noch schneller als native grunt.loadNpmTasks.

Der Unterschied kann auffällig sein, besonders in Projekten mit großen Codebasen.

Ohne jit-grunt

loading tasks     5.7s  ▇▇▇▇▇▇▇▇ 84%
assemble:compile  1.1s  ▇▇ 16%
Total 6.8s

Mit jit-grunt

loading tasks     111ms  ▇ 8%
loading assemble  221ms  ▇▇ 16%
assemble:compile   1.1s  ▇▇▇▇▇▇▇▇ 77%
Total 1.4s

1,4 Sekunden machen es noch nicht zu einem Speed-Daemon… also habe ich gelogen. Aber immerhin ist es sechsmal schneller als der traditionelle Weg! Wenn Sie neugierig sind, wie das möglich ist, lesen Sie über das ursprüngliche Problem, das zur Erstellung von jit-grunt führte.

Wie wird jit-grunt verwendet? Zuerst installieren

npm install jit-grunt --save

Ersetzen Sie dann alle Task-Ladeanweisungen durch eine einzige Zeile

module.exports = function (grunt) {
  // Intead of this:
  // grunt.loadNpmTasks('grunt-contrib-clean')
  // grunt.loadNpmTasks('grunt-contrib-watch')
  // grunt.loadNpmTasks('grunt-csso')
  // grunt.loadNpmTasks('grunt-postcss')
  // grunt.loadNpmTasks('grunt-sass')
  // grunt.loadNpmTasks('grunt-uncss')

  // Or instead of this, if you've used `load-grunt-tasks`
  // require('load-grunt-tasks')(grunt, {
  //   scope: ['devDependencies', 'dependencies'] 
  // })

  // Use this:
  require('jit-grunt')(grunt)

  grunt.initConfig({})
}

Fertig!

Besseres Laden von Konfigurationen

Im letzten Beispiel haben wir Grunt gesagt, wie es Tasks selbst laden soll, aber wir haben die Arbeit nicht ganz erledigt. Wie „Organizing Your Grunt Tasks“ vorschlägt, ist eines der nützlichsten Dinge, die wir hier tun wollen, die Aufteilung eines monolithischen Gruntfiles in kleinere, eigenständige Dateien.

Wenn Sie den erwähnten Artikel gelesen haben, wissen Sie, dass es besser ist, die gesamte Task-Konfiguration in externe Dateien zu verschieben. Anstatt also eine große einzelne gruntfile.js-Datei

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    clean: {/* task configuration goes here */},
    watch: {/* task configuration goes here */},
    csso: {/* task configuration goes here */},
    postcss: {/* task configuration goes here */},
    sass: {/* task configuration goes here */},
    uncss: {/* task configuration goes here */}
  })
}

Wir wollen das

tasks
  ├─ postcss.js
  ├─ concat.js
  ├─ cssmin.js
  ├─ jshint.js
  ├─ jsvalidate.js
  ├─ uglify.js
  ├─ watch.js
  └─ sass.js
gruntfile.js

Aber das zwingt uns, jede externe Konfiguration manuell in gruntfile.js zu laden, und das kostet Zeit! Wir brauchen eine Möglichkeit, unsere Konfigurationsdateien automatisch zu laden.

Dazu verwenden wir load-grunt-configs. Es nimmt einen Pfad, schnappt sich alle Konfigurationsdateien dort und gibt uns ein zusammengeführtes Konfigurationsobjekt, das wir für die Initialisierung der Grunt-Konfiguration verwenden.

So funktioniert es

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  const configs = require('load-grunt-configs')(grunt, {
    config: { src: 'tasks/.js' }
  })

  grunt.initConfig(configs)
  grunt.registerTask('default', ['cssmin'])
}

Grunt kann dasselbe nativ tun! Schauen Sie sich grunt.task.loadTasks (oder seinen Alias grunt.loadTasks) an.

Verwenden Sie es so

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({})

  // Load all your external configs.
  // It's important to use it _after_ Grunt config has been initialized,
  // otherwise it will have nothing to work with.
  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

Grunt lädt automatisch alle js- oder coffee-Konfigurationsdateien aus dem angegebenen Verzeichnis. Schön und sauber! Aber wenn Sie versuchen, es zu verwenden, werden Sie feststellen, dass es nichts tut. Wie kommt das? Wir müssen noch eine Sache tun.

Schauen wir uns noch einmal unseren gruntfile.js-Code an, diesmal ohne die Kommentare

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({})

  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

Beachten Sie, dass grunt.loadTasks Dateien aus dem Verzeichnis tasks lädt, sie aber nie unserer tatsächlichen Grunt-Konfiguration zuweist.

Vergleichen Sie es mit der Funktionsweise von load-grunt-configs

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  // 1. Load configs
  const configs = require('load-grunt-configs')(grunt, {
    config: { src: 'tasks/.js' }
  })

  // 2. Assign configs
  grunt.initConfig(configs)

  grunt.registerTask('default', ['cssmin'])
}

Wir initialisieren unsere Grunt-Konfiguration, *bevor* wir die Task-Konfiguration tatsächlich laden. Wenn Sie das starke Gefühl haben, dass dies dazu führt, dass wir eine leere Grunt-Konfiguration erhalten, haben Sie völlig Recht. Denn im Gegensatz zu load-grunt-configs importiert grunt.loadTasks lediglich Dateien in gruntfile.js. *Es tut nichts weiter.*

Wow! Wie machen wir uns das also zunutze? Lassen Sie uns das untersuchen!

Erstellen Sie zunächst eine Datei im Verzeichnis tasks namens test.js

module.exports = function () {
  console.log("Hi! I'm an external task and I'm taking precious space in your console!")
}

Lassen Sie uns jetzt Grunt ausführen

$ grunt

Wir sehen Folgendes auf der Konsole ausgegeben

> Hi! I'm an external task and I'm taking precious space in your console!

Beim Import von grunt.loadTasks wird jede Funktion ausgeführt, sobald die Dateien geladen werden. Das ist schön, aber was nützt uns das? Wir können immer noch nicht das tun, was wir eigentlich wollen – unsere Tasks konfigurieren.

Halten Sie mein Bier, denn es gibt eine Möglichkeit, Grunt von internen Konfigurationsdateien aus zu steuern! Die Verwendung von grunt.loadTasks beim Importieren übergibt die aktuelle Grunt-Instanz als erstes Argument der Funktion und bindet sie auch an this.

Wir können also unser Gruntfile aktualisieren

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    // Add some value to work with
    testingValue: 123
  })

  grunt.loadTasks('tasks')

  grunt.registerTask('default', ['cssmin'])
}

…und die externe Konfigurationsdatei tasks/test.js ändern

// Add `grunt` as first function argument
module.exports = function (grunt) {
  // Now, use Grunt methods on `grunt` instance
  grunt.log.error('I am a Grunt error!')

  // Or use them on `this` which does the same
  this.log.error('I am a Grunt error too, from the same instance, but from `this`!')

  const config = grunt.config.get()

  grunt.log.ok('And here goes current config:')
  grunt.log.ok(config)
}

Lassen Sie uns Grunt jetzt wieder ausführen

$ grunt

Und was wir bekommen

> I am Grunt error!
> I am Grunt error too, from the same instance, but from `this`!
> And here goes current config:
> {
    testingValue: 123
  }

Sehen Sie, wie wir von einer externen Datei aus auf native Grunt-Methoden zugegriffen und sogar die aktuelle Grunt-Konfiguration abrufen konnten? Denken Sie auch darüber nach? Ja, die volle Leistung von Grunt ist bereits da, direkt zum Greifen in jeder Datei!

Wenn Sie sich fragen, warum Methoden in externen Dateien unsere Haupt-Grunt-Instanz beeinflussen können, liegt das an der *Referenzierung*. grunt.loadTasks übergibt this und grunt an unsere aktuelle Grunt-Instanz – nicht eine Kopie davon. Durch das Aufrufen von Methoden auf dieser Referenz können wir unsere Haupt-Grunt-Konfigurationsdatei lesen und verändern.

Nun müssen wir tatsächlich etwas konfigurieren! Ein letzter Punkt…

Diesmal lassen wir das Laden der Konfiguration wirklich funktionieren

Nun, wir sind weit gekommen. Unsere Tasks werden automatisch und schneller geladen. Wir haben gelernt, wie man externe Konfigurationen mit nativen Grunt-Methoden lädt. Aber unsere Task-Konfigurationen sind immer noch nicht ganz da, weil sie nicht in der Grunt-Konfiguration landen.

Aber wir sind fast da! Wir haben gelernt, dass wir jede Grunt-Instanzmethode in importierten Dateien mit grunt.loadTasks verwenden können. Sie sind auf grunt und this Instanzen verfügbar.

Unter vielen anderen Methoden gibt es die wertvolle grunt.config Methode. Sie ermöglicht es uns, einen Wert in einer bestehenden Grunt-Konfiguration festzulegen. Die wichtigste, die wir in unserem Gruntfile initialisiert haben… erinnern Sie sich daran?

Wichtig ist die Art und Weise, wie wir Task-Konfigurationen definieren können. Genau das, was wir brauchen!

// tasks/test.js

module.exports = function (grunt) {
  grunt.config('csso', {
    build: {
      files: { 'style.css': 'styles.css' }
    }
  })

  // same as
  // this.config('csso', {
  //   build: {
  //     files: { 'style.css': 'styles.css' }
  //   }
  // })
}

Aktualisieren wir nun Gruntfile, um die aktuelle Konfiguration zu protokollieren. Wir müssen sehen, was wir getan haben, schließlich

module.exports = function (grunt) {
  require('jit-grunt')(grunt)

  grunt.initConfig({
    testingValue: 123
  })

  grunt.loadTasks('tasks')

  // Log our current config
  console.log(grunt.config())

  grunt.registerTask('default', ['cssmin'])
}

Führen Sie Grunt aus

$ grunt

…und hier ist, was wir sehen

> {
    testingValue: 123,
    csso: {
      build: {
        files: {
          'style.css': 'styles.css'
        }
      }
    }
  }

grunt.config setzt den csso-Wert beim Import, so dass der CSSO-Task nun konfiguriert und bereit ist, ausgeführt zu werden, wenn Grunt aufgerufen wird. Perfekt.

Beachten Sie, dass, wenn Sie zuvor load-grunt-configs verwendet haben, Sie Code wie diesen hatten, bei dem jede Datei ein Konfigurationsobjekt exportiert

// tasks/grunt-csso.js

module.exports = {
  target: {
    files: { 'style.css': 'styles.css' }
  }
}

Das muss in eine Funktion geändert werden, wie oben beschrieben

// tasks/grunt-csso.js

module.exports = function (grunt) {
  grunt.config('csso', {
    build: {
      files: { 'style.css': 'styles.css' }
    }
  })
}

OK, noch ein letztes Mal… dieses Mal wirklich!

Externe Konfigurationsdateien auf die nächste Stufe heben

Wir haben viel gelernt. Tasks laden, externe Konfigurationsdateien laden, eine Konfiguration mit Grunt-Methoden definieren… das ist gut, aber wo ist der Gewinn?

Halten Sie wieder mein Bier fest!

Bis dahin haben wir alle unsere Task-Konfigurationsdateien externalisiert. Unser Projektverzeichnis sieht also ungefähr so aus

tasks
  ├─ grunt-browser-sync.js  
  ├─ grunt-cache-bust.js
  ├─ grunt-contrib-clean.js 
  ├─ grunt-contrib-copy.js  
  ├─ grunt-contrib-htmlmin.js   
  ├─ grunt-contrib-uglify.js
  ├─ grunt-contrib-watch.js 
  ├─ grunt-csso.js  
  ├─ grunt-nunjucks-2-html.js   
  ├─ grunt-postcss.js   
  ├─ grunt-processhtml.js
  ├─ grunt-responsive-image.js  
  ├─ grunt-sass.js  
  ├─ grunt-shell.js 
  ├─ grunt-sitemap-xml.js   
  ├─ grunt-size-report.js   
  ├─ grunt-spritesmith-map.mustache 
  ├─ grunt-spritesmith.js   
  ├─ grunt-standard.js  
  ├─ grunt-stylelint.js 
  ├─ grunt-tinypng.js   
  ├─ grunt-uncss.js 
  └─ grunt-webfont.js
gruntfile.js

Das hält das Gruntfile relativ klein und die Dinge scheinen gut organisiert zu sein. Aber bekommen Sie ein klares Bild vom Projekt, nur indem Sie diese kalte und leblose Liste von Tasks überfliegen? Was tun sie tatsächlich? Was ist der Ablauf?

Können Sie sagen, dass Sass-Dateien durch grunt-sass, dann grunt-postcss:autoprefixer, dann grunt-uncss und schließlich durch grunt-csso laufen? Ist es offensichtlich, dass der clean-Task CSS bereinigt oder dass grunt-spritesmith eine Sass-Datei generiert, die ebenfalls aufgegriffen werden sollte, da grunt-watch Änderungen überwacht?

Es scheint, als wären die Dinge überall verstreut. Wir sind vielleicht zu weit mit der Externalisierung gegangen!

Also, endlich… was, wenn ich Ihnen sage, dass es doch einen besseren Weg gibt, Konfigurationen zu gruppieren… *basierend auf Features*? Anstelle einer nicht so hilfreichen Liste von Tasks erhalten wir eine sinnvolle Liste von Features. Was halten Sie davon?

tasks
  ├─ data.js 
  ├─ fonts.js 
  ├─ icons.js 
  ├─ images.js 
  ├─ misc.js 
  ├─ scripts.js 
  ├─ sprites.js 
  ├─ styles.js 
  └─ templates.js
gruntfile.js

Das erzählt mir eine Geschichte! Aber wie könnten wir das tun?

Wir haben bereits über grunt.config gelernt. Und glauben Sie es oder nicht, Sie können es mehrmals in einer einzigen externen Datei verwenden, um mehrere Tasks gleichzeitig zu konfigurieren! Lassen Sie uns sehen, wie es funktioniert

// tasks/styles.js

module.exports = function (grunt) {
  // Configuring Sass task
  grunt.config('sass', {
    build: {/* options */}
  })
  
  // Configuring PostCSS task
  grunt.config('postcss', {
    autoprefix: {/* options */}
  })
}

Eine Datei, mehrere Konfigurationen. Ziemlich flexibel! Aber es gibt ein Problem, das wir übersehen haben.

Wie gehen wir mit Tasks wie grunt-contrib-watch um? Seine Konfiguration ist ein monolithisches Ganzes mit Definitionen für jeden Task, die nicht aufgeteilt werden können.

// tasks/grunt-contrib-watch.js

module.exports = function (grunt) {
  grunt.config('watch', {
    sprites: {/* options */},
    styles: {/* options */},
    templates: {/* options */}
  })
}

Wir können nicht einfach grunt.config verwenden, um die watch-Konfiguration in jeder Datei festzulegen, da dies die gleiche watch-Konfiguration in bereits importierten Dateien überschreiben würde. Und sie in einer separaten Datei zu belassen, klingt auch nach einer schlechten Option – schließlich wollen wir alle zusammengehörigen Dinge nah beieinander halten.

Keine Sorge! grunt.config.merge zur Rettung!

Während grunt.config explizit Werte in der Grunt-Konfiguration festlegt und *überschreibt*, führt grunt.config.merge Werte rekursiv mit bestehenden Werten in anderen Grunt-Konfigurationsdateien zusammen und ergibt uns eine einzige Grunt-Konfiguration. Eine einfache, aber effektive Möglichkeit, zusammengehörige Dinge beieinander zu halten.

Ein Beispiel

// tasks/styles.js

module.exports = function (grunt) {
  grunt.config.merge({
    watch: {
      templates: {/* options */}
    }
  })
}
// tasks/templates.js

module.exports = function (grunt) {
  grunt.config.merge({
    watch: {
      styles: {/* options */}
    }
  })
}

Dies erzeugt eine einzige Grunt-Konfiguration

{
  watch: {
    styles: {/* options */},
    templates: {/* options */}
  }
}

Genau das, was wir brauchten! Wenden wir das auf das reale Problem an – unsere Styles-bezogenen Konfigurationsdateien. Ersetzen Sie unsere drei externen Task-Dateien

tasks
  ├─ grunt-sass.js
  ├─ grunt-postcss.js   
  └─ grunt-contrib-watch.js

…mit einer einzigen Datei tasks/styles.js, die sie alle kombiniert

module.exports = function (grunt) {
  grunt.config('sass', {
    build: {
      files: [
        {
          expand: true,
          cwd: 'source/styles',
          src: '{,**/}*.scss',
          dest: 'build/assets/styles',
          ext: '.compiled.css'
        }
      ]
    }
  })

  grunt.config('postcss', {
    autoprefix: {
      files: [
        {
          expand: true,
          cwd: 'build/assets/styles',
          src: '{,**/}*.compiled.css',
          dest: 'build/assets/styles',
          ext: '.prefixed.css'
        }
      ]
    }
  })

  // Note that we need to use `grunt.config.merge` here!
  grunt.config.merge({
    watch: {
      styles: {
        files: ['source/styles/{,**/}*.scss'],
        tasks: ['sass', 'postcss:autoprefix']
      }
    }
  })
}

Jetzt ist es viel einfacher zu erkennen, wenn man nur in tasks/styles.js nachsieht, dass Styles drei zusammengehörige Tasks haben. Ich bin sicher, Sie können sich vorstellen, dieses Konzept auf andere gruppierte Tasks auszudehnen, wie z. B. alles, was Sie mit Skripten, Bildern oder irgendetwas anderem tun möchten. Das gibt uns eine vernünftige Konfigurationsorganisation. Das Finden von Dingen wird viel einfacher sein, vertrauen Sie mir.

Und das ist alles! Der Kern dessen, was wir gelernt haben.

Das war's!

Grunt ist nicht mehr der neue Liebling, der es einst war, als es zum ersten Mal auf den Markt kam. Aber bis heute ist es ein unkompliziertes und zuverlässiges Werkzeug, das seine Arbeit gut erledigt. Mit richtiger Handhabung gibt es noch weniger Gründe, es gegen etwas Neueres auszutauschen.

Fassen wir zusammen, was wir tun können, um unsere Tasks effizient zu organisieren

  1. Laden Sie Tasks mit jit-grunt anstelle von load-grunt-tasks. Es ist dasselbe, aber wahnsinnig schneller.
  2. Verschieben Sie spezifische Task-Konfigurationen aus Gruntfile in externe Konfigurationsdateien, um Ordnung zu halten.
  3. Verwenden Sie native grunt.task.loadTasks, um externe Konfigurationsdateien zu laden. Es ist einfach, aber leistungsfähig, da es alle Grunt-Fähigkeiten freilegt.
  4. Denken Sie schließlich über einen besseren Weg nach, Ihre Konfigurationsdateien zu organisieren! Gruppieren Sie sie nach Feature oder Domäne anstelle des Tasks selbst. Verwenden Sie grunt.config.merge, um komplexe Tasks wie watch aufzuteilen.

Und schauen Sie sich auf jeden Fall die Grunt-Dokumentation an. Sie ist auch nach all den Jahren noch lesenswert.

Wenn Sie ein reales Beispiel sehen möchten, schauen Sie sich Kotsu an, ein auf Grunt basierendes Starter-Kit und ein statischer Website-Generator. Dort finden Sie noch mehr Tricks.

Haben Sie bessere Ideen, wie man Grunt-Konfigurationen noch besser organisieren kann? Bitte teilen Sie sie in den Kommentaren!