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自动配置的一种方式。在控制台上,将显示类似于以下的列表:
我们已将 [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/] 处:

将下载的文件放置在 [static / js] 文件夹中:
![]() |
完成上述操作后,使用Chrome浏览器访问静态视图[jQuery-01.html]:
![]() |
在 Google Chrome 中,执行 [Ctrl-Maj-I] 以调出开发者工具 [3]。通过 [Console] 和 [4] 标签页可以运行 JavaScript 代码。 下面我们将提供一些需要输入的 JavaScript 命令,并附上说明。
JS | 结果 |
|
:返回所有 id 为 [element1] 的元素集合,通常该集合包含 0 或 1 个元素,因为 HTML 页面中不能存在两个相同的 id。 | ![]() |
|
:将文本 [blabla] 应用于集合中的所有元素。这将导致页面显示的内容发生变化 | ![]() |
|
隐藏集合中的元素。文本 [blabla] 不再显示。 | ![]() |
|
:重新显示集合。这让我们看到,ID 为 [element1] 的元素具有属性 CSS style='display: none;',这导致该元素被隐藏。 | |
|
:显示集合中的元素。文本 [blabla] 再次出现。这是由属性 CSS style='display : block;' 确保的显示效果。 | ![]() |
|
: 为集合中的所有元素设置一个属性。此处的属性为 [style],其值为 [color: red]。文本 [blabla] 将变为红色。 | ![]() |
![]() | |
![]() |
需要注意的是,在整个操作过程中,浏览器的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] 创建一个控制台项目:
|
![]() |
- 在 [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¶m2=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] 是否确实表示一个整数。该整数可以是有符号的;
以下是部分日志示例:
验证器 [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]的有效性;
以下是部分日志:
输入值 | 日志 |
| |
| |
|
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("''", "'");
});
以下是部分执行日志:
输入值 | 日志 |
| |
| |
|
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''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''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] 非常相似。
以下是部分日志示例:
输入值 | 日志 |
| |
| |
|
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'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("''", "'");
});
以下是一些日志示例:
输入值 | 日志 |
众所周知,实数具有文化敏感性。 上文中的数字属于 [fr-FR] 文化。当输入 [2.5](英式记法)时,该数字会被接受。这是因为 [Globalize.parseFloat] 同时接受这两种记法:
切换到英语界面,分别输入 [+2,5] 和 [+2.5]。日志记录如下:
输入值 | 日志 |
[2,5] 存在问题。它被声明为有效的实数,但应写为 [2.5]。这是 [Globalize.parseFloat] 造成的:
在上文中,[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]
输入值 | 日志 |
| |
| |
| |
|
文化 [en-US]
输入值 | 日志 |
| |
| |
|
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] | 日志 |
| |
| |
| |
|
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元素,并移除该类。这样,原本被标记为红色的错误元素便恢复了其原始样式;


















































