YeomanのGeneratorを自作して作業効率を上げる!(2)

generator, yeoman

では今回からgenerator-generatorが作成したプラグインを弄っていきます。この記事はYeomanのGeneratorを自作して作業効率を上げる!の続きでgenerator-generatorによってgenerator-sampleを作成していることが前提となります。

Generator作成のステップ

では作成されたgenerator-sampleディレクトリに移動します。

generatorはnpmのパッケージとして管理されるので、generator-generatorによって作成されたひな形もnpmのパッケージの構成をとります。

ステップ0:開発環境の整備

ローカル開発を始める前に、開発環境を整備する必要があります。 Yeoman wikiにあるように、npm linkをすることによって開発中のgeneratorをリアルタイムに実行することができます。

1
2
cd generator-sample
npm link

この状態で

1
2
3
4
5
6
7
8
9
10
11
12
13
yo -h

#Usage: yo GENERATOR [args] [options]
#
#General options:
#  -h, --help     # Print generator's options and usage
#  -f, --force    # Overwrite files that already exist
#
#Please choose a generator below.
#
#Sample
#  sample:app
#

で↑のようにsample generatorがyoコマンドから見えてればOKです。

これでローカル環境で開発中のgeneratorが使用できるようになりました。

ステップ1:Generatorの把握

まずはメインとなるapp/index.jsがどのようなことをやっているかを把握します。 app/index.jsのなかではSampleGeneratorを通してユーザとの対話とその挙動を定義します。

1
2
3
4
5
6
7
8
9
10
11
var SampleGenerator = yeoman.generators.Base.extend({

  initializing: function () { ...  },

  prompting: function () { ... },

  writing: { ... },

  end: function () { ... }

});

デフォルトではinitializing, prompting, writing, endが定義されています。
ほかにも定義があり、YeomanではこれらをRunning Contextと呼んでいます。

呼ばれるタイミングは決まっているので、それに合わせて各Running Contextを定義する、というのがYeomanの対話部分の実装方法です。

ステップ2:ディレクトリ構造の把握

ところでgenerator-generatorはどのようにgenerator-sampleを作成したのでしょうか?

generatorを作成するからには、ひな形を作りたいディレクトリ構造があるはずです。

挙動を考えるに、ユーザから入力された情報をテンプレートにはめ込んで出力しているのが濃厚に思えます。rubyで言えばerbみたいなものがどこかにありそうです。

予想通り、それらのテンプレートファイルはapp/templatesに存在します。generatorはひな形作成に使用するテンプレートファイルの置き場所をデフォルトでapp/templatesに定めています。

つまり、app/templates以下に作成したいテンプレートファイルを置いて、app/index.jsで定義したgeneratorのユーザ入力を使ってひな形を作成する、というのがgeneratorの大まかな流れです。

ステップ3:対話の実装方法

では対話の実装方法について見ていきます。
ここが実装できるとひな形作成に必要な情報をユーザから入手できるようになります。

対話を実装するにあたり重要な部分はpromptingです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
prompting: function () {
  var done = this.async();

  // Have Yeoman greet the user.
  this.log(yosay(
    'Welcome to the astounding Sample generator!'
  ));

  var prompts = [{
    type: 'confirm',
    name: 'someOption',
    message: 'Would you like to enable this option?',
    default: true
  }];

  this.prompt(prompts, function (props) {
    this.someOption = props.someOption;

    done();
  }.bind(this));
}

重要なのは9行目のpromptsの定義からです。 generator-generatorが定義するひな形がありますが、その内容は

「’Would you like to enable this option?’と表示してsomeOptionというキーの値として入力値(true/false)を格納する」

です。簡単ですね。

でもちょっと待って下さい、僕達がやりたいのは大体ユーザから何らかの値や文字列を取得することではないでしょうか?

それも簡単です。promptsに追加してみましょう。

1
2
3
4
5
6
7
8
9
10
var prompts = [{
    type: 'confirm',
    name: 'someOption',
    message: 'Would you like to enable this option?',
    default: true
  },{
    name: 'someValue',
    message: 'What is your name?',
    default: 'someuser'
  }];

無事質問を追加できたでしょうか?どこか適当なディレクトリを作って実行してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mkdir /tmp/generator-test
cd !$
yo sample

#     _-----_
#    |       |    .--------------------------.
#    |--(o)--|    |      Welcome to the      |
#   `---------´   |     astounding Sample    |
#    ( _´U`_ )    |        generator!        |
#    /___A___\    '--------------------------'
#     |  ~  |
#   __'.___.'__
# ´   `  |° ´ Y `
#
#? Would you like to enable this option? Yes
#? What is your name? (someuser)

↑の16行目が追加した質問です。どうでしょうか?
こんな感じでほしい情報を追加していきます。

ステップ4:テンプレートとの組み合わせ

さて、欲しい情報は取れました。では具体的にどうやってテンプレートにはめ込んでいくのでしょうか?

テンプレートに値を渡すのに重要なステップは

1
2
3
4
5
this.prompt(prompts, function (props) {
  this.someOption = props.someOption;

  done();
}.bind(this));

の部分です。

propsには対話で得た情報が格納されています。someOptionと同様に、ステップ3で追加した質問はprops.someValueに格納されています。

テンプレートに渡されるのはこのインスタンスです。

つまりpropsからこのインスタンスに値を保存することで、ユーザから得た情報をテンプレートに渡すことができます。

ではテンプレート側はどのように値を参照するのでしょうか?

erbのように<%= %>で囲むだけです。拡張子も変える必要はありません。

例えば簡単なhtmlの中でsomeValueを参照したければ

1
2
3
4
5
<html>
<body>
name: <%= someValue %>
</body>
</html>

みたいな感じです。

ステップ5:値のはめこみ

いよいよテンプレートに値をはめ込みます。
値をはめ込むのはwritingで行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
writing: {
  app: function () {
    this.dest.mkdir('app');
    this.dest.mkdir('app/templates');

    this.src.copy('_package.json', 'package.json');
    this.src.copy('_bower.json', 'bower.json');
  },

  projectfiles: function () {
    this.src.copy('editorconfig', '.editorconfig');
    this.src.copy('jshintrc', '.jshintrc');
  }
}

writingはObjectを値にとっていますが、キー名(ここではapp,projectfile)はなんでもいいと思います。

mkdirは名前通りディレクトリ作成。copyは「テンプレートを解釈した上で」第一引数のから第二引数へコピーします。つまりcopyがテンプレートに値をはめ込むわけですね。 YeomanのDocを見るとcopyを一度に行うdirectoryというメソッドも用意されています。
このあたりはソースであるyeoman/generatorを見るのも勉強になりますよ。

さて、これで基本的なgeneratorは作ることができるようになったと思います。

あとはnpmのパッケージと同様にnpmに登録すればパッケージとして公開することができます。

いかがでしたか?
説明すると長いですが、実際手を動かしてみると簡単だと思います。
これで少しでも定形作業が楽になればと思います。

Comments