The Round

合同会社ナイツオの開発ブログ

静岡でGo言語やりたい人!!→こちら

Yeomanカスタムジェネレータを作成する

マツウラです。

Yeomanのyoでは、generatorによってWebアプリの骨組みを自動的に生成することができます。
今回はジェネレータで骨組みを生成しながら、基本的な処理について押さえていきます。

今回の実行環境は次の通りです。

  • Node.js 0.10.25
  • npm 1.3.24
  • yo 1.1.2
  • generator-generator 0.4.2

yoとgenerator-generatorをセットアップする

npmでインストールする前にnpmのバージョンを確認します。
バージョンが1.2.10以上であると、yoインストールの際にgruntとbowerが自動的にインストールされます。
以前にgruntをグローバルにインストールしている場合、アンインストールします。

  npm uninstall -g grunt

npmが1.2.10以上であり、以前にgruntをインストールしたことがなければ、単純にyo をインストールします。

  npm install -g yo

次にgeneratorをインストールします。

  npm install -g generator-generator

generator-generator

generator-generatorでジェネレータの骨組みを構築します。
ジェネレータの骨組みを出力してくれるジェネレータのジェネレータとも呼べるものです。こちらを使ってジェネレータを書いてゆきます。

ディレクトリの命名

generator-generatorでは、ディレクトリ名に特別な意味があります。
generator-_______の規則でディレクトリ名を付けることで、generatorがジェネレータ名を決定します。
generator-blog がディレクトリ名ならば、アプリの骨組みを出力するコマンドはyo blogとなります。

ジェネレータを収めるディレクトリを作成、作成したディレクトリに移動して次のコマンドを実行します。

  yo generator

するとAAキャラが表示され、次の質問をされます。

[?] Would you mind telling me your username on GitHub?
[?] What's the base name of your generator? (blog)
...

2番めの質問がジェネレータ名を決定しますが、何も入力しなければディレクトリ名から抽出されて決定します。
質問に答えるとジェネレータの骨組みとなるファイルが出力されます。

シンボリックリンクの作成

ディレクトリを作成したら、開発中に作成や実行をするためにシンボリックリンクを作成します。
作成したディレクトリでlinkコマンドを実行します。

  npm link

この作業を行うと、yo blog などのコマンドで適当なディレクトリに骨組みを出力することが可能になります。
コーディングの途中経過を確かめるために必要になります。

実際にアプリを出力するには次のコマンドを使います。
適当なディレクトリを作成して実行してみます。

yo <ジェネレータ名>

例えば「yo blog」となります。
これでアプリが出力されることになります。

次に生成されたジェネレータの骨組みについて見てゆきます。

生成されたジェネレータを見る

ディレクトリに生成されたファイルの中でもジェネレータの処理の開始点となるのがapp/index.jsファイルです。

これらの実行順序は、拡張メソッドが上から順に実行されます。

init -> askFor -> app -> projectfiles

もし、プライベートメソッドが必要ならば_dontRunMeのようにメソッドの先頭にアンダースコアを付けることで実装できます。

はじめに初期化関数で何をしているのか見てみます。

初期化関数

init: function () {
    this.pkg = yeoman.file.readJSON(path.join(__dirname, '../package.json'));

    this.on('end', function () {
      if (!this.options['skip-install']) {
        this.npmInstall();
      }
    });
  },

ここでは次のことをしています。

  • package.jsonへのアクセス
  • endイベントのリスナ作成

endイベントは拡張メソッドである、askFor, app, projectfilesが全て実行された後に呼び出されます。

ここではnpmを介して依存関係にあるパッケージをインストールします。上記では何もインストールはしていませんね。
bowerを介してパッケージをインストールするなら、bowerInstall()を使います。
npmInstall, bowerInstallについては/node_modules/yeoman-generator/lib/actions/install.jsを見てください。

次にaskForメソッドを見てゆきます。

askForメソッド

askFor: function () {
    var done = this.async();

    // have Yeoman greet the user
    console.log(this.yeoman);

    // replace it with a short and sweet description of your generator
    console.log(chalk.magenta('You\'re using the fantastic Dev 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));
  },

initメソッドに続き実行されるのはaskForメソッドです。

ここではユーザーに質問を行い、その答えを記憶しています。
これは主に必要となる依存関係についてユーザーに尋ねて、回答を出力するアプリの骨組みに適用することが目的です。

また、入力された値を後に使うためにthis.promptで回答を保持しています。
props引数には、promptsからnameと同名のプロパティにユーザーのレスポンスが渡されています。

仮にこのような質問を作った場合、

var prompts = [{
  name: ‘blogName’,
  message: ‘What do you want to call your blog?’
}]

アプリ生成時に次のような質問がされます。

[?] What do you want to call your blog? Shizuoka

ここではブログ名がShizuokaであると回答しました。
すると、this.blogName に Shizuokaと文字列が格納されるといった具合です。

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

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

最後にthis.asyncについてです。
this.asyncを呼ぶと処理待ちの必要があることをyeomanに知らせ、非同期タスクのコールバックとなる関数を返します。ここではdoneのことです。
このコールバック関数を実行するまで、次のメソッドは実行されません。
ここではユーザーの回答待ちに使っています。

その他にも非同期タスクが必要な場面があればthis.asyncを活用して下さい。

Prompts

this.promptに渡す引数、promptsはオブジェクトの配列です。
promptsは次の形を取ります。(inquirerパッケージを継承しています)

  • type : プロンプトのタイプ(デフォルトはinput)(input, confirm, list, rawlist)
  • name : プロンプトの名前(回答を格納するためにも使う)
  • message : ユーザーへの質問
  • default : 回答が無い場合に使う値

次にappおよびprojectfilesについて見てゆきます。

appとprojectfilesについて

app: function () {
    this.mkdir('app');
    this.mkdir('app/templates');

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

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

ここでは、this.mkdir, this.copyを使ってディレクトリの作成&ファイルのコピーを行っています。
this.copyでは第1引数にコピー元のファイル、第2引数に出力先を指定します。
出力先はappと同じ階層のルートになります。
this.copyの第1引数で参照されるフォルダは、ジェネレータ内のapp/templatesです。
このフォルダに入っているファイルがコピー元として認識されます。

同じような処理をする、app, projectfiles の2つは1つのメソッドにまとめることが出来ます。
ただ、先ほど書いたようにYeomanの実行順序は非常にわかりやすいです。そのため機能別にメソッドを分けるか、分けないかは開発者の自由です。

次に非常に便利なtemplateについて見てゆきます。

template

yo blogなどと、ジェネレータでもってアプリの骨組みを出力する際にthi.copyを使うと、app/templatesからファイルがコピーされます。
同様の機能をもったthis.templateメソッドもあります。

これらコピーの際にはテンプレートエンジンによる処理が行われています。(yeomanではLo-Dashが使われています)
テンプレートの表記については以下のようなものがあります。

  • <%=.....%> 変数の補完
  • <%.....%> 任意のJavascriptコードの実行
  • <%-.....%> 値をHTMLエスケープさせて補完

これらを用いることで、promptで保持した値を出力するファイルに適用させることが可能になります。
先ほど例としてブログの名前をユーザーに質問しました。
このブログ名をファイル中に挿入したい場合、ファイル中に

<html>
<head>
  <meta charset="utf-8">
    <title><%= blogName %></title>
</head>
  <body>
    <h1>Hello world!</h1>
  </body>
</html>

このように書くと<%....%>部分がShizuokaに置き換わります。

テンプレートはジェネレータにおいて非常に多くの場面で使われるので、より詳しく知りたい方はAPIドキュメントのtemplateを見てください。

ジェネレータからアプリを生成する

最後にサブジェネレータについて書いておきます。

サブジェネレータの作成

たとえばAngularJSなどを採用している場合、コントローラーのテンプレートをジェネレータに組み込んで、新しく必要な場合に随時出力することができます。
この仕組がサブジェネレータです。

サブジェネレータはジェネレータ内に追加のジェネレータを用意します。
コマンドの例は次の通りです。

  cd generator-<ジェネレータ名>
  yo generator:subgenerator “<this.nameの値>”

例えば

  cd generator-blog
  yo generator:subgenerator “post”

となります。
subgeneratorの後にスペースが入るので注意して下さい。

実行すると、appフォルダと同じ階層にpostサブジェネレータが生成されます。
サブジェネレータはジェネレータと大差ありません。templatesフォルダもあれば、index.jsの役割も同じです。
サブジェネレータでもジェネレータ同様に、this.copy, this.mkdirを始め、this.async, this.npmInstallなども使えます。

サブジェネレータを実行するには次の通り。

  yo <ジェネレータ名>:<サブジェネレータ名> 引数

たとえば「yo hello:post helloworld」で実行します。
サブジェネレータはジェネレータと異なりNameBaseを拡張して実装されています。そのため生成するテンプレートの名前が必要になります。

ここまででgenerator-generatorによるジェネレータを見てきました。
公式のジェネレータや、コミュニティのジェネレータを見ることで容易に把握できると思います。参考にしてください。