JavaScript
vue.js
Vue.jsDay 23

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

More than 1 year has passed since last update.

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

本記事ではvue-cli用のテンプレートを作成するうえでのTipsを紹介します。vue-cliの使い方自体はこの記事が詳しいので参照ください。

公式のテンプレートに満足できない

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

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

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でのコンパイル時にわたされます。

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

文字列を入力させたい

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

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

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

image

Yes/Noで答えさせたい

Yes/Noで答えを求めるには、次のようにtypeconfirmを指定します。

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

image

結果にはtruefalseが入ります。

選択させたい

選択肢を選ばせるには、typelistcheckboxを指定します。選択肢は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

image

listchoice2を選び、checkboxvue-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の質問自体がスキップされます。

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

プロンプトの結果から出力する

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

次の例はvue-routerの利用をYes/Noで答えて、その結果でpackage.jsonvue-routerを追加する場合のmeta.jstemplate/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.jsonvue-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を答えるように定義し、namehogeと入力すると、次のような結果になります。

html#component.vue
<template>
  <div>
    hoge
  </div>
</template>

<script lang="babel">
  export default{
    data() {
      return {
        name: 'sugoi component'
      }
    }
  }
</script>

<div>{{name}}</div>がHandlebarsによってコンパイルされ、namehogeに置き換えられてしまいます。もし質問にnameを定義してない場合にはundefinedが来たとみなられて<div></div>と置き換えられます。

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

<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のヘルパー関数を登録して使います(ビルトインのヘルパーもありますが、これだけじゃ無理)。

あらかじめ登録されているヘルパー

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

  • if_eq: ===
  • unless_eq: !==

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

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

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

checkbox向けのヘルパー

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)
    }
  }
}

次のように利用します。

{{#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>

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

おわりに

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

nakajmg/sample-vue-template