はじめに
こんにちは、モチベーションクラウドの開発にフリーのエンジニアとして参画している@HayatoKamonoです。
この記事は、「モチベーションクラウド Advent Calendar 2018」2日目の記事となります。
先に成果物のイメージ
$ yo mcs
_-----_
| | ╭──────────────────────────╮
|--(o)--| │ Welcome to MCS App │
`---------´ │ Generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What do you want to generate? (Use arrow keys)
❯ Vue Component
Vuex Store Module
? What's the component name? SampleComponent
create SampleComponent/SampleComponent.index.js
create SampleComponent/SampleComponent.vue
create SampleComponent/SampleComponent.stories.js
create SampleComponent/SampleComponent.specs.js
↑ この記事では、このようなものを作っていきます。
概要
モチベーションクラウドの開発チームでは2018年10月から改善期間と称して、開発に関するガイドラインやルール作りをはじめとする、様々な改善活動に取り組んでいます。
私が所属するフロントエンド開発チームでも、改善活動の一環として複数のガイドラインを作成しているのですが、その作成したガイドラインの1つに、コンポーネントを配置するディレクトリ構成・ファイル構成に関するガイドラインがあります。
.
├── atoms
├── molecules
├── organisms
│ └── SampleComponent
│ ├── index.js
│ ├── SampleComponent.specs.js
│ ├── SampleComponent.stories.js
│ └── SampleComponent.vue
├── templates
├── pages
└── decorators
簡単に説明をすると、components
ディレクトリ配下のディレクトリを、Atomic Designのatoms
、molecules
、organisms
、templates
、pages
で切り、そして、開発するコンポーネント毎にそのコンポーネント名でディレクトリを切るというディレクトリ構成で行くという方針で固まりました。
また、コンポーネント単位のディレクトリ配下には、「index.js」、単一ファイルコンポーネントの「ComponentName.vue」、Storybook用の「ComponentName.stories.js」、テスト用の「ComponentName.specs.js」を作成するというファイル構成で行くことに決まりました。
しかしながら、コンポーネント単位のディレクトリ以下に、毎回、4つのファイルを手動で決まった命名規則に合わせて作成するのは何気に面倒ですし、また、コード内に登場するコンポーネント名や、import
するファイル名などを毎回、手動で書いたりするのも面倒です。
そこで今回はこれらのファイル一式を雛形ファイルをもとに自動生成するジェネレーターを作ってみたいと思います。
Yeoman事始め
今回、ジェネレーターを作成するに当たっては、Yeomanを利用します。
What's Yeoman?
Yeoman helps you to kickstart new projects, prescribing best practices and tools to help you stay productive.
To do so, we provide a generator ecosystem. A generator is basically a plugin that can be run with theyo
command to scaffold complete projects or useful parts.
$ npm install -g yo
まずは、Yeomanをインストール。
$ mkdir generator-mcs
早速、ジェネレーター用のフォルダをYeomanの命名規則に従って作成します。
ジェネレーター用のフォルダ名は「generator-name」のように、「generator」をフォルダ名のprefixとし、ハイフンで次に続くジェネレーター名を区切る必要があります。
$ cd generator-mcs
$ npm init -y
$ npm install --save yeoman-generator yosay
次に、package.json
を作成し、今回の記事で作成するジェネレーターで使う依存モジュールを先にインストールしておきます。
※ 今回は、npm init -y
でpackage.json
の細かな設定はスキップしましたが、作成したジェネレーターをYeomanのGeneratorsページにインデックスさせたい場合は、package.json
のkeyword
に ["yeoman-generator"]
を指定、また、description
に任意の説明文を入れる必要があります。
$ mkdir -p generators/app generators/component
$ touch generators/app/index.js generators/component/index.js
今度は作業ディレクトリにgenerators
フォルダーを作成し、その中にapp
フォルダーとcomponent
フォルダーを作成します。
.
├── generators
│ ├── app
│ │ └── index.js
│ └── component
│ └── index.js
└── package.json
Yeomanにはapp generator
というメインのジェレネーターと、sub generator
というサブのジェネレーターの2種類のジェネレーターが作成可能です。
両者の使い分けは、app generator
は主にアプリケーション全体の雛形を作る為に利用し、sub generator
はより限定的な用途に利用するといったイメージです。
デフォルトの設定では、app
ディレクトリ以下のindex.js
がapp generator
に対応します。
そして、その他の任意の名前をつけるディレクトリ以下のindex.js
がそれぞれsub generator
に対応し、今回の例でいうと、component
ディレクトリ下のindex.js
がsub generator
に該当します。
$ yo mcs
例えば、この記事で作成しているジェネレーターの名前はgenertor-mcs
なので、app
ディレクトリ以下のapp generator
は上記のコマンドで後ほど実行出来ることになります。
$ yo mcs:component
また、先ほどgenerator
ディレクトリ以下に作成したcomponent
ディレクトリの名前は、そのまま、サブジェネレーターの名前となるので、上記のコマンドでこちらのサブジェネレーターを後ほど実行出来るようになります。
const Generator = require('yeoman-generator');
const yosay = require('yosay');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log(yosay('Welcome to MCS App Generator!'));
}
};
とりあえず、app generator
に対応するapp/index.js
を上記のように編集して、Hello World的なものを作り動作確認をしてみます。
$ npm link
動作確認をする為にも、上記のコマンドを実行して、現在作成しているジェネレーターをグローバールインストールしておきます。
$ yo mcs
_-----_
| | ╭──────────────────────────╮
|--(o)--| │ Welcome to MCS App │
`---------´ │ Generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
yo generator-name
を実行すると、無事にapp generator
の実行が確認出来るはずです。
This Generator is empty. Add at least one method for it to run.
というエラーが出るかもしれませんが、後で他にメソッドを追加すれば解消されるエラーなので、今は特に気にせず、次に進みます。
ちなみに、最初にインストールしたyosay
ライブラリは、上記の実行結果を見て分かる通り、Yeomanのイメージキャラクターのアスキーアートを出力する為のライブラリです。(見た目大事、楽しさ大事!)
今回は特にアプリケーション全体の雛形ファイルを作ることはしないので、一旦、app generator
はここまでとします。
雛形ファイルをもとにファイルを自動生成する
.
├── atoms
├── molecules
├── organisms
│ └── SampleComponent
│ ├── index.js
│ ├── SampleComponent.specs.js
│ ├── SampleComponent.stories.js
│ └── SampleComponent.vue
├── templates
├── pages
└── decorators
ここからは、この記事の概要で説明した通り、上記のようにcomponent単位のディレクトリ以下にindex.js
、componentName.vue
, componentName.stories.js
、componentName.specs.js
の4ファイルを雛形ファイルをもとに生成するsub generator
を作って行きます。
.
├── generators
│ ├── app
│ │ └── index.js
│ └── component
│ └── index.js
└── package.json
それでは、既に作成しておいたcomponent
ディレクトリ以下のindex.js
を編集して、yo mcs:component
で実行出来るジェネレーターを作成して行きます。
$ mkdir generators/component/templates
先に、component
ディレクトリ以下にtemplates
という名前で雛形ファイルを配置する為のディレクトリを作成しておきます。
Yeomanのデフォルトの設定では、templates
と名前の付いたディレクトリのパスが、後述するthis.templatePath()
で取得可能なパスになります。
$ cd generators/component/templates/
$ touch _index.js _ComponentName.vue _ComponentName.specs.js _ComponentName.stories.js
$ tree .
.
├── _ComponentName.specs.js
├── _ComponentName.stories.js
├── _ComponentName.vue
└── _index.js
続けて、templates
ディレクトリ以下に雛形となる4つのファイルを任意の名前で作成します。
現時点では単に空ファイルを作成しているだけですが、後ほど、これらの雛形ファイルにそれらしいベースとなるコードを追記したいと思います。
※ ファイル名は任意の名前で大丈夫です。_
をファイル名の先頭に付けることは必須ではありません。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
}
writing () {
this.fs.copyTpl(
this.templatePath('_index.js'),
this.destinationPath('ComponentName/ComponentName.index.js')
);
this.fs.copyTpl(
this.templatePath('_ComponentName.vue'),
this.destinationPath('ComponentName/ComponentName.vue')
);
this.fs.copyTpl(
this.templatePath('_ComponentName.stories.js'),
this.destinationPath('ComponentName/ComponentName.stories.js')
);
this.fs.copyTpl(
this.templatePath('_ComponentName.specs.js'),
this.destinationPath('ComponentName/ComponentName.specs.js')
);
}
};
早速、component/index.js
を上記のように編集し、雛形ファイルをもとに新規ファイルを生成してみます。
ファイルの書き込み処理は、Generator
クラスに定義されているwriting
メソッドの中に記述するようにします。
現段階では、コンポーネント名を直書きしていますが、後ほど、ユーザーからコンポーネント名の入力を受け付けるようにして、動的にコンポーネント名をファイル名に適用して行きます。
this.fs.copyTpl(
this.templatePath('_index.js'),
this.destinationPath('ComponentName/ComponentName.index.js')
);
ここでやっていることは、templates
ディレクトリ以下の雛形ファイルをコピー元として指定し、yo
コマンドが実際に実行されるカレントディレクトリ以下のComponentName/ComponentName.index.js
にファイルをコピーするという処理です。
$ mkdir ~/demo-project
$ cd ~/demo-project
$ yo mcs:component
ジェネレーター開発用の作業ディレクトリで、yo
コマンドでジェネレーターを実行してしまうと、作業ディレクト内にファイルが生成されてしまうので、ここからは、yo
コマンドを実行する時には、別に作成したプロジェクトディレクトリ内で実行して行きます。
.
└── ComponentName
├── ComponentName.index.js
├── ComponentName.specs.js
├── ComponentName.stories.js
└── ComponentName.vue
yo
コマンドを実行すると、コマンドを実行したカレントディレクトリ上に、ComponentName
というディクレトリが作られ、その中に4つのファイルが生成されていることが確認出来ました。
今度は動的にコンポーネント名を生成されるファイルに反映出来るようにして行きます。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
}
async prompting() {
// ユーザーの入力に関する情報をインスタンス変数に入れておく
this.answers = await this.prompt([
{
type: 'input',
name: 'componentName',
message: `What's the component name?`,
validate (input) {
if (input.length > 0) {
return true;
} else {
// 文字列を返すと検証エラー時にそのメッセージが出力表示される
return "You need to provide the component name.";
}
}
}
]);
}
writing () {
// ユーザーが入力したコンポーネント名をインスタンス変数を参照して渡す
this.fs.copyTpl(
this.templatePath('_index.js'),
this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.index.js`)
);
this.fs.copyTpl(
this.templatePath('_ComponentName.vue'),
this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.vue`)
);
this.fs.copyTpl(
this.templatePath('_ComponentName.stories.js'),
this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.stories.js`)
);
this.fs.copyTpl(
this.templatePath('_ComponentName.specs.js'),
this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.specs.js`)
);
}
};
ユーザーからの入力を受け付ける処理は、Generator
クラスに定義されているprompting()
の中に記述するようにします。
prompting
メソッドの中で実行しているprompt
メソッドは非同期実行され、promise
を返します。
そのため、async
, await
をprompting
メソッドのところでは使っています。
$ yo mcs:component
? What's the component name? SampleComponent
create SampleComponent/SampleComponent.index.js
create SampleComponent/SampleComponent.vue
create SampleComponent/SampleComponent.stories.js
create SampleComponent/SampleComponent.specs.js
yo
コマンドを実行すると、コンポーネント名を入力するプロンプトが表示され、入力したコンポーネント名が反映されたディレクトリとファイルが生成されることが確認出来ました。
<template>
</template>
<script>
export default {
name: "<%= componentName %>"
}
</script>
<style lang="scss" scoped>
</style>
ここまでは、空の雛形ファイルをもとに動作確認を進めていたので、templates/_ComponentName.vue
に簡易的なコードを加えて保存しておきます。
既に雛形ファイルをコピーする際に使用してきたfs.copyTpl
メソッドはejs template syntax
を使用しているので、雛形ファイルの方では、<%= componentName %>
のようなejs
の記法を使用することが可能です。
this.fs.copyTpl(
this.templatePath('_ComponentName.vue'),
this.destinationPath(`${this.answers.componentName}/${this.answers.componentName}.vue`),
{ componentName: this.answers.componentName }
);
_ComponentName.vue
内の<%= componentName %>
の部分に、ユーザーから入力されたコンポーネント名を反映出来るようにしてみます。
その為には、上記のコードのように、fs.copyTpl
メソッドの第3引数に対象の雛形ファイルに渡したい変数をkey
, value
ペアのオブジェクトで渡してあげます。
$ yo mcs:component
? What's the component name? SampleComponent
identical SampleComponent/SampleComponent.index.js
conflict SampleComponent/SampleComponent.vue
? Overwrite SampleComponent/SampleComponent.vue? (ynaxdH)
y) overwrite
n) do not overwrite
a) overwrite this and all others
x) abort
d) show the differences between the old and the new
h) Help, list all options
これで、再度、yo
コマンドを実行し、コンポーネント名を入力してあげれば、入力したコンポーネント名が生成される.vue
ファイルの該当箇所に反映されます。
尚、ファイル生成時に既に同じファイルが存在していて、既存のファイルと新規に生成されるファイルの内容が異なる場合は、上記のようにconflict
が発生します。
その際は、H
を押せば選択可能なオプションが表示されるので、その中から任意のオプションを選択すると良いです。
ちなみに、y
を押せば上書き保存してくれます。
app generatorからsub generatorに処理を委譲する
最後に、ジェネレーターから別のジェネレーターに処理を委譲する方法を紹介して、今回の記事を終わりにしたいと思います。
ここでは例として、app generator
からsub generator
に処理を委譲してみたいと思います。
const Generator = require('yeoman-generator');
const yosay = require('yosay');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log(yosay('Welcome to MCS App Generator!'));
}
// ユーザーにサブジェネレーターを選択してもらう
async prompting() {
const answers = await this.prompt([
{
type: 'list',
name: 'generatorName',
message: 'What do you want to generate?',
choices: [
{
name: 'Vue Component',
value: 'component'
},
{
name: 'Vuex Store Module',
value: 'module'
}
]
}
]);
// ユーザーが選択したサブジェネレーターに処理を委譲する
this.composeWith(
require.resolve(`../${answers.generatorName}`)
);
}
};
app generator
に対応するapp/index.js
を上記のように編集します。
これで、yo mcs
とコマンド実行した時に、先ほど作成したcomponent
サブジェネレーターを実行するのか、module
サブジェネレーターを実行するのかを、ユーザーに選択してもらえるようになり、選択されたサブジェネレーターに処理が委譲され実行されることになります。
※ 尚、2つ目の選択肢に対応するサブジェネレーターは未作成の為、選択するとファイルが見つからずエラーになります。上記の例はmodule
サブジェネレーターが仮に既に作成済みであったという想定の例になります。
$ yo mcs
_-----_
| | ╭──────────────────────────╮
|--(o)--| │ Welcome to MCS App │
`---------´ │ Generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What do you want to generate? (Use arrow keys)
❯ Vue Component
Vuex Store Module
? What's the component name? SampleComponent
create SampleComponent/SampleComponent.index.js
create SampleComponent/SampleComponent.vue
create SampleComponent/SampleComponent.stories.js
create SampleComponent/SampleComponent.specs.js
今後やりたいこと
- yosayライブラリで出力されるYeomanのイメージキャラクターの代わりに、モチベーションクラウドの広告に起用されている役所広司さんのアスキーアートをウェルカムメッセージに表示出来るようにしたい
- 実際のプロジェクトで使われているコードを雛形ファイルにし、今回作成したジェネレーターを実用的なものにしたい
- 単一ファイルコンポーネントの
<script>
部分をASTにparseしてprops
情報を抽出し、そのprops
情報をもとにStorybook addonのknobs
に対応したStorybookファイルを自動生成出来るようにしたい
参考
関連記事
こちらの記事はモチベーションクラウド Advent Calendar 2018に投稿した記事です。
他にも、以下の記事をモチベーションクラウド Advent Calendar 2018に投稿しています。