vue-cli用のテンプレートを作成する

Vue.jsにはプロジェクトの雛形をスキャフォールドするための[vue-cli](https://github.com/vuejs/vue-cli)というツールを公式が提供しています。
vue-cliを使うと、`.vue`ファイルによるコンポーネントの作成、開発用ローカルサーバー（差分更新ができる）、バンドル処理などを行える環境がすぐに構築できます。
vue-cliを使えば高速にプロジェクトをスタートさせることができます。

本記事ではvue-cli用のテンプレートを作成するうえでのTipsを紹介します。vue-cliの使い方自体は[この記事](http://qiita.com/567000/items/dde495d6a8ad1c25fa43)が詳しいので参照ください。


## 公式のテンプレートに満足できない
公式のテンプレートは良くできていますが、`vue-router`や`vuex`を使いたいときや、プロジェクトで個人的に必ず使うパッケージがあるときに、毎回同じような準備が必要になるのが面倒でした。

vue-cliは、公式が提供しているテンプレート以外に、GitHubのリポジトリやローカルのディレクトリからもスキャフォールドできます。ということで、公式のテンプレートをForkしてカスタマイズしてみました。

[nakajmg/webpack](https://github.com/nakajmg/webpack)

このテンプレートは`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でのコンパイル時にわたされます。

```js
{
  name: 'A Test Project',
  question1: true,
  question2: 'choice3'
}
```

### 文字列を入力させたい
文字列として`name`を入力させたい場合には次のようにします。

```js#meta.js
module.exports = {
  prompts: {
    name: {
      type: 'string'
      message: 'Project name'
    }
  }
}
```

これで`name`を入力するようにできます。

![image](https://qiita-image-store.s3.amazonaws.com/0/17969/334e6e2e-844a-555b-a66c-3ea94fd2b4cd.png)


### Yes/Noで答えさせたい
Yes/Noで答えを求めるには、次のように`type`に`confirm`を指定します。

```js#meta.js
module.exports = {
  prompts: {
    question1: {
      type: 'confirm',
      message: 'Use xxxx?'
    }
  }
}
```

![image](https://qiita-image-store.s3.amazonaws.com/0/17969/8c25ab1f-5680-e213-4276-d1d0484c77e3.png)


結果には`true`か`false`が入ります。


### 選択させたい
選択肢を選ばせるには、`type`に`list`か`checkbox`を指定します。選択肢は`choices`に配列で指定します。

```js#meta.js
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']
    }
  }
}
```

![image](https://qiita-image-store.s3.amazonaws.com/0/17969/9c2c5e21-6902-832e-6484-01c9cebc313e.png)


![image](https://qiita-image-store.s3.amazonaws.com/0/17969/f5ae003f-99e4-75d7-0656-2ac64b6400ae.png)


`list`で`choice2`を選び、`checkbox`で`vue-router`を選択すると、次のような結果が得られます。

```js
{
  question2: 'choice2'
  question3: {
    'vue-router': true
  }
}
```

`checkbox`を最初からチェックされた状態にしたい場合には、`default`オプションでチェックしたい項目を指定します。

```js
question3: {
  type: 'checkbox',
  message: 'Which use?',
  choices: ['vue-router', 'vuex'],
  default: ['vue-router']
}
```

上記の場合、`vue-router`に最初からチェックがついた状態になります。


### 特定の質問がYesのときだけ選択させたい
公式のテンプレートの`ESlint`使う？使うならプリセットどうする？の流れのやつです。

条件付きの質問のオプションに`when`で条件となる質問名を指定します。次の例では、`pre_question2`でYesを選択しないと、`question2`の質問自体がスキップされます。

```js#meta.js
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'
      ]
    }
  }
}
```

![image](https://qiita-image-store.s3.amazonaws.com/0/17969/8be4c77d-1a52-6f8a-5902-2cb0f7a56fe3.png)



## プロンプトの結果から出力する
vue-cliは`template`ディレクトリ以下のすべてのファイルを、プロンプトの結果を引数にしてHandlebarsでコンパイルを行います。条件分岐や値を展開するためには、ファイルの拡張子が`.js`や`.json`であってもHandlebarsの構文（Mustache）で書きます（すべてのファイルタイプでMustache記法を使うことになるので、シンタックスハイライトがぶっ壊れてなかなかつらいです）。

次の例は`vue-router`の利用をYes/Noで答えて、その結果で`package.json`に`vue-router`を追加する場合の`meta.js`と`template/package.json`の記述です。

```js#meta.js
module.exports = {
  prompts: {
    router: {
      type: 'confirm',
      message: 'Use vue-router?'
    }
  }
}
```

```json#package.json
{
  "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`ディレクトリに置いていている場合、意図と違う結果になります。

```html#template/component.vue
<template>
  <div>{{name}}</div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'sugoi component'
      }
    }
  }
</script>
```

プロンプトで`name`を答えるように定義し、`name`を`hoge`と入力すると、次のような結果になります。

```html#component.vue
<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>`と置き換えられます。

これを回避するには、コンパイルされたくない`{{}}`をエスケープする必要があります。

```html
<template>
  <div>\{{name}}</div>
</template>
```

めんどくさいですね。

### コンパイルさせたくないファイルを指定する
コンパイルさせたくないファイルは`skipInterpolation`で指定することができます。`.vue`ファイルをコンパイル対象から除外する場合には次のように指定します。

```js#meta.js
module.exports = {
  prompts: {
    ...
  },
  skipInterpolation: '*.vue'
}
```

これで`template`ディレクトリ以下のすべての`.vue`ファイルはHandlebarsのコンパイルの対象から外れます。

## filters
`filters`は特定の質問がYesのときにだけファイルをコピーするようにフィルタリングするオプションです。

たとえば、`vuex`を使うときにだけ`template/store`以下のファイルをコピーしたい場合には次のように記述します。

```js#meta.js
module.exports = {
  prompts: {
    vuex: {
      type: 'confirm',
      message: 'Use vuex?'
    }
  },
  filters: {
    'store/**/*': 'vuex'
  }
}
```

`Use vuex?`の質問にNoと答えたときには`template/store`以下のファイルはコピーされません。


## helpers
コンパイルに使われているHandlebarsですが、"ロジックレス"なテンプレートエンジンを名乗ってるだけあって、テンプレートで使える構文がかなり貧弱です。それを補うためには`helpers`でHandlebarsのヘルパー関数を登録して使います（[ビルトインのヘルパー](http://handlebarsjs.com/builtin_helpers.html)もありますが、これだけじゃ無理）。

### あらかじめ登録されているヘルパー
必要になるであろうヘルパーが２つ、あらかじめ登録されています。

- `if_eq`: `===`
- `unless_eq`: `!==`

ヘルパーは次のように利用します。

```html
{{#if_eq question2 'choice2'}}
  ここはquestion2がchoice2のときにレンダリングされる
{{/if_eq}}
```

これ以外に必要な関数が必要なときには、`helper`オプションで登録します。


### checkbox向けのヘルパー
[SimulatedGREG/electron-vue](https://github.com/SimulatedGREG/electron-vue)のテンプレートで使われてたヘルパーです。`type: 'checkbox'`な質問の答えによってレンダリングしたいときに役立ちます。

```js#meta.js
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)
    }
  }
}
```

次のように利用します。

```html
{{#isEnabled plugins 'vue-router'}}
  vue-routerがチェックされてるときにレンダリングされる
{{/#isEnabled}}
{{#isEnabled plugins 'vuex'}}
  vuexがチェックされてるときにレンダリングされる
{{/#isEnabled}}
```

## テンプレート作ってみた感想
今回いきなり公式の`webpack`テンプレートを参考にしてみましたが、ファイル数が多く、プロンプトの結果がどこで使われてるか把握するのがめんどくさかったです。

`webpack`テンプレートはESLintのプリセットを選択できるんですが、そのための条件分岐がほんとに汚いし（仕方ない）シンタックスハイライトも壊れてるしで、どこからどこまでが条件文だ？？？とかなりアレでした。👇一部抜粋:

```html#💩
<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>
```

もりもりのテンプレートから削除して作るのではなく、少なめのテンプレートに足す方がうまくいくように思いました。


## おわりに
今回テンプレートのルールを把握するために使った、小規模なテンプレートを置いておきますので、何かの参考になれば幸いです。

[nakajmg/sample-vue-template](https://github.com/nakajmg/sample-vue-template)