Wie Yeoman die Art und Weise, wie wir arbeiten, verändert hat

Avatar of Noam Elboim
Noam Elboim am

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

Der folgende Beitrag wurde von Noam Elboim verfasst, einem Entwickler bei myheritage.com. Noam hat sich kopfüber in Yeoman gestürzt – von niemandem zu wissen oder zu verstehen, wie es bei der Arbeit helfen könnte, bis hin zum Erstellen eines benutzerdefinierten Generators speziell für sie. Wenn Sie glauben, dass ein Werkzeug, das Ihnen hilft, neue Projekte nach Ihren Wünschen zu strukturieren, Ihnen helfen könnte, werfen Sie einen Blick auf Noams Reise!

An meinem ersten Tag bei MyHeritage erinnere ich mich an diesen Austausch mit dem Teamleiter der Webentwicklung

Teamleiter: Sie haben Erfahrung mit Node.js-Modulen wie Yeoman, richtig?

Ich: Ja, ich weiß ein bisschen.

Teamleiter: Ich denke, Yeoman hier zu verwenden, würde uns viel Zeit bei der Erstellung neuer Seiten sparen. Glauben Sie, dass Sie so etwas bewältigen können?

Nun, das war mein erster Tag; ich wusste kaum, wo mein Sitz war. Ich erinnere mich jedoch klar daran, was ich dachte, und ich verstand nicht, wie Yeoman hier helfen könnte.

Für mich und viele andere war Yeoman ein Werkzeug, um eine vollständige, funktionierende Anwendung in nicht mehr als zwei Sekunden von Grund auf neu zu generieren. Es war nicht dazu gedacht, Teile zu einer bestehenden Anwendung hinzuzufügen.

Ich lag komplett falsch. Lassen Sie mich Ihnen erzählen, wie wir bei MyHeritage ein Node.js-Modul erstellt haben, das die Funktionalität von Yeoman erweitert, um uns bei alltäglichen Aufgaben jede Menge Zeit zu sparen.

Das Problem

Die Erstellung neuer Seiten bei MyHeritage war früher eine herausfordernde Aufgabe. Jede neu erstellte Seite erforderte etwa zwei Tage Arbeit, um alles vorzubereiten, einschließlich Asset-Management, einer serverseitigen Komponente und einem clientseitigen Bootstrap einer Anwendung. Kurz gesagt: eine große Anzahl von Dateien.

Die meisten Entwickler erstellen nicht regelmäßig neue Seiten. Bevor die eigentliche Arbeit beginnen konnte, musste viel Zeit aufgewendet werden, nur um zu verstehen, wie die neue Seite erstellt werden kann. Dann, sobald die Seite erstellt und die Arbeit „erledigt“ war, blieben noch Stunden, um alles zu debuggen, was nicht funktionierte.

Zwei Tage waren einfach viel zu viel Zeit, um in eine so einfache Aufgabe zu investieren.

Ein Lichtblick

Ein paar Monate nach meinem Gespräch mit unserem Teamleiter war der Aufwand für die Erstellung neuer Seiten erheblich gestiegen und wir konnten diesen Weg nicht weitergehen. Wir wussten, dass eine sofortige Lösung notwendig war und dass sie einfach, leicht zu warten und offensichtlich sehr schnell zu bedienen sein musste.

Zuerst haben wir die Probleme bei der Erstellung einer neuen Seite aufgeschlüsselt. Der Prozess erforderte viele Schritte, einschließlich Routing-Konfigurationen und Übersetzungsdiensten, das Ausführen von Terminalbefehlen zum Erstellen bestimmter Dateien (wie A/B-Tests oder Sass-Dateien) und sogar das Ausführen einiger Gulp-Aufgaben (wie das Kompilieren von Sass oder das Erstellen von Sprites).

Zu diesem Zeitpunkt waren wir sicher, dass die Erweiterung von Yeoman uns am besten dienen würde, aber wir mussten immer noch herausfinden, wie es all diese Bedürfnisse erfüllen könnte, während seine Kern-Einfachheit und Benutzerfreundlichkeit erhalten blieben.

Ein paar Worte zu Yeoman

Yeoman hilft Ihnen, neue Projekte zu starten, und empfiehlt Best Practices und Tools, um produktiv zu bleiben.

Das ist die erste Zeile auf der Website von Yeoman und sie ist zu 100 % richtig.

Es ist ein Node.js-Modul mit Tausenden von Generatoren zur Auswahl. Alle Generatoren funktionieren im Grunde gleich – zuerst wird der Benutzer eine Reihe von Fragen in Benutzeroberflächen gestellt, die auf Inquirer.js basieren. Dann generiert Yeoman mithilfe der Eingabe des Benutzers die entsprechenden neuen Dateien.

Eine String-Eingabeaufforderung zur Angabe des Namens des Projekts.
Eine Checkbox-Aufforderung zur Konfiguration der „Master Page“.

Warum Yeoman

Wir hatten eine Reihe von Optionen, von der Verwendung eines voll funktionsfähigen Generators bis zum Erstellen von etwas von Grund auf. Es gibt ein paar Gründe, warum wir uns für Yeoman entschieden haben

Erstens, Einfachheit. Yeoman bietet eine klare API, einen Generator-Generator für den Einstieg und eine hohe Benutzerfreundlichkeit als Node.js-Modul.

Zweitens, Wartbarkeit. Yeoman wird von Tausenden von Menschen weltweit genutzt und basiert auf dem Backbone.js Model, was es ermöglicht, es auf eine Weise zu erweitern, die leicht zu verstehen ist.

Schließlich, Geschwindigkeit. Einfach ausgedrückt: Geschwindigkeit ist bei Yeoman gegeben.

Unsere Lösung

Wir wollten eine weitere Stufe zwischen den beiden Hälften des Yeoman-Flows einfügen, um die Daten zu erweitern, die wir vom Benutzer über eine geordnete Menge von Funktionen sammeln. Zum Beispiel möchten wir die Möglichkeit haben, snake_case aus CamelCase zu erhalten oder die Ausgabe eines Befehls, der im Terminal ausgeführt wird, zu erfassen. Wir benötigen diese Stufe, die über eine JSON-Datei konfigurierbar ist.

Wir zerlegen jede Funktion in ein Objekt, das den Namen der auszuführenden Funktion, die Argumente und den Speicherort der Ausgabe enthält. Wir verwenden Node.js, damit diese Funktionen asynchron ausgeführt werden und Promises zurückgeben. Wenn alle Promises aufgelöst sind, generieren wir die Dateien. Hier ist ein Beispiel dafür, wie die Generator-Klasse das Yeoman-Generator-Modul namens „generators“ verwendet.

var generators = require('yeoman-generator'),
    services, prompts, actions, preActions, mainActions,
    _this;
  
module.exports = Generator;

/**
 * Generator object for inheritance of Yeoman.Base with different services, prompts and actions files.
 * To use the generator in you Yeoman generator, create a new Generator instance with your local files and export the return value of Generator.getInstance()
 * @param externalService - local generator services.js file
 * @param externalPrompts - local generator prompts.json file
 * @param externalActions - local generator actions.json file
 * @constructor
 */
function Generator (externalService, externalPrompts, externalActions) {
  services = externalService;
  prompts = externalPrompts;
  actions = externalActions;
  preActions  = actions.pre;
  mainActions = actions.main;
}

/**
 * Get instance will create extension to the Yeoman Base with your local dependencies
 * @returns {Object} Yeoman Base extended object
 */
Generator.prototype.getInstance = function () {
  return generators.Base.extend({
    initializing: function () {
      _this = this;
      this.conflicter.force = true; // don't prompt when overriding files
    }, 
    prompting: function () { /* ... */  },
    writing:  function () { /* ... */  },
    end:  function () { /* ... */  }
  });
};

Wie man einen Generator erstellt

Sobald die Basis fertig ist, ist die Erstellung eines neuen Generators ein Kinderspiel. Wir müssen nur einen neuen Ordner erstellen, der 4 Dateien und einen Ordner mit Vorlagen enthält. Der Name des Ordners ist der Name des Generators. Die 4 Dateien sind

  1. Prompts – eine JSON-Datei voller Objekte, die abgefragte Fragen mit den gespeicherten Daten abgleichen.
    [
      {
        "type": "input",
        "name": "name",
        "message": "Name your generator:",
        "default": "newGenerator"
      },
      {
        "type": "list",
        "name": "commonServices",
        "message": "Want some common services:",
        "choices": ["yes", "no"]
      }
    ]

    In diesem Beispiel fragt das erste Element den Benutzer nach dem „Namen Ihres Generators:“ mit der Standardantwort „newGenerator“. Es speichert die Antwort im Datenobjekt unter dem Feld „name“.

    Das zweite Element stellt eine Mehrfachauswahlfrage mit den Optionen „ja“ und „nein“. Die Standardeinstellung ist die erste Wahl, „ja“.

  2. Services – eine JS-Datei mit allen Funktionen, die wir zur Erweiterung der Benutzerdaten benötigen.
    var exec  = require('child_process').exec,
      chalk = require('chalk'),
      Q     = require('q');
    
    module.exports.generateSprite = generateSprite;
    
    /**
     * Run gulp sprites in our new sprite folder
     * @param {String} name - the name of the project
     * @return {Object | Promise}
     */
    function generateSprite (name) {
      return Q.Promise(function (resolve, reject) {
        console.info(chalk.green("Compiling new sprite files..."));
        if (name) {
          exec('gulp sprites --folder ' + name, function () {
            resolve();
          });
        }
        else reject("Missing name in generateSprite");
      });
    }

    Dieses Beispiel erklärt einen Service namens „generateSprite“, der einen bereits für das Projekt konfigurierten Terminalbefehl ausführt. Dieses spezielle Skript erstellt einen Sprite-Ordner. Wenn die Funktion den Befehl erfolgreich ausführen kann, löst sie ein Promise auf, das die Operation als erfolgreich markiert. Vorlagen können basierend auf dem Ergebnis generiert werden.

  3. Actions – eine JSON-Datei, die alle Funktionen abgleicht, die wir vor der Dateigenerierung ausführen müssen – der „pre“-Abschnitt – und eine weitere Zuordnung von Vorlagendateien und ihrem endgültigen Speicherort, an dem die Dateien generiert werden, was der „main“-Abschnitt ist.
    {
      "pre": [
        {
          "dependencies": ["name"],
          "arguments": ["name"],
          "output": "spriteFolder",
          "action": "generateSprite"
        }
      ],
      "main": [
        {
          "dependencies": ["name"],
          "optionalDependencies": ["spriteFolder"],
          "templatePath": "output_file.js",
          "destinationPath": "./sprites/<%= name %>output_file.js"
        }
      ]
    }

    Im „pre“-Abschnitt gibt es eine Aufgabe, die nur ausgeführt wird, wenn das Feld „name“ definiert oder wahr ist (basierend auf den Anforderungen im Array „dependencies“ von Feldern). Die Argumente, die in der Servicefunktion im Objekt „args“ über das Array „arguments“ übergeben werden. Die Ausgabe der Funktion wird unter dem durch das Feld „output“ angegebenen Speicherort gespeichert. Der Name der Funktion wird durch das Feld „action“ definiert. In dieser Implementierung kann nur eine Funktion bereitgestellt werden, aber sie könnte leicht mehrere Funktionen unterstützen, indem sie ein Array in „action“ verwendet.

    Im „main“-Abschnitt gibt es eine Aufgabe mit ähnlicher Funktionalität. Wenn ein Feld in den „dependencies“ fehlt oder fehlerhaft ist, wird die Datei nicht generiert. Dies ist jedoch nicht der Fall bei „optionalDependencies“, die tatsächlich optional sind. Die dem Templating-Engine zur Verfügung gestellten Felder sind in den „dependencies“ und „optionalDependencies“ definiert.

    Der „templatePath“ enthält den Pfad zur Vorlagendatei und der „destinationPath“ ist das Ziel für die Datei. „destinationPath“ kann Daten aus dem „pre“-Abschnitt verwenden, wie wir hier mit dem Feld „name“ sehen (<%= name %>).

    Beachten Sie, dass wir die EJS-Templating-Sprache verwenden. Yeoman unterstützt tatsächlich zusätzliche Templating-Engines neben EJS, wie z. B. Handlebars.

  4. Index – eine JS-Datei. Yeoman verlangt, dass jeder Generator eine Indexdatei hat. Der Index verbindet die 3 anderen Dateien und erbt von der Basis. Grundsätzlich erstellt er eine Instanz der Basis mit getrennten Prompts, Services und Actions.
    var Generator   = require('../generator'),
        services    = require('./services'),
        prompts     = require('./prompts'),
        actions     = require('./actions');
        
    var generator = new Generator(services, prompts, actions);
        
    module.exports = generator.getInstance();

    Wie Sie in diesem Beispiel sehen können, erstellt er die Instanz und exportiert sie zur Verwendung mit Yeoman.

  5. Templates – so einfach wie es klingt: nur Vorlagen zur Erstellung von Dateien.
    <h1>My first template in <%= name %>!</h1>
    <% if (spriteFolder) { %>
      I even managed to create sprites for it!
    <% } %>

    Beachten Sie, dass das Gleichgewicht zwischen der Erweiterung von Daten und der Erstellung von Vorlagen vollständig von der Art des zu erstellenden Generators abhängt. Ein Generator, der nur neue Dateien ohne ausgefeilte Daten erstellt, benötigt möglicherweise keine Erweiterung. Umgekehrt möchten wir vielleicht einen Generator, der nur eine Reihe von Systembefehlen ausführt, der keine Vorlagen hat.

    Beide Fälle sind möglich und für einige Verwendungen sogar wahrscheinlich. Die Fähigkeit, beides zu tun oder nur eines davon, zeigt, wie flexibel es ist, einen Generator für jeden Bedarf zu verwenden und zu erstellen.

Was wir gewonnen haben

Seit wir unser Problem durch die Erstellung des Generators für neue Seiten gelöst haben, konnten wir Yeoman nutzen, um weitere Generatoren zu erstellen, darunter einen, der einen neuen Teil unserer Backend-API erstellt, und einen anderen, der eine voll funktionsfähige Angular-Anwendung (mit Unit-Tests und allem) erstellt.

Da wir Yeoman jeden Tag mehr und mehr nutzen, sind die Vorteile für uns ziemlich offensichtlich, aber es gibt einige Nachteile, die wir bemerkt haben und die Sie im Hinterkopf behalten sollten.

Vorteile

  • Schnell – Die Erstellung einer neuen Seite dauert jetzt etwa 1 Minute.
  • Best Practices und Konventionen – Yeoman zwingt Entwickler, Best Practices anzuwenden, und hilft sicherzustellen, dass Konventionen befolgt und von unserem Team gelernt werden. Ein Team kann sicherstellen, dass jede erstellte Seite die gleichen Konventionen verwendet, unabhängig davon, welcher Entwickler sie erstellt hat. Möchten Sie sicherstellen, dass jede Seite einen neuen und glänzenden Dienst nutzt? Fügen Sie ihn einfach in den Generator für „neue Seiten“ ein und fertig.
  • Sauberere Code-Umgebung – Da wir die Möglichkeit für Entwickler entfernt haben, Codeabschnitte zu kopieren/einzufügen, die sie nicht vollständig verstehen, treten Fehler tendenziell viel seltener auf und die unnötige Duplizierung von Code ist ein geringeres Problem.

Nachteile

  • Wartung – Das ist der schwierige Teil. Best Practices entwickeln sich und ändern sich tendenziell. Sie müssen immer sicherstellen, dass der Generator auf dem neuesten Stand der Codebasis ist, was Investitionen erfordert. Es ist kein „Hit and Run“-Projekt – Sie müssen es aktualisieren, während sich Ihre Codebasis und Technologie weiterentwickeln.
  • Für alle Verwendungszwecke geeignet – Das ist eher eine Herausforderung als ein Nachteil, aber wenn ein Generator nicht richtig implementiert wird, kann dies dazu führen, dass Entwickler den Generator, an dem Sie lange und hart gearbeitet haben, nicht verwenden. Es ist wichtig, dass der Generator generisch ist, aber dem Entwickler dennoch eine erhebliche Menge an Zeit bei sich wiederholenden und frustrierenden Aufgaben spart. Anstatt zum Beispiel eine Funktion zu implementieren, fügen Sie ein TODO mit einer Erklärung ein.

Fazit

Unsere Generatoren sind sehr spezifisch für unsere Projekte, daher sind diese nicht Open Source, aber es gibt viele weiter verbreitete Generatoren, die Sie verwenden oder als Referenz nutzen können, wenn Sie Ihren eigenen erstellen, wie z. B. diesen hier für Angular 2.

Die Verwendung von Yeoman hat unsere tägliche Arbeit wirklich einfacher und besser gemacht, nachdem wir Generatoren für viele sich wiederholende Aufgaben erstellt hatten. Wir haben sogar einen Generator-Generator erstellt! Aber es hat uns auch unerwartete Herausforderungen gebracht, die wir nicht ignorieren können. Es gibt ein Gleichgewicht zwischen dem, was automatisiert werden sollte, und dem, was manuell von einem Entwickler erledigt werden muss, und es ist eine Frage, dies für jedes Projekt und jeden Bedarf herauszufinden… Aber das ist ein Thema für ein anderes Mal.