Vue.jsにはプロジェクトの雛形をスキャフォールドするためのvue-cliというツールを公式が提供しています。
vue-cliを使うと、.vue
ファイルによるコンポーネントの作成、開発用ローカルサーバー(差分更新ができる)、バンドル処理などを行える環境がすぐに構築できます。
vue-cliを使えば高速にプロジェクトをスタートさせることができます。
本記事ではvue-cli用のテンプレートを作成するうえでのTipsを紹介します。vue-cliの使い方自体はこの記事が詳しいので参照ください。
公式のテンプレートに満足できない
公式のテンプレートは良くできていますが、vue-router
やvuex
を使いたいときや、プロジェクトで個人的に必ず使うパッケージがあるときに、毎回同じような準備が必要になるのが面倒でした。
vue-cliは、公式が提供しているテンプレート以外に、GitHubのリポジトリやローカルのディレクトリからもスキャフォールドできます。ということで、公式のテンプレートをForkしてカスタマイズしてみました。
このテンプレートはwebpack
テンプレートに、次のようなカスタマイズをしました。
- ESLintを
standard
に固定+ルールの追加 - vue-routerを使用するか選択できるように
- vuexを使用するか選択できるように
-
.vue
でSassを使用するか選択できるように - e2eとunitテストの選択肢の削除
このテンプレートを作るために、小さいサンプルを作って色々と試したました。そこから把握できたことを書ける範囲で残しておきます✍
テンプレートに必要なもの
template
ディレクトリさえあればいい
テンプレートに最低限必要なのはtemplate
ディレクトリだけです。毎回決まった組み合わせを使うなら、template
ディレクトリに一式ぶち込んでおくだけの運用でも十分でしょう。公式のテンプレートをスキャフォールドしたあとに、package.json
なり必要な箇所をいじって、空のリポジトリにtemplate
ディレクトリ作って入れておくとかでもよいと思います。
選択させたいならmeta.js
公式のテンプレートのように、Yes/Noで答えたり、選択肢から選ぶといったインタラクティブなプロンプトを作りたい場合にはmeta.js
(またはmeta.json
)が必要になります。
meta.js
に定義するのは主に次の4つです。
-
prompts
: 質問を定義する -
filters
: 質問の結果でコピーするファイルをフィルタリングする -
helpers
: Handlebarsのヘルパーを定義する -
skipInterpolation
: Handlebarsのコンパイルから除外するファイルを指定する
prompts
prompts
にはコマンドラインで入力したり選択する項目を定義します。prompts
に定義する項目はオプションで入力の種類を指定できます。vue-cliのインタラクティブなプロンプトは(SBoudrias/Inquirer.js)[https://github.com/SBoudrias/Inquirer.js#questions]を使って実装されているので、(Questionオブジェクト)[https://github.com/SBoudrias/Inquirer.js#questions]にあるようなオプションが利用できます。
プロンプトで入力/選択した結果は次のようなオブジェクトになり、後述するHandlebarsでのコンパイル時にわたされます。
{
name: 'A Test Project',
question1: true,
question2: 'choice3'
}
文字列を入力させたい
文字列としてname
を入力させたい場合には次のようにします。
module.exports = {
prompts: {
name: {
type: 'string'
message: 'Project name'
}
}
}
これでname
を入力するようにできます。
Yes/Noで答えさせたい
Yes/Noで答えを求めるには、次のようにtype
にconfirm
を指定します。
module.exports = {
prompts: {
question1: {
type: 'confirm',
message: 'Use xxxx?'
}
}
}
結果にはtrue
かfalse
が入ります。
選択させたい
選択肢を選ばせるには、type
にlist
かcheckbox
を指定します。選択肢はchoices
に配列で指定します。
module.exports = {
prompts: {
question2: {
type: "list",
message: 'Choose one from the list',
choices: [
'choice1',
'choice2',
'choice3'
]
},
question3: {
type: 'checkbox',
message: 'Which use?',
choices: ['vue-router', 'vuex']
}
}
}
list
でchoice2
を選び、checkbox
でvue-router
を選択すると、次のような結果が得られます。
{
question2: 'choice2'
question3: {
'vue-router': true
}
}
checkbox
を最初からチェックされた状態にしたい場合には、default
オプションでチェックしたい項目を指定します。
question3: {
type: 'checkbox',
message: 'Which use?',
choices: ['vue-router', 'vuex'],
default: ['vue-router']
}
上記の場合、vue-router
に最初からチェックがついた状態になります。
特定の質問がYesのときだけ選択させたい
公式のテンプレートのESlint
使う?使うならプリセットどうする?の流れのやつです。
条件付きの質問のオプションにwhen
で条件となる質問名を指定します。次の例では、pre_question2
でYesを選択しないと、question2
の質問自体がスキップされます。
module.exports = {
prompts: {
pre_question2: {
type: 'confirm',
message: 'Use xxx?'
},
question2: {
when: 'pre_question2'
type: 'list',
message: 'Choose one from the list',
choices: [
'choice1',
'choice2',
'choice3'
]
}
}
}
プロンプトの結果から出力する
vue-cliはtemplate
ディレクトリ以下のすべてのファイルを、プロンプトの結果を引数にしてHandlebarsでコンパイルを行います。条件分岐や値を展開するためには、ファイルの拡張子が.js
や.json
であってもHandlebarsの構文(Mustache)で書きます(すべてのファイルタイプでMustache記法を使うことになるので、シンタックスハイライトがぶっ壊れてなかなかつらいです)。
次の例はvue-router
の利用をYes/Noで答えて、その結果でpackage.json
にvue-router
を追加する場合のmeta.js
とtemplate/package.json
の記述です。
module.exports = {
prompts: {
router: {
type: 'confirm',
message: 'Use vue-router?'
}
}
}
{
"dependencies": {
"vue": "^2.1.0"{{router}},
"vue-router": "^2.1.1"{{/router}}
}
}
Use vue-router
の質問にYesで答えるとpackage.json
にvue-router
の行が追加されて出力されます。
{{}}をエスケープしたい
Vueコンポーネントのtemplate
ではHandlebarsと同じくMustache記法で値の参照をします。たとえば、次のようなVueコンポーネントをtemplate
ディレクトリに置いていている場合、意図と違う結果になります。
<template>
<div>{{name}}</div>
</template>
<script>
export default {
data() {
return {
name: 'sugoi component'
}
}
}
</script>
プロンプトでname
を答えるように定義し、name
をhoge
と入力すると、次のような結果になります。
<template>
<div>
hoge
</div>
</template>
<script lang="babel">
export default{
data() {
return {
name: 'sugoi component'
}
}
}
</script>
<div>{{name}}</div>
がHandlebarsによってコンパイルされ、name
がhoge
に置き換えられてしまいます。もし質問にname
を定義してない場合にはundefined
が来たとみなられて<div></div>
と置き換えられます。
これを回避するには、コンパイルされたくない{{}}
をエスケープする必要があります。
<template>
<div>\{{name}}</div>
</template>
めんどくさいですね。
コンパイルさせたくないファイルを指定する
コンパイルさせたくないファイルはskipInterpolation
で指定することができます。.vue
ファイルをコンパイル対象から除外する場合には次のように指定します。
module.exports = {
prompts: {
...
},
skipInterpolation: '*.vue'
}
これでtemplate
ディレクトリ以下のすべての.vue
ファイルはHandlebarsのコンパイルの対象から外れます。
filters
filters
は特定の質問がYesのときにだけファイルをコピーするようにフィルタリングするオプションです。
たとえば、vuex
を使うときにだけtemplate/store
以下のファイルをコピーしたい場合には次のように記述します。
module.exports = {
prompts: {
vuex: {
type: 'confirm',
message: 'Use vuex?'
}
},
filters: {
'store/**/*': 'vuex'
}
}
Use vuex?
の質問にNoと答えたときにはtemplate/store
以下のファイルはコピーされません。
helpers
コンパイルに使われているHandlebarsですが、"ロジックレス"なテンプレートエンジンを名乗ってるだけあって、テンプレートで使える構文がかなり貧弱です。それを補うためにはhelpers
でHandlebarsのヘルパー関数を登録して使います(ビルトインのヘルパーもありますが、これだけじゃ無理)。
あらかじめ登録されているヘルパー
必要になるであろうヘルパーが2つ、あらかじめ登録されています。
-
if_eq
:===
-
unless_eq
:!==
ヘルパーは次のように利用します。
{{#if_eq question2 'choice2'}}
ここはquestion2がchoice2のときにレンダリングされる
{{/if_eq}}
これ以外に必要な関数が必要なときには、helper
オプションで登録します。
checkbox向けのヘルパー
SimulatedGREG/electron-vueのテンプレートで使われてたヘルパーです。type: 'checkbox'
な質問の答えによってレンダリングしたいときに役立ちます。
module.exports = {
prompts: {
plugins: {
type: 'checkbox',
message: 'Which use?',
choices: ['vue-router', 'vuex'],
default: ['vue-router']
}
},
filters: {
isEnabled (list, check, opts) {
if (list[check]) return opts.fn(this)
else return opts.inverse(this)
}
}
}
次のように利用します。
{{#isEnabled plugins 'vue-router'}}
vue-routerがチェックされてるときにレンダリングされる
{{/#isEnabled}}
{{#isEnabled plugins 'vuex'}}
vuexがチェックされてるときにレンダリングされる
{{/#isEnabled}}
テンプレート作ってみた感想
今回いきなり公式のwebpack
テンプレートを参考にしてみましたが、ファイル数が多く、プロンプトの結果がどこで使われてるか把握するのがめんどくさかったです。
webpack
テンプレートはESLintのプリセットを選択できるんですが、そのための条件分岐がほんとに汚いし(仕方ない)シンタックスハイライトも壊れてるしで、どこからどこまでが条件文だ???とかなりアレでした。👇一部抜粋:
<script>
import Hello from './components/Hello'{{#if_eq lintConfig "airbnb"}};{{/if_eq}}
export default {
name: 'app',
components: {
Hello{{#if_eq lintConfig "airbnb"}},{{/if_eq}}
}{{#if_eq lintConfig "airbnb"}},{{/if_eq}}
}{{#if_eq lintConfig "airbnb"}};{{/if_eq}}
</script>
もりもりのテンプレートから削除して作るのではなく、少なめのテンプレートに足す方がうまくいくように思いました。
おわりに
今回テンプレートのルールを把握するために使った、小規模なテンプレートを置いておきますので、何かの参考になれば幸いです。