使用VueJS和Laravel对用户的输入进行实时验证

原文链接: https://itnext.io/vuejs-and-laravel-realtime-unique-input-validation-a6d9220be1c5


我经常面临的一个问题是需要对用户的输入做唯一性得实时验证。最常见的场景是当你在注册时验证用户名,用户必须不断尝试提交表单来验证输入的用户名是否可用使用。这是由于验证在服务端进行并展示结果给用户。

在我的项目中使用了Vuetify和VeeValidate,我将用它来解决我的问题。下面讨论的这些概念或方法,也可以使用其他的组件实现,但是也许需要一些修改调整。

我将使用一个从我的一系列多用户的网站应用(multi-tenant web applications)中提炼出一个DEMO项目。下面是演示项目的地址以及系列文章的第一部分:

laravel-tenancy-passport-demo
Laravel Passport and Hyn\Tenancy — Part 1

好戏开始!

在这个项目中,每一个注册用户都有自己唯一的子域名及其控制面板。域名是在注册时选择并且是唯一的,所以需要在用户填写域名的时候就立即告诉他是否可用,这需要通过API在数据库中检查域名是否可用,如果存在则返回false,反之true。

正如所上所说,这里使用VeeValidate进行验证,文档: https://baianat.github.io/vee-validate/

Vuetify 是一个非常棒的UI框架,它会大大提高开发效率。文档:https://vuetifyjs.com/en/getting-started/quick-start

VeeValidate 配置

使用极其简单,只需要引入并添加需要的配置即可。下面是我的app.js配置示例:

//Imports
import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuetify from 'vuetify'
import VeeValidate from 'vee-validate'
import App from '@/App'
import routes from '@/routes.js'

//Load Plugins
Vue.use(VueRouter)
Vue.use(Vuetify)
Vue.use(VeeValidate, { inject: false })

//Router configuration
const router = new VueRouter({
  mode: 'history',
  routes 
})

export const vm = new Vue({
    el: '#app',
    render: h => h(App),
    router
});

inject: false 这个配置需必要的,因为我们要在Vue实例外部添加错误信息。在接下来的Axios部分将解释这个配置为什么需要。然后在需要验证的组件注入$validator实例即可。更多的配置查看VeeValidate的文档。

API 接口

创建一个控制器Registration Controller和方法checkDomain, 并注册路由

<?php
use Illuminate\Support\Facades\Auth;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
//Auth Routes
Route::group(['prefix' => 'v1'], function () {
    Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
    Route::post('password/reset', 'Auth\ResetPasswordController@reset');
    Route::post('register', 'Auth\RegisterController@register');
    Route::post('checkDomain', 'Auth\RegisterController@checkDomain');
});
Route::group(['middleware' => 'auth:api', 'prefix' => 'v1'], function () {

    Route::apiResource('tickets', 'API\TicketController');
});

控制器

checkDomain方法将检查域名是否是唯一,如果不是唯一则验证失败,并返回422状态码和错误信息
。在Axios中配置了拦截器处理验证异常并将错误信息添加到VeeValidate实例中。所以不用担心如何处理。

当验证成功,我们需要返回VeeValidate要求的JSON格式

<?php
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
    /**
     * Checks if the fqdn is valid.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function checkDomain(Request $request)
    {
        $request->merge(['fqdn' => $request->fqdn . '.' . env('TENANT_URL_BASE')]);
        Validator::make($request->all(), [
            'fqdn' => 'unique:system.hostnames'
        ])->validate();
        return response()->json([
            'valid' => true,
            'data' => [
                'message' => 'Domain is available!'
            ]
        ], 200);
    }

下面是我使用的axios拦截器,它需要导入一个Vue个实例vm用于添加验证错误信息,所以请确保在app.js中有导出实例,像这样export const vm = new Vue()

import axios from 'axios'
import {vm} from '@/app.js'

let token = document.head.querySelector('meta[name="csrf-token"]')

const instance = axios.create({
    baseURL: '/api/v1/',
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'X-CSRF-TOKEN': token.content
    }
})

// Add a response interceptor
instance.interceptors.response.use(
  response => {

    return response;
  }, 
  error => {

    if (error.response.status === 401) {

        vm.$router.push({name: 'auth.login'});

    } else if (error.response.status === 422) {

        if (error.response.data.errors){

            for(let key in error.response.data.errors){

              vm.$validator.errors.add({field: key, msg: error.response.data.errors[key]})
            }
        }

    } else {

        console.error(error)
    }

    return Promise.reject(error);
})

export default instance

API文件

api.js中添加一个用户检查域名的方法,并且api.js可以在组件中使用

import axios from '@/config/axios.js'

export default {

    register(data) {
        return axios.post('register', data)
    },

    emailLink(data) {
        return axios.post('password/email', data)
    },

    resetPassword(data) {
        return axios.post('password/reset', data)
    },

    checkDomain(data) {
        return axios.post('checkDomain', data)
    }
}

注册页面

注意mounted的内容,定义一个验证方法isUnique ,每次验证时调用。然后使用Validator.extend()创建一个新的验证规则,这样就可以在字段上直接使用。如果在其他组件也会使用到可以将这个方法移到app.js

<template>
  <v-layout align-center justify-center>
    <v-flex xs12 sm8 md4>
      <v-card class="elevation-12">
        <v-toolbar dark color="primary">
            <v-toolbar-title>Register</v-toolbar-title>
        </v-toolbar>
        <v-card-text>
          <v-form aria-label="Register">

            <v-layout row>
              <v-flex xs12>
                <v-text-field
                  v-model="input.name"
                  v-validate="'required|max:255'"
                  data-vv-name="name"
                  :error-messages="errors.collect('name')"
                  label="Name"
                  name="name"></v-text-field>
              </v-flex>
            </v-layout>
            <v-layout row>
              <v-flex xs12>
                <v-text-field
                  v-model="input.email"
                  v-validate="'required|email|max:255'"
                  data-vv-name="email"
                  :error-messages="errors.collect('email')"
                  label="Email"
                  name="email"></v-text-field>
              </v-flex>
            </v-layout>
            <v-layout row>
              <v-flex xs12>
                <v-text-field
                  v-model="input.fqdn"
                  v-validate="'required|unique|max:255'"
                  data-vv-name="fqdn"
                  data-vv-delay="500"
                  :error-messages="errors.collect('fqdn')"
                  label="FQDN"
                  name="fqdn"
                  suffix=".app.itplog.com">

                  <v-fade-transition leave-absolute slot="append">
                  <v-progress-circular
                    v-if="validating"
                    size="24"
                    color="info"
                    indeterminate
                  ></v-progress-circular>
                  <v-icon v-else-if="errors.first('fqdn')" color="error">close</v-icon>
                  <v-icon v-else color="success">check</v-icon>
                </v-fade-transition> 

                </v-text-field>
              </v-flex>
            </v-layout>
            <v-layout row>
              <v-flex xs12>
                <v-text-field
                  v-model="input.password"
                  v-validate="'required|min:6'"
                  data-vv-name="password"
                  :error-messages="errors.collect('password')"
                  ref="password"
                  label="Password"
                  name="password"
                  type="password"></v-text-field>
              </v-flex>
            </v-layout>
            <v-layout row>
              <v-flex xs12>
                <v-text-field
                  v-model="input.password_confirmation"
                  v-validate="'required|confirmed:password'"
                  data-vv-as="password"
                  :error-messages="errors.collect('password_confirmation')"
                  label="Password Confirm"
                  name="password_confirmation"
                  type="password"></v-text-field>
              </v-flex>
            </v-layout>

            <v-btn 
              @click="validate" 
              :loading="loading" 
              color="primary">Submit</v-btn>
          </v-form>
        </v-card-text>
      </v-card>
    </v-flex>

    <v-dialog
      v-model="show"
      persistent
      width="300">
      <v-card>
        <v-card-text>
          {{message}}
          <div v-if="url">
          <p>Click on the URL to be directed to the personalized app login page</p>
          <p>
            <a :href="url">{{ url }}</a>
          </p>
          </div>
          <v-progress-linear
            v-show="loading"
            indeterminate
            class="mb-0"></v-progress-linear>
        </v-card-text>
      </v-card>
    </v-dialog>
  </v-layout>
</template>

<script>
  import Auth from '@/api/auth.js'
  import { Validator } from 'vee-validate'
  export default {
    inject: ['$validator'],
    data: () => ({
      input: {
        name: '',
        email: '',
        fqdn: '',
        password: '',
        passsword_confirmation: ''
      },
      //Dialog Data
      url: null,
      message: '',
      show: false,
      loading: false,
      //Unique Validation
      validating: false
    }),
    methods: {
      validate() {
        this.$validator.validateAll().then((result) => {
          if (result) {

            this.loading = true
            this.show = true
            this.message = 'Registering...'
            this.submit()
          } 
        })
      },
      submit(){
        Auth.register(this.input).then(({data}) => {
          this.loading = false
          this.message = data.message
          this.url = data.redirect
        }).catch(error => {
          this.loading = false
          this.show = false
          this.url = null
        })
      }
    },
    mounted (){
      //Unique fqdn check function
      const isUnique = (value) => {
        this.validating = true

        return Auth.checkDomain({fqdn: value}).then(({data}) => {
          this.validating = false
          return data

        })
        .catch(error => {
          this.validating = false
        })
      } 
      //Extend Validator instance with new validation function
      Validator.extend("unique", {
        validate: isUnique,
        getMessage: (field, params, data) => data.message
      });
    }
  }
</script>

最后来看一下fqdn字段。添加了unique规则,并且根据状态显示(使用v-if控制显示哪一个)不同的图标(使用插槽slot控制显示位置)。另外data-vv-delay控制触发验证的频率,降低API的负载

最后

希望对你有所帮助。