W ostatniej części omówiliśmy co to jest i do czego służy Yeoman. W tej części skoncentrujemy się na stworzeniu prostego szablonu. Głównie chodzi o pokazanie, że nie jest to rocket science, i da się to dość szybko opanować.

Naszym celem będzie stworzenie generatora który:

  • Wyświetli coś fajnego na ekranie
  • Udostępni nam wybór pomiędzy dwoma opcjami
  • Da możliwość stworzenie pojedynczego elementu (pliku)

No to zaczynamy.

Inicjalizacja generatora Yeoman

Nasz projekt musi spełniać określone konwencje:

  • Nazwa katalogu musi być generator-* gdzie * to nasza dowolna nazwa – jest to konwencja yo, która jest wykosztowana podczas znajdywania zainstalowanych generatorów
  • W tym katalogu musi być plik package.json
  • Do tego musimy posiadać folder app w katalogu głównym (jest inna opcja, ale ją pominiemy)
  • package.json musi posiadać:
    • name zaczynający się od generator-*
    • keywords muszą posiadać yeoman-generator
    • dependencies muszą mieć zainstalowaną paczkę yeoman-generatornpm install --save yeoman-generator
    • Jeżeli chcemy by nasz generator był widoczny w rejestrze generatorów to też musi zawierać description
    • files które listuje katalog app :)

Kilka wymogów konwencji jest, ale nie są one trudne. Nasz plik package.json powinien więc wyglądać mniej więcej tak:

{
  "name": "generator-gutek",
  "version": "1.0.0",
  "description": "",
  "main": "app/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "app"
  ],
  "keywords": [
    "generator-yeoman"
  ],
  "author": "Jakub Gutkowski",
  "license": "MIT",
  "dependencies": {
    "chalk": "^1.1.1",
    "lodash": "^4.6.1",
    "yeoman-generator": "^0.22.5",
    "yosay": "^1.1.0"
  }
}

Generator projektów

Mając tak przygotowany projekt, możemy zacząć pisać nasz generator. Na pierwszy rzut trafi generator projektów czyli coś co ma nam niby wygenerować strukturę katalogów wraz z plikami. Tutaj pójdziemy na łatwiznę i stworzymy dwie opcje:

  • Jedna tworząca folder z plikiem empty.txt, nazwa folderu zostanie podana
  • Drugi robiący to samo ale z plikiem index.html w którym zostanie ustawiona zmienna appname

Zanim jednak napiszemy jedną linijkę potrzebujemy dwóch pluginów do node:

npm install --save chalk
npm install --save yosay
npm install --save lodash

Jeden służy do kolorwania output, zaś drugi do ładnej wiadomości hello ;) Trzeci, będzie później przydatny ;)

Ok, mając tak wszystko tworzymy plik index.js w katalogu app. Nasz plik powinien zawierać strukturę:

'use strict';
var generators = require('yeoman-generator');
var yosay = require('yosay');
var chalk = require('chalk');

// instead of export we can create local
// var and use it.
module.exports = generators.Base.extend({
    
    // if you need to use options or arguments
    constructor: function () {
        generators.Base.apply(this, arguments);
    },
    
    // nice yo logo with our custom text
    initializing: function () {
    },
    
    // here we ask user for data
    prompting: function () {
    },
    
    // time to copy files
    writing: function () {
    },
    
    // send last info to end user
    end: function () {
    }
});

Wiadomość powitalna

My wypełnimy część z tych rzeczy ;) na pierwszy ogień niech pójdzie initializing. To tam możemy wyświetlić jakieś dodatkowe informacje dla użytkownika. Na przykład powitać go:

initializing: function () {
    this.log(yosay('Welcom to the incredible ' +
        'dump Yo generator!'));
},

To odpowiada dosłownie temu:

YoSay w akcji

Ok, to teraz pora, na zebranie informacji od użytkownika.

Zbieranie informacji

Możemy zbierać informacje na różne sposoby, możemy prosić o wybór jednej z opcji, kilku z opcji, potwierdzić czy też poprosić o wpisanie czegoś. My zaś zbierzemy dwie opcje, jedna to będzie wybór typu projektu do wygenerowania, druga jego nazwa.

W tym celu musimy przeciążyć prompting następująco:

prompting: function () {
    var done = this.async();
    
    // list of questions
    var prompts = [{
        type: 'list',
        name: 'type',
        message: 'Choose wisely Luke',
        choices: [{
            name: 'Whatever, force is with me',
            value: 'empty'
        }, {
            name: 'Force, where are you?',
            value: 'force'
        }]
    }, {
        type: 'input',
        name: 'appname',
        message: 'app or force or whatever name'
    }];
    
    // ask them, gather responses
    this.prompt(prompts, function (props) {
        this.type = props.type;
        this.appname = props.appname;
        
        done();
    }.bind(this));
},

Co daje nam:

Opcje wyboru

Pytanie o nazwę

Zapisywanie/Generowanie

Teraz, mając zebrane informacje, możemy zacząć działać. To znaczy, możemy skopiować pliki, dane itp. Do tego w trakcie kopiowania możemy wykorzystać język szablonów ejs. Przydatne jeżeli chcemy podmieć lub aktualizować pewne wartości w pliku źródłowym.

By mieć coś do kopiowania to w katalogu app musimy stworzyć katalog templates (znów, konwencja) a w nim dwa pod katalogi:

  • empty z plikiem empty.txt
  • force z plikiem index.html

Plik html może wyglądać tak:

<html>
<body>
<h1>Hello, <%= appname %></h1>
</body>
</html>

A więc nasza metoda writing będzie/powinna wyglądać tak:

writing: function () {
    this.templedata = {
        appname: this.appname,
        type: this.type
    }
    
    switch(this.type) {
        case 'empty':
            this.copy(this.type + '/empty.txt', this.appname + '/empty.txt');
            break;
        case 'force': 
            this.template(this.type + '/index.html', 
                this.appname + '/index.html',
                this.templatedata);
            break;
        default:
            this.log('you\'re done SMTH wrong'); 
    }
},

Co tak naprawdę spowoduje wyświetlenie wiadomości typu:

Końcowa wiadomość

Na końcu możemy poinformować użytkownika o zakończeniu akcji generacji za pomocą metody end. W naszym przypadku możemy napisać:

end: function () {
    this.log('\r\n');
    this.log('We\'re done, your\'re not the only one');
    this.log(chalk.green('\ti like green'));
    this.log(chalk.red('\tcoz i don\'t like your app name ' + 
        this.appname));
    this.log('\r\n');
}

Co da nam wynik:

Wiadomość końcowa

Inne opcje

Opcji ogólnie kopiowania, edycji, czy też parametrów które możemy przeciążyć jest dużo więcej, by się o nich dowiedzieć polecam przeczytać dokumentację API jak i krótki opis tworzenia generatorów na stronie Yeoman.

Można także szukać przykładów w sieci, każdy generator dostępny na liście generatorów ma swoje repo na github. Trzeba jednak uważać, wersja generatora Yeoman uległa zmianie i nie wszystkie projekty ją zaktualizowały. A jest kilka zmian chociażby w metodach do przeciążenia, czy też bazowej klasy (znajduje się ona gdzie indziej). Przez to, też jest trudniej o dobre i ciekawe przykłady dla najnowszego generatora :(

Do tego ja się też w tych opcja gubię, bo z jednej strony piszą prompting z drugiej używają tego jak funkcję czy też jako zbiór funkcji. A czasami w ogóle używają askFor… więc ciężko powiedzieć co jest aktualne i poprawne. Jak działa to super :)

Testowanie

Teraz, jak już mamy to wszystko napisane, możemy w command line wejść do katalogu gdzie mamy nasz własny generator i napisać:

npm link

Dzięki czemu, będziemy mogli używać yo nazwa_nasza bez konieczności wrzucania tego do publicznych repozytoriów.

Jeżeli wszystko poszło u was dobrze to pod koniec powinniście móc zobaczyć taki o to ekran:

Własny generator yo

Generator pliku

Na końcu chcieliśmy móc stworzyć osobno plik za pomocą naszego generatora. Tym razem poszedłem na łatwiznę i kod sobie po prostu ściągnąłem z generator-node by pokazać jak to można zrobić i że to nie jest takie trudne. Ogólne przy generatorach aplikacji, korzystamy z folderu app. Zaś z pod generatorów korzystamy ciutkę inaczej. Mianowicie na tym samym poziomie co folder app, tworzymy nowy folder o dowolnej nazwie. Ta nazwa będzie naszym nowym pod-generatorem.

Dla przykładu zawartego w kodzie, będzie to katalog o nazwie readme. Dzięki czemu będziemy mogli odpalić komendę:

yo gutek:readme

Która wygeneruje nam plik README.md na podstawie pliku pacakge.json.

Podobnie jak w folderze app, musimy stworzyć katalog templates i plik README.md:

# <%= projectName %> [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url]<%
if (includeCoveralls) { %> [![Coverage percentage][coveralls-image]][coveralls-url]<% } -%>

> <%= description %>

<% if (!content) { -%>
## Installation

```sh
$ npm install --save <%= projectName %>
```

## Usage

```js
var <%= safeProjectName %> = require('<%= projectName %>');

<%= safeProjectName %>('Rainbow');
```
<% } else { -%>
<%= content %>
<% } -%>
## License

<%= license %> © [<%= author.name %>](<%= author.url %>)


[npm-image]: https://badge.fury.io/js/<%= projectName %>.svg
[npm-url]: https://npmjs.org/package/<%= projectName %>
[travis-image]: https://travis-ci.org/<%= githubAccount %>/<%= projectName %>.svg?branch=master
[travis-url]: https://travis-ci.org/<%= githubAccount %>/<%= projectName %>
[daviddm-image]: https://david-dm.org/<%= githubAccount %>/<%= projectName %>.svg?theme=shields.io
[daviddm-url]: https://david-dm.org/<%= githubAccount %>/<%= projectName %>
<% if (includeCoveralls) { -%>
[coveralls-image]: https://coveralls.io/repos/<%= githubAccount %>/<%= projectName %>/badge.svg
[coveralls-url]: https://coveralls.io/r/<%= githubAccount %>/<%= projectName %>
<% } -%>

Teraz mając tak wszystko przygotowane, dodajemy do readme plik index.js:

'use strict';
var _ = require('lodash');
var generators = require('yeoman-generator');

module.exports = generators.Base.extend({
  constructor: function () {
    generators.Base.apply(this, arguments);

    this.option('generateInto', {
      type: String,
      required: false,
      defaults: '',
      desc: 'Relocate the location of the generated files.'
    });

    this.option('name', {
      type: String,
      required: true,
      desc: 'Project name'
    });

    this.option('description', {
      type: String,
      required: true,
      desc: 'Project description'
    });

    this.option('githubAccount', {
      type: String,
      required: true,
      desc: 'User github account'
    });

    this.option('authorName', {
      type: String,
      required: true,
      desc: 'Author name'
    });

    this.option('authorUrl', {
      type: String,
      required: true,
      desc: 'Author url'
    });

    this.option('coveralls', {
      type: Boolean,
      required: true,
      desc: 'Include coveralls badge'
    });

    this.option('content', {
      type: String,
      required: false,
      desc: 'Readme content'
    });
  },

  writing: function () {
    var pkg = this.fs.readJSON(
        this.destinationPath(
            this.options.generateInto, 'package.json'), {});
    this.fs.copyTpl(
      this.templatePath('README.md'),
      this.destinationPath(this.options.generateInto, 'README.md'),
      {
        projectName: this.options.name,
        safeProjectName: _.camelCase(this.options.name),
        description: this.options.description,
        githubAccount: this.options.githubAccount,
        author: {
          name: this.options.authorName,
          url: this.options.authorUrl
        },
        license: pkg.license,
        includeCoveralls: this.options.coveralls,
        content: this.options.content
      }
    );
  }
});

Od teraz nasza komenda yo gutek:readme powinna już śmigać :)

Kod przykładu

Jeżeli nie chciało się wam śledzić wszystkiego wraz ze mną to na github udostępniłem pełny przykład z tego bloga. Może się komuś przyda :)

Podsumowanie

Jak widać nie jest to trudne ale też to prostych nie należy. Głównie przez braki dokumentacji i masę różnych przekładów wykorzystujących to kod zupełnie inaczej. Jednak nie jest to aż tak skomplikowane jak tworzenie projektów w VS. Szczerze, jak mamy już gotowe pliki to kwestia jest tylko napisanie kodu je kopiującego. Więc… nie jest tak żle :)

Oczywiście chciałbym by dokumentacja była pełniejsza, albo by ktoś to trochę lepiej zademonstrował na przykładach. Tak to działa to dla mnie super dobrze.

A wy, korzystacie z yo? Pisaliście już szablony? Proste? Trudne to jest? Jakie są wasze odczucia?

2 KOMENTARZE

Comments are closed.