はじめに
こんにちは、モチベーションクラウドの開発にフリーのエンジニアとして参画している@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 theyocommand 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に投稿しています。
