コンポーネント作るのにテストだのStorybookだの関連するファイルも作ったり中身をいちいち書くのもメンドクセ(´ω`)
って思った人向けの道具について。
Hygenはあらかじめ作っておいた設定をベースにファイル等を自動生成する、Vagrantみたいなやつ。
インストール方法とかは親切な説明が用意してあるからこれを見れば良い。
インストールしたらジェネレータを作るコマンドを叩く:
hygen init self hygen generator new component
init selfで
_templates/component/new/hello.ejs.t
が作成される。
名前被り防ぐため_templates
から.hygen
に変更しとく。
プロジェクトルートに.hygen.js
ファイルを作って:
module.exports = { templates: `${__dirname}/.hygen`, }
Hygenのテンプレートディレクトリについての設定を加える。
デフォルトのままで問題なければ変更の下りは不要である。
Vueコンポーネント用テンプレートの作成
やること:
- 関数型のコンポーネントはJSファイル、そうでなければVueファイルを作る
- 関数型かどうかでデフォルトの内容を変える
- Storybookのstories.jsファイルを作る
- test.jsファイルを作る
hello.ejs.tのファイル名をindex.ejs.t
にして、
中身をVueファイル作るための内容に変える。
--- to: components/<%= name %>/index.vue --- <template> <div></div> </template> <script> export default { name: '<%= name %>', props: { select: { Type: String, default: 'foo' } } }; </script> <style lang="scss" scoped> </style>
これでコマンドを叩くと、
hygen component new --name List
components/List/index.vue
が生成される。
対話型プロンプトの追加
VueファイルかJSファイルかを指定するのに、オプションで --type vue
としてもいいが、指定するものが増えたらいちいち書くのも面倒になってくるのが人の性。
なんとHygenならNuxtとかインストールするときの選択肢表示してくれるみたいなやつも導入できるので、これを利用しない手はない。
.hygen/component/new/prompt.js
を作成して、
コンポーネント名の入力と関数型にするかどうかの質問を追加。
module.exports = [ { type: 'input', name: 'name', message: 'コンポーネントの名前(最初は大文字にする)', validate: str => /^[A-Z]/.test(str) }, { type: 'toggle', name: 'functional', message: '関数型コンポーネントにする?', enabled: 'Yes', disabled: 'No' } ]
設定内容は依存先のプロンプトランナーenquirerのドキュメント参照。
これでユーザーが入力した内容を変数で受け取れるようになる。
テンプレートでの変数名はnameと同じものになる。
Hygenが出力するファイルを変えるのは単純に拡張子を変えればいいので、
脳死で三項演算すればおk。
--- to: "components/<%= name %>/index.<%= functional ? 'js' : 'vue' %>" ---
テンプレート出力内容の切り替え
関数型かどうかで中身を変えるのは、ejsの文法に従って作る。
<% if(functional) { -%> export const <%= name %> = { name: '<%= name %>', functional: true, render(h) { return h('div', { staticClass: '<%= name.toLowerCase() %>' }); } }; export default <%= name %>; <% } else { -%> <template> <div></div> </template> <script> export default { name: '<%= name %>', props: { select: { type: String, default: 'foo' } } }; </script> <style lang="scss" scoped> </style> <% } -%>
無駄な改行を防ぐため、if~else文のタグをにしておくこと。
もし関数型もvueファイルにするならtemplateだけ分岐すれば良い。
<% if(functional) { -%> <template functional> <div></div> </template> <% } else { -%> <template> <div></div> </template> <% } -%>
ファイル生成の切り替え
to:
にnull
を指定すると、そのテンプレートからファイルが生成されなくなる。
--- to: "<%= stories ? `components/${name}/index.stories.js` : null %>" ---
プロンプトにStorybookについての質問を追加して、
{ type: 'toggle', name: 'stories', message: 'Storybookファイルは必要?', enabled: 'Yes', disabled: 'No', initial: 'Yes' }
実行するとStorybookの質問に応じてファイル生成が切り替わる。
テスト用のファイルも同じ方法で出来る。
ヘルパーの利用
Hygenのテンプレート作成中に何かしら欲しい機能が出てきたら、ヘルパーを利用することができる。
自作する場合は、.hygen.js
ファイルにhelpers
を追加する。
module.exports = { templates: `${__dirname}/.hygen`, helpers: { sample: () => 'Sample Helper.' } }
これをテンプレートで使うときは、h.ヘルパー関数名
という構文になる。
<%= h.sample() %>
プロンプトからの変数も渡すことができる。
<%= h.sample(name) %>
内蔵されているヘルパーも充実している。
// example: // <%= h.inflection.pluralize(name) %> // <%= h.capitalize(message) %> pluralize( str, plural ) singularize( str, singular ) inflect( str, count, singular, plural ) camelize( str, low_first_letter ) underscore( str, all_upper_case ) humanize( str, low_first_letter ) capitalize( str ) dasherize( str ) titleize( str ) demodulize( str ) tableize( str ) classify( str ) foreign_key( str, drop_id_ubar ) ordinalize( str ) transform( str, arr )
inflectionについてはライブラリのドキュメントに使い方が載っている。
キャメルケースへの変更など、ChangeCaseもヘルパー用意されている。
// example: <%= h.changeCase.camel(name) %> camel( str ) constant( str ) dot( str ) header( str ) isLower( str ) isUpper( str ) lower( str ) lcFirst( str ) no( str ) param( str ) pascal( str ) path( str ) sentence( str ) snake( str ) swap( str ) title( str ) upper( str )
コンポーネント名をクラス名として流用するとき、コンポーネント名がキャメルケースだったら、全部小文字にしたうえでハイフンで区切るみたいなことをしたくなると思うが、そういうのもヘルパー使えば一発でできる。
export const <%= name %> = { name: '<%= name %>', functional: true, render(h) { return h('div', { staticClass: '<%= h.changeCase.paramCase(name) %>' }); } }; export default <%= name %>;
複雑な処理をするプロンプトの作成
prompt.js
を削除して、代わりにindex.js
を作成する。
同じ内容で書き換えるとindex.jsは次のようになる。
module.exports = { prompt: async ({ inquirer }) => { const questions = [ { type: 'input', name: 'name', message: 'コンポーネントの名前(最初は大文字にする)', validate: str => /^[A-Z]/.test(str) }, { type: 'toggle', name: 'functional', message: '関数型コンポーネントにする?', enabled: 'Yes', disabled: 'No' }, { type: 'toggle', name: 'stories', message: 'Storybookファイルは必要?', enabled: 'Yes', disabled: 'No', initial: 'Yes' } ]; return inquirer .prompt(questions) .then(answers => { const questions = []; return inquirer.prompt(questions).then(nextAnswers => Object.assign({}, answers, nextAnswers)) }) } }
importするコンポーネントファイルを選択する
コンポーネントのimportするところ
import Icon from '@/components/Icon'; export default { name: 'App', components: { Icon }, }
これを自動生成に含める方法について。
コンポーネントをAtomicDesignベースにディレクトリ分けして作ったとする。
components/atoms/
components/molecules/
components/organisms/
index.jsに
分類を選択させるプロンプトを追加すれば、生成時にディレクトリ分けができるようになる。
{ type: 'select', name: 'atomic', message: 'コンポーネントの分類', choices: ['Atoms', 'Molecules', 'Organisms'] }
で、このうちMolecules
かOrganisms
が選択された場合に、importするコンポーネントを選択できるようにする。
ここまでの設定でコンポーネントの自動生成をすると、コンポーネントが全てディレクトリベースCompnentName/index.vue
になるので、Node.jsのfsを使ってリストを作ることができる。
const getComponents = () => { const dirs = ['atoms', 'organisms', 'molecules']; const capitalize = txt => txt.charAt(0).toUpperCase() + txt.slice(1); let components = []; dirs.forEach(dir => { const dirpath = process.cwd() + '/components/' + dir; fs.readdirSync(dirpath).forEach(file => { const name = capitalize(dir) + '/' + file; components.push({ name, value: name }); }); }); return components; }
で、prompt.then
のところに分岐を追加する。
return inquirer .prompt(questions) .then(answers => { const { atomic } = answers; const questions = []; if (atomic !== 'Atoms') { questions.push({ type: 'MultiSelect', name: 'importComponents', message: 'importするコンポーネントを選択', choices: getComponents() }); } else { answers.importComponents = null; } return inquirer.prompt(questions).then(nextAnswers => Object.assign({}, answers, nextAnswers)) })
これでAtoms以外を選択した時にマルチセレクトが表示される。
マルチセレクトで選択したものは配列になって渡される。
中身は ['Atoms/Foo']
になってるので、ディレクトリを小文字にしたりコンポーネント名だけ抽出したりという処理にはヘルパーを作って対応する。
helpers: { componentName: c => c.split('/')[1], componentPath: c => c.split('/')[0].toLowerCase() + '/' + c.split('/')[1] }
あとはこれらをテンプレート側で利用する。
<template> <div class="<%= h.changeCase.paramCase(name) %>"></div> </template> <script> <% importComponents && importComponents.forEach(c => { -%> import <%= h.componentName(c) %> from '@/components/<%= h.componentPath(c) %>/'; <% }) -%> export default { name: '<%= name %>', <% if (importComponents) { -%> components: { <%= importComponents.map(c => h.componentName(c)).join(', ') -%> }, <% } -%> props: { select: { Type: String, default: 'foo' } } }; </script> <style lang="scss" scoped> </style>