Vaadin 实践教程(一)
一、Vaadin 的世界
本章是对 web 开发和 Vaadin 相关技术的一般性介绍。我保证,这是你会看到 HTML 和 JavaScript 代码(甚至是 Python 解释器)的少数章节之一。
一行代码中的垃圾
当我开始我的 web 开发生涯时,我加入了南美最大的大学之一的招生部门的一个开发人员小组。部门操作由一个用 Java 编写的 web 应用提供支持,该应用包含 Struts 2(一个 Java web 框架)、Hibernate(一个数据库持久性框架)、Spring Framework(企业配置框架,或者如我们过去所描述的,应用的粘合剂)和许多其他库。
应用中的许多 web 页面共享一个通用的 UI(用户界面)模式:它们都有一个搜索数据的表单、一个显示数据的表格和操作数据的选项。换句话说,应用有大量的创建、读取、更新和删除( CRUD )视图。应用的代码包含了实现这些视图的助手。然而,这涉及到复制代码——这是我不太喜欢的。
当我发现 Google Web Toolkit (GWT)时,我开始研究 Java web 框架,希望找到帮助我解决这个问题的思路。GWT 包括一个 Java 到 JavaScript 的编译器,它允许开发者用 Java 而不是 JavaScript 来实现 web 用户界面。我喜欢这种创新的方法,随着我对它了解的越来越多,我发现有一个更成熟的 web 框架使用了同样的理念,只是它没有将 Java 代码编译成 JavaScript。它的名字,Vaadin。
在玩了几天 Vaadin 之后,我相对快速地实现了一个可重用的库,用一行 Java 动态地创建 CRUD 视图。这里有一个例子:
GridCrud<User> crud = new GridCrud<>(User.class);
图 1-1 显示了可以用这个库创建的 CRUD 视图的类型。
图 1-1
用 Vaadin 实现的 CRUD 视图
Vaadin 允许我使用运行在服务器端的 Java 编写 web 用户界面,这是我决定在未来的许多项目中采用它的主要原因。能够在应用的所有层中使用相同的编程语言消除了上下文转换的相关工作。类似地,当开发人员加入一个项目时,他们必须经历的学习曲线几乎是平坦的——如果他们了解 jva,他们几乎立即就可以使用 Vaadin 进行生产。
当你阅读这本书时,同样的事情也会发生在你身上——当你学习 Vaadin 时,你将很快能够为你的 Java 项目实现 web UIs。到本书结束时,您将拥有实现和维护 Vaadin 应用的技能,并且,为什么不像我对 CRUD 库所做的那样,创建和发布您自己的可重用库。
Note
如果你很好奇,CRUD 库是开源的,可以在 https://vaadin.com/directory/component/crud-ui-add-on 免费获得。
网络平台
有时候,用 Vaadin 开发感觉像是魔术。你输入一段 Java 代码,它应该在浏览器上显示一个按钮,这个按钮确实神奇地出现在那里。我想告诉你在软件开发中没有魔法这种东西。如果你理解了潜在的机制,你会发现没有什么窍门,你会处于一个更好的位置来掌握任何技术。
除了 Java 编程语言,基础技术是那些在网络平台中的技术。Web 平台是一组主要由万维网联盟开发并由 Web 浏览器实现的编程语言和 API 标准。这包括 HTTP、HTML、ECMAScript(管理 JavaScript 的标准)、DOM Events、XMLHttpRequest、CSS、Web 组件、Web Workers、WebSocket、WebAssembly、地理位置 API、Web 存储以及其他一些组件。
掌握所有这些技术可能令人望而生畏,但事实是,在日常 web 开发中,您将主要直接使用其中的三种:HTML、JavaScript 和 CSS。Vaadin 抽象掉了 HTML 和 JavaScript(以及相关的 API),所以大多数时候你只能使用 Java 和 CSS。然而,至少在某种程度上理解底层技术总是好的。
超文本标记语言
HTML(超文本标记语言)是浏览器在呈现网页时使用的源。超文本是指带有超链接的文本。当您从一个页面导航到另一个页面时,您可能已经单击了许多超链接。当您看到网页时,您看到的是 HTML 文档的渲染版本。HTML 文档是由标签和文本组成的文件(在内存或硬盘中),从 HTML5 开始,以文档类型声明开始:
<!DOCTYPE html>
大多数标签成对使用。例如:
<h1>It works!</h1>
在这个例子中,<h1>是开始标记,</h1>是结束标记。标签之间的文本是标签的内容,也可以包含其他 HTML 标签。在前面的例子中,文本Web 平台由浏览器使用标题样式呈现。标题有几个层次,例如,<h2>,<h3>等。
HTML 标签不仅格式化代码,而且呈现 UI 控件,如按钮和文本字段。以下代码片段呈现了一个按钮:
<button>Time in the client</button>
HTML 文档的主要部分由三个标签构成:
-
<html>:文档的根或顶层元素 -
<head>:关于文档的元数据,用于添加资源(图像、JavaScript、CSS)或配置页面标题等内容(使用<title>标签) -
<body>:文档的可呈现内容
开始标签可以包括属性。例如,您可以通过使用<html>标签的lang属性来指定页面的语言:
<html lang="en"></html>
如果我们将前面的代码片段放在<body>元素中,我们就可以形成一个所有浏览器都可以呈现的完整有效的 HTML 文档。清单 1-1 显示了一个完整有效的 HTML 文档。
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Web platform</title>
<link rel="stylesheet" href="browser-time.css">
</head>
<body>
<h1>It works!</h1>
<button>Time in the client</button>
</body>
</html>
Listing 1-1A complete HTML document
Note
HTML 不关心缩进。开发人员会这样做,一些人选择缩进<html>和<body>标签的内容,而另一些人则不愿意这样做。我不喜欢缩进,因为它们出现在几乎所有的文档中,它们只是把所有的东西都向右移动。然而,为了可读性,我在<body>标签中缩进了 HTML 标签的内容。在前面的例子中,这些标签都没有其他标签作为内容,所以在<body>标签中没有缩进的内容。此外,大多数 ide 都按照我在示例中使用的样式缩进。
如果您使用纯文本编辑器(下一章将介绍如何设置开发环境)创建一个browser-time.html文件,并在 web 浏览器中打开该文件,您将看到类似于图 1-2 中的截图。
图 1-2
在 web 浏览器中呈现的简单 HTML 文档
我鼓励您在您的计算机上尝试这种方法,并用代码进行实验。尝试添加标签,如<input>,并用<b>、<i>和<code>格式化文本。
Note
你可以在 Mozilla Developer Network (MDN)网站的 https://developer.mozilla.org/en-US/docs/Web/HTML/Element 找到 HTML 中所有标签的完整列表。事实上,MDN 是学习有关 Web 平台技术的一切的极好资源。
JavaScript 和 DOM
JavaScript 是一种多用途的、基于原型的(允许在没有预先定义类的情况下创建对象)、单线程的、具有一流功能的脚本编程语言。除了它的名字和Date对象(是 Java 的java.util.Date类的直接端口),JavaScript 和 Java 语言本身没有任何关系。然而,JavaScript 经常与 Java 一起用于开发 web 应用——Java 在服务器上,JavaScript 在客户机上。JavaScript 是网络浏览器的编程语言。
DOM (文档对象模型)是一个独立于语言的 API,它将 HTML(或者更一般地,XML)文档表示为一棵树。Web 浏览器将 DOM 实现为 JavaScript API。图 1-3 描述了上一节开发的 HTML 文档的 DOM 层次结构。
图 1-3
HTML 文档的文档对象模型示例
使用 JavaScript DOM API,开发人员可以添加、更改和删除 HTML 元素及其属性,从而创建动态网页。要将 JavaScript 逻辑添加到 HTML 文档中,可以使用<script>标签:
<!DOCTYPE html>
<html>
...
<body>
...
<script>
... JavaScript code goes here ...
</script>
</body>
</html>
我喜欢将 JavaScript 代码放在单独的文件中。添加 JavaScript 的另一种方法是将<script>标签的内容留空,并使用src属性来指定文件的位置:
<script src="time-button.js"></script>
回到上一节的 HTML 文档,使按钮工作的 JavaScript 逻辑可以放在 time-button.js 文件中(browser-time.html 文件旁边的文件),内容如下:
let buttons = document.getElementsByTagName("button");
buttons[0].addEventListener("click", function() {
let paragraph = document.createElement("p");
paragraph.textContent = "The time is: " + Date();
document.body.appendChild(paragraph);
});
我尽量把前面的 JavaScript 代码写得对 Java 开发者来说尽可能清晰。这个脚本将文档中的所有<button>元素作为一个数组,并向第一个元素添加一个 click 监听器(顺便说一下,这里只有一个按钮)。click listener 实现为一个函数,当用户单击按钮时调用该函数。这个监听器使用 DOM API 创建一个新的<p>元素,并设置它的文本内容来显示当前时间。然后,它将新创建的元素附加到<body>元素的末尾。结果如图 1-4 所示。
图 1-4
在 web 浏览器中运行的简单 JavaScript 应用
半铸钢ˌ钢性铸铁(Cast Semi-Steel)
CSS(级联样式表)是一种允许配置字体、颜色、间距、对齐和其他样式特征的语言,这些特征决定了 HTML 文档应该如何呈现。向 HTML 文档添加 CSS 代码的一个简单方法是在<head>元素中使用<style>标记:
<!DOCTYPE html>
<html>
<head>
...
<style>
... CSS code goes here ...
</style>
</head>
...
</html>
和 JavaScript 文件一样,我喜欢用单独的文件来定义 CSS 样式。这是通过在<head>部分使用一个<link>标签来完成的:
<head>
<link rel="stylesheet" href="browser-time.css">
<head>
Tip
<link>是没有结束标签(</link>)的标签之一。在 HTML5 中,不允许使用结束标记;然而,浏览器只是忽略了</link>或者在渲染页面时在>前添加一个/的货运惯例。
CSS 规则将样式应用于 HTML 文档。每个 CSS 规则都被写成一个针对 HTML 元素的选择器和带有应用于这些元素的样式的声明。例如,以下 CSS 规则更改整个 HTML 文档的字体:
html {
font: 15px Arial;
}
html部分是选择器。大括号内是声明。这个规则中只有一个声明,但是也可以定义多个声明。以下 CSS 规则将所有<h1>元素更改为全角(100%)、半透明蓝色背景色和 10 像素的填充(元素文本周围的空间):
h1 {
width: 100%;
background-color: rgba(22, 118, 243, 0.1);
padding: 10px;
}
选择器允许通过标记名(像前面的例子一样)、元素 ID、属性值等进行定位。最重要的选择器之一是类选择器。一个类选择器允许目标元素在它们的class属性中有一个指定的值。下面的代码片段显示了如何将time-button CSS 类添加到示例中的按钮:
<button class="time-button">Time in the client</button>
CSS 类选择器以句点开头,后跟目标类的名称。要设置上例中按钮的样式,可以使用如下规则:
.time-button {
font-size: 15px;
padding: 10px;
border: 0px;
border-radius: 4px;
}
此规则将字体大小更改为 15 像素,在按钮中的文本周围添加 10 像素的填充,移除边框,并使其边角略微变圆。结合这些概念,可以在一个单独的 browser-time.css 文件中设计整个 HTML 文档的样式:
html {
font: 15px Arial;
}
body {
margin: 30px;
}
h1 {
width: 100%;
background-color: rgba(22, 118, 243, 0.1);
padding: 10px;
}
.time-button {
font-size: 15px;
padding: 10px;
border: 0px;
border-radius: 4px;
}
图 1-5 显示了之前应用于 HTML 文档的 CSS 规则。
图 1-5
用自定义 CSS 样式呈现的 HTML 文档
Web 组件
Web 组件是一组允许创建可重用的自定义 HTML 元素的技术。在本节中,我将向您介绍主要技术:自定义元素。这应该足以让你理解关键的网络平台概念,并看到没有真正的魔法。
Web 组件是一个可重用的封装自定义标签。示例中的“客户机中的时间”按钮是这种组件的一个很好的候选。如果能够通过一个自定义标记在多个 HTML 文档中使用该组件,将会非常方便:
<time-button></time-button>
定制元素的名字中必须有一个破折号,这样浏览器(和你)就知道它不是一个标准的 HTML 元素。使用自定义元素需要两件事情:
-
实现一个扩展
HTMLElement(或者一个特定元素)的类。 -
使用
customElements.define(name, constructor)注册新元素。
以下是如何:
class TimeButtonElement extends HTMLElement {
constructor() {
super();
...
}
}
customElements.define("time-button", TimeButtonElement);
在构造函数中,您可以通过使用this.innerHTML或 DOM API 中的任何可用功能来定义元素的内容:
let button = document.createElement("button");
button.textContent = "Time in the client";
button.classList.add("time-button");
button.addEventListener("click", function () {
let paragraph = document.createElement("p");
paragraph.textContent = "The time is: " + Date();
document.body.appendChild(paragraph);
});
this.appendChild(button);
这会以编程方式创建按钮,并将其追加到自定义元素中。为了使元素在重用时更加灵活,允许指定像按钮中显示的文本这样的值而不是硬编码它们是一个好主意("Time in the client"):
button.textContent = this.getAttribute("text");
有了它,按钮可以按如下方式使用:
<time-button text="Time in the client"></time-button>
只需在文档中添加更多的<time-button>标签,就可以多次使用该组件。清单 1-2 展示了一个完整的 HTML 文档,其中包含两个不同文本的按钮,清单 1-3 展示了配套的 time-button.js 文件,该文件实现并注册了定制元素。
class TimeButtonElement extends HTMLElement {
constructor() {
super();
let button = document.createElement("button");
button.textContent = this.getAttribute("text");
button.classList.add("time-button");
button.addEventListener("click", function () {
let paragraph = document.createElement("p");
paragraph.textContent = "The time is: " + Date();
document.body.appendChild(paragraph);
});
this.appendChild(button);
}
}
customElements.define("time-button", TimeButtonElement);
Listing 1-3A custom element implemented in JavaScript (time-button.js)
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Web platform</title>
<link rel="stylesheet" href="browser-time.css">
</head>
<body>
<h1>It works!</h1>
<time-button text="Time in the client"></time-button>
<time-button text="What time is it?"></time-button>
<script src="time-button.js"></script>
</body>
</html>
Listing 1-2An HTML document reusing a custom element
您只需要一个文本编辑器和一个浏览器来尝试这一点。如果您是 web 开发新手,我建议您这样做。尝试创建这些文件,将它们放在同一个目录中,并在 web 浏览器中打开 HTML 文件。在继续之前,请确保您了解正在发生的事情。旅程的客户端步骤以图 1-6 结束,它显示了到目前为止开发的最终纯 HTML/JavaScript 应用的屏幕截图。
图 1-6
最终的纯客户端 web 应用
服务器端技术
有了 Web 平台的基础,您现在可以接近等式中同样激动人心的服务器端了。简而言之,这意味着理解什么是 web 服务器,如何向 web 服务器添加自定义功能,以及如何将客户端(浏览器)与 web 服务器连接起来。
网络服务器
术语网络服务器用于指代硬件和软件实体。在硬件领域,web 服务器是一台包含 web 服务器软件和资源的机器,这些资源包括 HTML 文档、JavaScript 文件、CSS 文件、图像、音频、视频甚至 Java 程序。在软件领域,web 服务器是通过 HTTP(浏览器理解的协议)向客户端(web 浏览器)提供主机(硬件 web 服务器)中的资源的软件。本书使用术语 web 服务器的软件定义。图 1-7 显示了客户端-服务器架构中的主要组件以及通过 HTTP 请求和响应的数据流。
图 1-7
HTTP 上的客户机-服务器体系结构
通常,web 服务器被称为 HTTP 服务器或应用服务器,这取决于所提供的内容是静态的还是动态的。静态 web 服务器将 URL 映射到主机中的文件,并在浏览器请求时发送它们。动态 web 服务器是静态 web 服务器,但它为开发人员提供了在提供托管文件之前处理它们的可能性。
web 服务器是可以在您的机器上安装和运行的软件。您的计算机中可能已经安装了一个(或几个)。事实上,几乎所有的 Unix 平台(Linux、macOs、FreeBSD 和其他平台)都自带 Python,而 Python 又包含一个模块,可以轻松地运行 HTTP web 服务器来为当前目录中的文件提供服务。在 Windows 系统上,您必须安装 Python,或者更好的是,启用 WSL(Linux 的 Windows 子系统)并使用 Windows store 安装 Linux 发行版,例如 Ubuntu,默认情况下它包含 Python。
根据 Python 的版本,您必须运行以下命令之一来启动允许通过 HTTP 访问当前目录中的文件的 web 服务器:
> python -m SimpleHTTPServer 8080
> python3 -m http.server 8080
Tip
如果您的计算机上安装了 Node.js,您也可以使用npm install -g http-server安装一个 HTTP 服务器,并使用http-server -p 8080运行它。
您可以使用 URL 从连接到您的网络甚至互联网的任何设备请求 HTML 文档(假设防火墙和其他安全机制不会阻止对您的 web 服务器的访问)。图 1-8 显示了我从手机上请求 HTML 文档时的示例(根据您使用的浏览器和操作系统,您得到的结果可能略有不同)。请注意我是如何使用我的 IP 地址来访问文件,而不是直接在浏览器中打开它的。
图 1-8
从 web 服务器提供的 HTML 文档
公共网关接口
CGI(公共网关接口)是从网络服务器提供动态内容的最简单的方法之一。我就避开 CGI 死了还是好不好的讨论。我的目的是让您向服务器端技术迈进一步,从实用的角度来看,这项技术很容易理解。
CGI 定义了一种方法,允许 web 服务器与服务器中的外部程序进行交互。这些程序可以用任何编程语言实现。CGI 将 URL 映射到这些程序。外部程序使用标准输入(STDIN)和标准输出(STDOUT)与客户端通信,这在 Java 中可以通过System.in和System.out对象获得。标准程序和 CGI 程序的主要区别在于输出应该以包含Content-Type标题的一行开始。例如,要提供纯文本,输出必须以Content-Type: text/html开头,后面跟一个空行,后面跟要提供的文件的内容。
Python HTTP 服务器模块包括 CGI。要启用它,使用--cgi参数启动服务器:
> python -m SimpleHTTPServer 8080 --cgi
> python3 -m http.server 8080 --cgi
有了这个服务器,当你使用这个服务器的时候,CGI 程序应该放在 cgi-bin 目录下。其他 web 服务器可能使用不同的位置,可能需要安装额外的模块才能工作。
让我们看看如何用 Java 实现一个 CGI 程序。从版本 11 开始,可以创建一个可以直接从命令行执行的 Java 程序:
#!/usr/bin/java --source 11
public class ServerTime {
public static void main(String[] args) {
System.out.println("Content-Type: text/plain\n");
System.out.println("It works!");
}
}
Note
上例中的第一行是一个 she bang——一个基于 Unix 的系统识别的幻数,用于确定文件是脚本还是可执行的二进制文件。记得使用完整路径到 java 命令,并用 chmod +x server-time 使文件可执行。在 Windows 系统中,你可以创建一个*。bat* 文件来调用 java 程序。在这种情况下,您需要一个单独的文件来放置 Java 代码。
如果您将这个文件命名为 server-time (不要使用*。java* 扩展)并将其放在 cgi-bin 目录中(相对于您启动 Python web 服务器的位置),您将能够在*http://localhost:8080/CGI-bin/server-time*从浏览器访问该程序。图 1-9 显示了结果。
图 1-9
用 Java 编写的 CGI 程序返回的纯文本
前面的例子并没有真正创建动态内容——每次请求页面时,浏览器都显示完全相同的内容。然而,很容易理解 Java 程序可以做的不仅仅是返回硬编码的字符串。它可以读取一个 HTML 文件,并通过替换一个占位符来处理它以添加动态内容,然后将结果发送给STDOUT。这是许多 web 框架使用的一种技术。下面是一个可能的实现:
#!/usr/bin/java --source 11
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
public class ServerTime {
public static void main(String[] args) throws IOException {
Path path = Path.of("cgi-bin/template.html");
String template = Files.readString(path,
Charset.defaultCharset());
String content = "Time in the server: " + new Date();
String output = template.replace("{{placeholder}}", content);
System.out.println("Content-Type: text/html\n");
System.out.println(output);
}
}
这个程序获取 cgi-bin/template.html 文件,读取其内容,并用包含服务器中计算的时间的字符串替换{{placeholder}}。模板文件可能是这样的:
<!DOCTYPE html>
<html lang="en">
<head>
<title>CGI Example</title>
</head>
<body>
<h1>It works!</h1>
{{placeholder}}
</body>
</html>
图 1-10 显示了在浏览器中调用程序的结果。现在,这是一个动态页面,每次你请求时,它都会显示不同的内容。
图 1-10
由 CGI 程序生成的动态 HTML 文档
小型应用
Jakarta Servlet (以前称为 Java Servlet)是 Java 的 API 规范,允许开发人员使用 Java 编程语言扩展服务器的功能。虽然在上一节中我已经向您展示了如何用 Java 实现这一点,但是与 Java servlets 相比,CGI 有几个缺点。CGI 的主要挑战与这样一个事实有关,即每次用户请求一个页面时,服务器都会启动一个新的进程(或者服务器中的 CGI 程序)。这降低了对服务器的请求速度,潜在地消耗了更多的内存,使得使用内存中的数据缓存更加困难,并且降低了程序的可移植性。
Servlets 是用于 web 开发的 Java 解决方案,它通过一个可靠的、久经考验的 API 提供了请求-响应协议(如 HTTP)的面向对象的抽象。servlet 是一个 Java 程序(或类),它由一个名为 servlet 容器的软件组件管理。servlet 容器是 Jakarta Servlet API 的具体实现。有些 web 服务器包括现成的 servlet 容器实现。最流行的是 Apache Tomcat 和 Eclipse Jetty。
Tip
如何在 Tomcat 和 Jetty 之间抉择?我在这里没有一个好的答案,但是有一个快速的指南可以帮助你开始你自己的研究。两者都可以投入生产,并且经过了许多严肃项目的测试。Tomcat 更受欢迎,更快地结合了最新版本的规范。Jetty 似乎用在高性能是关键的项目中,优先考虑整合社区所需的修复,而不是支持最新版本的规范。
简而言之,操作系统运行进程。JVM 作为执行字节码编译的程序(用 Java、Kotlin、Scala、Groovy 或其他 JVM 语言编写)的进程运行。Tomcat 和 Jetty Java 服务器都是用 Java 实现的(尽管 servlet 容器可以用任何语言实现,只要它们符合 Jakarta Servlet 规范)。Java 服务器读取组成 Java web 应用的文件,而 Java web 应用又与 servlet API 交互来处理请求并产生响应。图 1-11 显示了该堆栈的概况。
图 1-11
服务器端 Java 堆栈
下面是一个简单的 servlet 实现:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/*")
public class ServletExample extends HttpServlet {
private String replace;
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws
IOException, ServletException {
response.setContentType("text/plain");
response.getWriter().println("It works!");
}
}
这个类使用 servlet API 将纯文本写入响应,类似于 CGI 程序将响应写入STDOUT的方式。@WebServlet注释配置了服务器用来向这个 servlet 发送请求的 URL。在这种情况下,对应用的所有请求都将被发送到由 servlet 容器创建和管理的ServletExample类的一个实例。
要编译这个类,您需要将 servlet API 添加到 JVM 类路径中(下一章将展示如何用 Maven 完成这一步)。Java 服务器附带了这个 API。例如,下载 Apache Tomcat 9.0 的 ZIP 文件,并提取其中的内容。可以在 http://tomcat.apache.org 下载服务器。您将在 Tomcat 安装的 lib 目录中找到一个包含 servlet API 的 servlet-api.jar 文件。复制该文件的完整路径,并如下编译ServletExample类(更改您自己的 JAR 文件的位置):
javac ServletExample.java -cp /apache-tomcat/lib/servlet-api.jar
Java web 应用需要特定的目录结构,以便供 Java 服务器使用。在 Tomcat 安装的 webapps 目录中创建一个新的 java-web-development 目录。这是您的应用的根目录。编译好的 Java 类文件应该放在你的应用根目录下的子目录中: WEB-INF/classes 。创建这个目录并将 ServletExample.class 文件复制到其中。
使用 bin/startup.sh 或 bin/startup.bat 脚本启动服务器。例如,您可能需要使用chmod +x *.sh使这些文件可执行。现在可以在http://localhost:8080/Java-we b-development调用 servlet。图 1-12 显示了结果。
图 1-12
由运行在 Tomcat 上的 Java servlet 生成的文本文档
您还可以使用 Java 服务器提供静态 HTML、CSS 和 JavaScript 文件。作为一个练习,尝试将前面几节中开发的示例文件复制到应用的根目录下。您需要将 servlet 映射到不同的 URL 以使文件可用(例如,@WebServlet("/example")),重新编译并重启服务器。你可以通过运行 shutdown.sh 或者 shutdown.bat 来停止服务器。
使用 Vaadin 进行 Web 开发
至此,您应该清楚,不管 HTML、JavaScript 和 CSS 文件是由 Java 程序、Java servlet 还是文件系统生成的,提供这些文件都没有什么神奇之处。大多数 Java web 框架使用这些技术来简化 web 开发。Vaadin 就是其中之一。
Vaadin 的核心包括一个 Java servlet ( VaadinServlet),它为您处理一切(或几乎一切)。这个 servlet 扫描您的类并构建一个组成 web 用户界面的组件树。这个组件树与浏览器中的 DOM 相似(但不相同)。您使用一个名为 Vaadin 流的 Java API 来构建这个树。
当在浏览器中调用 Vaadin 应用时,Vaadin servlet 用一个轻量级客户端 JavaScript 引擎进行响应。当用户在浏览器中与应用交互时,客户端引擎通过发送请求和接收来自 Vaadin servlet 的响应,动态地添加、删除或修改 DOM 中的元素(实现为 Web 组件)。
客户端引擎是一组静态资源,包括在您构建应用时由 Vaadin 自动生成的配置文件、Web 组件和 HTML 文档。这些资源是由一个 Maven 插件创建的,你将在下一章中了解到。
摘要
通过教你 web 开发的基础知识,这一章让你处于一个很好的位置,开始学习 Vaadin 的细节。您看到了 web 平台如何允许您使用 HTML 文档呈现 Web 页面,这些文档可以使用 CSS 规则进行样式化,并使用 JavaScript 进行动态修改。您不仅通过使用可以用任何编程语言编写的 CGI 程序,还通过创建部署到实现 Jakarta servlet API 的 Servlet 容器的 Servlet,学习了什么是 web 服务器以及如何为它们添加功能。您还了解了 Vaadin 如何包含一个 servlet 实现,该实现与客户端引擎通信以在浏览器中呈现 Web 组件。
下一章将教你如何设置你的开发环境,以及如何创建和调试 Vaadin 应用。在此期间,您将学习框架中的关键基本概念。
二、设置开发环境
本章解释了如何设置开始用 Java 和 Vaadin 编码所需的所有工具。简而言之,您需要 Java 开发工具包和 Java 集成开发环境。您将学习 Vaadin 的基础知识,以及如何编译、运行和调试您自己的程序以及本书中包含的示例。
安装 JDK
Java 开发工具包(JDK)是开发 Java 应用所需的工具和库的集合。这包括 Java 编译器,它将您的纯文本源代码文件转换成 Java 运行时环境(JRE)可以运行的二进制文件。任何需要运行编译后的 Java 程序的设备都需要 JRE。对于 Vaadin 应用,设备很可能是 Java 服务器。JRE 是几年前 Java 出现在市场上时创造的著名和经典的“编译一次,在任何地方运行”口号的来源。JDK 包括 JRE 和附加工具。
JDK 有几个高质量的发行版。Vaadin 推荐亚马逊 Corretto,这是一个由亚马逊提供支持的免费开源发行版。Amazon Corretto 基于 JDK 的参考实现 OpenJDK。我个人推荐改为采用 OpenJDK 。在写这本书的时候,这个项目正在以 Adoptium 的名字转移到 Eclipse Foundation,所以你可能不得不寻找这个名字而不是 AdoptOpenJDK,这取决于你何时阅读本章。AdoptOpenJDK(或 Adoptium)是 OpenJDK 的高质量、企业就绪版本,符合 Oracle 的技术兼容性工具包(从迁移到 Eclipse Foundation 开始)。该项目是开源的,让社区可以使用所有的构建和测试脚本。
Tip
可以把 JDK 想象成由全球开发者社区开发的一套标准,通过所谓的 Java 社区过程(JCP)来实现整个过程,通过 Java 规范请求(JSR)来实现 JDK 的特定特性或部分。任何人都可以采用这些标准并执行它们。OpenJDK 是一个实际上是源代码的实现。发行版获取源代码并生成二进制文件,然后将它们打包到一个归档文件中,开发人员可以将它们安装到自己的机器上。
要安装 JDK,进入 https://adoptopenjdk.net (或 https://adoptium.net ),选择 OpenJDK 16(或你在那里找到的最新版本),点击页面底部的大按钮下载安装程序。网页截图如图 2-1 所示。
图 2-1
adoptopenjdk 下载页面
Linux、Windows 和 macOS 的安装包都是可用的。点击其他平台按钮可以看到所有平台。选择与您的机器相匹配的一个,运行安装程序,并按照显示的步骤操作。
要再次检查 JDK 是否可以使用,请打开终端或命令行窗口,并运行以下命令来确认编译器的版本与您下载并安装的版本相匹配:
> javac -version
javac 16.0.1
您还可以通过在命令行中运行java -version来检查 JVM 是否准备好被调用。该命令应该报告您刚刚安装的 JDK 的版本和发行版。例如:
> java -version
openjdk version "16.0.1" 2021-04-20
OpenJDK Runtime Environment AdoptOpenJDK-16.0.1+9 (build 16.0.1+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK-16.0.1+9 (build 16.0.1+9, mixed mode, sharing)
Tip
如果你安装了旧版本的 JDK,你可以使用像 *SDKMAN 这样的工具!*你可以在 https://sdkman.io 了解更多。
安装 IDE
集成开发环境(IDE)是一种应用,它在单个图形用户界面中包含软件开发所需的工具。一个好的 IDE 应该包括带有语法高亮的源代码编辑器、构建工具和调试器。Java 的主要 IDE 是 Eclipse IDE、Visual Studio 代码、Apache NetBeans,以及本书使用的免费 IntelliJ IDEA 社区版。您可以使用任何 IDE,但是本书中的屏幕截图和说明是为 IntelliJ IDEA 量身定制的,因为本章没有足够的篇幅来涵盖所有内容。我实际上已经使用 Eclipse IDE 开发了这本书的例子,但是我决定展示 IntelliJ IDEA,因为它提供了一种快速运行 Maven 项目的方法(稍后将详细介绍)。
要安装 IntelliJ IDEA,请转到 www.jetbrains.com/idea 并点击下载选项。网页截图如图 2-2 所示。
图 2-2
IntelliJ IDEA 网站
选择您的操作系统,下载免费的社区版安装程序,并运行它。第一次运行 IDE 时,可以使用它提供的默认配置选项。
使用本书中的例子
这本书包含多个独立的例子。为了方便起见,所有示例都聚集在一个项目中,您可以将该项目导入到您的 IDE 中。通过位于 www.apress.com/9781484271780 的图书产品页面下载代码。在这个页面上,您会找到一个 GitHub 上的 Git 资源库的链接,您可以在这里下载 ZIP 文件形式的代码。将 ZIP 文件的内容解压到硬盘中的任意位置。
Tip
或者,如果您是 Git 用户,您可以使用 Git 来克隆存储库。
在 IDE 中导入代码
下载并提取源代码后,运行 IntelliJ IDEA,在图 2-3 所示的欢迎屏幕中点击打开或导入选项。如果您已经是 IntelliJ IDEA 用户,并且打开了项目,要么关闭所有项目以查看欢迎屏幕,要么改为选择文件 ➤ 打开。
图 2-3
IntelliJ IDEA 的欢迎屏幕
IntelliJ IDEA 显示一个窗口,用于从硬盘上的文件中选择一个项目。转到解压源代码的目录(或文件夹),选择该目录下的 pom.xml 文件,如图 2-4 所示。出现提示时,确保选择打开为项目选项。
图 2-4
聚合器项目 pom.xml 文件
项目导入后,检查它是否与您安装的 JDK 相关联。选择包含项目视图中所有其他子目录的根(最顶层)目录,并选择文件 ➤ 项目结构...。在项目结构窗口的左侧,点击项目并确保在窗口右侧的项目 SDK 和项目语言级别部分选择了 JDK 版本 16 或更高版本。图 2-5 显示了该配置。
图 2-5
IntelliJ IDEA 中的 JDK 配置
运行示例
由于 Maven 和 IntelliJ IDEA 提供的良好集成,构建和运行示例变得轻而易举。点击 IDE 窗口右侧的 Maven 或者在菜单栏中选择视图 ➤ 工具窗口 ➤ Maven 。您会看到书签中包含的所有示例都带有一个蓝色的“m”图标。从第二章开始的所有示例均可在此视图中找到。
Caution
第一章的例子不是基于 Maven 的。你必须按照第一章中的说明来构建和运行这些例子。
从运行我为你准备的欢迎项目开始。在 Maven 视图中,选择 welcome-to-vaadin 项目并点击上面带有“播放”图标的 Run Maven Build 按钮。见图 2-6 。
图 2-6
运行欢迎项目
第一次构建 Vaadin(或者一般来说,Maven)项目时,花费的时间可能比您预期的要长。这是因为 Maven 必须将所有依赖项(JAR 文件形式的 Java 库)下载到您的本地 Maven 存储库中。一旦这些依赖项位于您的本地存储库中,运行项目就会变得更快。
Caution
Vaadin 自动在中安装 Node.js。您的主目录中的 vaadin 目录。下载所需的客户端依赖项需要 Node.js。你不需要担心这个问题,除非你已经安装了 Node.js。如果是这样,请检查您使用的是 Node.js 版本 10 或更高版本。
运行完项目后,在运行窗口(应用的日志)中等待一条消息,显示“前端编译成功”,然后转到 http://localhost:8080 。你应该会看到我为你准备的欢迎信息和关于 Vaadin 的有趣事实。
Note
Maven 是一个项目管理工具。它允许你使用一个名为 pom.xml 的文件来管理你的项目。在这个文件中,您定义了 Java 项目的依赖项(例如,Vaadin、Spring、Hibernate)以及有助于构建项目的插件。IntelliJ IDEA 对 Maven 有很好的支持,所以你很少需要使用命令行来运行 Maven。解释 Maven 超出了本书的范围。如果你想了解更多,请访问官方网站 https://maven.apache.org 。
您的第一个 Vaadin 应用
创建新的 Vaadin 项目有多种方法。Vaadin 建议使用在 https://start.vaadin.com 可用的工具来创建新项目。不幸的是,在撰写本文时,这个在线工具不包括创建没有生成视图和 Spring Boot 的最小项目的选项。当你读这本书的时候,这可能会改变,但是为了你的方便,我已经创建了一个最小的空项目,你可以用它作为模板开始编写你的应用。您可以在本书附带的源代码的 ch02/empty-project/ 目录中找到这个空项目(参见 www.apress.com/9781484271780 处的说明)。
要创建一个新项目,复制 empty-project 目录及其内容,并将该目录的名称更改为您希望用作新项目名称的名称。例如,如果您在 Linux 计算机上,请在命令行中运行以下命令:
cd ch02
cp -r empty-project ~/my-first-vaadin-app
这将把新项目放在主目录中,但是可以随意使用任何其他位置。以与包含本书示例的聚合器项目相同的方式在 IDE 中导入项目,并在 pom.xml 文件中将artifactID更改为更合适的名称。例如:
<project ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-first-vaadin-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
...
<project>
项目视图应该类似于图 2-7 中的截图。
图 2-7
一个最小的 Vaadin 项目结构
一切就绪,开始编写您的第一个 Vaadin 应用!
Note
Vaadin 允许在服务器端 Java 或客户端 TypeScript 中实现视图。服务器端 Java 是指在你的服务器上运行的 Java 代码。使用 Java 编写视图,这些视图构成了应用的 web 用户图形界面。客户端 TypeScript 意味着用 TypeScript 编程语言实现的视图。这段代码被编译成 JavaScript 并在 web 浏览器中运行。这本书的重点是服务器端 Java 视图。
路线和视图
通过右键单击com.example包并选择 New ➤ Java Class 来创建一个名为HelloWorldView的新 Java 类。IntelliJ IDEA 将生成以下类:
public class HelloWorldView {
}
Note
从现在开始,您将看到的大多数代码片段中都省略了包声明。
您将在这个类中实现 Java 应用的 web 用户界面(UI)。这个类叫做视图,在同一个项目中你可以有多个。每个视图应该映射到不同的 URL。视图的完整 URL 由 web 服务器的位置(例如,开发环境中的 http://localhost:8080/ )和路由后缀组成。要将后缀映射到视图,使用@Route注释。例如:
import com.vaadin.flow.router.Route;
@Route("hello-world")
public class HelloWorldView {
}
这定义了一个路由,允许您使用类似*http://localhost:8080/hello-world*的 URL 访问视图。如果您指定一个空字符串(@Route("")),那么您将使用 http://localhost:8080 来访问视图。
Caution
通过将光标放在要导入的类上并点击 Ctrl+space ,确保从com.vaadin包中导入正确的类。一个常见的错误是从包含与 Vaadin 的类同名的类的java.awt包中导入类。请注意这一点,以避免编译错误。
用户界面组件和布局
Vaadin 包括一组 UI 组件。UI 组件是可以添加到用户界面中的可重用元素。例如,您可以分别使用Button类和TextField类来添加按钮和文本字段。
一种特殊的 UI 组件是布局。布局允许您向其中添加其他 UI 组件(包括其他布局),以便垂直、水平或以您需要的任何其他方式排列它们。
用@Route标注的类必须是 UI 组件。要使HelloWorldView成为一个 UI 组件,您必须扩展一个现有的组件。因为视图通常包含许多 UI 组件,所以视图扩展某种布局类是有意义的。
Tip
或者,您可以扩展Composite类,它提供了一种更好的方法来封装视图中的逻辑。下一章将展示如何使用这个类。
当你阅读这本书和使用这个框架时,你会了解到 Vaadin 中许多可用的 UI 组件。现在,只要延长VerticalLayout:
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@Route("hello-world")
public class HelloWorldView extends VerticalLayout {
}
这应该已经可以工作了,但是在浏览器中看起来会很无聊。实现一个将段落添加到视图的构造函数,如下所示:
public HelloWorldView() {
add(new Paragraph("Hello, World!"));
}
按照前面的说明运行应用,并将浏览器指向*http://localhost:8080/hello-world*。结果如图 2-8 所示。
图 2-8
用 Vaadin 实现的“Hello,World”应用
恭喜你!您已经成功实现了您的第一个 Vaadin 应用——用 Java 和 Vaadin 编写的经典“Hello,World”应用。
Caution
在运行一个新的应用之前,确保通过点击工具栏中的停止按钮或 IntelliJ IDEA 中的运行视图来停止以前运行的应用。否则,服务器会抱怨端口 8080 不可用。
事件和侦听器
可以想象,Vaadin 包含了商业应用中常见的各种 UI 组件。当您开发 Vaadin 应用时,您可以组合这些 UI 组件来组成更复杂的视图。例如,将文本字段、组合框和按钮等组件添加到垂直或水平布局中,以构建用于数据输入的表单。
输入组件使用事件编程模型,该模型允许您对诸如单击或 UI 中所选值的更改之类的事情做出反应。为了向您展示这个概念,请尝试将以下类添加到项目中:
@Route("greeting")
public class GreetingView extends VerticalLayout {
public GreetingView() {
var button = new Button("Hi");
button.addClickListener(event ->
Notification.show("Hello!")
);
add(button);
}
}
这段代码创建了一个按钮,并添加了一个事件侦听器来对点击做出反应。侦听器被实现为显示通知的 lambda 表达式。试试看。通过在 IDE 中选择构建 ➤ 构建项目来编译项目,并使用浏览器转到*http://localhost:8080/greeting*。
排除故障
调试是发现和修复软件缺陷的过程,也称为 bug。调试器是一种允许您暂停正在运行的程序的执行以检查其状态的工具。它还允许您执行任意代码,一次一步地运行您的程序(逐行)。
调试器通常在用特定编程语言实现的应用上工作。IntelliJ IDEA 包括一个用于 Java 应用的调试器。Web 浏览器包括 HTML、CSS 和 JavaScript 代码的调试器。您必须尽早学习如何使用这些调试器来查找您遇到的错误,并帮助您理解程序是如何工作的。
服务器端调试
如果您想在示例应用中添加断点,您必须在调试模式下运行它们。停止应用,在 Maven 视图中右键单击项目,并在上下文菜单中选择*Debug‘my-first-vaa din-app…’*选项。
尝试在应用中添加一个断点,方法是单击显示通知的代码行的行号右侧。参见图 2-9 。
图 2-9
IntelliJ 思想中的一个断点
现在转到浏览器并单击按钮。程序执行暂停,调试器显示在调试视图中。您可以看到当前范围内的变量并检查它们的值。例如,您可以通过检查event变量中的值来查看屏幕上发生点击的位置。您也可以逐行继续执行程序,或者直到遇到另一个断点(如果有的话)。熟悉此视图中的选项。它们会在项目开发的某个时候变得有用。
客户端调试
在客户端,您会遇到需要查看浏览器中 DOM 的当前状态来调整布局问题的情况。所有主要的网络浏览器都包含了对开发者来说很好的工具。在谷歌浏览器的情况下,你可以通过按 F12 来激活这个工具。
找到 UI 特定部分的 HTML 的一个好方法是右键单击您感兴趣的 UI 组件并选择 Inspect 。用你的应用的按钮试试这个。图 2-10 显示了 Chrome DevTools 和在浏览器中形成按钮的 HTML。
图 2-10
检查完浏览器中的元素后
您还可以修改 HTML 和 CSS 样式,这在您实现视图并尝试微调应用的外观时非常方便。
摘要
本章使您能够有效地开始使用 Vaadin。您学习了如何安装 JDK 和 Java IDE。您下载并导入了本书中包含的示例,并学习了如何使用 IntelliJ IDEA 运行它们。您还编写了自己的第一个 Vaadin 应用,并首次尝试了在服务器端用 Java 实现 web 用户界面的方法。您了解了使用@Route注释可以将视图映射到 URL,并且理解了 Vaadin 如何允许您通过将 UI 组件添加到布局中来编写视图,以及通过实现事件监听器来对浏览器中的用户操作做出反应。最后但同样重要的是,您快速地看了一下服务器端和客户端上可用的调试器。
下一章将更深入地介绍布局的概念。开发 Vaadin 应用时,掌握布局是关键。你必须确保你能使视图在屏幕上的位置和大小看起来像你想要的那样。
三、布局
布局是在屏幕上组织 UI 组件的主要机制。例如,您可能希望创建一个带有垂直对齐的文本字段和组合框的表单。或者您可能想要一个带有文本字段和水平对齐的按钮的搜索栏。Vaadin 包括这些场景的布局和更多。本章教你如何使用 Vaadin 中包含的所有布局组件。
垂直和水平布局
可以说,最明显的排列 UI 组件的方式是从上到下或者从左到右。VerticalLayout和HorizontalLayout类就是为这些场景设计的,是大多数 Vaadin 应用的基本构建模块。
您可以通过创建一个新实例并向其中添加 UI 组件来使用布局。然后,布局负责如何在屏幕上显示所包含的组件。看一下下面的例子:
@Route("vertical-layout")
public class VerticalLayoutView extends VerticalLayout {
public VerticalLayoutView() {
add(
new Paragraph("Paragraph 1"),
new Paragraph("Paragraph 2"),
new Button("Button")
);
}
}
这个视图扩展了VerticalLayout并添加了两个段落和一个按钮。如图 3-1 所示,这些组件以垂直方式展示,形成一列。
图 3-1
以垂直布局排列的组件
通过更改视图扩展的布局类型,可以更改组件的排列方式。例如,切换到HorizontalLayout,下面的视图将组件水平排列成一行,如图 3-2 所示:
图 3-2
以水平布局排列的组件
@Route("horizontal-layout")
public class HorizontalLayoutView extends HorizontalLayout {
public HorizontalLayoutView() {
add(
new H1("Horizontal layout"),
new Paragraph("Paragraph 1"),
new Paragraph("Paragraph 2"),
new Button("Button")
);
}
}
成分组成
因为布局是 UI 组件,所以可以将布局添加到布局中。假设您想创建一个包含按钮的工具栏。您可以使用一个HorizontalLayout作为工具栏,并将它放在一个VerticalLayout中,在这里您还可以添加其他组件。这里有一个例子:
@Route("composition")
public class CompositionView extends VerticalLayout {
public CompositionView() {
var toolbar = new HorizontalLayout(
new Button("Button 1"),
new Button("Button 2"),
new Button("Button 3")
);
add(
toolbar,
new Paragraph("Paragraph 1"),
new Paragraph("Paragraph 2"),
new Paragraph("Paragraph 3")
);
}
}
您可以根据需要设置任意多级嵌套。图 3-3 显示了结果。
图 3-3
垂直布局中的水平布局
当您实现一个视图(或者一般的 UI 组件)时,您正在定义一个组件树。树的根是视图本身。在前面的例子中,根是CompositionView(也是一个VerticalLayout)。作为直接孩子,根包含工具栏(HorizontalLayout)和三个段落。该组件树的可视化表示如图 3-4 所示。
图 3-4
组件树的一个例子
当您请求一个视图时,Vaadin 使用这个组件树在浏览器中显示相应的 HTML。
复合类
前面例子中的CompositionView类并没有隐藏它是一个VerticalLayout的事实。该类的客户将知道这一点,并将能够访问VerticalLayout中的方法。实际上,扩展布局很好,因为您很少直接实例化视图——当视图在浏览器中被调用时,Vaadin 扫描用@Route注释的类并根据需要创建实例。然而,如果你想隐藏扩展类,你可以使用Composite类。下面是对上一个例子的重构,它使用了Composite类:
@Route("composition")
public class CompositionView extends Composite<VerticalLayout> {
public CompositionView() {
var toolbar = new HorizontalLayout(
new Button("Button 1"),
new Button("Button 2"),
new Button("Button 3")
);
var mainLayout = getContent(); // returns a VerticalLayout
mainLayout.add(
toolbar,
new Paragraph("Paragraph 1"),
new Paragraph("Paragraph 2"),
new Paragraph("Paragraph 3")
);
}
}
注意Composite类如何接受一个指定要封装的组件类型的参数。因为CompositionView不再是VerticalLayout,所以不能直接调用add方法。相反,您应该使用getContent()方法获得一个VerticalLayout的实例,然后使用这个实例构建 UI。
这个类的客户也不能直接访问VerticalLayout中的方法。然而,getContent()方法是公共的,CompositionView类的客户端可以使用它来访问底层的VerticalLayout。要解决这个问题,您可以用Component替换Composite类中的参数(VerticalLayout),覆盖initContent()方法,并将构建 UI 的所有逻辑移到那里:
@Route("composition")
public class CompositionView extends Composite<Component> {
@Override
protected Component initContent() {
var toolbar = new HorizontalLayout(
new Button("Button 1"),
new Button("Button 2"),
new Button("Button 3")
);
return new VerticalLayout(
toolbar,
new Paragraph("Paragraph 1"),
new Paragraph("Paragraph 2"),
new Paragraph("Paragraph 3")
);
}
}
getContent()方法仍然是公共的,但是它返回一个类型为Component的对象,而不是VerticalLayout的对象。现在,您可以更改实现,例如,使用不同类型的布局,而不会破坏应用的其他部分。当您开发的可重用组件不是映射到 URL 的视图时(例如,介绍客户或订单数据的表单),这尤其有用。
Tip
Vaadin 中的所有 UI 组件都直接或间接地从Component扩展而来。
实现可重用的 UI 组件
您可以使用所有面向对象的技术,通过继承来实现 UI 组件,方法是在Composite类的帮助下扩展现有组件、封装和数据隐藏。
前一个例子中的工具栏是视图的一部分,您可能希望将其实现为可重用的 UI 组件。您可以将代码移动到单独的类中,而不是在视图本身中实现工具栏:
public class Toolbar extends Composite<Component> {
@Override
protected Component initContent() {
return new HorizontalLayout(
new Button("Button 1"),
new Button("Button 2"),
new Button("Button 3")
);
}
}
注意,这个类没有用@Route注释。最有可能的是,当浏览器中请求某个 URL 时,您不想只显示一个工具栏,对吗?当您需要工具栏时,您所要做的就是创建一个新的实例并将其添加到布局中:
var toolbar = new Toolbar();
someLayout.add(toolbar);
Tip
你不必总是延长Composite。有些情况下,扩展VerticalLayout、HorizontalLayout、Button或任何其他类是更好的选择。使用 is-a 关系测试来帮助你决定。例如,Toolbar 不是 HorizontalLayout,因为布局可能甚至在运行时被改变为VerticalLayout。另一方面,假设的SendButton 是一个 Button,因此在这种情况下扩展Button类是有意义的。
访问组件树
Vaadin 的一个优点是它使得在运行时动态构建视图变得容易。例如,CRUD 组件可以使用 Java 反射 API 来检查给定域类的属性(如User、Order、Customer等)。)并根据 Java 属性的类型创建匹配的 UI 组件(例如,String属性的文本字段和Boolean的复选框)。另一个例子是根据用户拥有的特权显示某些组件所需的逻辑。
当实现这种动态 ui 时,能够修改布局中的组件树是很有用的。让我们来看看VerticalLayout和HorizontalLayout中的一些方法,它们将帮助你做到这一点。
您可以使用getChildren()方法将布局中的组件作为 Java Stream获取:
toolbar.getChildren().forEach(component -> {
... do something with component ...
});
类似地,您可以使用getParent()方法获得父组件:
toolbar.getParent().ifPresent(component -> {
CompositionView view = (CompositionView) component;
... do something with view ...
});
您可以分别使用remove(Component...)和removeAll()方法移除单个组件或所有包含的组件:
var button = new Button();
toolbar.add(button);
toolbar.remove(button); // removes only button
toolbar.removeAll(); // removes all the contained components
当动态构建视图时,replace(Component, Component)方法会很有用:
var button = new Button();
toolbar.add(button);
toolbar.replace(button, new Button("New!"));
填充、边距和间距
关于布局区域中的空间,您可以控制三个功能:
-
**填充:**布局内边框周围的空间
-
**边距:**版面外边框周围的空间
-
**间距:**布局中组件之间的间距
您可以使用setPadding(boolean)、setMargin(boolean)和setSpacing(boolean)方法激活或停用这三个功能。有相应的方法返回这些方法中的任何一个是否被激活。以下视图允许您切换这些值,以查看它对 UI 的影响:
@Route("padding-margin-spacing")
public class PaddingMarginSpacingView extends Composite<Component> {
@Override
protected Component initContent() {
var layout = new HorizontalLayout();
layout.getStyle().set("border", "1px solid");
layout.setPadding(false);
layout.setMargin(false);
layout.setSpacing(false);
layout.add(
new Paragraph("Toggle:"),
new Button("Padding", e ->
layout.setPadding(!layout.isPadding())),
new Button("Margin", e ->
layout.setMargin(!layout.isMargin())),
new Button("Spacing", e ->
layout.setSpacing(!layout.isSpacing()))
);
return layout;
}
}
这个视图创建了HorizontalLayout并使用getStyle()方法设置了一个可见的边框,该方法返回一个对象来设置 CSS 属性。
Tip
您可以使用浏览器的开发工具来检查布局,并在选择 HTML 元素时查看边框。我选择用 CSS 在代码中设置边框,这样你就可以在书的截图中看到我在解释什么。
所有功能都被停用,然后三个按钮被添加到布局中。每个按钮切换一个单独的功能。图 3-5 显示了所有功能被禁用时的视图。
图 3-5
禁用填充、边距和间距的HorizontalLayout
将图 3-5 与图 3-6 进行比较,其中所有的填充、边距和间距都被激活。
图 3-6
边距、填充和间距已激活
Tip
对于这些特性,布局有合理的默认值。默认情况下,VerticalLayout和HorizontalLayout都激活了间距并取消了边距。此外,VerticalLayout启用填充,而HorizontalLayout禁用。
胶料
要调整 UI 组件的大小,可以使用setWidth(String)和setHeight(String)方法,并以 CSS 绝对或相对长度单位指定一个值(见表 3-1 中最常用单位的快速参考)。例如:
表 3-1
CSS 中常用的长度单位
|单位
|
描述
|
| --- | --- |
| cm | 厘米 |
| mm | 毫米 |
| in | 英寸 |
| px | 像素 |
| % | 父规模的百分比 |
| em | 父级的字体大小 |
| rem | 根字体的大小 |
| lh | 行高 |
button1.setWidth("100%");
button2.setWidth("80%");
button3.setWidth("300px");
或者,您可以将长度与单位分开:
button4.setWidth(10, Unit.EM);
通过设置一个null值,也可以有一个未定义的宽度或高度。如果您想为宽度和高度设置未定义的尺寸,您可以使用setSizeUndefined()方法。同样,您可以使用setWidthFull()、setHeightFull()和setSizeFull()方法分别设置 100%的宽度、高度或两者。未定义的大小会使组件缩小到只使用显示其内容所需的空间。图 3-7 显示了不同宽度的按钮示例。
图 3-7
相对、绝对和未定义的宽度
默认情况下,像Button这样的组件具有未定义的大小。对于HorizontalLayout也是如此;然而,VerticalLayout默认配置为 100%宽度。
Tip
您还可以使用setMaxWidth(String)、setMinWidth(String)、setMaxHeight(String)和setMinHeight(String)方法配置组件的最大和最小宽度和高度。
生长
实际上,grow 属性决定了组件的大小。但是,此属性是在布局中配置的。grow 属性设置组件在布局中所占的空间比例。您可以通过调用setFlexGrow(double, HasElement)方法来进行配置。例如:
var layout = new HorizontalLayout(
button1,
button2
);
layout.setWidthFull();
layout.setFlexGrow(1, button1);
layout.setFlexGrow(2, button2);
在这段代码中,button1使用 1 个单位的空间,而button2使用 2 个单位的空间。布局本身使用 1 + 2 = 3 个单元。因为它是全宽的,所以布局占用的空间和窗口中的一样多,这被测量为 3。图 3-8 有助于形象化这个概念。
图 3-8
不同生长构型
如果将零(0)设置为增长值,则组件不会调整大小,并将占用容纳其内容所需的空间。
对齐
把一个VerticalLayout想象成一个有许多行的列,把一个HorizontalLayout想象成一个有许多列的行。事实上,我希望 Vaadin 用ColumnLayout和RowLayout来代替,但那是另一个话题了。对齐意味着元件如何放置在VerticalLayout的每一行或HorizontalLayout的每一列。副轴的种类:a VerticalLayout中的 x 轴和 a HorizontalLayout中的 y 轴。
若要对齐组件,请使用布局中的方法,而不是对齐的组件中的方法。例如,如果您认为按钮并不真正“关心”它被放在哪里,这是有意义的——另一方面,布局关心它,以便它可以适当地调整所包含的组件。setAlignSelf(Alignment, Component...)方法在指定组件的副轴上设置对齐。例如,假设你有一堆按钮在一个VerticalLayout(一个ColumnLayout,我只能希望…也许我可以扩展VerticalLayout并使用更好的名字来代替…不,我们不要这样做…至少现在…抱歉,回到正题):
var buttons = new VerticalLayout(
button1,
button2,
button3,
button4
);
然后,您可以调整VerticalLayout副轴(水平轴)上每个按钮的对齐方式,如下所示:
buttons.setAlignSelf(FlexComponent.Alignment.CENTER, button1);
buttons.setAlignSelf(FlexComponent.Alignment.START, button2);
buttons.setAlignSelf(FlexComponent.Alignment.END, button3);
图 3-9 显示了结果。你能看到那里的一排排吗?
图 3-9
在 a 的副轴(水平)上对齐VerticalLayout
如果您将布局类型更改为HorizontalLayout,为其设置 100%的高度(默认情况下VerticalLayout的宽度为 100%,因此您不需要这样做),并保持相同的对齐方式,您将获得如图 3-10 所示的效果。你能看到那里的柱子吗?
图 3-10
在HorizontalLayout的副轴(垂直)上对齐
您可以使用setAlignItems(Alignment)方法为您没有单独指定对齐的组件设置默认对齐。
Tip
有更容易记住的方法来设置副轴中的对准:使用VerticalLayout,您可以使用setHorizontalComponentAlignment和setDefaultHorizontalComponentAlignment。有了HorizontalLayout,就可以使用setVerticalComponentAlignment和setDefaultHorizontalComponentAlignment。不过,我更喜欢较短的名字,因为这样可以很容易地改变类型布局,至少不会破坏代码。
调整内容模式
主轴上的对齐(y 轴在VerticalLayout中,x 轴在HorizontalLayout中)称为调整内容模式。在主轴中,在VerticalLayout中只有一列,在HorizontalLayout中只有一行。使用setJustifyContentMode(JustifyContentMode)方法设置该列或行在主轴上的对齐方式(内容对齐模式)。例如:
var layout = new HorizontalLayout(
new Button(justifyContentMode.name())
);
layout.setWidthFull();
layout.getStyle().set("border", "1px solid");
layout.setJustifyContentMode(JustifyContentMode.START);
这段代码创建了一个HorizontalLayout,向其添加了一个按钮,使其全幅显示,显示了一个边框,并将其 justify-content 模式设置为START。图 3-11 显示了三个这样的布局,带有不同的调整内容模式。
图 3-11
具有不同调整内容模式的三个VerticalLayout
在第二个轴中,您看到在HorizontalLayout中只有一行。添加更多的组件不会增加更多的行,而是增加更多的列。这些列之间(或VerticalLayout中的行之间)可以有空格。您可以通过三种额外的内容对齐模式来控制如何使用行(或列)中的空白区域:BETWEEN、AROUND、EVENLY。图 3-12 显示了这些模式。
图 3-12
在三个不同的HorizontalLayout中调整主轴中组件之间的间距
BETWEEN如果看图 3-12 的截图就不言自明了。另外两个的区别在于,AROUND单独增加每个组件之间的空间(在HorizontalLayout的情况下是左和右),而不考虑那里是否已经有空间,而EVENLY在组件之间分配空间,使这些空间具有相同的大小。
Caution
在撰写本文时,有一个问题阻止了布局按预期工作。作为一种变通方法,当您使用影响组件间距的调整内容模式时,您必须在布局调用setSpacing(false)中取消间距。访问 https://github.com/vaadin/vaadin-ordered-layout/issues/66 了解更多关于这个问题的信息。
卷动
假设您有一个包含 100 个按钮的布局,并且您希望该布局的高度为 200 像素(并显示边框,以便我们可以看到我们在做什么):
VerticalLayout layout = new VerticalLayout();
for (int i = 1; i <= 100; i++) {
layout.add(new Button("Button " + i));
}
layout.setHeight("200px");
layout.getStyle().set("border", "1px solid");
当您运行这段代码时,您并没有得到预期的结果。见图 3-13 。
图 3-13
组件泄漏到布局之外
此外,滚动发生在页面级别,而不是布局级别。组件允许你控制滚动的方式和位置。您可以控制方向(水平或垂直),并根据需要禁用滚动。Scroller组件通过构造函数或setContent(Component方法获取要滚动的组件。例如:
VerticalLayout layout = new VerticalLayout();
for (int i = 1; i <= 100; i++) {
layout.add(new Button("Button " + i));
}
layout.setHeight("200px");
Scroller scroller = new Scroller(layout);
scroller.setHeight("200px");
scroller.getStyle().set("border", "1px solid");
现在滚动条只在 200 像素高的布局中显示。见图 3-14 。
图 3-14
Scroller组件
灵活布局
当VerticalLayout和HorizontalLayout不足以满足您的需求时,您可以通过使用FlexLayout类来访问 CSS flexbox 布局的所有特性。
方向
FlexLayout类的主要特点是它允许你设置包含的组件在主轴上显示的方向。使用setFlexDirection(FlexDirection)方法并指定以下选项之一:
-
ROW:组件从左到右排成一行。 -
ROW_REVERSE:组件从右到左排成一行。 -
COLUMN:组件从上到下排成一列。 -
COLUMN_REVERSE:组件自下而上排成一列。
例如,以下代码片段使用 ROW_REVERSE 显示按钮,如图 3-15 所示:
图 3-15
一个方向为ROW_REVERSE的FlexLayout
var layout = new FlexLayout(
new Button("1"),
new Button("2"),
new Button("3")
);
layout.setFlexDirection(FlexLayout.FlexDirection.ROW_REVERSE);
包装
默认情况下,组件排成一行。您可以使用 wrap 属性来更改此行为:
layout.setFlexWrap(FlexLayout.FlexWrap.WRAP);
图 3-16 显示了ROW方向和环绕激活的布局。
图 3-16
在FlexLayout中激活包裹
对齐
当环绕激活时,您可以使用setAlignContent(ContentAlignment)方法对齐行或列(取决于方向设置为ROW还是COLUMN)。例如:
layout.setAlignContent(
FlexLayout.ContentAlignment.SPACE_BETWEEN);
layout.setSizeFull();
结果如图 3-17 所示。
图 3-17
一个FlexLayout与SPACE_BETWEEN内容对齐
收缩
shrink 属性类似于 grow 属性,只是它控制组件在容器中如何收缩。您可以按如下方式配置该属性:
button1.setWidth("200px");
button2.setWidth("200px");
layout.setWidth("300px");
layout.setFlexShrink(1d, button1);
layout.setFlexShrink(2d, button2);
注意按钮的组合宽度是 400 像素,而布局只有 300 像素。收缩值的总和为 1 + 2 = 3(单位),因此整个空间为 3。button1使用 1 个单位,而button2使用 2 个单位。图 3-18 显示了结果。
图 3-18
具有不同收缩值的两个按钮
Tip
收缩定义了当没有足够的空间容纳所包含的组件时如何使用空间。Grow 定义了当容器大于所包含组件的大小时如何使用空间。
其他布局
Vaadin 中有更多可用的布局。当我们在本书后面讨论响应式设计时,我们会看到它们的实际应用。作为一个快速的介绍,让我们简单地提一下他们:SplitLayout、FormLayout和AppLayout。
SplitLayout允许您将组件添加到两个可调整大小的区域,用户可以在浏览器中通过滑动拆分条来调整这些区域。您可以设定方向以显示垂直或水平分割。
是一个响应式布局,用于显示带有输入组件的表单,如文本字段、组合框和按钮。布局对其大小的变化做出反应,根据配置显示更多或更少的列。
AppLayout是一种快速获得屏幕上典型应用区域分布的方法。它在顶部定义了一个导航栏,在左侧定义了一个抽屉(例如,适用于菜单)和一个内容区域。
摘要
这一章是你 Vaadin 旅程中的重要一步。在开发 ui 时,理解如何定位和调整组件是关键。首先,您学习了如何使用Composite类创建 UI 组合。然后,您了解了VerticalLayout和HorizontalLayout,以及它们如何通过让您在主轴和副轴上对齐组件来覆盖广泛的用例。您了解了如何启用和禁用边距、填充和间距,以及如何使用 grow 属性设置组件的大小和布局中的可用空间。最后,您了解了强大的FlexLayout组件及其附加特性,比如配置方向和环绕模式的可能性。
现在你知道了如何在布局中放置组件和确定组件的大小,下一章将会非常有趣!您将了解 Vaadin 中所有的输入、交互和可视化组件。
四、UI 组件
“组件”这个词在软件开发中可能有点超载,以至于感觉任何东西都可以是组件:一个类、一个模块、一个 JAR、一个 HTML 元素、用户界面中的一个输入字段……这个列表还在继续。然而,采用这个词的含义并没有错,在本书中,我使用术语“UI 组件”来指代 UI 的一部分,它可以封装在 Java 类中,其实例可以添加到 Vaadin 布局中。当上下文清楚时,我也使用术语“组件”来指代 UI 组件。
本章是关于数据输入(字段)、动作调用或交互(按钮和菜单)和可视化(图标、图像等)的 UI 组件。Vaadin 包括一组 UI 组件,涵盖了您在开发业务应用时会遇到的大多数需求,所以请准备好查看最常用的 UI 组件。
Note
有一个 UI 组件您在这里看不到:组件Grid。别担心。第六章深入探讨了这个强大的组件。
输入组件
输入组件允许用户以多种格式输入数据(例如,字符串、数字、日期,甚至自定义类型)。这些组件中的大多数都包括setValue(...)和getValue()方法。使用这些方法,您可以分别修改和返回组件中的值。它们还包括一个设置标题的setLabel(String)方法、一个设置组件内部提示文本的setPlaceholder(String)方法、一个设置大小的setWidth(String)、setHeight(String)方法,以及几个使用特定于每个组件的特性的附加方法。
Tip
本章没有足够的篇幅来介绍 Vaadin 中每个组件的所有特性。有关最新列表、示例和 API 文档,请访问 https://vaadin.com/components 。
文本输入
最常用的 UI 组件之一是TextField。下面是一个基本的用法示例:
TextField textField = new TextField();
textField.setLabel("Name");
textField.setPlaceholder("enter your full name");
图 4-1 显示了浏览器中的文本字段。
图 4-1
浏览器中呈现的一个TextField
大多数输入字段(或简称为字段)都包含控制字段中数据的功能。看看这段代码:
textField.setAutoselect(true);
textField.setAutofocus(true);
textField.setClearButtonVisible(true);
textField.setValue("John Doe");
这段代码将文本字段配置为在获得焦点时自动选择组件内的所有文本(光标准备好允许输入)。它将焦点设置在字段上,以便用户可以立即开始输入,显示一个按钮来清除包含的值(字段内的 X 图标),并在代码示例中用一个流行的名称预设文本值(至少在我的书和讲座中,尤其是讲座中)。图 4-2 显示了结果。
图 4-2
带有自动选择、自动对焦、清除按钮和默认值的TextField
这里,有另一套有用的方法:
textField.setRequired(true);
textField.setMinLength(2);
textField.setMaxLength(10);
textField.setPattern("^[a-zA-Z\\s]+");
textField.setErrorMessage("Letters only. Min 2 chars");
这段代码激活一个可视指示器,让用户知道该字段需要一个值,并且不能留空。它还设置字段中允许的最小和最大长度,以及该值必须匹配的模式。如果模式与正则表达式不匹配,组件会显示一条错误消息。
Note
Java API for regular expressions 是在 Java 1.4 中引入的,已经成为搜索和操作文本和数据的强大工具。有很多关于这个主题的好资源。Oracle 的 Java 教程有一堂关于正则表达式的精彩课程。您可以在 https://docs.oracle.com/javase/tutorial/essential/regex 访问该课程。 Java 正则表达式 (Apress,2004)是一本讲述 Java 正则表达式引擎的经典之作。JRebel 的备忘单(可从 www.jrebel.com/resources/java-regular-expressions-cheat-sheet 获得)是我在使用正则表达式时参考的另一个资源。
Vaadin 组件使用事件侦听器与服务器进行交互。例如,假设我们想要检查前面示例的文本字段中的值,并在值无效时显示通知。实现这一点的一种方法是在字段中添加一个值改变监听器:
textField.addValueChangeListener(event -> {
if (textField.isInvalid()) {
Notification.show("Invalid name");
}
});
当您按 ENTER 键或通过单击字段外部或按 TAB 键来移除焦点时,将执行 lambda 表达式。例如,当输入中有数字或少于两个字符时,lambda 表达式本身检查字段中的值是否无效,如果是,则显示一个通知。见图 4-3 。
图 4-3
在TextField中输入验证
如果您希望在每次值发生变化时都进行处理,而不是等待回车键或焦点丢失发生,您可以按如下方式进行配置:
textField.setValueChangeMode(ValueChangeMode.EAGER);
查看ValueChangeMode中每个常量的 Javadoc,了解所有可用的配置选项。
Tip
虽然大多数 ide 都能够显示 Javadoc 文档和外部库的源代码,但是你也可以在网上的 https://vaadin.com/api 找到。有关如何显示 Javadoc 或导航外部库的源代码的详细信息,请参考 IDE 文档。
密码输入
PasswordField类包含了TextField的大部分功能,但是提供了一种方便的方式来启用密码输入。这里有一个例子:
PasswordField passwordField = new PasswordField();
passwordField.setLabel("Password");
passwordField.setRevealButtonVisible(true);
在浏览器中,输入字符串显示为一组点。参见图 4-4 。
图 4-4
答PasswordField
如果启用了“显示”按钮(如前面的示例),用户可以按“眼睛”按钮来查看输入的密码。参见图 4-5 。
图 4-5
一个PasswordField随着它的内容显露出来
当您通过调用getValue()方法获得值时,您获得了字段中引入的实际文本(显然不是一堆点):
passwordField.addValueChangeListener(event -> {
String password = passwordField.getValue();
System.out.println("Password: " + password);
});
您还可以从event参数中获取字段值,这样就可以将代码从包含该值的实际字段中分离出来:
passwordField.addValueChangeListener(event -> {
String password = event.getValue(); // like this
System.out.println("Password: " + password);
});
这种技术适用于本章描述的所有输入字段。
布尔输入
Checkbox类封装了一个布尔值。这里有一个基本的例子:
Checkbox checkbox = new Checkbox();
checkbox.setLabelAsHtml("I'm <b>learning</b> Vaadin!");
checkbox.addValueChangeListener(event -> {
Boolean value = event.getValue();
Notification.show("Value: " + value);
});
这一次,代码使用 HTML 字符串设置标签。如果不需要显示 HTML,仍然可以使用常规的setLabel(String)方法。
Caution
建议尽可能使用纯文本,以避免跨站点脚本攻击(也称为 XSS 攻击)。确保在将包含 HTML 的字符串传递给 UI 之前对其进行净化,尤其是当它们来自用户输入或外部服务时。开放 Web 应用安全项目(OWASP)为此提供了一个 Java 库。可以在 https://owasp.org/www-project-java-html-sanitizer 了解更多。
图 4-6 显示了点击组件后上一个例子的截图。
图 4-6
浏览器中呈现的一个Checkbox
Checkbox类可以采取一个初始的未确定状态来直观地提示用户他们还没有点击复选框。这不会影响复选框中存储的值。这符合复选框的 HTML5 标准。例如,以下代码将该值设置为 true,激活未确定状态,并在通知中显示该值:
checkbox.setValue(true);
checkbox.setIndeterminate(true);
Boolean initialValue = checkbox.getValue();
Notification.show("Initial value: " + initialValue);
注意图 4-7 中显示的值是true(不是null,不是false)。
图 4-7
一个状态不确定的Checkbox
因为初始值是true,一旦你点击复选框,它的值就会变成false。
日期和时间输入
DatePicker类允许你显示一个日期输入的输入框。让我们使用类中的几个构造函数之一,而不是使用缺省的构造函数(当你需要它的时候它是可用的)。研究下面的例子:
@Route("datepicker")
public class DatePickerView extends Composite<Component> {
@Override
protected Component initContent() {
DatePicker datePicker = new DatePicker(
"Enter a memorable date",
LocalDate.now(),
event -> showMessage(event.getValue())
);
return new VerticalLayout(datePicker);
}
private void showMessage(LocalDate date) {
Notification.show(
date + " is great!"
);
}
}
构造函数接受一个字段标签(相当于使用setLabel(String)方法),后跟一个最初显示的日期值(相当于使用setValue(LocalDate)方法),以及一个值更改监听器(相当于使用addValueChangeListener(ValueChangeListener)方法)。这一次,侦听器被实现为一个 lambda 表达式,该表达式调用一个方法并从event对象获取值(用户选择的日期)。图 4-8 显示了这个例子的截图。
图 4-8
浏览器中呈现的一个DatePicker
以下是一些可用于进一步配置日期选取器的方法:
// shows the calendar only when clicking on the calendar icon
// not when clicking the field
datePicker.setAutoOpen(false);
// shows an X button to clear the value
datePicker.setClearButtonVisible(true);
// sets the date that's visible when the calendar is opened
// * works only when no date value is set
datePicker.setInitialPosition(LocalDate.now().minusMonths(1));
// sets the minimum and maximum dates
datePicker.setMin(LocalDate.now().minusMonths(3));
datePicker.setMax(LocalDate.now().plusMonths(3));
web 应用中的一个常见需求是配置日期格式,不仅在输入组件中使用,而且在显示日期的任何地方都使用。实现这一点的最佳方式是设置组件的区域设置。例如,如果应用将由加拿大官员或政府机构使用,您可以按如下方式设置区域设置:
datePicker.setLocale(Locale.CANADA);
如果未设置语言环境,Vaadin 会在创建新会话时配置浏览器报告的语言环境。
Caution
在写这本书的时候,没有办法直接设置一个DatePicker的日期格式。更多信息见 https://github.com/vaadin/vaadin-date-picker-flow/issues/156 期。
如果需要用户输入时间,可以使用TimePicker类:
TimePicker timePicker = new TimePicker("Pick a time");
timePicker.addValueChangeListener(event -> {
LocalTime value = event.getValue();
Notification.show("Time: " + value);
});
存储在字段中的值属于类型LocalTime。当您需要获取日期和时间时,您可以使用DateTimePicker类:
DateTimePicker dateTimePicker = new DateTimePicker("When?");
图 4-9 显示了时间组件。DateTimePicker显示的是DatePicker旁边的TimePicker。
图 4-9
浏览器中呈现的一个TimePicker和一个DateTimePicker
数字输入
NumberField 类允许您从用户那里获取一个Double值。这里有一个例子:
NumberField numberField = new NumberField("Rating");
numberField.setHasControls(true);
numberField.setMin(0.0);
numberField.setMax(5.0);
numberField.setStep(0.5);
numberField.setClearButtonVisible(true);
numberField.setHelperText("From 0.0 to 5.0");
numberField.addValueChangeListener(event -> {
Double value = event.getValue();
Notification.show("Your rating: " + value);
});
大多数方法都是不言自明的。您可以启用控件的可视化,以允许用户使用字段中的+和–按钮调整数值,方法是根据步长值增加或减少数值。方法允许你显示如何使用这个字段的提示。见图 4-10 。
图 4-10
浏览器中呈现的一个NumberField
如果你需要用Integer而不是Double输入,那就用IntegerField类来代替。它有一个类似的 API。
单项选择输入
有几个组件以在定义的值之间进行选择的形式获得输入。例如,您可以使用ComboBox类询问用户他们在公司的哪个部门工作:
ComboBox<String> comboBox = new ComboBox<>("Department");
comboBox.setItems("R&D", "Marketing", "Sales", "HR");
结果如图 4-11 所示。
图 4-11
浏览器中的一个ComboBox
可用值通过使用String作为参数类型的setItems(T...)方法来设置。您可以使用任何类型作为参数。例如,您可能有一个封装部门的类或枚举,而不是将它们作为字符串:
public class Department {
private Integer id;
private String name;
public Department(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return name;
}
}
在这种情况下,您可以如下使用ComboBox类:
ComboBox<Department> comboBox = new ComboBox<>("Department");
comboBox.setItems(
new Department(1, "R&D"),
new Department(2, "Marketing"),
new Department(3, "Sales")
);
有时,您必须配置每个选项在浏览器中显示的字符串。默认情况下,ComboBox类使用被指定为参数的类的toString()方法(本例中为Department)。您可以按如下方式覆盖它:
comboBox.setItemLabelGenerator(Department::toString);
此外,您可以包含任何自定义逻辑来生成每个字符串值:
comboBox.setItemLabelGenerator(department -> {
String text = department.getName() + " department";
return text;
});
结果如图 4-12 所示。
图 4-12
自定义项目标签生成器
在前面的例子中,我通过创建Department类的新实例来设置项目。这只是为了向您展示在输入字段中使用 JavaBean 作为条目的想法。在实际的应用中,您最有可能得到一个对象的Collection,您可以将它传递给setItems方法的一个版本。例如,如果您有一个服务类,代码可能如下所示:
List<Department> list = service.getDepartments();
ComboBox<Department> comboBox = new ComboBox<>("Department");
comboBox.setItems(list);
这种可能性是使 Vaadin 如此强大的特性之一——您可以用普通 Java 直接从表示层访问您的后端。服务类可以使用 Hibernate/JPA(Jakarta Persistence API)、Spring、CDI(Contexts and Dependency Injection)等技术,以及在庞大且久经考验的 Java 生态系统中可用的许多其他技术。
Vaadin 中还有两个单选输入字段— RadioButtonGroup和ListBox。这里有一个RadioButtonGroup的例子:
RadioButtonGroup<Department> radio = new RadioButtonGroup<>();
radio.setItems(list);
ListBox组件以类似的方式工作:
ListBox<Department> listBox = new ListBox<>();
listBox.setItems(list);
这两个类的 API 类似于ComboBox中的 API,这使得它们几乎完全可以互换。图 4-13 显示了浏览器中的这些组件。
图 4-13
Vaadin 中的单选输入字段
多重选择输入
要允许用户从列表中选择一个或多个项目,可以使用CheckboxGroup和MultiSelectListBox类。这里有一个例子:
CheckboxGroup<Department> checkboxes = new CheckboxGroup<>();
checkboxes.setItems(service.getDepartments());
MultiSelectListBox<Department> listBox = new MultiSelectListBox<>();
listBox.setItems(service.getDepartments());
因为这些组件允许用户选择几个值,getValue()方法返回一个包含所选值的Set:
Set<Department> departments = listBox.getValue();
图 4-14 显示了浏览器中的这些组件。
图 4-14
Vaadin 中的多选输入字段
文件上传
Upload类允许你显示一个输入域,将文件从用户的机器传输到服务器。它支持单个或多个文件,并从桌面拖放。下面是基本用法:
MemoryBuffer receiver = new MemoryBuffer();
Upload upload = new Upload(receiver);
upload.addSucceededListener(event -> {
InputStream in = receiver.getInputStream();
... read the data from in ...
});
MemoryBuffer类是Receiver接口的一个实现。该接口用于向Upload组件提供一种写入上传数据的方式。有几种实现方式:
-
MemoryBuffer:将数据存储在存储器中 -
FileBuffer:使用File.createTempFile(String, String)方法创建一个File -
MultiFileMemoryBuffer:多个文件上传,将数据存储在内存中 -
MultiFileBuffer:使用File.createTempFile(String, String)方法创建多个File
假设我们想做一个程序来计算字母 A 在一个纯文本文件中出现的次数。这需要限制接受文件的类型并处理其内容。让我们从处理部分开始:
upload.addSucceededListener(event -> {
InputStream in = receiver.getInputStream();
long count = new Scanner(in).findAll("[Aa]").count();
Notification.show("A x " + count + " times");
});
当用户上传一个文件时,监听器从Receiver获取InputStream,并使用 Java Scanner查找字母 a 的所有出现次数,然后在通知中显示结果。参见图 4-15 。
图 4-15
浏览器中呈现的一个Upload组件
当用户点击上传文件时...点击按钮,会出现一个标准的文件选择器,用户可以在他们的硬盘中选择一个文件。默认情况下允许所有文件。要对此加以限制,您可以设置想要接受的 MIME 类型。例如,纯文本文件的 MIME 类型是text/plain:
upload.setAcceptedFileTypes("text/plain");
图 4-16 显示了如何在 macOS 系统上禁用(灰显)图像文件(JPG)供选择。
图 4-16
macOS 上仅接受文本文件的文件选择器示例
交互组件
交互组件允许用户触发应用中的动作。例如,在表单中保存数据或导航到另一个 URL。
小跟班
在前一章我们已经看到了按钮,但是我们并没有真正研究它们。到目前为止,您很可能已经猜到了按钮的基本用法(如果我已经很好地完成了我的工作,至少,我真心希望我做到了)。无论如何,这就是:
Button button = new Button("Time in the server, please");
button.addClickListener(event ->
Notification.show("Sure: " + LocalTime.now())
);
代码很容易理解。当单击按钮时,lambda 表达式中的代码在服务器中执行。您可以使用构造函数添加点击侦听器:
Button button = new Button("Time in the server", event ->
Notification.show("Sure: " + LocalTime.now())
);
Tip
我更喜欢通过使用相应的方法而不是构造函数来显式地添加点击侦听器和任何其他类型的事件侦听器。在书籍、教程和演示中,我使用更紧凑的版本,因为我在那里(虚拟地或亲自地)解释代码。然而,在现实生活中的项目中,代码的大小和复杂性可能会增加,我喜欢代码为我说话的清晰性。不仅帮助其他开发者,也帮助我自己。我记得有一次,我试图找到某个按钮被点击时执行的代码。我用我的 IDE 在一堆源文件中搜索“clickListener”。幸运的是,我只有一根火柴。不幸的是,这是错误的按钮。有问题的按钮有一个来自数据库的标题,所以我无法通过它进行搜索。这个按钮可能被命名为“按钮”、“b”或类似的名称。再做一点工作,我找到了实例,并注意到 click listener 是使用构造函数添加的。
假设您希望应用只显示一次时间(为了简单起见,每次页面刷新)。您可以使用setEnabled(boolean)方法隐藏点击监听器中的按钮,如下所示:
Button button = new Button("Time in the server", event -> {
Notification.show("Sure: " + LocalTime.now());
event.getSource().setVisible(false);
});
我们不能使用button实例,因为它还没有被定义。相反,您可以使用getSource()方法来获取触发事件的组件。
或者,您可以调用setEnabled(false)来禁用按钮,而不是完全隐藏它,以避免改变视图的整体外观。如果是这种情况,在点击后禁用按钮的更好方法是使用setDisableOnClick(boolean)方法:
button.setDisableOnClick(true);
图 4-17 显示了浏览器中的示例。
图 4-17
浏览器中呈现的禁用的Button
事件是如何发送到服务器的?
当用户单击添加了侦听器的组件时,单击侦听器允许您在服务器中运行代码。事实上,当浏览器中发生某个动作时,所有侦听器都会调用服务器中的代码,例如,单击、获得焦点、按键,甚至当组件附加到视图时。您将在 Vaadin 的许多 UI 组件中找到这些事件的方法。
我们知道所有与 web 应用的交互都是通过 HTTP 请求发生的(或者我们将在第八章看到的 WebSocket)。如果我们使用网络浏览器的开发工具,我们可以看到当你点击一个按钮时发出的请求。图 4-18 显示了当你点击前一个例子中的按钮时,Vaadin 发送给服务器的请求。
图 4-18
点击事件后的 HTTP 请求
请求 URL 包括两个参数。您可以按如下方式读取这些参数及其值:
-
V-R = uidl:AVaadinRequest of typeUserIinterfaceD定义 L 语言这表明该请求旨在处理 UI 状态的变化。
-
V-uiId = 11:VaadinUserI接口 Id 标识符。这指示已更改的用户界面的数字标识符;在这种情况下,UI 的标识符被指定为 11。如果您在不同的浏览器选项卡中打开视图,您应该得到不同的值。
这些参数指向VaadinServlet类,该类又委托给更专门的类。然后,Vaadin 获取请求的有效负载,并相应地对其进行处理。有效载荷以 JSON 格式发送,如图 4-19 所示。
图 4-19
uidl 请求的负载
有效负载包括 UI 中关于更改的所有细节。在这种情况下,单击事件。它还包括事件发生的节点(或组件)(在本例中,按钮被分配了 ID 5)。有了这些信息,Vaadin 就可以在组件树中导航,找到 ID 为 5 的组件,并调用任何可能已经添加到其中的 click 侦听器。
如果您检查响应(在图 4-19 所示的响应选项卡中),您将会看到如下内容(出于清晰和间隔的原因,大部分内容已被省略):
for (;;);
[{
"syncId": 2,
"clientId": 2,
...
"changes": [{
"node": 5,
"type": "put",
"key": "disabled",
"feat": 3,
"value": ""
},
...
{
"node": 8,
"type": "put",
"key": "innerHTML",
"feat": 1,
"value": "Sure: 17:19:17.323399"
}, {
"node": 9,
"type": "attach"
}, {
"node": 9,
"type": "put",
"key": "tag",
"feat": 0,
"value": "vaadin-notification"
},
...
],
"timings": [668, 2]
}]
响应也是 JSON 格式的,包括请求处理后应该在视图中进行的更改的信息。例如,响应声明应该禁用节点 ID 5(按钮),并且应该附加文本为“Sure: 17:19:17.323399”的通知(如图所示)。这些正是我们在前一节的例子中在点击监听器中编程的东西。
链接
链接(或锚点)允许用户请求不同的 URL。它可以是应用中的 URL,也可以是外部网站。这里有一个不要脸塞的例子:
Anchor blogLink = new Anchor("https://www.programmingbrain.com",
"Visit my technical blog");
您可以使用 UI 组件来代替字符串:
Anchor vaadinLink = new Anchor("https://vaadin.com",
new Button("Visit vaadin.com"));
最后,您可以在运行时生成内容。例如,以下代码创建一个链接,该链接指向运行时在服务器中生成的文本文件:
Anchor textLink = new Anchor(new StreamResource(
"text.txt",
() -> {
String content = "Time: " + LocalTime.now();
return new StringInputStream(
content, Charset.defaultCharset());
}
), "Server-generated text");
当用户点击最后一个锚点时,会生成一个名为 text.txt 的新文件,并将其返回给浏览器。Chrome 将这个文件下载到客户机的硬盘上。图 4-20 显示了之前示例中的锚。
图 4-20
浏览器中的Anchor组件
菜单
虽然在移动优先的应用中并不流行,但菜单在商业应用中有很大的用途,这些应用通常几乎只在桌面浏览器上使用。即使在移动优先的应用中,你也可以找到显示“...”的典型按钮这告诉用户有更多的选择。Vaadin 包括在 web 应用中显示顶级、多级和上下文菜单的组件。
从顶层开始,使用以下结构构建菜单:
MenuBar > MenuItem > SubMenu > MenuItem > SubMenu > MenuItem > ...
一旦你研究了一个例子,这就变得更有意义了:
MenuBar menuBar = new MenuBar();
MenuItem file = menuBar.addItem("File");
file.getSubMenu().addItem("New");
file.getSubMenu().addItem("Open");
MenuItem edit = menuBar.addItem("Edit");
edit.getSubMenu().addItem("Copy");
edit.getSubMenu().addItem("Paste");
结果如图 4-21 所示。
图 4-21
两级菜单
每个包含文本的选项都是一个MenuItem。如果你想创建更多的关卡,你必须从一个MenuItem中获取SubMenu并向其添加更多的MenuItem对象。您可以根据需要设置多个级别。
使用MenuItem的构造函数或addClickListener(ComponentEventListener)方法添加一个点击监听器,当用户点击一个项目时执行代码:
edit.getSubMenu().addItem("Copy", event ->
Notification.show("Copy selected"));
MenuItem paste = edit.getSubMenu().addItem("Paste");
paste.addClickListener(event ->
Notification.show("Paste selected"));
您还可以使用setEnable(boolean)方法启用或禁用项目,或者使用setCheckable(boolean)方法使它们可切换。
要添加上下文菜单(当用户右键单击一个组件时显示的菜单),可以使用ContextMenu类并指定一个目标,通常是一个布局组件,用户可以右键单击它来查看菜单:
HorizontalLayout target = new HorizontalLayout(
new Text("Right click here")
);
ContextMenu contextMenu = new ContextMenu(target);
contextMenu.addItem("Copy");
contextMenu.addItem("Paste");
您可以将 click listeners 添加到项目中,并根据需要在上下文菜单中设置多个级别。图 4-22 显示了浏览器中的上下文菜单。
图 4-22
浏览器中的一个ContextMenu
可视化组件
本章的最后一节介绍了交互式组件,最后一节介绍了一些交互性不强的组件,它们有助于丰富向用户呈现数据的方式。让我们来看看它们。
通知和对话框
在这一章中,我们一直在使用Notification类,但是它比我们目前所看到的要多。为了了解它的更多特性,让我们定义一些需求。假设我们必须显示一个通知
-
单击视图中的按钮时会显示
-
如果是第一次显示,则保持可见
-
可以通过点击里面的按钮来关闭
-
如果之前显示过,2 秒钟后消失
-
显示在页面的中央
事实是,我认为你有知识和经验来实现这一点。你敢自己去实施吗?试试看!
当你决定尝试这个练习时,让我给你讲个小故事。我在芬兰图尔库的一个下雨的周六晚上写下了这篇文章。我的全职工作没有让我在工作日有太多时间来写这本书,所以我决定在周六全力以赴完成这一章。公式很简单——我从一个代码示例开始,然后将我对它的抽象想法转化为文字。棘手的部分是实际开始写代码和段落,但一旦我开始,想法和文字就流动起来,我就把事情做完了。然后就是练习,到最后,练习总会有回报。现在去打开你的 IDE,用 Vaadin 做一些练习。
我接受的不做这个练习的唯一借口是,你正在飞机上读这篇文章,没有笔记本电脑或互联网连接。因此,清单 4-1 给出了一个可能的解决方案。我希望你的比我的更好。
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@Route("notification")
public class NotificationView extends Composite<Component> {
private boolean firstTime = true;
@Override
protected Component initContent() {
return new VerticalLayout(
new Button("Notification", event -> {
Notification notification = new Notification();
notification.add(new VerticalLayout(
new Text("Here it is!")));
notification.setPosition(Notification.Position.MIDDLE);
if (firstTime) {
notification.setDuration(0);
notification.add(new Button("Close", e ->
notification.close()));
} else {
notification.setDuration(2000);
}
firstTime = false;
notification.open();
})
);
}
}
Listing 4-1Some of the features of the Notification class
弹出对话框类似于通知,但是它们有更多的功能。这里有一个基本的例子:
new Dialog(
new VerticalLayout(
new H2("Title"),
new Text("Text!"),
new Button("Button!!!")
)
).open();
默认情况下,用户可以在对话框打开时与应用的其他部分进行交互。您可以通过使其模式化来改变这一点:
dialog.setModal(true);
如果您想避免当用户在对话框外单击时对话框消失,您可以使用以下方法:
dialog.setCloseOnOutsideClick(false);
在这种情况下,您可能应该提供一种关闭对话框的方法。一种方法是使用 ESC 键关闭它:
dialog.setCloseOnEsc(true);
最后,您可以使对话框可拖动,如下所示:
dialog.setDraggable(true);
图 4-23 显示了浏览器中的一个对话框。
图 4-23
浏览器中呈现的一个Dialog
制表符
选项卡是允许用户在与共享主题相关的视图之间移动的好方法。在 Vaadin 中,选项卡只是一种以选项卡样式显示一组“按钮”的方式,由程序员决定对这些按钮上的单击做出反应,最有可能的是,当每个选项卡按钮被单击(或选择)时,在视图中显示不同的布局。这里有一个基本的例子:
Tab order = new Tab("Order");
Tab delivery = new Tab("Delivery");
Tabs tabs = new Tabs(order, delivery);
结果如图 4-24 所示。
图 4-24
在浏览器中呈现的Tabs组件
标签的内容在哪里?就像我之前提到的,Tabs组件不包含这个功能。不幸的是。
Tip
在 Vaadin 8 和更早的版本中,选项卡组件的 API 包括一个容器和所有逻辑,当选择一个选项卡时,这些逻辑可以在“页面”之间自动切换。如果您来自这些版本的 Vaadin,并且错过了这种行为,您可能想看看由本书作者开发和维护的位于 https://vaadin.com/directory/component/paged-tabs 的 Vaadin 目录中的开源免费分页标签组件。哈!两个无耻的插头在一个章节里。
要使标签有用,您必须实现逻辑,使它们按照您的意愿工作。最常见的是,这意味着当选项卡被选中时显示不同的组件。这可以使用侦听器来实现。这里有一个例子:
VerticalLayout tabsContainer = new VerticalLayout();
Tab order = new Tab("Order");
Tab delivery = new Tab("Delivery");
Tabs tabs = new Tabs(order, delivery);
tabs.addSelectedChangeListener(event -> {
Tab selected = event.getSelectedTab();
tabsContainer.removeAll();
if (order.equals(selected)) {
tabsContainer.add(buildOrderTab());
} else if (delivery.equals(selected)) {
tabsContainer.add(buildDeliveryTab());
}
});
buildOrderTab()和buildDeliveryTab()方法可以返回包含您想要显示的组件的任何布局。该代码使用 if else if 结构,您可以根据需要向其中添加更多事例。或者,您可以使用一个Map来匹配标签和布局,以显示或实现您想要在布局之间切换的任何算法。你可以在 https://vaadin.com/components/vaadin-tabs/java-examples 找到例子。
Caution
前面的例子有一个错误。当您请求查看时,订单选项卡不显示其内容。选择更改侦听器尚未启动。你能解决这个问题吗?提示:将逻辑提取到一个单独的方法中。
核标准情报中心
Vaadin 带有一套 600 多个现成的图标。您可以创建图标并将其添加到任何布局中:
Icon icon = VaadinIcon.YOUTUBE.create();
layout.add(icon);
像大多数 UI 组件(包括我们在本章中看到的所有组件)一样,您可以设置它的大小:
icon.setSize("4em");
许多 UI 组件都包含了setIcon(Icon)方法。例如,下面是如何将图标添加到按钮:
Button button = new Button("Edit");
button.setIcon(VaadinIcon.EDIT.create());
您可以将这两行合并成一行:
Button button = new Button("Edit", VaadinIcon.EDIT.create());
图 4-25 显示了这些组件的屏幕截图。
图 4-25
浏览器中呈现的 Vaadin 图标
Tip
在 https://vaadin.com/components/vaadin-icons/java-examples 可以看到所有图标。
形象
在 Vaadin 应用中有两种显示图像的方式。如果您有一个指向图像文件的 URL(内部或外部),您可以简单地创建一个图像组件,如下所示:
Image photo = new Image(
"https://live.staticflickr.com/65535/50969482201_be1163c6f1_b.jpg",
"Funny dog"
);
photo.setWidthFull();
在构造函数中的 URL 之后,您可以(也应该)传递一个替换文本,如果图像无法显示,web 浏览器可以使用该文本。前面的示例还设置了图像的宽度,以使用尽可能多的水平空间。
如果图像文件来自数据库或文件系统,您可以使用InputStream来读取数据。例如,如果您将一个文件放在 Maven 项目的标准 resources 目录中,您可以创建一个如下所示的映像:
StreamResource source = new StreamResource("logo", () ->
getClass().getClassLoader()
.getResourceAsStream("vaadin-logo.png")
);
Image logo = new Image(source, "Logo");
logo.setWidth("20%");
图 4-26 可以看到 Vaadin 的 logo 和搞笑狗。
图 4-26
浏览器中呈现的两个Image组件
摘要
本章向您介绍了在 Vaadin 中用于数据输入、交互和可视化的大多数 UI 组件的关键知识。您了解了输入组件如何工作,如何从用户那里获得不同类型的值,包括字符串、日期、文件等等。当用户与应用交互时,JSON 消息是如何在客户机和服务器之间发送的。您还了解了如何显示通知、弹出对话框、图标和图像,但是请记住,Vaadin 中的 UI 组件比本章的篇幅所能显示的要多。Vaadin 在不断发展,因此新版本中会添加新的 UI 组件。您可以在 https://vaadin.com/components 浏览所有组件及其 API。
下一章将探讨业务应用中的一个关键概念:数据绑定——一种将 Java bean 中的值与输入 UI 组件中的值连接起来的机制。