Vue.js のフォームバリデーションライブラリ VeeValidate を評価してみた

前職より引き続き Vue.js を用いた開発業務を行っているのですが、毎回必ず発生するフォームバリデーション処理の実装につらみを感じていたので、VeeValidate というライブラリを評価してみました。
2020.07.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前職で Vue.js を用いた開発業務を行っていました。

クラスメソッドに入社後も Vue.js を使った開発業務を担当することになったのですが、フォームバリデーション処理が本質的ではない割に毎回発生することに負担を感じていたので、その辺りを肩代わりしてくれるライブラリを探すことにしました。

その結果 VeeValidate というライブラリが割と引っ掛かり、レビューブログやドキュメントを眺めたりした感じ良さげだったので、評価してみました。

環境情報

  • Node.js: 12.18.2
  • Vue.js: 2.6.11
  • Vuetify: 2.3.3
  • VeeValidate: 3.3.7
  • Vue CLI: 4.4.6

Vuetify 組み込みバリデーションを使用する場合

以前よりコンポーネントライブラリ Vuetify と組み合わせて使用しています。

Vuetify は組み込みのバリデーション機構を持っています。

こちらはAWSアカウントIDとバケット名を検査するフォームの例です (本当はもっと細かいバリデーションが必要ですが、あくまでも簡易的なものということで…)。

Vuetify,VeeValidateフォームサンプル

https://github.com/teknocat/veevalidate-test/blob/master/src/components/VuetifyForm.vue

VuetifyForm.vue

<template>
  <v-form
    ref="form"
    v-model="valid"
  >
    <v-text-field
      v-model="awsAccountId"
      :rules="[rules.required, rules.awsAccountId]"
      label="AWSアカウントID"
      required
    ></v-text-field>

    <v-text-field
      v-model="bucketName"
      :rules="[rules.required, rules.bucketName]"
      label="バケット名"
      required
    ></v-text-field>

    <v-btn
      :disabled="!valid"
      color="success"
      class="mr-4"
      @click="submit"
    >
      Submit
    </v-btn>
  </v-form>
</template>

<script>
export default {
  name: 'VuetifyForm',
  data: () => ({
    valid: true,
    awsAccountId: '',
    bucketName: '',
    rules: {
      required: (value) => !!value || '必須項目です',
      awsAccountId: (value) => {
        return /^[0-9]{12}$/.test(value) || 'AWSアカウントID は12桁の数値です';
      },
      bucketName: (value) => {
        return /^[a-z0-9.-]{3,63}$/.test(value) || 'バケット名 のフォーマットが正しくありません';
      },
    },
  }),
  methods: {
    submit () {
      const result = this.$refs.form.validate();
      console.log('submit', result);
    },
  },
}
</script>

バリデーション動作はするのですが、以下のようなつらみがありました。

  • ルールやメッセージ処理を各コンポーネントの中で実装するため、他コンポーネント間での流用がしにくい
    • 共通化を独自で設計から考えるのは辛いし、フォーム内の各コントローラへの参照の嵐になりそう(私は諦めた)
  • 複数コントロール間の依存関係をもったバリデーションが難しい
    • 各コントロールのイベントや値を見張ってチェックする必要があり、とても辛い(バグも出やすい)
  • ルール評価処理中に、どのコントロールを対象にしているかの情報が取得出来ない?
    • コード上 rules.required のメッセージ「必須項目です」に、本当は各フィールド名(AWSアカウントIDバケット名)を入れたい
  • メッセージのI18n化を考えると、より複雑になる

VeeValidate を使用する場合

VeeValidate でほぼ同等のバリデーションを実装すると、以下のような感じになります。

https://github.com/teknocat/veevalidate-test/blob/master/src/components/VeeValidateForm.vue

VeeValidateForm.vue

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }" immediate>
    <form>
      <ValidationProvider v-slot="{ errors, valid }" name="AWSアカウントID" rules="required|awsAccountId">
        <v-text-field
          v-model="awsAccountId"
          :error-messages="errors"
          label="AWSアカウントID"
          required
          :success="valid"
        ></v-text-field>
      </ValidationProvider>

      <ValidationProvider v-slot="{ errors, valid }" name="バケット名" rules="required|bucketName">
        <v-text-field
          v-model="bucketName"
          :error-messages="errors"
          label="バケット名"
          required
          :success="valid"
        ></v-text-field>
      </ValidationProvider>

      <v-btn class="mr-4" @click="submit" :disabled="invalid" color="success">submit</v-btn>
    </form>
  </ValidationObserver>
</template>

<script>
import { required } from 'vee-validate/dist/rules';
import { localize, extend, ValidationObserver, ValidationProvider } from 'vee-validate';
import ja from 'vee-validate/dist/locale/ja.json';
extend('required', required);
extend('awsAccountId', (value) => {
  return /^[0-9]{12}$/.test(value) || '{_field_} は12桁の数値です';
});
extend('bucketName', (value) => {
  return /^[a-z0-9.-]{3,63}$/.test(value) || '{_field_} のフォーマットが正しくありません';
});
localize('ja', ja);
export default {
  components: {
    ValidationProvider,
    ValidationObserver,
  },
  data: () => ({
    awsAccountId: '',
    bucketName: '',
  }),
  methods: {
    submit () {
      this.$refs.observer.validate().then(result => {
        console.log('submit', result);
      });
    },
  },
}
</script>

ValidationObserver, ValidationProvider で従来のフォーム、コントロールを囲むような感じになり、一見テンプレートが複雑になったような感じです。

一方、ルールやメッセージ定義は extend を使うことでコンポーネントから独立しているため、定義を一箇所にまとめることが容易になっています。 例は SFC に extend 処理も入れていますが、例えば定義したファイルを main.js から呼ぶことでグローバルな設定にも出来ます。

また、VeeValidate にはよく使うルール+メッセージが組み込まれており、この例では required を使用しています。ソース上では以下の行のみ書いており、必須チェックの実装やメッセージ定義が無いことがわかります(日本語の組み込み済メッセージを表示するために locale の設定が別途必要)。

import { required } from 'vee-validate/dist/rules';

extend('required', required);

一例として、以下のようなルールがあらかじめ組み込まれています。 バリデーションを実装していた人間にとっては、かゆい所に手が届くようなラインナップです。

  • alpha_num 数字とアルファベットのみ
    • 全角は対象外
  • between 数値の範囲
  • confirmed 確認用フィールド
  • email
  • required_if あるフィールドが特定の値の場合のみ別フィールドを必須にする
  • アップロードするファイルの拡張子や MIME type チェック、ファイルサイズチェックなど

さらに、Vuetify 組み込みの課題に上げていた、メッセージに対象フィールド名を入れられない問題についても、{_field_} と書くだけで実際のフィールド名に置換してくれます(I18nへの対応も確認済)。

VeeValidate 評価ポイント

Overview に利点が上げられていますが、個人的には以下のポイントを評価しました。

また、Vue 3.0 に対する言及を含め、本人が降臨している reddit の投稿が決め手となりました。

最後に

VeeValidate は取っ掛かり複雑な仕組みに見えるものの、開発が進むにつれ、コンポーネントの増加、バリデーション要件の複雑化により得られる恩恵は大きくなりそう、という印象です。

メッセージ以外も含めた統一的な I18n 化(Vue I18nとの連携)の仕組みを実現するにはひと工夫必要で、現時点で一応動作しているものの、完全にこれでイケる、というところまでは至っていません。追加できるような情報があれば、別途記載したいと思います。

なお、本サンプルは以下のリポジトリに置いています。