Skip to content

6. 客户端 JavaScript 验证

在上一章中,我们探讨了服务器端验证。现在让我们回到 Spring 应用程序 MVC 的架构:

BD

目前,发送给客户端的页面中尚未包含 JavaScript。现在我们将探讨这项技术,它将首先帮助我们实现客户端验证。其原理如下:

  • 由 JavaScript 将数据值提交至 Web 服务器;
  • 因此,在执行 POST 之前,它可以验证数据的有效性,并在数据无效时阻止 POST 的执行;

我们将使用之前已在服务器端验证过的表单。现在,我们将提供同时在客户端和服务器端进行验证的功能。

:本主题较为复杂。对该主题不感兴趣的读者可直接跳至第7段。

6.1. 项目功能

我们将展示项目的几个视图以介绍其功能。初始页面通过 URL 和 [http://localhost:8080/js01.html] 生成

 

验证机制已在客户端和服务器端两端实施。由于POST仅在客户端判定值有效时才会执行,因此服务器端的验证始终成功。 因此,我们提供了一个链接以禁用客户端验证。在此模式下,系统将恢复我们之前研究过的运行模式。以下是一个示例:

123
  • 在 [1] 中,显示输入的值;
  • 在 [2] 中,显示与输入相关的错误信息;
  • 在 [3] 中,显示错误汇总,其中每条错误包含:
    • 所验证字段的名称、
    • 错误代码,
    • 该错误代码的默认消息;

现在,启用客户端验证:

  • 在 [1] 中,输入的值。可以注意到,错误的输入具有特殊的样式;
  • 在 [2] 中,显示与错误输入相关的错误信息。这些信息与服务器生成的完全一致;
  • 在 [3-4] 中,已无内容,因为只要存在错误输入,向服务器发送的 POST 就不会发生;

6.2. 服务器端验证

6.2.1. 配置

首先创建一个新的 Maven 项目 [springmvc-validation-client]:

我们按以下方式扩展该项目:

  

类 [Config] 用于配置该项目。它与之前项目中的配置完全相同:


package istia.st.springmvc.config;


import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

}

类 [Main] 是该项目的可执行类:


package istia.st.springmvc.main;

import istia.st.springmvc.config.Config;

import java.util.Arrays;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;

public class Main {
    public static void main(String[] args) {
        // 启动应用程序
        ApplicationContext context = SpringApplication.run(Config.class, args);
        // 显示 Spring 找到的 Bean 列表
        System.out.println("Liste des beans Spring");
        String[] beanNames = context.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
  • 第 13 行,Spring Boot 通过配置文件 [Config] 启动;
  • 第15-20行:在此示例中,我们将演示如何显示由Spring管理的对象列表。如果有时感觉Spring未管理某个组件,这将非常有用。这是验证该情况的一种方法,也是验证Spring Boot自动配置的一种方式。在控制台上,将显示类似于以下的列表:
Liste des beans Spring
basicErrorController
beanNameHandlerMapping
beanNameViewResolver
config
defaultServletHandlerMapping
defaultTemplateResolver
defaultViewResolver
dispatcherServlet
dispatcherServletRegistration
embeddedServletContainerCustomizerBeanPostProcessor
error
errorAttributes
faviconHandlerMapping
faviconRequestHandler
handlerExceptionResolver
hiddenHttpMethodFilter
http.mappers.CONFIGURATION_PROPERTIES
httpRequestHandlerAdapter
jacksonObjectMapper
jsController
layoutDialect
localeChangeInterceptor
localeResolver
mappingJackson2HttpMessageConverter
mbeanExporter
mbeanServer
messageConverters
messageSource
multipart.CONFIGURATION_PROPERTIES
multipartConfigElement
multipartResolver
mvcContentNegotiationManager
mvcConversionService
mvcUriComponentsContributor
mvcValidator
objectNamingStrategy
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration$Empty
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafViewResolverConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebLayoutConfiguration
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration$DispatcherServletConfiguration
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration$EmbeddedTomcat
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration$ObjectMappers
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter$FaviconConfiguration
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.store
org.springframework.context.annotation.ConfigurationClassPostProcessor.enhancedConfigurationProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor
org.springframework.context.annotation.MBeanExportConfiguration
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration
propertySourcesPlaceholderConfigurer
requestContextListener
requestMappingHandlerAdapter
requestMappingHandlerMapping
resourceHandlerMapping
serverProperties
simpleControllerHandlerAdapter
spring.mvc.CONFIGURATION_PROPERTIES
spring.resources.CONFIGURATION_PROPERTIES
templateEngine
templateResolver
thymeleafResourceResolver
thymeleafViewResolver
tomcatEmbeddedServletContainerFactory
viewControllerHandlerMapping
viewResolver

我们已将 [Config] 类中定义的对象高亮显示出来。

6.2.2. 表单模型

继续探索该项目:

  

类 [Form01] 是用于接收提交值的类。其定义如下:


package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;

public class Form01 {

    // 提交的值
    @NotNull
    @AssertFalse
    private Boolean assertFalse;
    
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotBlank
    private String strNotEmpty;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    @NotBlank
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @NotNull
    @DecimalMax(value = "3.4")
    @DecimalMin(value = "2.3")
    private Double double1;
    
    @NotNull
    private Double double2;
    
    @NotNull
    private Double double3;
    
    @URL
    @NotBlank
    private String url;
    
    // 客户端验证
    private boolean clientValidation = true;
    // 本地化
    private String lang;
    ...
}

我们再次遇到了之前见过的验证器。此外,我们将引入“特定验证”的概念。这种验证无法通过预定义的验证器来实现。这里我们将要求 [double1+double2] 必须位于 [10,13] 的范围内。

6.2.3. 验证器

[JsController] 的验证器如下:

  

package istia.st.springmvc.controllers;

import istia.st.springmvc.models.Form01;
...

@Controller
public class JsController {

    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
    }
...

    // 准备视图 vue-01 的模板
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
...
    }
}
  • 第 9 行,操作 [/js01];
  • 第 10 行:实例化一个类型为 [Form01] 的对象,并自动将其放入模板中,关联键 [form01];
  • 第 10 行:将区域设置和模板注入参数中;
  • 第 11 行:利用这些信息,准备视图;
  • 第 12 行:显示视图 [vue-01.xml];

方法 [setModel] 如下:


    // 视图 vue-01 的模板准备
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // 仅支持 fr-FR、en-US 语言环境
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
        // 可能出现的提示信息
        if (message != null) {
            model.addAttribute("message", message);
        }
}
  • 方法 [setModel] 的目的是将以下内容放入模板中:
    • 有关区域设置的信息,
    • 作为最后一个参数传递的消息;
  • 第 14 行:将区域设置信息(语言、国家)放入模板中;
  • 第16-18行:将作为参数传递的消息(如有)放入区域设置中;
  • 第 8、12 行:区域设置信息也会存储在表单 [Form01] 中。JavaScript 将使用这些信息;

在表单 [vue-01.xml] 中输入的值将提交至后续操作 [/js02]:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        ...
}
  • 第 2 行:注解 [@Valid Form01 formulaire] 确保提交的值将接受类 [Form01] 中的验证器检查。 我们知道在 [10,13] 区间内存在一个特定的验证规则 [double1+double2]。当处理到第 3 行时,该验证尚未执行;
  • 第 3 行:创建以下 [Form01Validator] 对象:
  

package istia.st.springmvc.validators;

import istia.st.springmvc.models.Form01;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class Form01Validator implements Validator {

    // 验证范围
    private double min;
    private double max;

    // 构造函数
    public Form01Validator(double min, double max) {
        this.min = min;
        this.max = max;
    }

    @Override
    public boolean supports(Class<?> classe) {
        return Form01.class.equals(classe);
    }

    @Override
    public void validate(Object form, Errors errors) {
        // 已验证对象
        Form01 form01 = (Form01) form;
        // [double1] 的值
        Double double1 = form01.getDouble1();
        if (double1 == null) {
            return;
        }
        // [double2] 的值
        Double double2 = form01.getDouble2();
        if (double2 == null) {
            return;
        }
        // [double1+double2]
        double somme = double1 + double2;
        // 验证
        if (somme < min || somme > max) {
            errors.rejectValue("double2", "form01.double2", new Double[] { min, max }, null);
        }
    }

}
  • 第 8 行:为了实现特定的验证,我们创建了一个实现 Spring 接口 [Validator] 的类。该接口有两个方法:第 21 行的 [supports] 和第 26 行的 [validate];
  • 第21-23行:方法[supports]接收一个[Class]类型的对象。它必须返回true以表示支持该类,否则返回false
  • 第 22 行:我们规定类 [Form01Validator] 仅验证类型为 [Form01] 的对象;
  • 第15-18行:回顾一下,我们希望在区间[10,13]内实现约束[double1+double2]。 与其局限于该区间,我们将检查 [double1+double2] 约束在 [min, max] 区间内的有效性。这就是为什么我们有一个包含这两个参数的构造函数;
  • 第 26 行:调用 [validate] 方法时,传入已验证对象的实例(即 [Form01] 的实例)以及当前已知的错误集合 [Errors errors]。 如果由方法 [validate] 执行的验证失败,则必须在集合 [Errors errors] 中创建一个新元素;
  • 第43行:验证失败。使用[Errors.rejectValue]方法向集合[Errors errors]添加一个元素,其参数如下:
    • 参数 1:通常为出错字段的名称。此处测试了字段 [double1, double2]。可填写其中之一,
    • 关联的错误消息,或者更准确地说,是在外部化消息文件中对应的键

[messages_fr.properties]


form01.double2=[double2+double1] doit être dans l''intervalle [{0},{1}]

[messages_en.properties]


form01.double2=[double2+double1] must be in [{0},{1}

这里有通过 {0} 和 {1} 配置的消息。因此,必须为该消息提供两个值。这就是方法 [Errors.rejectValue] 的第三个参数所做的工作。

    • 第四个参数是错误的默认消息;

让我们回到操作 [/js02]:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (ObjectError error : result.getAllErrors()) {
                buffer.append(String.format("[name=%s,code=%s,message=%s]", error.getObjectName(), error.getCode(), error.getDefaultMessage()));
            }
            setModel(formulaire, model, locale, buffer.toString());
            return "vue-01";
        } else {
            redirectAttributes.addFlashAttribute("form01", formulaire);
            return "redirect:/js01.html";
        }
}
  • 第 4 行:验证器 [Form01Validator] 带以下参数执行:
    • 参数 1:正在验证的对象,
    • 参数 2:该对象的错误列表。该列表即作为操作参数传递的 [BindingResult result] 对象。若验证失败,该对象将新增一条错误;
  • 第 5 行:检测是否存在验证错误;
  • 第 7-10 行:遍历错误列表,并为每个错误记录:
    • 被验证对象的名称、
    • 其错误代码,
    • 其默认错误信息;
  • 第10行:利用这些信息,构建视图[vue-01.xml]的模板。这次,视图中包含一条消息,即各种错误消息拼接并简化的版本;
  • 第12-15行:如果所有提交的值均有效,则将客户端重定向至操作[/js01],并将提交的值作为Flash属性传递;

6.2.4. 视图

视图 [vue-01.xml] 结构复杂。我们仅介绍其中一小部分:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-1.10.2.min.js"></script>
        ...
    </head>
    <body>
        <!-- 标题 -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- 菜单 -->
        <p>
...
        </p>
        <!-- 表单 -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
            <table>
                <thead>
                    <tr>
                        <th class="col1" th:text="#{form01.col1}">Contrainte</th>
                        <th class="col2" th:text="#{form01.col2}">Saisie</th>
                        <th class="col3" th:text="#{form01.col3}">Validation client</th>
                        <th class="col4" th:text="#{form01.col4}">Validation serveur</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- 必填 -->
                    <tr>
                        <td class="col1">required</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
                        </td>
                        <td class="col3">
                            <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
                        </td>
                        <td class="col4">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
...
                </tbody>
            </table>
            <p>
            <!-- 提交按钮 -->
            <input type="submit" th:value="#{form01.valider}" value="Valider" onclick="javascript:postForm01()" />
            </p>
        </form>
        <!-- 服务器端验证器消息 -->
        <br/>
        <fieldset class="fieldset">
            <legend>
                <span th:text="#{server.error.message}"></span>
            </legend>
            <span th:text="${message}" class="error"></span>
        </fieldset>
    </body>
</html>

本页面使用了外部化消息文件中的一些消息:

[messages_fr.properties]


form01.title=Formulaire - Validations côté client - locale=
form01.col1=Contrainte
form01.col2=Saisie
form01.col3=Validation client
form01.col4=Validation serveur
form01.valider=Valider
server.error.message=Erreurs détectées par les validateurs côté serveur

[messages_en.properties]


form01.title=Form - Client side validation - locale=
form01.col1=Constraint
form01.col2=Input
form01.col3=Client validation
form01.col4=Server validation
form01.valider=Validate
server.error.message=Errors detected by the validators on the server side

让我们回到页面的代码:

  • 第 8 行:大量 JavaScript 库的导入,此处我们可以忽略;
  • 第14行:显示服务器在模板中设置的区域设置;
  • 第59行:显示服务器在模板中设置的消息;

第33至44行的代码是新增的。让我们来分析一下:


<!-- 必填 -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
    <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

最简单的方法或许是查看该 Thymeleaf 片段生成的代码 HTML:


<!-- 必填 -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
                            
  </td>
</tr>

我们将在客户端使用一个名为 [jquery.validate] 的验证库。所有 [data-x] 属性都是为该库准备的。当禁用客户端验证时,这些属性将不会被使用。因此目前无需理解它们。我们可以重点关注以下 Thymeleaf 代码行:


<input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />

该行会生成以下 HTML 行:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" />

在上文中,生成 [data-val-required="Le champ est obligatoire"] 属性存在困难。实际上,该属性的关联值来自外部化的消息文件。 因此,我们不得不通过一个 Thymeleaf 表达式来获取该值。该表达式如下:[th:attr="data-val-required=#{NotNull}"]。该表达式被求值后,其结果原样填入生成的 HTML 标签中。 它被命名为 [th:attr],因为我们用它来生成 Thymeleaf 中未预定义的属性。我们遇到过预定义属性 [th:text, th:value, th:class, ...],但不存在 [th:data-val-required] 属性。

6.2.5. 样式表

在上文中,我们遇到了诸如 [class="field-validation-valid"] 这样的 CSS 类。其中某些类被 JavaScript 验证库所使用。它们在以下 [form01.css] 文件中定义:

  

@CHARSET "UTF-8";

/*自定义样式*/
body {
    background-image: url("/images/standard.jpg");
}

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: AliceBlue;
}

.col4 {
    background: Lavender;
}

.error {
    color: red;
}

.fieldset{
    background: Lavender;
}
/* 验证助手样式
-----------------------------------------------------------*/
.field-validation-error {
    color: #f00;
}

.field-validation-valid {
    display: none;
}

.input-validation-error {
    border: 1px solid #f00;
    background-color: #费用;
}

.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}

.validation-summary-valid {
    display: none;
}

6.3. 客户端验证

6.3.1. jQuery与JavaScript入门

客户端验证通过 JavaScript 实现。我们将借助 jQuery 框架,该框架提供了许多简化 JavaScript 开发的函数。我们将介绍理解本章及后续章节脚本所需的 jQuery 基础知识。

我们创建一个静态文件 HTML [JQuery-01.html],并将其放置在文件夹 [static / vues] 中:

 

该文件内容如下:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>
  • 第 6 行:导入 jQuery;
  • 第10-12行:页面中ID为[element1]的元素。我们将对该元素进行操作。

我们需要下载文件 [jquery-1.11.1.min.js]。可在 jQuery 的最新版本中找到它,位于 URL [http://jquery.com/download/] 处:

Image

将下载的文件放置在 [static / js] 文件夹中:

  

完成上述操作后,使用Chrome浏览器访问静态视图[jQuery-01.html]:

在 Google Chrome 中,执行 [Ctrl-Maj-I] 以调出开发者工具 [3]。通过 [Console] 和 [4] 标签页可以运行 JavaScript 代码。 下面我们将提供一些需要输入的 JavaScript 命令,并附上说明。

JS
结果
$("#element1")
:返回所有 id 为 [element1] 的元素集合,通常该集合包含 0 或 1 个元素,因为 HTML 页面中不能存在两个相同的 id
$("#element1").text("blabla")
:将文本 [blabla] 应用于集合中的所有元素。这将导致页面显示的内容发生变化
$("#element1").hide()
隐藏集合中的元素。文本 [blabla] 不再显示。
$("#element1")
:重新显示集合。这让我们看到,ID 为 [element1] 的元素具有属性 CSS style='display: none;',这导致该元素被隐藏。
$("#element1").show()
:显示集合中的元素。文本 [blabla] 再次出现。这是由属性 CSS style='display : block;' 确保的显示效果。
$("#element1").attr('style','color: red')
: 为集合中的所有元素设置一个属性。此处的属性为 [style],其值为 [color: red]。文本 [blabla] 将变为红色。
Tableau
Dictionnaire

需要注意的是,在整个操作过程中,浏览器的URL并未发生变化。没有与Web服务器进行任何交互。所有操作都在浏览器内部完成。现在,让我们查看该页面的源代码:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

这是初始文本。它完全没有反映我们在第10-12行对元素所做的操作。在调试JavaScript时,这一点非常重要。因此,查看已显示页面的源代码通常是徒劳的。

我们已经掌握了足够的信息,可以理解接下来的 jS 脚本。

6.3.2. jS 验证库

我们将使用 jQuery 生态系统中的库。围绕 jQuery,有一系列项目,这些项目又衍生出了相应的库。 我们将使用由微软创建并捐赠给 jQuery 基金会的 [jquery.validate.unobstrusive] 验证库。 下文我们将称其为MS验证库,或简称为MS库。要获取该库,需要Microsoft Visual Studio开发环境。我尚未发现其他获取途径。 可以使用 [Visual Studio Community] 或 [http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx](2014年12月)这类免费版本。若读者不愿按照后续步骤操作,可直接从本文档网站上的示例中获取该库及其依赖的库。

使用 Visual Studio [1-4] 创建一个控制台项目:

12
34
  • 在 [5] 中,即控制台项目;
  • 在 [6-7] 中:我们将向该项目添加 [NuGet] 包。 [NuGet]是Visual Studio的一项功能,可用于下载DLL格式的库,同时也支持jS格式的库。
  • 若需将文件转换为 [9-10] 格式,请使用关键词 [jQuery] 进行搜索;
  • 若为 [11-13],请按指定顺序下载用于客户端验证所需的 jS 库;
  • 在 [14] 中,请同时下载 [Microsoft jQuery Unobtrusive Ajax] 库,我们稍后将使用该库;
  • 在 [15-16] 中,使用关键词 [globalize] 搜索软件包;
  • 在 [17] 中,请下载库文件 [jQuery.Validation.Globalize];

这些下载内容在项目 [18] 的 [Scripts] 文件夹中安装了若干 jS 库。其中并非所有文件都实用。每个文件都有两个副本:

  • [js]:库的可读版本;
  • [min.js]:库的不可读版本,即所谓的“压缩版”(minified)。它并非真正不可读,而是纯文本。 但内容难以理解。这是生产环境中应使用的版本,因为该文件比对应的 [js] 版本更小,因此能提升客户端与服务器之间的通信速度;

[min.map]版本并非必需。在[cultures]文件夹中,只需保留应用程序所管理的语言环境即可。

使用 Windows 资源管理器,将这些文件复制到项目 [springmvc-validation-client] 的 [static / js / jquery] 文件夹中,并仅保留有用的 [20] 文件:

在 [21] 中,仅保留两种语言环境:

  • [fr-FR]:法国法语;
  • [en-US]:USA中的英语;

6.3.3. 导入 jS 验证库

要使用这些库,必须通过视图 [vue-01.xml] 导入:


<head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
        <script type="text/javascript" src="/js/client-validation.js"></script>
        <script type="text/javascript" src="/js/local.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${locale}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
  • 第 11 行:导入一个尚未提及的 jS 文件;
  • 第13-18行:由Thymelaf解释执行的jS脚本。它负责客户端的区域设置管理;

6.3.4. 客户端语言环境管理

客户端本地化由以下脚本 jS 实现:


<script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${locale}]];
                    Globalize.culture(culture);
                    /*]]>*/
</script>
  • 第3-4行:jS代码,其中包含Thymeleaf表达式[[${locale}]]。 请注意该表达式的特殊语法。这是因为它位于 JavaScript 中。表达式 [[${locale}]] 将被视图模型中键 [locale] 的值所替换;

由这些行生成的 HTML 流中的结果如下:


<script>
            /*<![CDATA[*/
                    var culture = 'en-US';
                    Globalize.culture(culture);
                    /*]]>*/
</script>

第 3-4 行设置了客户端语言环境。我们仅处理两个语言环境:[fr-FR] 和 [en-US]。这就是我们只导入了两个语言环境文件的原因:


        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>

客户端使用的语言环境由服务器端确定。让我们回到服务器端的代码:


    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
    }

    // 准备视图 vue-01 的模板
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // 仅支持 fr-FR 和 en-US 语言环境
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
...
}
  • 第 20 行:将区域设置 [fr-FR] 或 [en-US] 放入视图模板 [vue-01.xml] 中(第 4 行)。 这里存在一个可能引发问题的细节。虽然客户端的法语区域设置标记为 [fr-FR],但在服务器端却标记为 [fr_FR]。 正因如此,在第 14 行和第 18 行,该区域设置以这种形式存储在接收提交值的 [Form01 formulaire] 对象中;

需注意以下重要事项。脚本


<script>
            /*<![CDATA[*/
                    var culture = 'en-US';
                    Globalize.culture(culture);
                    /*]]>*/
</script>

会根据服务器传来的区域设置来更改客户端的区域设置。这并不会使页面显示的消息实现国际化,而仅仅是改变了某些依赖于国家区域设置的信息的解释方式。 在 [fr_FR] 区域设置下,实际数值 [12,78] 是有效的,但在 [en-US] 区域设置下则无效。因此应写为 [12.78]。 同样,日期 [12/01/2014] 在 [fr-FR] 区域设置中是有效的,但在 [en-US] 区域设置中则应写为 [01/12/2014]。 [jquery / globalize]文件夹中的文件可处理此类问题:

  

错误消息的国际化仅在服务器端进行管理。我们将看到,页面 HTML / jS 随附了与服务器管理的区域设置相对应的错误消息: 对于 [fr_FR] 语言环境为法语,对于 [en_US] 语言环境为英语。

6.3.5. 消息文件

视图 [vue-01.xml] 使用以下国际化消息:

  

[messages_fr.properties]


NotNull=Le champ est obligatoire
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide
typeMismatch=Format invalide
Future.form01.dateInFuture=La date doit être postérieure ou égale à celle d''aujourd'hui
Past.form01.dateInPast=La date doit être antérieure ou égale à celle d''aujourd'hui
Min.form01.intMin10=La valeur doit être supérieure ou égale à 10
Max.form01.intMax100=La valeur doit être inférieure ou égale à 100
Size.form01.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
Length.form01.str4=La chaîne doit avoir quatre caractères exactement
Email.form01.email=Adresse mail invalide
URL.form01.url=URL invalide
Range.form01.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form01.hhmmss=Tapez l''heure sous la forme hh:mm:ss
form01.hhmmss.pattern=^\\d{2}:\\d{2}:\\d{2}$
DateInvalide.form01=Date invalide
form01.str4.pattern=^.{4,4}$
form01.int1014.max=14
form01.int1014.min=10
form01.strBetween4and6.pattern=^.{4,6}$
form01.intMax100.value=100
form01.intMin10.value=10
form01.double1.min=2.3
form01.double1.max=3.4
Range.form01.double1=La valeur doit être dans l'intervalle [2,3-3,4]
form01.title=Formulaire - Validations côté client - locale=
form01.col1=Contrainte
form01.col2=Saisie
form01.col3=Validation client
form01.col4=Validation serveur
form01.valider=Valider
form01.double2=[double2+double1] doit être dans l''intervalle [{0},{1}]
form01.double3=[double3+double1] doit être dans l''intervalle [{0},{1}]
locale.fr=Français
locale.en=English
client.validation.true=Activer la validation client
client.validation.false=Inhiber la validation client
DecimalMin.form01.double1=Le nombre doit être supérieur ou égal à 2,3
DecimalMax.form01.double1=Le nombre doit être inférieur ou égal à 3,4
server.error.message=Erreurs détectées par les validateurs côté serveur

[messages_en.properties]


NotNull=Field is required
NotEmpty=Field can''t be empty
NotBlank=Field can''t be empty
typeMismatch=Invalid format
Future.form01.dateInFuture=Date must be greater or equal to today''s date
Past.form01.dateInPast=Date must be lower or equal today''s date
Min.form01.intMin10=Value must be higher or equal to 10
Max.form01.intMax100=Value must be lower or equal to 100
Size.form01.strBetween4and6=String must have between 4 and 6 characters
Length.form01.str4=String must be exactly 4 characters long
Email.form01.email=Invalid mail address
URL.form01.url=Invalid URL
Range.form01.int1014=Value must be in [10,14]
AssertTrue=Only value True is allowed
AssertFalse=Only value False is allowed
Pattern.form01.hhmmss=Time must follow the format hh:mm:ss
form01.hhmmss.pattern=^\\d{2}:\\d{2}:\\d{2}$
DateInvalide.form01=Invalid Date
form01.str4.pattern=^.{4,4}$
form01.int1014.max=14
form01.int1014.min=10
form01.strBetween4and6.pattern=^.{4,6}$
form01.intMax100.value=100
form01.intMin10.value=10
form01.double1.min=2.3
form01.double1.max=3.4
Range.form01.double1=Value must be in [2.3,3.4]
form01.title=Form - Client side validation - locale=
form01.col1=Constraint
form01.col2=Input
form01.col3=Client validation
form01.col4=Server validation
form01.valider=Validate
form01.double2=[double2+double1] must be in [{0},{1}]
form01.double3=[double3+double1] must be in [{0},{1}]
locale.fr=Français
locale.en=English
client.validation.true=Activate client validation
client.validation.false=Inhibate client validation
DecimalMin.form01.double1=Value must be greater or equal to 2.3
DecimalMax.form01.double1=Value must be lower or equal to 3.4
server.error.message=Errors detected by the validators on the server side

文件 [messages.properties] 是英语消息文件的副本。最终,任何与 [fr] 不同的语言环境都将使用英语消息。 需要提醒的是,文件 [messages_fr.properties] 适用于所有与 [fr_XX] 相同的语言环境,例如 [fr_CA] 或 [fr_FR]。

视图 [vue-01.xml] 使用这些消息的键。若读者希望了解这些键对应的值,请返回本段查看。

6.3.6. 区域设置变更

视图 [vue-01.xml] 包含四个链接:


<body>
        <!-- 标题 -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- 菜单 -->
        <p>
            <a id="locale_fr" href="javascript:setLocale('fr_FR')">
                <span th:text="#{locale.fr}"></span>
            </a>
            <a id="locale_en" href="javascript:setLocale('en_US')">
                <span style="margin-left:30px" th:text="#{locale.en}"></span>
            </a>
            <a id="clientValidationTrue" href="javascript:setClientValidation(true)">
                <span style="margin-left:30px" th:text="#{client.validation.true}"></span>
            </a>
            <a id="clientValidationFalse" href="javascript:setClientValidation(false)">
                <span style="margin-left:30px" th:text="#{client.validation.false}"></span>
            </a>
        </p>
        <!-- 表单 -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
            ...

其中部分如下所示 [1]:

让我们来看看这两个用于将语言环境切换为法语或英语的链接:


            <a id="locale_fr" href="javascript:setLocale('fr_FR')">
                <span th:text="#{locale.fr}"></span>
            </a>
            <a id="locale_en" href="javascript:setLocale('en_US')">
                <span style="margin-left:30px" th:text="#{locale.en}"></span>
</a>

点击这些链接会触发脚本 jS 的执行,该脚本位于文件 [local.js] [2] 中。 在这两种情况下,调用的都是函数 jS [setLocale]:


// 区域设置
function setLocale(locale) {
    // 更新区域设置
    lang.val(locale);
    // 提交表单——这不会触发客户端验证器——因此我们没有禁用客户端验证
    document.form.submit();
}

要理解第 4 行,需要先说明一下背景。视图 [vue-01.xml] 包含一个名为 [lang] 的隐藏字段:


<input type="hidden" th:field="*{lang}" th:value="*{lang}" value="true" />

该字段对应于 [Form01] 中的 [lang] 字段:


    // 本地化
    private String lang;

隐藏字段在需要丰富提交值时非常实用。通过 JavaScript 可以为其赋值,该值会被作为用户正常输入的内容一并提交。Thymeleaf 生成的代码 HTML 如下:


<input type="hidden" value="en_US" id="lang" name="lang" />

参数 [value] 的值即为生成 HTML 时 [Form01.lang] 字段的值。 需要特别注意的是节点 [id="lang"] 的标识符 jS。该标识符由以下 [] 函数使用:


// 全局变量
var lang;

// 文档就绪
$(document).ready(function() {
    // 全局引用
    lang = $("#lang");
});

// 本地化
function setLocale(locale) {
    // 更新区域设置
    lang.val(locale);
    // 提交表单——由于未知原因,这并未触发客户端的验证器
    // 因此未禁用验证
    document.form.submit();
}
  • 第 5-8 行:函数 jS [$(document).ready(f)] 是在浏览器加载完服务器发送的整个文档后执行的函数。其参数是一个函数。 使用函数 jS [$(document).ready(f)] 来初始化已加载文档的 jS 环境;
  • 第 7 行:表达式 [$("#lang")] 是一个 jQuery 表达式。其值为 DOM 节点(具有 [id='lang'] 属性)的引用;
  • 第 2 行:在函数外部声明的变量对所有函数都是全局的。此处意味着在 [$(document).ready()] 中初始化的变量 [lang] 在第 11 行的函数 [setLocale] 中也是已知的;
  • 第13行:修改由[lang]标识的节点的[value]属性。 如果 lang 的值为 [xx_XX],则该节点的 HTML 标签变为:

<input type="hidden" value="xx_XX" id="lang" name="lang" />

JavaScript 允许修改 DOM 元素的值(文档对象模型)。

  • 第 16 行:[document] 指代 DOM。[document.form] 指代该文档中找到的第一个表单。 一个 HTML 文档可能包含多个 <form> 标签,因此可能包含多个表单。此处我们只有一个。[document.form.submit] 会像用户点击了一个具有 [type='submit'] 属性的按钮一样,提交该表单。 表单的值被提交到了哪个操作?要了解这一点,需要查看 [vue-01.xml] 中表单的 [form] 标签:

        <!-- 表单 -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">

接收提交值的操作是由 [th:action] 属性指定的。因此,该操作即为 [/js02.html]。 需要提醒的是,在此名称中,后缀 [.html] 将被移除,最终执行的将是操作 [/js02]。 需要重点理解的是,节点 [lang] 的新值 [xx_XX] 将以 [lang=xx_XX] 的形式发布。 然而,我们已将应用程序配置为拦截参数 [lang],并将其解释为区域设置的变更。因此,在服务器端,区域设置将变为 [xx_XX]。让我们看看即将执行的操作 [/js02]:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (ObjectError error : result.getAllErrors()) {
                buffer.append(String.format("[name=%s,code=%s,message=%s]", error.getObjectName(), error.getCode(),
                        error.getDefaultMessage()));
            }
            setModel(formulaire, model, locale, buffer.toString());
            return "vue-01";
        } else {
            redirectAttributes.addFlashAttribute("form01", formulaire);
            return "redirect:/js01.html";
        }
    }

    // 准备视图模板 vue-01
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // 仅支持 fr-FR、en-US 语言环境
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
        ...
}
  • 第 2 行:操作 [/js02] 将接收封装在参数 [Locale locale] 中的新区域设置 [xx_XX]:
  • 第 5-12 行:如果某些提交的值无效,则将显示视图 [vue-01.xml],并使用新区域设置 [xx_XX] 显示错误消息。 此外,第 11 行将变量 [locale=xx-XX] 放入模板中。在客户端,该值将用于更新客户端的区域设置。我们已对此过程进行了描述;
  • 第14-15行:如果提交的值均有效,则重定向至后续操作[/js01]:

    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
}
  • 第 2 行,注入新的区域设置 [xx_XX];
  • 第 3 行:方法 [setModel] 将把客户端的区域设置设为 [xx-XX];

现在让我们观察该区域设置在视图 [vue-01.xml] 中的影响。目前我们尚未完整展示该视图,因为它包含超过 300 行代码。不过,其中大部分行都是类似以下序列的重复:


<!-- 必填 -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
    <td class="col3">
      <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
    </td>
  <td class="col4">
      <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

该代码显示以下片段:

错误消息 [2] 源自第 5 行中的属性 [th:attr="data-val-required=#{NotNull}"]。[#{NotNull}] 是一条本地化消息。根据服务器端的区域设置,第 5 行会生成以下标签:


<input type="text" data-val="true" data-val-required="Field is required" id="strNotEmpty" name="strNotEmpty" />

或者生成以下标签:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" />

[data-x] 的属性由验证库 jS 调用。

最终,需注意这两个更改区域设置的链接:

  • 会触发对输入值的 POST 验证;
  • 同时在服务器端和客户端更改区域设置;
  • 生成包含错误消息的 HTML 页面,这些错误消息将传递给 jS 验证库,且消息内容采用所选区域设置的语言;

6.3.7. POST 按钮用于提交输入的值

让我们来研究一下按钮 [Valider],它负责将视图 [vue-01.xml] 中输入的值提交出去。其代码 HTML 如下:


<!-- 提交按钮 -->
<input type="submit" value="Valider" onclick="javascript:postForm01()" />

如果浏览器启用了 JavaScript,点击该按钮将触发 [postForm01] 方法的执行。 如果该函数返回布尔值 [False],则 submit 不会被调用。如果返回其他值,则会调用该方法。该函数位于文件 [local.js] 中:

 

它通过视图 [vue-01.xml] 中的第 6 行被导入:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
...
        <script type="text/javascript" src="/js/local.js"></script>
</head>

在此文件中,可找到以下代码:


// 全局变量
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // 全局引用
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// 表单提交
function postForm01() {
...
}
  • 第 8-16 行:函数 jS [$(document).ready(f)] 是在浏览器加载完服务器发送的整个文档后执行的函数。其参数是一个函数。 使用函数 jS [$(document).ready(f)] 来初始化已加载文档的 jS 环境;
  • 第 10-14 行:要理解这些行,需要同时查看 Thymeleaf 代码和生成的 HTML 代码;

相关的 Thymeleaf 代码如下:


<form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
...
<input type="text" th:field="*{double1}" th:value="*{double1}" ... />
...
<input type="text" th:field="*{double2}" th:value="*{double2}" />
...
<input type="text" th:field="*{double3}" th:value="*{double3}" ... />
...
<input type="hidden" th:field="*{clientValidation}" th:value="*{clientValidation}" value="true" />

该代码生成了以下 HTML 代码:


<form action="/js02.html" method="post" name="form" id="form">
...
<input type="text" id="double1" name="double1" .../>
....
<input type="text" value="" id="double2" name="double2" />
...
<input value="" id="double3" name="double3" .../>
...
<input type="hidden" value="false" id="clientValidation" name="clientValidation" /> 

每个 [th:field='x'] 属性会生成两个 HTML 属性:[name='x'] 和 [id='x']。 [name] 属性是提交值的名称。因此,对于 HTML <input type='text'> 标签中,将字符串 x=y 放入提交的值 name1=val1&name2=val2&... 中。而 [id='x'] 属性则由 JavaScript 使用,用于标识 DOM(文档对象模型)中的某个元素。 加载的文档 HTML 实际上会被转换为一个名为 DOM 的 JavaScript 树,其中每个节点都通过其 [id] 属性进行标识。

让我们回到 [$(document).ready()] 函数的代码:


// 全局变量
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // 全局引用
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// 表单提交
function postForm01() {
...
}
  • 第 10 行:表达式 [$("#form")] 是一个 jQuery 表达式。其值为对 DOM 中具有 [id='form '] 属性的节点的引用;
  • 第 10-14 行:获取 DOM 上的五个节点的引用;
  • 第2-6行:在函数外部声明的变量对所有函数都是全局的。这里的意思是,在[$(document).ready()]中初始化的变量[formulaire, clientValidation , double1, double2, double3],在第19行的函数[postForm01]中也能被访问;

现在,让我们研究函数 [postForm01]:


// 表单提交
function postForm01() {
    // 客户端验证模式
    var validationActive = clientValidation.val() === "true";
    if (validationActive) {
        // 清除服务器端错误
        clearServerErrors();
        // 表单验证
        if (!formulaire.validate().form()) {
            // 不提交
            return false;
        }
    }
    // 英式日期格式
    var value1 = double1.val().replace(",", ".");
    double1.val(value1);
    var value2 = double2.val().replace(",", ".");
    double2.val(value2);
    var value3 = double3.val().replace(",", ".");
    double3.val(value3);
    // 允许提交
    return true;
}

需要提醒的是,该函数 jS 在表单的 [submit] 之前执行。 如果它返回布尔值 [false](第 11 行),则不会触发提交。如果返回其他值(第 22 行),则会触发提交。

  • 关键代码位于第 4-12 行;
  • 第 4 行:获取隐藏字段 [clientValidation] 的值。若需启用客户端验证,该值为 'true';否则为 'false';
  • 第6行:若需进行客户端验证,则清除服务器端可能存在的错误信息(因用户刚更改了区域设置);
  • 第 9 行:需要说明的是,变量 [formulaire] 代表标签 HTML <form> 的节点,即表单。 该表单包含尚未介绍的 jS 验证器,后续段落将对此进行说明。 表达式 [formulaire.validate().form()] 强制执行表单中所有 jS 验证器。若被测试的值均有效,其值为 [true];否则,其值为 [false];
  • 第 11 行:如果被测试的值中至少有一个无效,则返回值 [false]。这将阻止表单中的 [submit] 发送至服务器;
  • 第15-20行:标识符[double1, double2, double3]代表表单中的三个实数。 根据区域设置的不同,输入的值也会不同。在 [fr-FR] 区域设置下,输入的是 [10,37];而在 [en-US] 区域设置下,输入的是 [10.37]。以上是关于数据输入的部分。 对于文化设置[fr-FR],提交给[double1]的值将显示为[double1=10,37]。 到达服务器端后,值 [10,37] 将被拒绝,因为服务器期望接收 [10.37]——这是 Java 中实数的默认格式。因此,第 15-20 行将这些数字的输入值中的逗号替换为句点;
  • 第15行:表达式 [double1.val()] 将输入的字符串转换为节点 [double1]。表达式 [double1.val().replace(",", ".")] 将该字符串中的逗号替换为句点。 结果为字符串 [value1];
  • 第 16 行:指令 [double1.val(value1)] 将该值 [value1] 赋值给节点 [double1]。

从技术上讲,如果用户输入的是 [10,37] 而不是实际的 [double1], 根据上述指令,节点 [double1] 的值变为 [10.37],而最终提交的值将是 [param1=val1&double1=10.37&param2=val2],该值将被服务器接受;

  • 第22行:返回值[true],以便表单中的[submit]得以执行;

需要注意的是,函数 jS [postForm01]:

  • 若客户端验证已启用,则执行表单中的所有 jS 验证器,并阻止表单中的 [submit] 发送至服务器(若任何输入值被判定为无效);
  • 允许 [submit] 执行,原因可能是客户端验证未启用,或者虽然启用了客户端验证,但所有输入值均有效;

剩下的是第 [3] 行的指令:


    // 清除服务器错误
clearServerErrors();

函数 [clearServerErrors] 的目的是清除视图 [vue-01.xml] 第 4 列中的消息:

在上方的屏幕截图中,我们点击了链接 [English]。我们看到这会触发对所输入值的 POST 处理,而不会触发 jS 验证器。 当 POST 返回时,[Server Validation] 列会显示可能的错误信息。 如果现在在启用了验证器的情况下点击按钮, 那么 [Client Validation] [4] 列将填入相关信息。 如果不采取任何措施,[Server Validation]列中的内容将保留下来,这会造成混淆,因为当jS验证器检测到错误时,服务器不会被调用。 为避免这种情况,我们在函数 [postForm01] 中清空了 [Server Validation] 列。这是由 [] 函数完成的:


function clearServerErrors() {
    // 清除服务器错误信息
    $(".error").each(function(index) {
        $(this).text("");
    });
}

错误消息的一个特点是,它们都属于类 [error]。例如,对于 [vue-01.html] 中数组的第一行:


<span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>

而 DOM 中只有这些节点具有该类。我们在函数 [clearServerErrors] 中使用了该属性:


function clearServerErrors() {
    // 清除服务器错误信息
    $(".error").each(function(index) {
        $(this).text("");
    });
}
  • 第 3 行:表达式 [$(".error")] 返回 DOM 中具有类 [error] 的节点集合;
  • 第 3 行:表达式 [$(".error").each(function(index){f}] 针对集合中的每个节点执行函数 [f]。该函数接收一个参数 [index](此处未使用),该参数表示节点在集合中的序号;
  • 第 4 行:表达式 [$(this)] 表示迭代中的当前节点。该节点是一个 HTML <span> 标签。表达式 [$(this).text("")] 将空字符串赋值给 <span> 标签显示的文本;

接下来我们将探讨不同的 jS 验证器。

6.3.8. [required] 验证器

让我们来分析表单中的第一个元素:

[1] 行由 [vue-01.xml] 视图的以下序列生成:


<!-- 必填 -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
    <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [strNotEmpty]:


    @NotNull
    @NotBlank
private String strNotEmpty;

[1-2] 约束要求字段 [strNotEmpty] 必须是现有字符串 [NotNull],且不能为空,也不能仅由空格组成 [NotBlank]。 我们希望使用 JavaScript 在客户端实现这一约束。

让我们分析第 5 行和第 8 行。第 11 行没有问题,它显示了与字段 [strNotEmpty] 相关的错误信息。首先来看第 5 行:


<input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />

基于此代码,Thymeleaf 将生成以下标签:


<input type="text" data-val="true" data-val-required="Field is required" id="strNotEmpty" name="strNotEmpty" value="x" />
  • [data-val='true'] 属性由 jQuery 验证库使用。该属性的存在表明该节点的值正在接受验证;
  • [data-val-X='msg'] 属性提供两项信息。[X] 是验证器的名称,[msg] 是与该验证器作用节点上的无效值相关的错误消息。 这仅作为参考信息,不会触发错误消息的显示;
  • [required] 是 Microsoft 验证库 [jquery.validate.unobstrusive] 所识别的验证器。无需对其进行定义。但后续情况并非总是如此;
  • [data-x] 标签会被 HTML5 忽略。只有当有 JavaScript 代码来处理它们时,这些标签才有用;

现在我们来看看第 8 行:


<span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>

该行用于显示验证器 [required] 的错误信息。若出现错误,验证库 jS 将动态地将数组中的 HTML 行替换为以下代码:


<tr>
    <td class="col1">required</td>
    <td class="col2">
        <input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" aria-required="true" aria-invalid="true" aria-describedby="strNotEmpty-error" class="input-validation-error">
    </td>
    <td class="col3">
        <span class="field-validation-error" data-valmsg-for="strNotEmpty" data-valmsg-replace="true">
            <span id="strNotEmpty-error" class="">Le champ est obligatoire</span>
        </span>
    </td>
    <td class="col4">
        <span class="error"></span>
    </td>
</tr>
</tr>
  • 第 4 行:节点 [strNotEmpty] 的类已更改。它变成了 [input-validation-error],这导致出错字段被标记为红色;
  • 第 7 行:[span] 的类已更改。它变成了 [field-validation-error],这将导致 [span] 的文本显示为红色;
  • 第 8 行:原先为空的 [span] 现在包含文本 [Le champ est obligatoire]。该文本源自第 4 行的 [data-val-required="Le champ est obligatoire"] 标签;
  • 第 7 行:要显示第 4 行节点 [strNotEmpty] 的错误消息,需在第 7 行使用属性 [data-valmsg-for="strNotEmpty"] 和 [data-valmsg-replace="true"];

6.3.9. 验证器 [assertfalse]

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填,assertfalse -->
<tr>
    <td class="col1">required, assertfalse</td>
    <td class="col2">
        <input type="radio" th:field="*{assertFalse}" value="true" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />
        <label th:for="${#ids.prev('assertFalse')}">true</label>
        <input type="radio" th:field="*{assertFalse}" value="false" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />
        <label th:for="${#ids.prev('assertFalse')}">false</label>
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="assertFalse" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [assertFalse]:


    @NotNull
    @AssertFalse
private Boolean assertFalse;

我们希望使用 JavaScript 在客户端重现此约束。第 12-17 行现已成为标准代码:

  • 第12-14行:当[assertFalse]字段出现错误时,显示第6行[data-val-assertfalse]属性携带的消息,或同一行[data-val-required]属性携带的消息。 需注意,这些消息已进行本地化处理,即显示为用户先前选择的语言;若用户未作选择,则显示为法语;
  • 第5-10行:显示带有JavaScript验证器的单选按钮,用户点击其中一个按钮时,验证器即被触发。

这两个按钮的构建方式相同。我们先来看第一个:


<input type="radio" th:field="*{assertFalse}" value="true" data-val="true" th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />

经 Thymeleaf 处理后,该行代码变为:


<input type="radio" value="true" data-val="true" data-val-required="Le champ est obligatoire" data-val-assertfalse="Seule la valeur False est acceptée" id="assertFalse1" name="assertFalse" />

这里有两个验证器:一个名为 [data-val="true"],包含两个子验证器 [required] 和 [data-val-required="Le champ est obligatoire"];另一个名为 [assertfalse],包含子验证器 [data-val-assertfalse="Seule la valeur False est acceptée"]。 需要提醒的是,[data-val-X] 属性的值是验证器 X 的错误信息。

我们已经介绍了验证器 [required]。 这里的新特点是,我们可以将多个验证器关联到一个输入值上。虽然验证器 [required] 已被验证库 MS(Microsoft)所识别,但验证器 [assertFalse] 则不然。 因此,我们将学习如何创建一个新的验证器。我们将创建多个验证器,并将它们放置在 [client-validation.js] 文件中:

  

该文件与其他文件一样,由视图 [vue-01.xml] 导入(见下文第 6 行):


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
        ...
        <script type="text/javascript" src="/js/client-validation.js"></script>
...
</head>

添加验证器 [assertfalse] 主要涉及创建以下两个函数 jS:


// -------------- assertfalse
$.validator.addMethod("assertfalse", function(value, element, param) {
    return value === "false";
});

$.validator.unobtrusive.adapters.add("assertfalse", [], function(options) {
    options.rules["assertfalse"] = options.params;
    options.messages["assertfalse"] = options.message.replace("''", "'");
});

说实话,我并不是JavaScript专家,这种语言对我来说依然充满神秘感。它的基础虽然简单,但基于这些基础构建的库往往非常复杂。为了编写上面的代码,我参考了网上找到的代码。 正是链接 [http://jsfiddle.net/LDDrk/] 为我指明了方向。如果该链接依然有效,建议读者浏览一下,因为它内容详尽,还附带了一个可运行的示例。该示例展示了如何创建一个新的验证器,也让我得以创建本章中的所有验证器。让我们回到代码:

  • 第2-4行:定义新的验证器。函数[$.validator.addMethod]的第一个参数是验证器的名称,第二个参数是定义该验证器的函数;
  • 第2行:该函数有三个参数:
    • [value]:待验证的值。若值有效,该函数应返回 [true];否则返回 [false],
    • [element]:待验证值所属的 HTML 元素,
    • [param]:一个包含验证器参数相关值的对象。我们尚未介绍这一概念。此处验证器 [assertFalse] 没有参数。 无需额外信息即可判断值 [value] 是否有效。但如果需要验证值 [value] 是否为区间 [min, max] 内的实数,情况就不同了。 在这种情况下,我们需要知道 [min] 和 [max]。这两个值被称为验证器的参数;
  • 第6-9行:这是MS验证库中必不可少的一个函数。函数[$.validator.unobtrusive.adapters.add]的第一个参数是验证器的名称,第二个参数是验证器的参数数组,第三个参数是一个函数;
  • 验证器 [assertFalse] 没有参数。因此第二个参数是一个空数组;
  • 该函数仅有一个参数,即 [options] 对象,其中包含待验证元素的信息,并需为此定义两个新属性 [rules] 和 [messages];
    • 第 7 行:为验证器 [assertFalse] 定义规则 [rules]。 这些规则是验证器 [assertFalse] 的参数,与第 2 行中的参数 [param] 相同。这些参数位于 [options.params] 中;
    • 第8行:定义了验证器[assertFalse]的错误消息。该消息位于[options.message]中。关于错误消息,我们遇到以下问题。在消息文件中,我们会发现以下消息:

Range.form01.int1014=La valeur doit être dans l''intervalle [10,14]

双引号对 Thymeleaf 而言是必需的。它会将其解释为单引号。如果使用单引号,Thymeleaf 则不会显示该内容。现在,这些消息也将作为验证库 MS 的错误消息使用。然而,JavaScript 会将其显示为两个引号。 因此,在第 8 行,我们将错误消息中的双引号替换为单引号。

为了观察具体情况,我们可以添加 jS 日志代码:


// 日志
var logs = {
    assertfalse : true
}


// -------------- assertfalse
$.validator.addMethod("assertfalse", function(value, element, param) {
    // 日志
    if (logs.assertfalse) {
        console.log(jSON.stringify({
            "[assertfalse] value" : value
        }));
        console.log("[assertfalse] element");
        console.log(element);
        console.log(jSON.stringify({
            "[assertfalse] param" : param
        }));
    }
    // 有效性测试
    return value === "false";
});

$.validator.unobtrusive.adapters.add("assertfalse", [], function(options) {
    // 日志
    if (logs.assertfalse) {
        console.log(jSON.stringify({
            "[assertfalse] options.params" : options.params
        }));
        console.log(jSON.stringify({
            "[assertfalse] options.message" : options.message
        }));
        console.log(jSON.stringify({
            "[assertfalse] options.messages" : options.messages
        }));
    }
    // 代码
    options.rules["assertfalse"] = options.params;
    options.messages["assertfalse"] = options.message.replace("''", "'");
});

该代码使用了 jSON、JSON3 和 [http://bestiejs.github.io/json3/] 库。如果启用日志(第 3 行),控制台将显示以下内容:

页面初次加载时,会生成以下日志:

 

已执行函数 jS [$.validator.unobtrusive.adapters.add]。可知以下信息:

  • [options.params] 是一个空对象,因为验证器 [assertFalse] 没有参数;
  • [options.message] 是我们在 [data-val-assertFalse] 属性中为验证器 [assertFalse] 构建的错误消息;
  • [options.messages] 是一个包含被验证元素其他错误消息的对象。这里我们可以看到我们放入 [data-val-required] 属性中的错误消息;

现在为字段 [assertFalse] 输入一个错误值并进行验证:

 

随后将获得以下日志:

其中显示了以下内容:

  • 被测试的值是 [true](第 118 行);
  • 被测试的元素 HTML 是 ID 为 [assertFalse1] 的单选按钮(第 122 行);
  • 验证器 [assertFalse] 没有参数(第 123 行);

就是这样。从中我们能得到什么启示?

对于验证器 jS,我们需要定义:

  • 在待验证的 HTML 标签中,定义 [data-val-X='msg'] 属性,该属性同时定义了 X 验证器及其错误信息;
  • 两个需放入文件 [client-validation.js] 中的函数 jS:
    • [$.validator.addMethod("X", function(value, element, param)],
    • [$.validator.unobtrusive.adapters.add("X", [param1, param2], function(options)] ;

接下来,我们将基于该首个验证器的实现,仅介绍新增内容。

6.3.10. [asserttrue] 验证器

该验证器显然与 [assertFalse] 验证器类似。

[1] 行由 [vue-01.xml] 视图中的以下序列生成:


<!-- 必填,asserttrue -->
<tr>
    <td class="col1">asserttrue</td>
    <td class="col2">
        <select th:field="*{assertTrue}" data-val="true" th:attr="data-val-asserttrue=#{AssertTrue}">
            <option value="true">True</option>
            <option value="false">False</option>
        </select>
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="assertTrue" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
    </td>
</tr>

以下行涉及表单 [Form01] 中的字段 [assertTrue]:


    @NotNull
    @AssertTrue
private Boolean assertTrue;

第 1-16 行没有新内容。它们使用了一个名为 [asserrtrue] 的验证器,该验证器需在文件 [client-validation.js] 中定义:


// -------------- asserttrue
$.validator.addMethod("asserttrue", function(value, element, param) {
    return value === "true";
});

$.validator.unobtrusive.adapters.add("asserttrue", [], function(options) {
    options.rules["asserttrue"] = options.params;
    options.messages["asserttrue"] = options.message.replace("''", "'");
});

6.3.11. 验证器 [date] 和 [past]

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填, 日期, 过去 -->
<tr>
    <td class="col1">required, date, past</td>
    <td class="col2">
        <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-past=#{Past.form01.dateInPast}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="dateInPast" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [dateInPast]:


    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;

日期验证器的代码行如下:


<input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-past=#{Past.form01.dateInPast}" />

其中包含三个验证器 [data-val-X]:required、date、past。我们需要在 [client-validation.js] 中定义与这两个新验证器相关的函数:


logs.date = true;
// -------------- 日期
$.validator.addMethod("date", function(value, element, param) {
    // 有效期
    var valide = Globalize.parseDate(value, "yyyy-MM-dd") != null;
    // 日志
    if (logs.date) {
        console.log(jSON.stringify({
            "[date] value" : value,
            "[date] valide" : valide
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("date", [], function(options) {
    options.rules["date"] = options.params;
    options.messages["date"] = options.message.replace("''", "'");
});


logs.past = true;
// -------------- 过去
$.validator.addMethod("past", function(value, element, param) {
    // 有效期
    var valide = value <= new Date().toISOString().substring(0, 10);
    // 日志
    if (logs.past) {
        console.log(jSON.stringify({
            "[past] value" : value,
            "[past] valide" : valide
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("past", [], function(options) {
    options.rules["past"] = options.params;
    options.messages["past"] = options.message.replace("''", "'");
});

在解释代码之前,让我们先看看输入比今天日期更晚的日期时生成的日志:

 

首先需要注意的是,待验证的日期以字符串形式传入,格式为 [aaaa-mm-jj]。这解释了后续的几行:


var valide = Globalize.parseDate(value, "yyyy-MM-dd") != null;

库 [globalize.js] 调用了上述函数 [Globalize.parseDate]。第一个参数是作为字符串的日期,第二个参数是其格式。如果日期无效,返回指针 null;否则返回结果日期。

[past] 验证器的有效性通过以下代码进行验证:


var valide = value <= new Date().toISOString().substring(0, 10);

以下是在控制台对表达式 [new Date().toISOString().substring(0, 10)] 的评估结果:

  

字符串 [value] 必须在字母顺序上位于字符串 [new Date().toISOString().substring(0, 10)] 之前,才算有效。

需要注意的是,所使用的 Chrome 版本会以 [yyyy-mm-dd] 的格式显示日期。对于不支持此格式的浏览器,应明确告知用户使用该输入格式。

6.3.12. [future] 验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填, 日期, 未来 -->
<tr>
    <td class="col1">required, date, future</td>
    <td class="col2">
        <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-future=#{Future.form01.dateInFuture}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="dateInFuture" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [dateInFuture]:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
  • 第 5 行,出现了一个新的验证器 [data-val-future];

该验证器当然与验证器 [past] 非常相似。需要在 [client-validation.js] 中添加的两个函数如下:


// -------------- 未来
$.validator.addMethod("future", function(value, element, param) {
    var now = new Date().toISOString().substring(0, 10);
    return value > now;
});

$.validator.unobtrusive.adapters.add("future", [], function(options) {
    options.rules["future"] = options.params;
    options.messages["future"] = options.message.replace("''", "'");
});

6.3.13. 验证器 [int] 和 [max]

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填, 整数, max(100) -->
<tr>
    <td class="col1">required, int, max(100)</td>
    <td class="col2">
        <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch},data-val-max=#{Max.form01.intMax100},data-val-max-value=#{form01.intMax100.value}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMax100" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [intMax100]:


    @NotNull
    @Max(value = 100)
    private Integer intMax100;

第 5 行中,有两个新的验证器:[int] 和 [max]。后者有一个参数:最大值。让我们查看第 5 行生成的代码 HTML:


<!-- 必填, 整数, 最大值(100) -->
<tr>
    <td class="col1">required, int, max(100)</td>
    <td class="col2">
        <input type="text" data-val="true" data-val-int="Format invalide" data-val-max-value="100" data-val-required="Le champ est obligatoire" data-val-max="La valeur doit être inférieure ou égale à 100" value="" id="intMax100" name="intMax100" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMax100" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">

    </td>
</tr>

回顾一下 [data-X] 各属性的含义:

  • [data-val="true"] 表示有验证器与元素 HTML 相关联;
  • [data-val-required] 引入了验证器 [required] 及其消息;
  • [data-val-int] 引入了验证器 [int] 及其消息;
  • [data-val-max] 引入了验证器 [max] 及其消息;
  • [data-val-max-value="100"] 为验证器 [max] 引入了一个名为 [value] 的参数。[100] 是该参数的值。 这是我们首次接触验证器的参数概念。

文件 [client-validation.js] 已补充了以下验证器 [int]:


logs.int = true;
// -------------- 整数
$.validator.addMethod("int", function(value, element, param) {
    // 有效期
    valide = /^\s*[-\+]?\s*\d+\s*$/.test(value);
    // 日志
    if (logs.int) {
        console.log(jSON.stringify({
            "[int] value" : value,
            "[int] valide" : valide,
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("int", [], function(options) {
    options.rules["int"] = options.params;
    options.messages["int"] = options.message.replace("''", "'");
});
  • 第 5 行:使用正则表达式验证字符串 [value] 是否确实表示一个整数。该整数可以是有符号的;

以下是部分日志示例:

1
2
3
{"[int] value":"x","[int] valide":false}
{"[int] value":"11","[int] valide":true}
{"[int] value":"11x","[int] valide":false}

验证器 [max] 按以下方式添加到 [client-validation.js] 中


// -------------- max 需与 [int] 或 [number] 配合使用
logs.max = true;
$.validator.addMethod("max", function(value, element, param) {
    // 日志
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] value" : value,
            "[max] param" : param
        }));
    }
    // 有效期
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // 日志
        if (logs.max) {
            console.log(jSON.stringify({
                "[max] valide" : true
            }));
        }
        // 结果
        return true;
    }
    var max = Globalize.parseFloat(param.value);
    var valide = val <= max;
    // 日志
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] valide" : valide
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("max", [ "value" ], function(options) {
    options.rules["max"] = options.params;
    options.messages["max"] = options.message.replace("''", "'");
});

接下来我们将处理由属性 [data-val-max-value="100"] 引入的验证器 [max] 的参数 [value]。

  • 第 35 行,参数 [value] 被纳入函数 [$.validator.unobtrusive.adapters.add] 的第二个参数中;
  • 第 3 行,对象 [param] 将不再为空,而是包含 {"value":100};

要理解第3至33行的代码,需知当同一元素HTML上存在多个验证器时:

  • 无法确定验证器的执行顺序;
  • 一旦某个验证器判定该元素无效,验证器的执行即刻停止。此时,该验证器的错误消息将被关联到该无效元素上;

让我们分析一下代码:

  • 第 12 行:检查是否为数字。如果验证器 [int] 在验证器 [max] 之前执行,则该条件必然为真,因为无效值会终止验证器的执行;
  • 第13-22行:如果不是数字,则说明验证器[int]尚未执行。 此时将测试值标记为有效,以便让验证器 [int] 执行其任务,并使用其自身的错误信息将该元素标记为无效;
  • 第23-24行:计算[value]的有效性;

以下是部分日志:

输入值
日志
x

{"[max] value":"x","[max] param":{"value":"100"}}
{"[max] valide":true}
{"[int] value":"x","[int] valide":false}
111

{"[max] value":"111","[max] param":{"value":"100"}}
{"[max] valide":false}
111x

{"[max] value":"111x","[max] param":{"value":"100"}}
{"[max] valide":true}
{"[int] value":"111x","[int] valide":false}

6.3.14. [min]验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填,整数,最小值(10) -->
<tr>
    <td class="col1">required, int, min(10)</td>
    <td class="col2">
        <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch},data-val-min=#{Min.form01.intMin10},data-val-min-value=#{form01.intMin10.value}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMin10" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [intMin10]:


    @NotNull
    @Min(value = 10)
private Integer intMin10;

第 5 行引入了一个新的验证器 [min] [data-val-int=#{typeMismatch}],其参数为 [value] [data-val-min-value=#{form01.intMin10.value}"]。 这与验证器 [max] 的情况类似。在 [client-validation.js] 中添加以下代码:


logs.min = true;
//-------------- min 应与 [int] 或 [number] 结合使用
$.validator.addMethod("min", function(value, element, param) {
    // 日志
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] value" : value,
            "[min] param" : param
        }));
    }
    // 有效期
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // 日志
        if (logs.min) {
            console.log(jSON.stringify({
                "[min] valide" : true
            }));
        }
        // 结果
        return true;
    }
    var min = Globalize.parseFloat(param.value);
    var valide = val >= min;
    // 日志
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] valide" : valide
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("min", [ "value" ], function(options) {
    options.rules["min"] = options.params;
    options.messages["min"] = options.message.replace("''", "'");
});

以下是部分执行日志:

输入值
日志
x

{"[min] value":"x","[min] param":{"value":"10"}}
{"[min] valide":true}
{"[int] value":"x","[int] valide":false}
11

{"[min] value":"11","[min] param":{"value":"10"}}
{"[min] valide":true}
{"[int] value":"11","[int] valide":true}
8x

{"[min] value":"8x","[min] param":{"value":"10"}}
{"[min] valide":true}
{"[int] value":"8x","[int] valide":false}

6.3.15. [regex]验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填, 正则表达式 -->
<tr>
    <td class="col1">required, regex</td>
    <td class="col2">
        <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" data-val="true"    th:attr="data-val-required=#{NotNull},data-val-regex=#{Size.form01.strBetween4and6}, data-val-regex-pattern=#{form01.strBetween4and6.pattern}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="strBetween4and6" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [strBetween4and6]:


    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;

第 5 行生成以下 HTML:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" data-val-regex="La chaîne doit avoir entre 4 et 6 caractères" data-val-regex-pattern="^.{4,6}$" value="" id="strBetween4and6" name="strBetween4and6" />

该标签引入了验证器 [regex] [data-val-regex="La chaîne doit avoir entre 4 et 6 caractères"] 及其参数 [pattern] [data-val-regex-pattern="^.{4,6}$"]。 参数 [pattern] 是待验证值必须满足的正则表达式。 此处的正则表达式用于验证字符串长度在 4 到 6 个任意字符之间。验证器 [regex] 已在验证库 MS 中预定义。因此,无需在文件 [client-validation.js] 中添加任何内容。

6.3.16. 验证器 [email]

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填, 电子邮件 -->
<tr>
    <td class="col1">required, email</td>
    <td class="col2">
        <input type="text" th:field="*{email}" th:value="*{email}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-email=#{Email.form01.email}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="email" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [email]:


    @NotNull
    @Email
    @NotBlank
    private String email;

第 5 行生成以下 HTML 行:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" data-val-email="Adresse mail invalide"    value="" id="email" name="email" />

此标签引入了验证器 [email] [data-val-email="Adresse mail invalide"]。 验证器 [email] 已在验证库 MS 中预定义。因此,[client-validation.js] 文件中无需添加任何内容。

6.3.17. 验证器 [range]

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填,整数,范围 (10,14) -->
<tr>
    <td class="col1">required, int, range (10,14)</td>
    <td class="col2">
        <input type="text" th:field="*{int1014}" th:value="*{int1014}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch}, data-val-range=#{Range.form01.int1014},data-val-range-max=#{form01.int1014.max},data-val-range-min=#{form01.int1014.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="int1014" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [int1014]:


    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;

第 5 行生成以下 HTML 行:


<input type="text" data-val="true" data-val-range-max="14" data-val-range="La valeur doit être dans l&#39;&#39;intervalle [10,14]" data-val-int="Format invalide" data-val-required="Le champ est obligatoire" data-val-range-min="10" value="" id="int1014" name="int1014" />

该标签引入了一个新的验证器 [range] [data-val-range="La valeur doit être dans l&#39;&#39;intervalle [10,14]"],该验证器有两个参数 [range] [range] 和 [client-validation.js] [max]。

在文件 [client-validation.js] 中,我们按以下方式定义验证器 [range]:


// -------------- 该范围应与 [int] 或 [number] 结合使用
logs.range=true
$.validator.addMethod("range", function(value, element, param) {
    // 日志
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] value" : value,
            "[range] param" : param
        }));
    }
    // 有效期
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // 日志
        if (logs.min) {
            console.log(jSON.stringify({
                "[range] valide" : true
            }));
        }
        // 已完成
        return true;
    }
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);    
    var valide = val >= min && val <= max;
    // 日志
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] valide" : valide
        }));
    }
    // 已完成
    return valide;
});

$.validator.unobtrusive.adapters.add("range", [ "min", "max" ], function(options) {
    options.rules["range"] = options.params;
    options.messages["range"] = options.message.replace("''", "'");
});

它与之前研究过的验证器 [min] 和 [max] 非常相似。

以下是部分日志示例:

输入值
日志
x

{"[range] value":"x","[range] param":{"min":"10","max":"14"}}
{"[int] value":"x","[int] valide":false}
8

{"[range] value":"8","[range] param":{"min":"10","max":"14"}}
{"[range] valide":false}
11

{"[range] valide":true}
{"[int] value":"11","[int] valide":true}

6.3.18. [number]验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- double1:必填,数字,范围 (2.3,3.4) -->
<tr>
    <td class="col1">double1 : required, number, range (2.3,3.4)</td>
    <td class="col2">
        <input type="text" th:field="*{double1}" th:value="*{double1}" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-range=#{Range.form01.double1},data-val-range-max=#{form01.double1.max},data-val-range-min=#{form01.double1.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="double1" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('double1')}" th:errors="*{double1}" class="error">Donnée erronée</span>
    </td>
</tr>

以下内容涉及表单 [Form01] 中的字段 [double1]:


    @NotNull
    @DecimalMax(value = "3.4")
    @DecimalMin(value = "2.3")
private Double double1;

第 5 行生成以下 HTML 行:


<input type="text" data-val="true" data-val-number="Format invalide" data-val-range-max="3.4"     data-val-range="La valeur doit être dans l&#39;intervalle [2,3-3,4]" data-val-required="Le champ est obligatoire" data-val-range-min="2.3" value="" id="double1" name="double1" />

该标签引入了一个新的验证器 [number],其属性为 [data-val-number="Format invalide"]。该验证器在文件 [client-validation.js] 中定义如下:


// -------------- 数字
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    var valide = !isNaN(Globalize.parseFloat(value));
    // 日志
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] valide" : valide
        }));
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("number", [], function(options) {
    options.rules["number"] = options.params;
    options.messages["number"] = options.message.replace("''", "'");
});

以下是一些日志示例:

输入值
日志
x
 {"[number] value":"x","[number] valide":false}
-2,5
{"[number] value":"-2,5","[number] valide":true}
{"[range] value":"-2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":false}
2,5
{"[number] value":"+2,5","[number] valide":true}
{"[range] value":"+2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}
+2.5
{"[number] value":"+2.5","[number] valide":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}

众所周知,实数具有文化敏感性。 上文中的数字属于 [fr-FR] 文化。当输入 [2.5](英式记法)时,该数字会被接受。这是因为 [Globalize.parseFloat] 同时接受这两种记法:

Globalize.parseFloat("3.3")
3.3
Globalize.parseFloat("3,3")
3.3

切换到英语界面,分别输入 [+2,5] 和 [+2.5]。日志记录如下:

输入值
日志
x
 {"[number] value":"x","[number] valide":false}
2,5
{"[number] value":"+2,5","[number] valide":true}
{"[range] value":"+2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":false}
+2.5
{"[number] value":"+2.5","[number] valide":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}

[2,5] 存在问题。它被声明为有效的实数,但应写为 [2.5]。这是 [Globalize.parseFloat] 造成的:

Globalize.parseFloat("2,5")
25

在上文中,[Globalize.parseFloat] 忽略了逗号,并将该数值视为 25。在 [en-US] 的语境中,一个实数可以包含小数点和逗号,这些逗号有时用于分隔千位。

可以通过以下方式改进:


// -------------- 数字
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    // 仅管理 [fr-FR] 和 [en-US] 这两种作物
    var pattern_fr_FR = /^\s*[-+]?[0-9]*\,?[0-9]+\s*$/;
    var pattern_en_US = /^\s*[-+]?[0-9]*\.?[0-9]+\s*$/;
    var culture = Globalize.culture().name;
    // 有效性测试
    var valide;
    if (culture === "fr-FR") {
        valide = pattern_fr_FR.test(value);
    } else if (culture === "en-US") {
        valide = pattern_en_US.test(value);
    } else {
        valide = !isNaN(Globalize.parseFloat(value));
    }
    // 日志
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] culture" : culture,
            "[number] valide" : valide
        }));
    }
    // 结果
    return valide;
});
  • 第 5 行:[fr-FR] 文化中实数的正则表达式;
  • 第 6 行:针对 [en-US] 区域设置的实数正则表达式;
  • 第 7 行:当前语言环境名称。在本例中,即为上述两种语言环境之一;
  • 第 9-16 行:对输入值的有效性验证;
  • 第15行:已预设文化既非[fr-FR]也非[en-US]的情况;

日志现在显示如下内容:

语言环境 [fr-FR]

输入值
日志
x

 {"[number] value":"x","[number] culture":"fr-FR","[number] valide":false}
-2,5

{"[number] value":"-2,5","[number] culture":"fr-FR","[number] valide":true}
{"[range] value":"-2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":false}
2,5

{"[number] value":"+2,5","[number] culture":"fr-FR","[number] valide":true}
{"[range] value":"+2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}
+2.5

{"[number] value":"+2.5","[number] culture":"fr-FR","[number] valide":false}

文化 [en-US]

输入值
日志
x

{"[number] value":"x","[number] culture":"en-US","[number] valide":false}
2,5

{"[number] value":"+2,5","[number] culture":"en-US","[number] valide":false}
+2.5

{"[number] value":"+2.5","[number] culture":"en-US","[number] valide":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}

6.3.19. [custom3]验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- double3:必填,数字,custom3 -->
<tr>
    <td class="col1">double3 : required, number, custom3</td>
    <td class="col2">
        <input type="text" th:field="*{double3}" th:value="*{double3}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-custom3=${custom3.message},data-val-custom3-field=${custom3.otherFieldName},data-val-custom3-max=${custom3.max},data-val-custom3-min=${custom3.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="double3" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('double3')}" th:errors="*{double3}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [double3]:


    @NotNull
    private Double double3;

这里我们要研究一个验证器,它不再验证输入的单个值,而是验证两个输入值之间的关系。在此,我们要求 [double1+double3] 必须位于 [10,13] 的区间内。

第 5 行生成以下 HTML 行:


<input type="text" data-val="true" data-val-custom3-min="10.0" data-val-number="Invalid format"
    data-val-custom3="[double3+double1] must be in [10,13]" data-val-custom3-max="13.0" data-val-custom3-field="double1"    data-val-required="Field is required" value="" id="double3" name="double3" />

该行引入了由属性 [data-val-custom3="[double3+double1] must be in [10,13]"] 声明的新验证器 [custom3]。该验证器具有以下参数:

  • [field],由属性 [data-val-custom3-field="double1"] 声明。该参数指定了其值参与 [double3] 有效性计算的字段;
  • [min] 由属性 [data-val-custom3-min="10.0"] 声明。该参数是 [min, max] 区间中的最小值,[double1+double3] 必须位于该区间内;
  • [max] 由属性 [data-val-custom3-max="13.0"] 声明。该参数是 [min, max] 区间中的最大值,[double1+double3] 必须位于该区间内;

在 [client-validation.js] 中,该验证器的管理方式如下:


// -------------- custom3 与 [number] 结合使用
logs.custom3 = true;
$.validator.addMethod("custom3", function(value1, element, param) {
    // 第二个值
    var value2 = $("#" + param.field).val();
    // 日志
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] value1" : value1,
            "[custom3] param" : param,
            "[custom3] value2" : value2            
        }))
    }
    // 第一个值
    var valeur1 = Globalize.parseFloat(value1);
    if (isNaN(valeur1)) {
        // 让验证器 [number] 来处理
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : true
            }))
        }
        return true;
    }
    // 第二个值
    var valeur2 = Globalize.parseFloat(value2);
    if (isNaN(valeur2)) {
        // 无法进行有效性计算
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : false
            }))
        }
        return false;
    }
    // 有效性计算
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);
    var somme = valeur1 + valeur2;
    var valide = somme >= min && somme <= max;
    // 日志
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] valide" : valide
        }))
    }
    // 结果
    return valide;
});

$.validator.unobtrusive.adapters.add("custom3", [ "field", "max", "min" ], function(options) {
    options.rules["custom3"] = options.params;
    options.messages["custom3"] = options.message.replace("''", "'");
});

以下是一些日志示例:

输入值
[double1,double3]
日志
[x,1]

{"[custom3] value1":"1","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"x"}
{"[custom3] valide":false}
[1,x]

{"[number] value":"x","[number] culture":"en-US","[number] valide":false}
[1,20]

{"[custom3] value1":"20","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"1"}
{"[custom3] valide":false}
[1,10]

{"[number] value":"10","[number] culture":"en-US","[number] valide":true}
{"[custom3] value1":"10","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"1"}
{"[custom3] valide":true}

6.3.20. [url]验证器

行 [1] 由视图 [vue-01.xml] 的以下序列生成:


<!-- 必填,网址 -->
<tr>
    <td class="col1">required, url</td>
    <td class="col2">
        <input type="text" th:field="*{url}" th:value="*{url}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-url=#{URL.form01.url}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="url" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
    </td>
</tr>

这些行涉及表单 [Form01] 中的字段 [url]:


    @URL
    @NotBlank
    private String url;

第 5 行生成以下 HTML 行:


<input type="text" data-val="true" data-val-url="Invalid URL" data-val-required="Field is required" value="" id="url"    name="url" />

它引入了具有属性 [data-val-url] 的验证器 [url]。该验证器在验证库 jQuery 中已预定义。[client-validation.js] 中无需添加任何内容。

6.3.21. 启用/禁用客户端验证

只要客户端验证处于启用状态,就永远不会看到服务器端验证,因为只有当值在客户端被声明为有效时,这些值才会发送到服务器。要查看服务器端验证的运行情况,必须禁用客户端验证。视图 [vue-01.xml] 提供了两个链接来管理此启用/禁用操作:


<a id="clientValidationTrue" href="javascript:setClientValidation(true)">
    <span style="margin-left:30px" th:text="#{client.validation.true}"></span>
</a>
<a id="clientValidationFalse" href="javascript:setClientValidation(false)">
    <span style="margin-left:30px" th:text="#{client.validation.false}"></span>
</a>

这两个链接不会同时显示:

这些链接的翻译(HTML)如下:


<a id="clientValidationTrue" href="javascript:setClientValidation(true)">
    <span style="margin-left:30px">Activer la validation client</span>
</a>
<a id="clientValidationFalse" href="javascript:setClientValidation(false)">
    <span style="margin-left:30px">Inhiber la validation client</span>
</a>

脚本 jS [setClientValidation] 定义在文件 [local.js] 中(参见上文)。在该文件的函数 [$(document).ready] 中,使用了验证链接:


// 文档就绪
$(document).ready(function() {
    // 全局引用
...
    activateValidationTrue = $("#clientValidationTrue");
    activateValidationFalse = $("#clientValidationFalse");
    clientValidation = $("#clientValidation");
...
    // 验证链接
    // clientValidation 是由服务器设置的隐藏字段
    var validate = clientValidation.val();
    setClientValidation2(validate === "true");
});
  • 第 5 行:引用了客户端验证激活链接;
  • 第 6 行:引用客户端验证的停用链接;
  • 第 7 行:引用表单中一个隐藏字段,该字段以布尔值形式存储激活状态的最新状态 [true : validation client activée, false : validation client désactivée]。该字段位于视图 [vue-01.xml] 中,形式如下:

<input type="hidden" th:field="*{clientValidation}" th:value="*{clientValidation}" value="true" />

,对应于表单 [Form01] 中的字段 [clientValidation]:


// 客户端验证
private boolean clientValidation = true;
  • 第 11 行:获取隐藏字段的值;
  • 第 12 行:调用以下函数 [setClientValidation2]:

function setClientValidation2(activate) {
    // 链接
    if (activate) {
        // 客户端验证已启用
        activateValidationTrue.hide();
        activateValidationFalse.show();
        // 正在解析表单验证器
        $.validator.unobtrusive.parse(formulaire);
    } else {
        // 客户端验证已禁用
        activateValidationFalse.hide();
        activateValidationTrue.show();
        // 正在禁用表单验证器
        formulaire.data('validator', null);
    }
}
  • 第 1 行:参数 [activate] 的值为 [true](若需启用客户端验证),否则为 false;
  • 第5-6行:显示禁用链接,隐藏启用链接;
  • 第 8 行:为使客户端验证正常工作,需解析文档以查找 [data-val-X] 验证器。函数 [$.validator.unobtrusive.parse] 的参数是待解析表单的标识符 jS;
  • 第11-12行:显示激活链接,隐藏停用链接;
  • 第14行:表单中的验证器被禁用。从现在起,表单中就仿佛不存在jS验证器;

这个 [setClientValidation2] 函数有什么用?它用于管理 POST。由于 [clientValidation] 字段是一个隐藏字段,它会被提交,并随服务器返回的表单一起返回。 随后利用该值将客户端验证恢复至 POST 执行前的状态。实际上,不同请求之间并不存在 jS 的状态记忆。 因此,服务器必须在新视图中传递能够初始化该视图的 jS 的信息。这通常在 [$(document).ready] 函数中完成。

让我们回到处理客户端验证启用/禁用链接点击事件的 [setClientValidation] 函数:


// 客户端验证
function setClientValidation(activate) {
    // 管理客户端验证的启用/禁用
    setClientValidation2(activate);
    // 将用户的选项存储在隐藏字段中
    clientValidation.val(activate ? "true" : "false");
    // 进行额外调整
    if (activate) {
        // 客户端验证已启用
        // 清除服务器上的所有错误消息
        clearServerErrors();
        // 表单提交成功
        formulaire.validate().form();
    } else {
        // 客户端验证已禁用
        // 清除所有客户端错误信息
        clearClientErrors();
    }
}
  • 第 4 行:调用我们刚刚看到的 [setClientValidation2] 函数;
  • 第 6 行:将用户的选项存储在隐藏字段中,以便在下次调用 POST 时检索;
  • 第 11 行:如果客户端验证处于活动状态,则清除视图中 [serveur] 列的错误消息。我们在第 6.3.7 节中描述了 [clearServerErrors] 函数;
  • 第 13 行:执行 jS 验证器,以在视图的 [client] 列中显示可能的错误消息;
  • 第 17 行:如果客户端验证被禁用,则清除视图中 [client] 列的错误消息。让我们在 Chrome 开发者控制台中查看一个出错元素的 HTML 代码:

<td class="col2">
    <input type="text" data-val="true" data-val-int="Format invalide" data-val-max-value="100" data-val-required="Le champ est obligatoire" data-val-max="La valeur doit être inférieure ou égale à 100" value="" id="intMax100" name="intMax100" aria-required="true"        class="input-validation-error" aria-describedby="intMax100-error">
</td>
<td class="col3">
    <span class="field-validation-error" data-valmsg-for="intMax100" data-valmsg-replace="true">
        <span id="intMax100-error" class="">Le champ est obligatoire</span>
    </span>
</td>
  • 第 2 行,可以看到表格的第 2 列中,该错误元素的样式为 [class="input-validation-error"];
  • 第5行,可以看到表格的第3列中,错误消息的样式为[class="field-validation-error"];

所有出错的元素均是如此。我们在以下函数中使用这两项信息:


// 清除客户端错误
function clearClientErrors() {
    // 清除客户端错误消息
    $(".field-validation-error").each(function(index) {
        $(this).text("");
    });
    // 将错误输入的类更改为 CSS
    $(".input-validation-error").each(function(index) {
        $(this).removeClass("input-validation-error");
    });
}
  • 第4-6行:查找所有具有类[field-validation-error]的DOM元素,并清除其显示的文本。这样就清除了错误信息;
  • 第8-10行:查找所有具有类[input-validation-error]的DOM元素,并移除该类。这样,原本被标记为红色的错误元素便恢复了其原始样式;