Vaadin 实践教程(三)
八、服务器推送
服务器推送是 Vaadin 中的一个特性,它允许您在不需要用户交互的情况下更新 UI。例如,您可能希望显示服务器上正在运行的任务的进度,或者通知用户有新的工作项目可用。
在 Vaadin 中激活和使用服务器推送非常简单,只需要一个注释和一个对UI:access(Command)方法的调用。在本章中,您将了解什么是服务器推送,何时使用,如何使用,以及可供您使用的配置选项。
什么时候使用服务器推送?
当您希望从不同于服务器为处理来自客户端的请求而创建的线程的线程中对 UI 进行更改时,可以使用服务器推送。例如,如果您有一个在服务器中执行长期运行任务的按钮,您可能希望创建一个新的Thread来运行任务逻辑并立即将控件返回给浏览器,以便用户可以继续使用应用中的其他功能。任务完成后,您可以在 UI 中显示结果。您不能简单地从新线程更新 UI,但幸运的是,这是服务器推送解决的问题。
让我们看一个例子来理解为什么以及何时必须使用服务器推送。看看这个视图的实现:
@Route("no-push")
public class NoPushView extends Composite<Component> {
private VerticalLayout layout;
@Override
protected Component initContent() {
layout = new VerticalLayout(
new Button("Run long task", event -> runLongTaks()),
new Button("Does this work?", event -> addText()));
return layout;
}
private void runLongTaks() {
try {
Thread.sleep(5000);
Notification.show("Task completed.");
} catch (InterruptedException ignored) {
}
}
private void addText() {
layout.add(new Paragraph("It works!"));
}
}
该视图包含一个带有 click listener 的按钮,该按钮模拟一个需要 5 秒钟才能完成的长时间运行的任务。5 秒钟后,代码尝试在 UI 中显示通知。还有一个按钮可以简单地向布局添加文本。如果您运行这个应用,您将看到 UI 被锁定了 5 秒钟。Vaadin 的客户端引擎检测到请求耗时过长,并显示一个进度条(见图 8-1 )。
图 8-1
长时间运行的任务会导致浏览器中显示进度条
你还会注意到*是这样工作的?显示进度条时,*按钮似乎不起作用。然而,5 秒钟后,通知显示在浏览器中,与 UI 的其他交互的结果也发生了(参见图 8-2 中添加到布局中的文本)。这表明,即使客户端可以发送服务器成功处理的附加请求,长时间运行的任务也会阻止对 UI 的更改。
图 8-2
在服务器中长时间运行任务后,用户界面立即发生变化
让我们向改进应用迈出一步。由于任务需要很长时间才能完成,我们可以将逻辑转移到一个单独的线程中。这允许初始线程(由服务器启动)完成,并让 Vaadin 处理请求并立即返回响应。我们还可以在启动新线程之前通知用户任务正在运行:
private void runLongTaks() {
Notification.show("Running the task...");
new Thread(() -> {
try {
Thread.sleep(5000);
Notification.show("Task completed.");
} catch (InterruptedException ignored) {
}
}).start();
}
不幸的是,如果您尝试该应用,您将永远看不到任务已完成的通知。相反,您将在服务器日志中看到一个异常:
java.lang.IllegalStateException: UI instance is not available. It means that you are calling this method out of a normal workflow where it's always implicitly set. That may happen if you call the method from the custom thread without 'UI::access' or from tests without proper initialization.
服务器推送的工作原理
让我们更详细地研究一下上一节的例子。当您单击按钮时,会创建一个新线程来处理服务器中的请求。点击监听器中的代码在这个线程中执行。这段代码又创建了另一个线程。原始线程结束,浏览器和服务器都准备好处理来自用户的未来 UI 交互。稍后,5 秒任务完成,处理对Notification.show("Task completed.")的调用。但为时已晚。响应已关闭。浏览器不希望有任何更改,因为它已经在 5 秒钟前处理了响应。对用户界面的新更改会丢失。
只有在原始请求的线程中进行更新时,才会对 UI 进行更新。看一下图 8-3 。浏览器中的一个事件(比如单击按钮或更改文本字段的值)会向服务器发出一个请求。通常,这导致 UI 中的变化(例如,显示通知)。这是我们到目前为止使用框架的方式。
图 8-3
更改用户界面的请求和响应
服务器推送是一个使用 Vaadin 中的注释激活的特性(您很快就会了解到),它允许单独的线程更新浏览器中的 UI。图 8-4 说明了这个概念。UI 事件向服务器发送一个请求,服务器产生一个带有 UI 更新的响应。然而,对于服务器推送,如果启动了一个新的单独线程,这个新线程可以发送推送更新,导致 UI 中的可见变化。
图 8-4
从服务器中单独的线程推送更新
执行异步用户界面更新
为了能够使用服务器推送更新 UI,您必须使用@Push注释来启用它。这个注释需要放在实现AppShellConfigurator接口的类中:
@Push
public class AppConfiguration implements AppShellConfigurator {
}
Note
当用户在浏览器中请求应用时,Vaadin 检测并使用实例来配置客户端引擎。
启用服务器推送后,您现在可以向客户端发送更新。这些更新是异步 UI 更新,可以根据您使用的配置手动或自动发送到客户端。
自动服务器推送更新
默认情况下,如果您将更改 UI 的代码包含在一个Command对象中,并调用UI类的access(Command)方法,服务器推送更改会自动发送到客户端。例如,要使上一节中的示例起作用,我们需要这样做:
private void runLongTaks() {
Notification.show("Running the task...");
var ui = UI.getCurrent();
new Thread(() -> {
try {
Thread.sleep(5000);
ui.access(() -> {
Notification.show("Task completed.");
});
} catch (InterruptedException ignored) {
}
}).start();
}
这段代码获取一个对当前UI的引用,我们在启动线程之前初始化这个引用。在原始线程(请求线程)的范围内获得这个引用很重要,因为 Vaadin 使用 Java 的ThreadLocal来存储这些引用。
Tip
总是将在单独线程中运行的任何代码包含在一个UI:access(Command)调用中,并确保从该线程外部获得UI实例。
如果您运行该应用,您不仅可以在长时间运行的任务运行时向布局添加文本,还可以在任务完成 5 秒后显示通知(参见图 8-5 )。
图 8-5
服务器推送更新
手动服务器推送更新
可以控制服务器推送更新发送到客户端的确切时间。为此,您必须使用@Push注释来配置服务器推送模式:
@Push(value = PushMode.MANUAL)
public class AppConfiguration implements AppShellConfigurator {
}
当您从一个单独的线程对 UI 执行更改时,您仍然需要使用access(Command)方法,但是现在您可以在任何时候调用 UI 类的push()方法来将 UI 更改发送到客户端:
doBusinessStuff();
ui.access(() -> {
updateUI();
ui.push();
}
doSomeMoreBusinessStuff();
ui.access(() -> {
updateUI();
ui.push();
}
下面是一个长期运行的任务示例版本,它随着任务的进展更新一个ProgressBar组件:
@Route("manual-push")
public class ManualPushView extends Composite<Component> {
private VerticalLayout layout;
private ProgressBar progressBar = new ProgressBar(0, 10);
private Button button;
@Override
protected Component initContent() {
button = new Button("Run long task", event -> runLongTaks());
button.setDisableOnClick(true);
layout = new VerticalLayout(button,
new Button("Does this work?", event -> addText()),
progressBar);
return layout;
}
private void runLongTaks() {
Notification.show("Running the task...");
progressBar.setValue(0);
var ui = UI.getCurrent();
new Thread(() -> {
try {
for (int i = 0; i <= 10; i++) {
Thread.sleep(1000);
double progress = i;
ui.access(() -> {
progressBar.setValue(progress);
ui.push();
});
}
ui.access(() -> {
Notification.show("Task completed.");
button.setEnabled(true);
ui.push();
});
} catch (InterruptedException ignored) {
}
}).start();
}
private void addText() {
layout.add(new Paragraph("It works!"));
}
}
我们添加了一个值在 0 到 10 之间的ProgressBar。runLongTask()方法中的一个循环每秒更新一次进度条,并使用push()方法将更改发送给客户端。当循环结束时,另一个服务器推送更新被发送到客户机,通知任务完成。
看看我们如何在调用长期运行任务的按钮上调用setDisableOnClick(boolean)方法。当您运行这种任务以防止用户多次启动作业时,这很方便。图 8-6 显示了应用运行任务时的屏幕截图。
图 8-6
从服务器端线程手动更新的用户界面
该示例还展示了如何仅在我们需要更新 UI 时调用access(Command )方法。一个典型的错误是在不需要的时候在Command中调用业务逻辑。例如:
ui.access(() -> {
doBusinessStuff();
updateUI();
ui.push();
}
当业务逻辑需要相当长的时间来运行时,这种负面影响会更加明显。相反,将业务逻辑调用移到Command实现之外。
Caution
access(Command)方法锁定用户会话。这意味着当Command实现中的代码运行时,其他线程不能对 UI 进行更改。
使用线程池
在前面的例子中,我们通过直接创建新的Thread实例来使用线程。Java 线程很昂贵,而且消耗内存。为了强调这一点,让我们做一个实验(改编自 Petter Holmströ的演讲):
public class MaxThreadsExperiment extends Thread {
public static void main(String... args) {
new MaxThreadsExperiment().start();
}
public static final AtomicInteger count = new AtomicInteger();
@Override
public void run() {
try {
System.out.println(count.incrementAndGet());
new MaxThreadsExperiment().start();
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这个 Java 程序递归地创建线程,直到你得到一个OutOfMemoryError。在我用来运行这个实验的虚拟机中,我很快就发现了错误:
...
1994
1995
1996
1997
1998
1999
2000
Exception in thread "Thread-1999" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at MaxThreadsExperiment.run(MaxThreadsExperiment.java:15)
2000 个线程是极限。根据您部署应用的服务器,这个数字可能更小或更大。关键是,考虑到 Servlet 容器创建线程来处理请求,2000(或者当我在我的开发机器上运行它时大约是 4000)对于 web 应用来说可能太少了。线程是有限的资源,应该如此对待。
在软件设计中,池是在使用资源之前创建和初始化资源的地方。池中的客户端可以请求资源、使用资源,然后将其返回到池中。资源可以是数据库连接、服务、文件、线程或任何其他资源。线程池是一个软件实体,它可以创建预先配置好数量的线程,这些线程可以执行提交者发送的任务。
Java 包含了ExecutorService接口以及现成的线程池实现。任务提交者(您的代码)可以提交(执行)任务(Runnable对象),这些任务在线程队列中运行,当池中有线程可用时,在线程中执行。图 8-7 描绘了该过程。
图 8-7
具有四个线程池的执行器服务
因为线程池应该在应用开始产生任务之前准备好,所以初始化它的好地方是一个ServletContextListener。当ServletContext被初始化和销毁时,ServletContextListener接口允许你运行逻辑。
Note
应用启动时会创建一个ServletContext对象。web 应用的每个实例只有一个ServletContext实例。
继续前面几节的例子,我们可以创建一个扩展ServletContextListener的新类,或者使用现有的类(如果有的话)。事实上,我们已经有了一个很好的候选对象,所以我们将使用之前创建的AppConfiguration类来启用服务器推送,而不是创建一个新的类(如果您愿意,您可以这样做)。我们需要做的就是将该类添加到extends列表中,实现这些方法,并用@WebListener标记该类,以允许 Servlet 容器检测该类:
@Push
@WebListener
public class AppConfiguration
implements AppShellConfigurator, ServletContextListener {
private static ScheduledExecutorService executorService;
public static ExecutorService getExecutorService() {
return executorService;
}
@Override
public void contextInitialized(ServletContextEvent event) {
executorService = Executors.newScheduledThreadPool(3);
}
@Override
public void contextDestroyed(ServletContextEvent event) {
executorService.shutdown();
}
}
这里,我们使用了类型为ScheduledExecutorService的静态实例(它实现了ExecutorService)。公共 getter 允许应用的其他部分获得对服务的引用并使用它。我们使用Executors类创建一个新实例,并配置一个有三个线程的线程池。这个数字在这个演示应用中用于演示目的,以便您可以使用它并查看线程的行为,但是您可能应该在生产中使用一个更大的数字,或者甚至在运行时进行配置。此外,为了防止线程“永远”存在于 JVM 中,在应用停止时关闭 executor 服务也很重要
下一步是修改视图以使用线程池(或执行器服务),而不是手动创建和启动线程。以下是重构的结果:
private void runLongTaks() {
Notification.show("Running the task...");
var ui = UI.getCurrent();
AppConfiguration.getExecutorService().execute(() -> {
try {
Thread.sleep(5000);
ui.access(() -> {
Notification.show("Task completed.");
});
} catch (InterruptedException ignored) {
}
});
}
我们获取对ExecutorService的引用,并调用execute(Runnable)方法来提交一个新任务。这个任务将被分配给池中的一个空闲线程(如果有的话),或者排队直到有一个空闲线程可用。尝试应用并点击运行长任务按钮,比如说,十次。查看任务是如何提交的,但三个一批地完成。图 8-8 显示了一个例子。
图 8-8
在三个线程的池中完成的任务
Note
图 8-8 中的结果并不意味着任务三个一批地同时执行。结果是应用使用方式的结果。几个任务被快速发送(通过点击按钮)。因为线程在服务器中休眠了 5 秒钟,所以池中的所有三个线程都变得繁忙。由于任务一个接一个地快速发送,所有的线程几乎同时完成它们的任务。
WebSocket 与长轮询
默认情况下,当您启用服务器推送时,Vaadin 使用 WebSocket 协议将更改发送到客户端。WebSocket 是一种通信协议,就像 HTTP 一样。通过 WebSocket,客户端与服务器建立永久连接,从而实现参与者之间的全双工通信。
作为 WebSocket 的替代方法,您可以通过在@Push注释中设置LONG_POLLING传输来使用 HTTP 进行服务器推送:
@Push(transport = Transport.LONG_POLLING)
public class AppConfiguration implements AppShellConfigurator {
}
轮询是一种技术,客户端通过这种技术不断向服务器发送请求,要求更改 UI。如果客户机-服务器是一个对话,那么常规轮询应该是这样的:
-
客户:我有什么变化吗?
-
**服务器:**否
-
客户:我有什么变化吗?
-
**服务器:**否
-
客户:我有什么变化吗?
-
**服务器:**是。添加文本为“Hello”的通知。
-
**客户:**谢谢。对我来说有什么变化吗?
-
**服务器:**否
-
…
向服务器请求更改的客户端代表 HTTP 请求。这些请求是定期进行的,例如,每隔一定的秒数。如果他们完成了,比如说,每 30 秒,用户界面就会慢慢更新。如果将频率降低到 2 秒,UI 更新会更快。越快对用户越好,但对网络流量最不利(尤其是如果你使用的是收费的云提供商)。
长轮询是一种技术,通过这种技术,客户端以一种智能的方式轮询服务器,以减少请求的数量,同时保持快速的 UI 更新。对于长轮询,客户端向服务器发出请求,服务器保存请求,直到有更改返回。只有在这时,响应才会被发送到客户端,客户端一直在静静地等待,没有发出新的请求。对话应该是这样的:
-
客户:我有什么变化吗?
-
(2 分钟后...)
-
**服务器:**是。添加文本为“Hello”的通知。
-
**客户:**谢谢。对我来说有什么变化吗?
-
…
在 WebSocket 和长轮询之间进行选择取决于您的应用的确切需求、部署它的基础设施以及它的用途。一般来说,WebSocket 效率更高,因此,根据经验,只有当 WebSocket 协议不可用时才使用长轮询,例如,当 web 代理阻止它时。
摘要
在这一章中,你学到了很多关于线程、异步 UI 更新以及客户端和服务器之间的对话的知识。您了解了何时需要使用@Push注释来启用服务器推送。您了解了如何自动向客户机发送服务器推送更新,以及如何手动发送它们。您还了解了线程池以及它们如何帮助您避免臭名昭著的OutOfMemoryError。最后,您大致了解了 WebSocket 和长轮询是如何工作的。
在下一章中,您将通过使用 Vaadin 的 Element API 从服务器控制浏览器中的文档对象模型。
九、元素 API
在第一章中,我们了解了网络平台及其核心技术。这些是在浏览器中驱动网络的技术。Vaadin 抽象出了 Web 平台中的许多概念,但当您需要进入下一个级别时,它不会妨碍您。
Vaadin 10 中引入了元素 API,以允许在浏览器中对 DOM 进行直接的服务器端操作。实际上,Element API 是一组 Java 类,包含读取、创建和修改网页中 HTML 元素的方法。
Vaadin 还包括在从服务器调用的浏览器中执行 JavaScript 的功能。它让您可以访问 HTML 历史 API,并获得浏览器配置的详细信息(供应商、版本、底层操作系统)和更多功能。
创建 DOM 元素
让我们直接进入代码。下面是如何创建一个<div>元素,而不需要输入任何 HTML 代码,只需要服务器端 Java:
@Route("creating-dom-elements")
public class CreatingDomElementsView extends Div {
}
Vaadin 包括诸如Div、Span、H1、H2、H3等类,以及许多其他可以用作 UI 组件起点的类。如果您在浏览器中检查 DOM,您会看到它包含以下内容:
...
<div id="outlet">
<flow-container-root-2521314 id="ROOT-2521314" style="">
<div></div>
</flow-container-root-2521314>
</div>
...
最里面的空<div>是我们的。我们用自己的双手创建了它(显然是通过代码)。为了证明这一点,让我们定义它的id属性:
@Route("creating-dom-elements")
public class CreatingDomElementsView extends Div {
public CreatingDomElementsView () {
Element div = getElement();
div.setAttribute("id", "our-div");
}
}
看看我们如何使用getElement()方法获得对元素的引用(div)。一个Element是浏览器中 HTML 元素的 Java 表示(在本例中是一个<div>元素)。准备好 Java 引用后,我们调用setAttribute(String, String)将id属性设置为our-div。下面是我们在浏览器中看到的内容:
...
<div id="outlet">
<flow-container-root-2521314 id="ROOT-2521314" style="">
<div id="our-div"></div>
</flow-container-root-2521314>
</div>
...
Note
<flow-container-root-X>元素是由 Vaadin 创建的,旁边还有页面中更多的元素和代码。这些都是 Vaadin 的客户端引擎需要的实现细节,用框架实现视图的时候不需要担心。
您不需要创建一个完整的类并扩展 Vaadin 提供的 HTML UI 组件之一来向页面添加更多元素。您可以创建Element类的新实例,并将它们附加到其他实例中。例如,我们可以向我们的<div>添加一个新的<span>元素,如下所示:
public CreatingDomElementsView () {
Element div = getElement();
div.setAttribute("id", "our-div");
Element span = new Element("span");
span.setText("Greetings from the low-level API!");
div.appendChild(span);
}
Element(String)构造函数接收要创建的标签的名称。我们使用setText(String)方法设置了<span>元素的内部文本。浏览器中的 HTML 现在看起来像这样:
...
<flow-container-root-2521314 id="ROOT-2521314" style="">
<div id="our-div">
<span>Greetings from the low-level API!</span>
</div>
</flow-container-root-2521314>
...
可以加个标题吗?没错。动手吧(结果见图 9-1 ):
图 9-1
用 Vaadin 的元素 API 实现的 UI
Element div = getElement();
div.setAttribute("id", "our-div");
Element h1 = new Element("h1");
h1.setText("Element API example");
div.appendChild(h1);
Element span = new Element("span");
span.setText("Greetings from the low-level API!");
div.appendChild(span);
我们可以用我们在前面章节中使用的高级 API(组件 API)来实现这个 UI。事实上,代码要短得多:
@Route("with-components")
public class WithComponentsView extends Div {
public WithComponentsView() {
setId("our-div");
add(
new H1("Component API example"),
new Span("Greetings from the high-level API!")
);
}
}
这突出了使用组件 API 的优势。在你可能需要的情况下,元素 API 就在那里。也许你需要在 HTML 元素中设置一个组件 API 没有提供的属性。或者您正在创建一个新组件或集成一个现有组件。您总是可以从 Vaadin 中的任何一个Component获得一个代表浏览器中 HTML 元素(标签)的Element引用,包括您的定制组件。
Tip
Vaadin 包含了ElementFactory类和有用的静态方法来创建许多标准的 HTML 元素。例如,不是通过使用new Element("span")直接创建Element的实例来创建<span>元素,而是可以调用ElementFactory.createSpan()。
创建自定义组件
如果您想要实现无缝连接到服务器端的新客户端 UI 组件,Element API 是一个有用的工具。我们来开发一个例子。
在前面的章节中,我们使用扩展来创建新的视图。同样的方法可以用来创建 UI 组件。您可以扩展Div、Span、H1、H2、H3、Input或其他在浏览器中呈现相应 HTML 元素的服务器端组件。另一个选择是使用@Tag注释。例如,如果我们想要创建一个定制的服务器端组件,在浏览器中呈现一个<img>元素,我们可以使用下面的代码:
@Tag("img")
public class PictureButton extends Component {
}
@Tag注释告诉 Vaadin,当您将组件的一个实例添加到一个布局中时,应该将哪个标签添加到浏览器中的 DOM 中:
var button = new PictureButton();
var layout = new VerticalLayout(button);
事实上,Vaadin 服务器端组件使用这个标签。例如,如果您检查它的类层次结构,您会发现Button类扩展了GeneratedVaadinButton,而后者又用
@Tag("vaadin-button")
...
public abstract class GeneratedVaadinButton ...
Note
vaadin-button标签是一个定制的 HTML 元素或 Web 组件。所有服务器端的 Vaadin 组件都作为 Web 组件实现。我们不会在本书中深入探讨 Web 组件。现在,知道 web 组件是一组 Web 标准就足够了,它允许开发人员定义新的 HTML 元素,如vaadin-button。可以在非 Java 应用中使用这些 Web 组件。有关这方面的更多细节,请参见 https://vaadin.com/components 中每个组件的 HTML 示例。
让我们回到我们正在开发的定制PictureButton组件。我们已经知道如何设置 HTML 元素的属性,所以让我们使用这些知识来设置<img>元素的src属性:
@Tag("img")
public class PictureButton extends Component {
public PictureButton(String imageUrl) {
getElement().setAttribute("src", imageUrl);
}
}
我们可以创建一个使用这个组件的视图(见图 9-2 ):
图 9-2
用元素 API 实现的自定义组件
@Route("custom-component")
public class CustomComponentView extends Composite<Component> {
@Override
protected Component initContent() {
var button = new PictureButton(
"https://live.staticflickr.com/65535/51154022090_22fd569976_k.jpg");
var layout = new VerticalLayout(button);
layout.setAlignItems(Alignment.CENTER);
return layout;
}
}
好像图像太大了。图片中的生物是一个谜…
式样
Element API 包括Style类,允许您设置和取消设置 CSS 属性。您可以使用getStyle()方法获得对Style类的引用:
Style style = getElement().getStyle();
让我们使用这个对象在我们正在开发的PictureButton组件上设置一些 CSS 样式:
public PictureButton(String imageUrl) {
getElement().setAttribute("src", imageUrl);
Style style = getElement().getStyle();
style.set("border", "1em solid #333");
style.set("box-sizing", "border-box");
style.set("box-shadow", "1em 1em 1em #777");
}
图 9-3 显示了浏览器中的结果,我一直向下滚动到右下角。谜团依然存在。
图 9-3
用Style类设置 CSS 样式
Tip
将boxing-sizing CSS 属性设置为border-box以允许图像周围的边框包含在元素的总宽度和高度中。如果没有它,您可能会在浏览器中看到不希望看到的水平滚动条。
Mixin 接口
通过使用Style类来设置宽度,我们可以马上解决这个谜:
style.set("width", "100%");
或者我可以在截图之前调整浏览器窗口的大小。但是那太无聊了!用前面的代码片段设置宽度可以解决这个问题,但是不会给PictureButton类增加灵活性。如果一个视图需要不同的宽度或大小呢?我们可以重构代码,添加一个公共方法来设置宽度:
public void setWidth(String width) {
getElement().getStyle().set("width", width);
}
我们将不得不做一些类似的事情来获取宽度,获取和设置高度,获取和设置最大宽度和高度,设置全尺寸、全宽度和未定义尺寸的快捷方式…听起来像是很多工作。因为添加这种 API 来管理组件的大小是很常见的,所以 Vaadin 包含了一个带有默认方法的接口,这些方法实现了我们所需要的。这个接口是统称为 mixin 接口的一组接口的一部分。下面是我们如何轻松地向PictureButton类添加调整大小的方法:
@Tag("img")
public class PictureButton extends Component implements HasSize {
...
}
还有许多其他可用的 mixin 接口,无论是否使用@Tag注释,您都可以在任何定制组件中使用它们。这些是其中的一些:
-
HasSize:组件尺寸 -
HasStyle:组件样式 -
ClickNotifier:鼠标点击事件 -
HasEnabled:启用或禁用元素 -
HasElement:获取底层Element实例 -
HasText:获取并设置文本内容注意你可以在
https://vaadin.com/vaadin-reference-card找到更多细节和一个更长的 mixin 接口列表。
使用 mixin 接口时,我们不需要实现任何方法。将接口添加到implements声明足以启用这些特性。有了HasSize,我们现在可以调用我们对VerticalLayout或HorizontalLayout这样的组件使用过的调整方法。例如,我们可以如下设置一个PictureButton组件的宽度:
var button = new PictureButton(
"https://live.staticflickr.com/65535/51154022090_22fd569976_k.jpg");
button.setWidth("65%");
多亏了HasSize界面,我们才能揭开谜底。见图 9-4 。
图 9-4
实现HasSize mixin 接口的定制组件
Note
如果你对图 9-4 中的生物(他也出现在图 4-26 中)感到好奇,他的名字叫德拉科——一只有趣、友好、有时狂热的英国牛头犬,喜欢晒太阳,透过他最喜欢的窗户看人,还喜欢打呼噜。
处理事件
为了处理与 web 浏览器中元素的交互,Element API 包含了addEventListener(String, DomEventListener)方法。第一个参数指定要处理的 DOM 事件的名称。例如,我们可以向Element添加一个点击监听器,如下所示:
getElement().addEventListener("click", event -> {
... server-side logic here ...
});
Note
有许多事件类型,不可能在本书中一一列举。要探索选项,请前往 https://developer.mozilla.org/en-US/docs/Web/Events 。
我们可以用这个方法给PictureButton组件添加一个按钮效果。当用户在图像上按下鼠标按钮时,我们可以移除阴影并缩小组件的大小,以产生按钮被按下的效果。
@Tag("img")
public class PictureButton extends Component implements HasSize {
public PictureButton(String imageUrl) {
getElement().setAttribute("src", imageUrl);
Style style = getElement().getStyle();
style.set("border", "1em solid #333");
style.set("box-sizing", "border-box");
String shadow = "1em 1em 1em #777";
style.set("box-shadow", shadow);
getElement().addEventListener("mousedown", event -> {
style.set("transform", "scale(0.93)");
style.remove("box-shadow");
});
}
}
我们使用 CSS 转换来缩小<img>元素的比例,并删除其box-shadow属性。当mousedown事件在元素上发生时,运行这个逻辑。该事件不同于click事件。当鼠标按钮被按下但在放开之前,触发mousedown事件。在完全单击鼠标按钮后,触发click事件。图 9-5 显示了mousedown事件触发时的组件。
图 9-5
处理一个mousedown事件
此时,PictureButton保持被按下的状态(没有阴影效果,尺寸缩小),所以不能再进行点击。当用户释放按钮时,我们需要重置元素的阴影并缩放回原始状态。这可以通过为mouseup事件添加一个监听器来实现:
getElement().addEventListener("mouseup", event -> {
style.set("transform", "scale(1)");
style.set("box-shadow", shadow);
});
这里仍然有一个边缘案例。如果用户在图像上按下鼠标按钮,将指针拖出图像,然后释放鼠标按钮,按钮将保持按下状态。为了解决这个问题,我们需要运行我们在mouseup上运行的相同逻辑,但是这一次,当鼠标指针离开图像时(pointerleaves)。由于逻辑是相同的,我们可以将事件监听器分配给一个变量,并将其用于mouseup和pointerleaves事件:
DomEventListener listener = event -> {
style.set("transform", "scale(1)");
style.set("box-shadow", shadow);
};
getElement().addEventListener("mouseup", listener);
getElement().addEventListener("pointerleave", listener);
没有添加外部点击监听器选项的按钮不是一个好按钮。我们希望让PictureButton类的客户端添加一个监听器,这样它们就可以对点击事件做出反应。为此,我们可以接受一个SerializableConsumer形式的监听器,并在点击事件触发时调用它(通过调用它的accept(T)方法)。作为参考,下面是PictureButton类的完整实现,包括添加外部点击监听器的可能性:
@Tag("img")
public class PictureButton extends Component implements HasSize {
public PictureButton(String imageUrl,
SerializableConsumer<DomEvent> clickListener) {
getElement().setAttribute("src", imageUrl);
Style style = getElement().getStyle();
style.set("border", "1em solid #333");
style.set("box-sizing", "border-box");
String shadow = "1em 1em 1em #777";
style.set("box-shadow", shadow);
getElement().addEventListener("click", clickListener::accept)
.addEventData("event.clientX")
.addEventData("event.clientY");
getElement().addEventListener("mousedown", event -> {
style.set("transform", "scale(0.93)");
style.remove("box-shadow");
});
DomEventListener listener = event -> {
style.set("transform", "scale(1)");
style.set("box-shadow", shadow);
};
getElement().addEventListener("mouseup", listener);
getElement().addEventListener("pointerleave", listener);
}
}
看看我们是如何在添加事件监听器之后使用addEventData(String)方法添加事件数据的。在这种情况下,我们感兴趣的是获取点击事件发生时的水平和垂直坐标。下面是使用该组件的视图的完整实现:
@Route("custom-component")
public class CustomComponentView extends Composite<Component> {
@Override
protected Component initContent() {
var button = new PictureButton(
"https://live.staticflickr.com/65535/51154022090_22fd569976_k.jpg",
event -> {
JsonObject data = event.getEventData();
var x = data.getNumber("event.clientX");
var y = data.getNumber("event.clientY");
Notification.show("Clicked at " + x + ", " + y);
});
button.setWidth("65%");
var layout = new VerticalLayout(button);
layout.setAlignItems(Alignment.CENTER);
return layout;
}
}
我们使用getEventData()方法从事件中获取数据,使用JsonObject类的getNumber(String)方法获取特定值。图 9-6 显示了一个click事件被触发后的组件(截图中的坐标与狗的鼻子相匹配,以防你想知道我点击了哪里)。
图 9-6
服务器端点击监听器
Caution
确保在读取值时使用JsonObject类中正确的 getter。例如,clientX属性是一个数值,所以您必须使用getNumber(String)方法。如果值是Boolean或String,分别使用getBoolean(String)或getString(String)。
JavaScript 集成
Vaadin 应用可以在浏览器中集成 JavaScript 代码。这种集成允许您在浏览器中从服务器端 Java 调用 JavaScript 函数,从 JavaScript 调用服务器端 Java 方法。简而言之,有两种机制可以实现这一点:
-
Page和Element类中的executeJs(String, Serializable...)方法,用于调用浏览器中运行的 JavaScript 表达式 -
用于从浏览器调用服务器中的方法的
@ClientClickable注释和element.$serverJavaScript 对象
向 Vaadin 应用添加 JavaScript
您可以将自己的 JavaScript 文件添加到 Vaadin 项目中的两个位置,具体取决于您使用的打包方式:
-
对于 JAR 包,使用 PROJECT_ROOT/frontend/ 。
-
对于 WAR 包,使用PROJECT _ ROOT/src/main/resources/META-INF/resources/frontend/。
您可以为文件创建任何子目录结构。
Note
Vaadin(使用一个名为 Webpack 的工具)处理 frontend/ 目录中的文件,生成一个单独的包,其中包含您的应用需要的所有客户端依赖项。
例如,让我们创建一个新的 JavaScript 文件,PROJECT _ ROOT/frontend/script . js,内容如下:
alert("Hello there! It's me. The script!");
我们可以将该文件包含在 Vaadin 视图中,如下所示:
@Route("javascript-integration")
@JsModule("script.js")
public class JavascriptIntegrationView extends Div {
}
很容易认为 script.js JavaScript 文件只有在浏览器请求JavascriptIntegrationView时才被加载。然而,正如您在图 9-7 的截图中看到的,当您请求应用的任何视图时,该文件被加载(截图中默认的空视图)。如果您记得文件被编译成一个包,这就很容易理解了。
图 9-7
浏览器中加载的 JavaScript 文件
从 Java 调用 JavaScript
让我们开发一个显示按钮和图像的视图。当用户单击该按钮时,图像可见性被切换(可见/不可见)。让我们从管道开始:
@Route("javascript-integration")
@JsModule("script.js")
public class JavascriptIntegrationView extends Div {
public JavascriptIntegrationView() {
var image = new Image(
"https://live.staticflickr.com/65535/51154022090_22fd569976_k.jpg",
"dog");
image.setMaxWidth("100%");
Button button = new Button("Toggle visibility", event -> {
});
add(button, image);
getElement().getStyle().set("display", "grid");
getElement().getStyle().set("padding", "1em");
getElement().getStyle().set("max-width", "700px");
}
}
这里没什么新鲜的。结果如图 9-8 所示。
图 9-8
用于切换图像可见性的 UI
我们可以使用本书前几章介绍的组件来开发 UI 的所有功能。事实上,如果您正在实现这样的视图,我会建议您这样做。然而,我们在这里学习与 Web 平台的集成,因此我们将要实现的内容将帮助您了解如何集成现有的 JavaScript 组件和库,以及实现 Vaadin 核心可能无法提供的现成功能。
回到代码。如果您还记得之前的实验,脚本是在视图加载后立即执行的。相反,我们希望在点击按钮时运行一个 JavaScript 函数。下面是 script.js 文件中实现函数的错误方式:
function toggle() {
...
}
这不正确的原因与该函数的创建和调用范围有关。我们需要确保我们了解范围。实现这一点的一个简单方法是通过将一个 JavaScript 对象添加到 DOM 中的一个众所周知的对象来创建一个名称空间:
window.ns = {
toggle: function() {
...
}
}
该脚本将名为ns(您可以使用任何名称)的对象添加到浏览器中始终存在的window对象中。在这个对象中,我们可以定义 JavaScript 函数,调用如下:
ns.toggle();
下面是如何从 Vaadin 视图调用它:
UI.getCurrent().getPage().executeJs("ns.toggle()");
我们可以在 JavaScript 函数中接受参数。例如,我们可能需要想要显示/隐藏的 HTML 元素:
window.ns = {
toggle: function(element) {
...
}
}
从 Java 向函数传递一个参数如下所示:
UI.getCurrent().getPage().executeJs("ns.toggle($0)",
image);
提醒一下,image对象是一个类型为Image的 Vaadin 组件。进行调用时,子字符串$0被替换为image。
为了实现切换功能,我们可以使用现有的库——jQuery。我们可以下载库文件并将其放入项目中,但是 jQuery 也是通过内容交付网络(CDN)提供的,这实际上意味着我们可以获得托管在公共服务器上的 JavaScript 文件的链接。这对于我们的目的来说很实用,所以让我们将它添加到应用中:
@Route("javascript-integration")
@JsModule("script.js")
@JsModule("https://code.jquery.com/jquery-3.6.0.min.js")
public class JavascriptIntegrationView extends Div {
...
}
现在我们可以在script.js文件中使用 jQuery:
window.ns = {
toggle: function(element) {
jQuery(element).fadeToggle();
return `Toggled at ${new Date().toLocaleString()}`;
}
}
表达式jQuery(element).fadeToggle()是库中可用的众多函数之一。它使用渐隐动画隐藏或显示选定的元素(element)。如果您想看到淡入淡出的效果,您必须运行示例应用。
我们在浏览器中返回函数被调用的时间,只是为了学习如何在 Java 端使用返回值。下面是对该函数的调用以及如何使用返回值:
Button button = new Button("Toggle visibility", event -> {
UI.getCurrent().getPage()
.executeJs("return ns.toggle($0)", image)
.then(value -> Notification.show(value.asString()));
});
因为对 JavaScript 函数的调用是异步的,所以我们必须使用then(SerializableConsumer)方法在返回值可用时使用它。
从 JavaScript 调用 Java
我们还可以从 JavaScript 函数调用服务器中的 Java 方法。例如,假设我们想要实现一个在客户端处理的 click listener,并从中调用一个服务器端方法。为了进行设置,我们可以在script.js文件中添加一个init方法:
window.ns = {
init: function(element, view) {
},
toggle: function(element) {
jQuery(element).fadeToggle();
return `Toggled at ${new Date().toLocaleString()}`;
}
}
我们可以从 Java 调用这个方法,如下所示:
public JavascriptIntegrationView() {
...
UI.getCurrent().getPage()
.executeJs("return ns.init($0, $1)", image, this);
...
}
当请求视图时,init(element, view)函数只被调用一次。我们正在传递我们想要初始化的元素(image)和视图本身。稍后我们可以使用view对象从脚本中调用 Java 方法。但是首先,让我们添加服务器端的方法。这个方法应该用@ClientCallable注释:
...
public class JavascriptIntegrationView extends Div {
...
@ClientCallable
public void showClickNotification(Integer x, Integer y) {
var message = String.format("Clicked at %d, %d", x, y);
Notification.show(message, 3000, Position.BOTTOM_END);
}
}
现在,我们可以实现这个函数,看看如何从它调用 Java 方法:
window.ns = {
init: function(element, view) {
element.onclick = event =>
view.$server.showClickNotification(event.clientX,
event.clientY);
},
...
}
$server对象是由 Vaadin 添加的。有了这个对象,我们就可以在相应的 Java 类中调用标有@ClientCallable的方法。图 9-9 显示了点击切换按钮后的结果和图像本身。
图 9-9
运行中的自定义 JavaScript 组件
摘要
本章为您提供了实现 Vaadin 核心中不包含的功能所需的工具。您了解了如何使用 Element API 在浏览器中创建和操作 HTML 元素。您看到了这个 API 如何允许您设置 CSS 样式。您还了解了如何在应用中包含 JavaScript 文件,以及如何从服务器端 Java 方法调用浏览器中的 JavaScript 函数,以及如何从浏览器中的 JavaScript 函数调用服务器端 Java 方法。
十、定制样式和响应能力
在第一章中,你学习了层叠样式表(CSS)的基础知识,以及如何编写规则来改变 HTML 文档的外观。Vaadin 不仅允许你通过元素 API 使用 CSS(正如你在第九章中所学的),还允许你在单独的中使用 CSS。可以添加到项目中的 css 文件。
除了 CSS 之外,Vaadin 还包括一些组件,可以简化快速实现响应式 ui。响应式 UI 根据呈现它的屏幕的大小来调整它的结构。当你想支撑宽度窄、高度长的手机等设备时,这是很有用的。
内置主题
Vaadin 管理主题中的应用样式。主题是一组 CSS 文件和相关资源,如字体和图像,它们定义了应用的外观。诸如主要的背景和前景颜色、字体、间距以及 UI 如何适应不同的视图大小之类的东西是在构成主题的 CSS 规则中定义的。Vaadin 有两个主题:
-
Lumo: 默认主题。我们已经在前几章的例子中使用了这个主题。
-
**材质:**一个基于谷歌材质设计的 Vaadin 主题。访问
https://material.io了解更多材料设计信息。
这两种主题都有两种变体——亮和暗。
应用可以在实现了AppShellConfigurator接口的类中使用@Theme注释来定义主题。例如,下面的代码片段显示了如何激活材质主题(参见图 10-1 ):
图 10-1
物质主题
@Theme(themeClass = Material.class)
public class AppConfiguration implements AppShellConfigurator {
}
Tip
您可以使用@NoTheme注释停用默认的 Lumo 主题。当您想要完全控制加载的 CSS 文件并从头开始设计应用时,这很有用。如果 Vaadin 没有找到@Theme或@NoTheme注释,默认使用 Lumo 主题。
使用主题变体
像主题一样,主题变体是一组 CSS 文件和相关资源。不同之处在于,每个应用只能有一个主题,而可以有多个变体,其中一个每次都是活动的。Lumo 和 Material 主题都包含两种变体。以下是如何激活 Lumo 主题的黑暗版本(见图 10-2 ):
图 10-2
Lumo 主题的黑暗变体
@Theme(themeClass = Lumo.class, variant = Lumo.DARK)
public class AppConfiguration implements AppShellConfigurator {
}
Note
如果你一直在尝试这本书的例子或者编写你自己的 Vaadin 应用,试着改变它们的活动主题。就像加个注释那么简单!尝试材质主题的黑暗变体,看看它是什么样子的。请记住,不可能有多个主题。可以有几个主题变体(甚至是自定义的)并在运行时改变它们(见 https://vaadin.com/learn/tutorials/toggle-dark-theme )。
使用组件变体
几个 Vaadin 组件包括主题变体。组件主题变体只影响包含该变体的组件。例如,您可以通过添加ButtonVariant.LUMO_PRIMARY变体使按钮看起来更突出:
Button button = new Button("Primary ");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
图 10-3 显示了具有不同主题变量的几个按钮和文本字段。
图 10-3
组件主题变体
Tip
使用 IDE 的自动完成功能探索可用的变体。
使用 CSS 设置样式
根据我们目前所看到的,我们有四种 Vaadin 应用的可能样式——两种主题,每种主题有两种变体。现在让我们看看如何通过添加修改可用主题的自定义 CSS 规则来扩展选项。
导入 CSS 文件
我们在第九章中看到了如何使用元素 API 为 UI 中的特定组件或 HTML 元素设置自定义 CSS 属性。例如,我们可以给一个Div组件添加一个带阴影的边框,如下所示:
Div div = new Div();
Style style = div.getStyle();
style.set("border", "1em solid #333");
style.set("box-shadow", "1em 1em 1em #777");
这是改变 UI 组件样式的一种快速简单的方法。但是,当您想要改变应用的整体外观时,最好拥有可以被多个视图使用的单独的 CSS 文件。
让我们看看如何向 Vaadin 应用添加自定义 CSS 规则。图 10-4 显示了一个简单的视图,有一个H1,一个TextField,一个Button,没有自定义 CSS。
图 10-4
具有默认主题和样式的视图
开发人员经常问我的一个问题是如何减少H1组件前后的空间。这是可以理解的,因为通常一个H1组件是一个 UI 的视图或部分中最顶层的组件,并且可能有必要优化空间使用。幸运的是,使用 CSS 很容易调整这一点:
h1 {
margin-top: 0.15em;
margin-bottom: 0;
}
我们可以将这个 CSS 放在前端/ 目录中的一个文件中,使用我们想要的任何文件名(例如 custom-styles.css ),并使用@CssImport注释将它加载到一个视图中:
@Route("custom-css")
@CssImport("./custom-styles.css")
public class CustomCss extends Composite<Component> {
}
我们可以使用元素 API 并为H1组件设置 CSS 属性,但是将样式放在一个单独的文件中允许我们更容易地重用应用中每个H1组件的样式。图 10-5 显示了结果。
图 10-5
添加到视图中的自定义 CSS 文件
Caution
使用@CssImport注释导入的文件包含在应用包中。这意味着一个视图会受到其他视图导入的 CSS 文件内容的影响。
使用 Lumo 主题属性
Lumo 主题包括一组 CSS 属性(或变量),允许快速对主题进行常规更改。这些属性可以被视为主题的参数,并在适用时调整所有组件的样式。
Tip
CSS 属性以--开头。
您可以在一个 CSS 文件中设置 Lumo 主题所使用的 CSS 属性的值,这个 CSS 文件可以使用前面小节中所示的@CssImport注释来导入。以下示例显示了如何更改 UI 组件的字体和圆度(参见图 10-6 中的结果):
图 10-6
用 CSS 属性自定义 Lumo 主题
html {
--lumo-font-family: "Courier New", Courier, monospace;
--lumo-border-radius: 0px;
}
Lumo 主题中定义了许多 CSS 属性。清单 10-1 展示了一些可用属性的例子。
html {
--lumo-font-family: "Courier New", Courier, monospace;
--lumo-font-size: 1rem;
--lumo-font-size-xxxl: 3rem;
--lumo-font-size-xxl: 2.25rem;
--lumo-font-size-xl: 1.75rem;
--lumo-font-size-l: 1.375rem;
--lumo-font-size-m: 1.125rem;
--lumo-font-size-s: 1rem;
--lumo-font-size-xs: 0.875rem;
--lumo-font-size-xxs: 0.8125rem;
--lumo-line-height-m: 1.4;
--lumo-line-height-s: 1.2;
--lumo-line-height-xs: 1.1;
--lumo-border-radius: 0px;
--lumo-size-xl: 4rem;
--lumo-size-l: 3rem;
--lumo-size-m: 2.5rem;
--lumo-size-s: 2rem;
--lumo-size-xs: 1.75rem;
--lumo-space-xl: 1.75rem;
--lumo-space-l: 1.125rem;
--lumo-space-m: 0.5rem;
--lumo-space-s: 0.25rem;
--lumo-space-xs: 0.125rem;
--lumo-shade-5pct: rgba(26, 26, 26, 0.05);
--lumo-shade-10pct: rgba(26, 26, 26, 0.1);
--lumo-shade-20pct: rgba(26, 26, 26, 0.2);
--lumo-shade-30pct: rgba(26, 26, 26, 0.3);
--lumo-shade-40pct: rgba(26, 26, 26, 0.4);
--lumo-shade-50pct: rgba(26, 26, 26, 0.5);
--lumo-shade-60pct: rgba(26, 26, 26, 0.6);
--lumo-shade-70pct: rgba(26, 26, 26, 0.7);
--lumo-shade-80pct: rgba(26, 26, 26, 0.8);
--lumo-shade-90pct: rgba(26, 26, 26, 0.9);
--lumo-primary-text-color: rgb(235, 89, 5);
--lumo-primary-color-50pct: rgba(235, 89, 5, 0.5);
--lumo-primary-color-10pct: rgba(235, 89, 5, 0.1);
--lumo-error-text-color: rgb(231, 24, 24);
--lumo-error-color-50pct: rgba(231, 24, 24, 0.5);
--lumo-error-color-10pct: rgba(231, 24, 24, 0.1);
--lumo-success-text-color: rgb(62, 229, 170);
--lumo-success-color-50pct: rgba(62, 229, 170, 0.5);
--lumo-success-color-10pct: rgba(62, 229, 170, 0.1);
--lumo-shade: hsl(0, 0%, 10%);
--lumo-primary-color: hsl(22, 96%, 47%);
--lumo-error-color: hsl(0, 81%, 50%);
--lumo-success-color: hsl(159, 76%, 57%);
--lumo-success-contrast-color: hsl(159, 29%, 10%);
}
Listing 10-1A custom Vaadin theme based on Lumo properties
Note
解释每个属性超出了本书的范围。这些特性在 https://vaadin.com/docs/latest/ds/foundation 的官方文档中有详细记载。在撰写本文时,在 https://demo.vaadin.com/lumo-editor 有一个在线主题编辑器。
向 UI 组件添加 CSS 类
您可以向任何组件添加 CSS 类来设置组件的样式。例如:
Div div = new Div();
div.addClassName("styled-div");
以及相应的 CSS 规则:
.styled-div {
border: 1px solid red;
}
随着应用的增长,您需要为自己定义的 CSS 类制定一致的约定。提高可维护性的一个好方法是在 CSS 类名中使用 Java 类名(视图)。假设我们有以下观点:
@Route("css-classes")
public class CssClassesView extends Composite<Component> {
@Override
protected Component initContent() {
var header = new Div(VaadinIcon.VAADIN_H.create(),
new H1("Title"),
new Anchor("https://vaadin.com?utm_source=apressbook",
"Log out"));
Grid<String> grid = new Grid<>(String.class);
grid.setItems("item1", "item2", "item3", "");
var content = new Div(grid);
var layout = new Div();
layout.add(header, content);
return layout;
}
}
我们将组件分组为连贯的部分(使用Div类),但除此之外,我们并不“关心”视图在浏览器中呈现时的样子。图 10-7 显示确实如此!
图 10-7
没有 CSS 样式的视图
然而,如果我们将 CSS 类名添加到重要的部分,我们——或者更好,一个掌握 CSS 的网页设计师——可以完全改变视图的外观。我们将在 CSS 类的名称中使用 Java 类的名称(CssClassesView),附加一个适当的字符串来区分我们想要样式化的组件:
layout.addClassName(getClass().getSimpleName());
header.addClassName(getClass().getSimpleName() + "-header");
content.addClassName(getClass().getSimpleName() + "-content");
在浏览器中,这些组件呈现为
<div class="CssClassView">
...
<div class="CSSClassView-header">
...
<div class="CssClassView-content">
...
我们可以如下加载一个新的 CSS 文件:
@Route("css-classes")
@CssImport("./custom-classes.css")
public class CssClassesView extends Composite<Component> {
...
}
最后,我们可以在 custom-classes.css 文件中设置视图的样式。下面是一个后端 Java 开发者的尝试(结果见图 10-8 ):
图 10-8
使用 CSS 类设置视图样式
.CssClassesView {
display: flex;
flex-direction: column;
}
.CssClassesView-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: var(--lumo-primary-text-color);
color: var(--lumo-primary-contrast-color);
padding-left: 1em;
padding-right: 1em;
}
.CssClassesView-header h1 {
color: var(--lumo-primary-contrast-color);
}
.CssClassesView-header a {
color: var(--lumo-primary-contrast-color);
}
样式阴影 DOM
Vaadin 组件作为 Web 组件实现。Web 组件是封装在单个可重用单元中的一组 HTML 资源。例如,Button在浏览器中呈现为<vaadin-button>。Web 组件包括影子 DOM ,它是不会“污染”页面中 HTML 文档的 HTML。这意味着 Web 组件中的样式不会泄露给页面的其他部分,页面中的样式也不会影响 Web 组件。您仍然可以设计 Web 组件的样式,但是您需要以一种特殊的方式来完成。
假设我们想要改变上一节示例中的Grid的头部样式。如果我们检查浏览器中的 DOM,我们会看到在一个<vaadin-grid> Web 组件的影子 DOM 中有一个<th>元素。我们可以使用下面的 CSS 选择器来设置这个元素的样式:
:host th {
background: var(--lumo-primary-color-10pct);
}
:host选择阴影 DOM。我们在 Web 组件的影子 DOM 中选择了<th>元素。哪个 Web 组件?我们在@CssImport注解中回答了这个问题。假设我们将前面的 CSS 规则放在一个名为 vaadin-grid.css 的文件中(可以使用任何名称)。当我们加载这个文件时,我们可以指定想要样式化的 Vaadin Web 组件(图 10-9 显示了结果):
图 10-9
设计 Vaadin Web 组件的样式
...
@CssImport(value = "./vaadin-grid.css", themeFor = "vaadin-grid")
public class CssClassesView extends Composite<Component> {
...
}
响应式网页设计
响应式网页设计是利用技术使布局和组件适应不同的设备。在不同的屏幕尺寸下使用时,响应式 web 应用会改变它们的布局。
Vaadin 提供了一些特殊的组件来简化响应式 ui 的实现。当这些组件不符合您的要求时,您可以随时使用 CSS 来实现您的目标。
形状布局
FormLayout组件使得在根据屏幕宽度变化的许多列中显示其他组件变得容易。它还将输入组件的标签放在组件的顶部,而不是旁边。事实上,我们在图 10-3 中使用了FormLayout。下面是实现(请注意构造函数的结尾):
@Route("form-layout")
public class FormLayoutView extends Composite<Component> {
@Override
protected Component initContent() {
Button increaseRadiation = new Button("Increase radiation",
VaadinIcon.ARROW_UP.create());
increaseRadiation
.addThemeVariants(ButtonVariant.LUMO_ERROR);
Button shutDownCooling = new Button("Shutdown cooling",
VaadinIcon.POWER_OFF.create());
shutDownCooling
.addThemeVariants(ButtonVariant.LUMO_SUCCESS);
NumberField temperature = new NumberField("Temperature");
temperature
.addThemeVariants(TextFieldVariant.LUMO_ALIGN_CENTER);
NumberField pressure = new NumberField("Pressure");
pressure
.addThemeVariants(TextFieldVariant.LUMO_ALIGN_CENTER);
NumberField hydrogen = new NumberField("Hydrogen");
hydrogen
.addThemeVariants(TextFieldVariant.LUMO_ALIGN_CENTER);
NumberField oxygen = new NumberField("Oxygen");
oxygen.addThemeVariants(TextFieldVariant.LUMO_ALIGN_CENTER);
DatePicker shutdownDate = new DatePicker("Shutdown date");
Button update = new Button("Update reactor",
VaadinIcon.WARNING.create());
update.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
FormLayout form = new FormLayout(increaseRadiation,
shutDownCooling, temperature, pressure, hydrogen,
oxygen, shutdownDate);
VerticalLayout layout = new VerticalLayout(
new H1("Nuclear Reactor"), form, update);
layout.setAlignItems(Alignment.CENTER);
layout.setAlignSelf(Alignment.END, update);
return layout;
}
}
仅仅通过向一个FormLayout添加输入组件,我们就可以在宽屏中得到两列,在窄屏中得到一列。图 10-10 显示了狭窄窗口中的视图。
图 10-10
有反应的形式
您可以配置用于特定最小宽度的列数。这里有一个例子:
form.setResponsiveSteps(
new ResponsiveStep("1px", 1),
new ResponsiveStep("600px", 2),
new ResponsiveStep("800px", 3)
);
如果一个视窗(屏幕)的宽度大于等于 800 像素,表单将显示三列。如果宽度为 600 像素,则使用两列,依此类推。图 10-11 以宽屏显示表单。
图 10-11
自定义响应步骤
应用布局
组件为 web 应用提供了一种流行的布局样式。它包括一个共享的导航条(表头)抽屉(菜单),以及内容区。该组件实现了RouterLayout,因此您可以在多个视图中使用该布局。要将其用作路由器布局,您需要扩展AppLayout。以下示例显示了如何向导航栏添加一个徽标,并向抽屉添加一个Tabs组件:
public class BusinessAppLayout extends AppLayout {
public BusinessAppLayout() {
Image logo = new Image("https://i.imgur.com/GPpnszs.png",
"Vaadin Logo");
logo.setHeight("44px");
addToNavbar(new DrawerToggle(), logo);
Tabs tabs = new Tabs(new Tab("Home"), new Tab("CRM"),
new Tab("Financial"), new Tab("Marketing"),
new Tab("Sales"), new Tab("Inventory"),
new Tab("Manufacturing"), new Tab("Supply chain"),
new Tab("HR"));
tabs.setOrientation(Tabs.Orientation.VERTICAL);
addToDrawer(tabs);
}
}
您可以使用addToNavbar(Component...)和addToDrawer(Component...)方法向每个部分添加组件。您的应用中的视图可以使用如下布局(参见图 10-12 ):
图 10-12
使用由AppLayout实现的路由器布局的视图
@Route(value = "app-layout", layout = BusinessAppLayout.class)
public class AppLayoutView extends Composite<Component> {
@Override
protected Component initContent() {
return new VerticalLayout(new H1("Business Application"),
new Button("Let's do business!",
event -> Notification.show("Success!")));
}
}
AppLayout组件响应迅速。如果您调整浏览器窗口的大小,您会看到抽屉会相应地显示或隐藏。用户可以通过点击视图左上角的DrawerToggle来切换抽屉的可见性。图 10-13 在一个更小的窗口中显示了相同的视图。
图 10-13
AppLayout在小视窗中隐藏抽屉
CSS 媒体查询
CSS 允许你针对不同的屏幕尺寸有选择地应用样式。当我们开发一个带有菜单和内容区域的响应式视图时,让我们来看看它的实际应用:
@Route("css")
@CssImport("./styles.css")
public class CssView extends Composite<Component> {
@Override
protected Component initContent() {
Div menu = new Div(new RouterLink("Option 1", getClass()),
new RouterLink("Option 2", getClass()),
new RouterLink("Option 3", getClass()));
menu.addClassName(getClass().getSimpleName() + "-menu");
Div content = new Div(new H1("Hello!"), new Paragraph(
"Try resizing the window to see the effect in the UI"));
content
.addClassName(getClass().getSimpleName() + "-content");
Div layout = new Div(menu, content);
layout.addClassName(getClass().getSimpleName());
return layout;
}
}
我们在重要的部分添加了 CSS 类名,并导入了 styles.css 文件。让我们使用移动优先的方法,并设计应用的样式,使其在小屏幕上看起来不错。目前,这是视图在所有屏幕尺寸下的外观。下面是 CSS(图 10-14 显示结果):
.CssView {
display: flex;
flex-direction: column;
height: 100%;
}
.CssView-menu {
display: flex;
flex-direction: row;
background-color: var(--lumo-primary-color-10pct);
}
.CssView-menu a {
margin-left: 1em;
white-space: nowrap;
}
.CssView-content {
margin-left: 1em;
}
由于我们的目标是最有可能以纵向模式(高度大于宽度)使用的移动设备,因此在一列中显示组件是有意义的。这就是我们在视图的 flex 显示中设置列方向的原因。该菜单被配置为一个灵活的行,以便所有选项以水平方式显示在屏幕顶部。我们还在每个选项(<a>元素)的左边添加了一个边距,并配置了nowrap以避免在某些浏览器中显示多行文本。对于内容区域,我们添加了一个小的边距,将它与视图的边界分开。
图 10-14
移动优先的设计
随着移动版本的准备就绪和默认设置,我们可以通过添加 CSS 媒体查询来针对更大的屏幕。这些查询允许您根据显示页面的设备的特征应用样式。例如,我们可以将最小宽度为 800 像素的屏幕作为目标,如下所示:
@media screen and (min-width: 800px) {
.CssView {
display: flex;
flex-direction: row;
}
.CssView-menu {
display: flex;
flex-direction: column;
padding: 1em;
}
.CssView-menu a {
margin-bottom: 1em;
margin-left: 0em;
}
}
这些样式将覆盖在媒体查询之外设置的任何样式(即,我们已有的移动样式)。我们正在改变一些事情。首先,视图现在是一行而不是一列。我们可以在左侧显示菜单,而不是在大屏幕的顶部。第二,菜单中的选项是一列。第三,我们调整了选项的边距,在每个选项的底部增加了空间,并删除了我们为移动版本增加的左边空间(请记住,我们正在覆盖样式)。图 10-15 显示了一个更大的浏览器窗口对视图的影响。
图 10-15
使用 CSS 媒体查询的响应设计
摘要
这一章真的提高了你的 Vaadin 技能!您了解了 Vaadin 中可用的内置主题,以及如何使用主题和组件变体。您了解了如何通过导入自定义 CSS 文件来使用 CSS 设计应用的样式,以及如何使用 Lumo 主题属性快速调整外观。您看到了如何向单个 UI 组件添加 CSS 类,以及如何在 Vaadin Web 组件的 shadow DOM 中设置样式。
您还了解了使用FormLayout和AppLayout组件的响应式 web 设计,以及针对不同屏幕尺寸的 CSS 媒体查询。
下一章通过向您介绍 Vaadin 流——一种在 TypeScript 中实现视图的方法,继续探索客户端技术。
十一、使用 TypeScript 的客户端视图
在本书的前几章中,我们一直在编写 Java 代码。让我们休息一下,用另一种编程语言:TypeScript 来实现视图。
我要求流动与。我要求合并
术语 Vaadin 被广泛用于指代允许你用 Java 编写 web UIs 的服务器端类。但是,Vaadin 也允许您使用 TypeScript 编程语言实现 ui。在网上,你会找到诸如 Vaadin 平台、Vaadin 流、Vaadin 融合和(普通)Vaadin 之类的术语。让我们澄清一些定义:
-
Vaadin: 一套开发 Java web 应用的工具。这包括一个免费的开源 web 框架,这是一个生成新项目、文档和付费订阅的在线工具,除了免费提供的工具和服务之外,还有其他工具和服务。您可以使用 Java、TypeScript 或两者来实现 UI。通常,术语Vaadin用于指代Vaadin 流或Vaadin 融合。
-
**Vaadin 平台:**使用量下降的 Vaadin 的同义词。
-
**Vaadin 流程:**vaa din 的一部分,允许你用 Java 实现 ui。
-
va adin Fusion:va adin 的一部分,它允许您在 TypeScript 中实现 ui,本章将对此进行介绍。
Note
有人将 Vaadin Flow 和 Vaadin Fusion 称为 Vaadin 中的两个 web 框架。有些人使用了图书馆这个词。其他的,术语模块。我更愿意把它们看作是的特色。我把 Vaadin 流看成 Java,把 Vaadin Fusion 看成 TypeScript。
TypeScript 快速入门
TypeScript 是 JavaScript 的超集。每个 JavaScript 程序也是一个类型脚本程序。TypeScript 增加了静态类型并将程序编译成 JavaScript。
安装 TypeScript 编译器
如果您想在 Vaadin 应用中使用 TypeScript,您不必安装任何附加工具。然而,如果您想尝试下一节中的代码,您将需要 Node.js 。您可以在 https://nodejs.org 下载安装程序。Node.js 包含了NPM—一个管理 JavaScript 包的工具。您可以通过运行以下命令来检查该工具是否正常工作,并确认您获得了作为输出报告的版本:
> npm --version
使用 npm ,您可以安装 TypeScript 包,如下所示:
> npm install --global typescript
这个包包括一个 TypeScript 编译器。您可以通过运行以下命令来检查编译器是否准备好,并确认您得到了一个版本报告:
> tsc –version
Version 4.2.4
在 TypeScript 中实现“Hello,World”
TypeScript 中的“Hello,World”需要一行代码:
console.log("Hello, World!");
如果我们将这一行放到一个名为 hello.ts 的文件中,我们可以使用下面的命令来编译它:
> tsc hello.ts
默认情况下,TypeScript 编译器在同一目录中创建新的 hello.js 文件。要运行程序,我们可以执行以下命令:
> node hello.js
我们应该在终端中得到预期的输出:
Hello, World!
让我们与 Java 世界做一个比较。Java 编译器 javac 接受一个*。java* 源文件并产生一个*。类字节码文件。java 工具启动一个 JVM 并运行。类文件。在类型脚本方面,类型脚本编译器 tsc 接受一个。ts* 源文件并产生一个*。js* JavaScript 文件。节点工具运行*。js* 文件。Node.js 是 JavaScript 的运行时,就像 web 浏览器一样。
静态打字
如果您有 Java 经验,阅读 TypeScript 代码是很容易的。看一下这个例子:
class Bicycle {
private color: string;
private speed: number = 0;
private gear: number = 1;
public constructor(color: string) {
this.color = color;
}
public speedUp(increase: number): void {
this.speed += increase;
}
public applyBreak(decrease: number): number {
return this.speed -= decrease;
}
public changeGear(newGear: number): number {
return this.gear = newGear;
}
public print(): void {
console.log(
`${this.color} bicycle: ${this.speed} Km/h (${this.gear})`);
}
}
我很确定你理解了这门课的每一点。我用一种 Java 开发人员尽可能容易理解的方式对它进行了编码,但是 TypeScript 包含了使代码更加简洁的特性。
Note
讲授 TypeScript 的所有特性超出了本书的范围。你可以在网上找到优秀的学习资源。例如,官方 TypeScript 网站( www.typescriptlang.org )包括文档和一本手册。如果你想深入学习这门语言及其生态系统,我还推荐亚当·弗里曼的基本打字稿。
我们可以通过在文件末尾添加一些代码来使用Bicycle类(我已经将其命名为 bicycle.ts ):
class Bicycle {
...
}
let redBicycle = new Bicycle("red");
redBicycle.print();
redBicycle.speedUp(10);
redBicycle.changeGear(2);
redBicycle.speedUp(10);
redBicycle.changeGear(3);
redBicycle.speedUp(8);
redBicycle.applyBreak(5);
redBicycle.print();
我们可以编译该文件,运行它,并查看输出:
> tsc bicycle.ts
> node bicycle.js
red bicycle: 0 Km/h (1)
red bicycle: 23 Km/h (3)
与普通 JavaScript 相比,TypeScript 代码的一个优点是它是类型安全的。如果我们在调用时在参数中使用了不正确的类型,比如说,speedUp函数错误地传递了一个字符串而不是一个数字,我们将看到一个编译错误:
> tsc bicycle.ts
bicycle.ts:38:20 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
38 redBicycle.speedUp("10");
~~~~
Found 1 error.
类型安全语法在 TypeScript 中是可选的。如果需要,您可以随时随地使用普通 JavaScript。这使得在您的 TypeScript 程序中包含任何其他 JavaScript 库成为可能。
带 Lit 的 Web 组件
就像我们在第一章中看到的,Web 组件是一个具有封装的逻辑、样式和结构的定制元素。在客户端,Vaadin 组件被实现为 Web 组件。例如,一个Button在浏览器中被渲染为一个<vaadin-button>。 Lit 是一个用于实现 Web 组件的 JavaScript 库,它是使用 Vaadin Fusion 在 TypeScript 中创建客户端视图的基础。
创建新的 Lit 项目
为了帮助您使用 Lit 并试验您自己的 Web 组件,让我们用所需的最低配置创建一个新项目。该项目基于 npm,并将包括一个用于快速实验的网络服务器。我们可以从在硬盘上为项目创建一个新目录开始,命名为 lit-element/ (你可以使用任何你想要的名字)。在这个项目中,我们需要一个 package.json 文件,内容如下:
{
"scripts": {
"start": "tsc && wds"
},
"dependencies": {
"lit": "*"
},
"devDependencies": {
"@web/dev-server": "⁰.1.17"
}
}
package.json 文件类似于 pom.xml 文件,它声明了 Java 项目的依赖关系。您可以在需要时向该文件添加更多的依赖项。在本例中,我们定义了一个名为start的脚本,它调用 TypeScript 编译器并启动 web 服务器。我们将 Lit 添加为运行时依赖项,将 web 服务器(dev-server)添加为开发依赖项。
接下来,我们需要配置 TypeScript 编译器。这可以通过一个 tsconfig.json 文件来完成。这个文件由 tsc 工具读取,并允许我们配置诸如源目录、目标目录和许多其他选项。我们现在需要的是:
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"experimentalDecorators": true
}
}
这设置了我们要生成的编译后的 JavaScript 版本(ES2018),要使用的模块系统(ESNext),TypeScript 如何在文件中查找模块(Node),并启用实验性装饰器。
除此之外,我们需要通过创建一个包含以下内容的 web-dev-server.config 文件来配置 web 服务器:
export default ({
nodeResolve: true
});
要安装依赖项,请运行
> npm install
npm 工具自动生成一个 package-lock.json 文件。这个文件可以看作是 Maven 中的有效 POM 的等价物,它包含了用于构建项目的确切的依赖树。此外, npm 工具创建一个 node_modules/ 目录,组成库的实际文件就在这个目录中。这类似于 Java WAR 文件中的 WEB-INF/libs 。
现在我们准备编码了!
创建“Hello,World”Web 组件
我们需要一个新的 TypeScript 文件来实现 Web 组件。姑且称之为 hello-web-component.ts 。在这个文件中,我们需要的第一件事是包含我们想要从 Lit 库中使用的对象和类。以下是如何:
import {LitElement, html} from 'lit';
import {customElement } from 'lit/decorators.js';
我们正在导入LitElement类来创建新的定制元素、html标记模板(一个可以处理模板文字的函数)和customElement装饰器(类似于 Java 中的注释)。
现在让我们将最简单的“Hello,World”编码为一个 Web 组件:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('hello-web-component')
export class HelloWebComponent extends LitElement {
render() {
return html`
<div>Hello, World!</div>
`;
}
}
为了定义一个定制的 HTML 元素,我们需要一个扩展了LitElement并且用@customElement修饰的类。标签的名称应该总是包含一个破折号(-)。在render()函数中,我们可以使用html标记的模板函数返回将在浏览器中形成 Web 组件结构的 HTML。
要使用 Web 组件,我们只需要创建一个 HTML 文件,该文件导入由 TypeScript 编译器生成的 JavaScript 文件,并在文档中的某个位置添加<hello-web-component>元素。例如,我们可以创建下面的demo.html文件:
<html>
<head>
<meta charset="UTF-8">
<title>Web Component example</title>
</head>
<body>
<script type="module" src="hello-web-component.js"></script>
<hello-web-component></hello-web-component>
</body>
</html>
要启动 web 服务器,我们可以调用在 package.json 文件中定义的start脚本:
> npm run start
可以使用http://localhost:8000/demo . html请求页面。图 11-1 显示了浏览器中的结果。
图 11-1
用 Lit 实现的 Web 组件
使用 Vaadin Fusion 的客户端视图
现在我们对 TypeScript 和 Lit 有了基本的了解,让我们把所有的东西放在一起,用 Vaadin Fusion 实现一个客户端视图。当您想要引入水平扩展(添加更多服务器)或者需要 Vaadin Flow 目前不提供的客户端功能(例如,离线功能)时,客户端视图是一个不错的选择。
启用客户端引导
假设您有一个带有一个视图的常规 Vaadin 应用:
@Route("flow")
public class FlowView extends Composite<Div> {
public FlowView() {
getContent().add(new Text("Hello from Vaadin Flow!"));
}
}
当您至少有一个用 Vaadin Flow 实现的视图时,框架会检测到它,并设置一个客户端引导来处理服务器和客户端之间的所有进一步的通信。如果你检查一个 Vaadin 应用的 target/ 目录,你会发现两个由 Vaadin 自动生成的文件(见图 11-2 )。
图 11-2
生成的 index.ts 和 index.ts 文件
当您在浏览器中请求应用时,index.html和 index.ts 文件被提供,并且在您编译应用时被创建。为了能够使用 Vaadin Fusion 实现客户端视图,我们需要设置这些文件的自己的版本。最简单的方法是编译项目,简单地将两个文件从目标/ 目录复制到前端/ 目录。您可以使用 IDE 或命令行来完成此操作:
> mvn package
> cp target/index.* frontend/
在index.html文件中,我们需要添加一个定制样式来确保客户端视图与应用的主题相匹配。我们只需在<style>部分添加以下内容:
<custom-style>
<style include="lumo-typography"></style>
</custom-style>
并在 index.ts 文件中,导入 Lumo 主题:
import '@vaadin/vaadin-lumo-styles/all-imports';
就这样。该应用已准备好使用 Vaadin 流的客户端视图!
添加客户端视图
要添加用 TypeScript 和 Lit 实现的客户端视图,我们需要创建一个新的*。ts* 文件在前端/ 目录下,导入,在 index.ts 文件中定义路线。您可以为该文件使用任何想要的名称。作为一个可选的约定,让我们使用定制 HTML 元素的名称(用@customElement decorator 配置)作为添加了的文件的名称。ts 扩展( fusion-view.ts ):
import { LitElement, customElement, html } from 'lit-element';
@customElement('fusion-view')
export class FusionView extends LitElement {
render() {
return html`
<div>Hello from Vaadin Fusion!</div>
`;
}
}
在 index.ts 文件中,我们可以如下导入该文件(无需指定文件扩展名):
...
import './fusion-view';
...
最后,我们需要为视图定义路线。客户端路由指定了一个路由(在 URL 中使用)和一个 HTML 元素,以显示何时调用该路由。为此,我们可以如下修改routes常数:
const routes = [
{ path: 'fusion', component: 'fusion-view'},
// for server-side, the next magic line sends all unmatched routes:
...serverSideRoutes // IMPORTANT: this must be the last entry in the array
];
这将把fusion设置为呈现我们之前创建的fusion-view Web 组件的路径。这意味着可以使用*http://localhost:8080/fusion*调用视图。见图 11-3 。
图 11-3
使用 Vaadin Fusion 实现的客户端视图
添加 Vaadin 组件
我们在整本书中使用的 Java 组件是作为 Web 组件在客户端实现的。Web 也可以在客户端视图中使用这些 Web 组件,而无需在服务器中安装 Java 组件。要使用 Vaadin 组件,我们必须首先导入它。例如,要使用 Vaadin 按钮,我们可以在包含视图实现的 TypeScript 文件中添加以下导入声明:
import '@vaadin/vaadin-button/vaadin-button';
然后,我们可以在render()函数中使用<vaadin-button>组件:
@customElement('some-view')
export class SomeView extends LitElement {
render() {
return html`
<vaadin-button>Click me!</vaadin-button>
`;
}
}
我们可以将相同的概念应用于 Vaadin 集合中的所有组件。下面的示例展示了如何在客户端视图中组合输入组件和布局:
import { LitElement, customElement, html } from 'lit-element';
import '@vaadin/vaadin-ordered-layout/vaadin-vertical-layout';
import '@vaadin/vaadin-ordered-layout/vaadin-horizontal-layout';
import '@vaadin/vaadin-combo-box/vaadin-combo-box';
import '@vaadin/vaadin-button/vaadin-button';
import '@vaadin/vaadin-text-field/vaadin-text-field';
import '@vaadin/vaadin-icons';
@customElement('vaadin-components-view')
export class VaadinComponentsView extends LitElement {
render() {
return html`
<vaadin-vertical-layout theme="padding">
<h1>Vaadin Components</h1>
<vaadin-horizontal-layout>
<vaadin-combo-box
placeholder='Select a language...'
items='[
"Java", "TypeScript", "JavaScript"
]'
></vaadin-combo-box>
<vaadin-button>
<iron-icon icon="vaadin:check"></iron-icon>
Select
</vaadin-button>
</vaadin-horizontal-layout>
</vaadin-vertical-layout>
`;
}
}
始终记得导入文件并在 index.ts 文件中为视图定义一条路线(参见图 11-4 ):
图 11-4
Vaadin web 组件
import './vaadin-components-view';
...
const routes = [
{ path: 'fusion', component: 'fusion-view'},
{ path: 'vaadin-components', component: 'vaadin-components-view'},
...serverSideRoutes
];
事件监听器
为了响应用户交互,我们可以使用监听器。例如,我们可以向按钮添加一个点击监听器,如下所示:
<vaadin-button @click='${this.clickHandler}'>Click me!</vaadin-button>
然后在类级别定义 greet 函数:
clickHandler() {
... logic here ...
}
当我们需要修改其他 HTML 元素时,我们可以使用query装饰器。这里有一个视图,包含一个<vaadin-text-field>、一个<vaadin-button>和一个<vaadin-notification>,它们一起向用户显示个性化的问候(见图 11-5 ):
import { LitElement, customElement, html, query } from 'lit-element';
import '@vaadin/vaadin-ordered-layout/vaadin-vertical-layout';
import '@vaadin/vaadin-text-field/vaadin-text-field';
import '@vaadin/vaadin-button/vaadin-button';
import '@vaadin/vaadin-notification/vaadin-notification';
@customElement('greeting-view')
export class GreetingView extends LitElement {
@query('#greeting-notification')
private notification: any;
private name: string = '';
render() {
return html`
<vaadin-vertical-layout theme="padding">
<h1>Greeting</h1>
<vaadin-text-field
id='name'
label="What's your name?"
@value-changed=
'${(event:any) => this.setName(event.detail.value)}'
></vaadin-text-field>
<vaadin-button @click='${this.greet}'>
Send
</vaadin-button>
</vaadin-vertical-layout>
<vaadin-notification id='greeting-notification'>
</vaadin-notification>
`;
}
setName(newName: string) {
this.name = newName;
this.notification.close();
}
greet() {
let message:string = 'Hello, ' + this.name;
this.notification.renderer = (root:any) =>
root.textContent = message;
this.notification.open();
}
}
@query装饰器让notification对象保存<vaadin-notification>元素。稍后我们可以使用这个对象来设置一个定制的渲染器,以便在通知中向用户显示定制的消息。还要注意当文本字段中的值改变时,我们如何更新模型。在这个例子中,模型只是类中的一个字符串对象(name)。
被动观点
即使上一节的例子是有效的并且运行良好,Lit 最有趣的特性之一是可以将组件的 HTML 内容定义为封装在实现 Web 组件的类中的状态的函数。让我们通过以一种被动的方式重新实现前面的例子来看看这一点。
我们需要一组导入和一个类:
import { LitElement, customElement, html, state } from 'lit-element';
import '@vaadin/vaadin-ordered-layout/vaadin-vertical-layout';
import '@vaadin/vaadin-text-field/vaadin-text-field';
import '@vaadin/vaadin-button/vaadin-button';
import '@vaadin/vaadin-notification/vaadin-notification';
@customElement('reactive-view')
export class ReactiveView extends LitElement {
// TODO
}
在类内部,我们可以定义视图的状态。这个视图的状态是由什么构成的?我们有一个文本字段和一个通知,其中都有我们无法预测的值。视图的状态由文本字段中的名称和通知的可见性(可见/隐藏)决定。我们可以将这两个值相加作为ReactiveView类的属性,并用state()修饰它们:
...
@customElement('reactive-view')
export class ReactiveView extends LitElement {
@state()
private notificationOpen = false;
@state()
private name = '';
}
state() decorator 将属性标记为 reactive。我们可以在render()方法中使用这些反应特性。但是在这样做之前,有必要考虑一下其他需要的操作。我们需要对视图做什么改变?当用户输入一个名字时,我们需要更新模型(?? 和 ?? 属性)。当用户点击按钮时,我们需要更新模型(notificationOpen属性)。此外,我们还需要设置通知中的文本,但我们将在稍后实现。现在让我们添加以下方法:
...
@customElement('reactive-view')
export class ReactiveView extends LitElement {
...
setName(newName: string) {
this.name = newName;
this.notificationOpen = false;
}
greet() {
this.notificationOpen = true;
}
}
现在我们可以将render()方法实现为状态的函数:
...
@customElement('reactive-view')
export class ReactiveView extends LitElement {
...
render() {
return html`
<vaadin-vertical-layout theme="padding">
<h1>Greeting</h1>
<vaadin-text-field
label="What's your name?"
@value-changed='${(event:CustomEvent) => this.setName(event.detail.value)}'
></vaadin-text-field>
<vaadin-button @click='${this.greet}'>Send</vaadin-button>
</vaadin-vertical-layout>
<vaadin-notification
.opened="${this.notificationOpen}"
></vaadin-notification>
`;
}
setName(newName: string) {
this.name = newName;
this.notificationOpen = false;
}
greet() {
this.notificationOpen = true;
}
}
元素的工作方式要求我们定义一个渲染器来显示我们想要在通知中显示的文本。为此,我们可以按如下方式修改元素:
<vaadin-notification
.opened="${this.notificationOpen}"
.renderer=${this.greetingRenderer}
></vaadin-notification>
而greetingRenderer需要定义为类中的一个属性:
greetingRenderer = (root: HTMLElement) => {
let message = 'Hello, ' + this.name;
root.textContent = message;
}
就这样。我们现在有了一个反应视图!图 11-5 为截图。
图 11-5
被动的客户端视图
关于离线功能的一句话
到目前为止,我们还没有直接与服务器“对话”。所有的观点是 100%客户端。在浏览器中加载后,即使您停用网络连接,视图仍会继续运行。您不能再次请求视图,也不能请求其他视图,但是已经在浏览器中呈现的视图应该继续工作。客户端和服务器之间没有通信。图 11-6 显示了前一节的例子在没有网络连接的情况下工作的截图(在 Chrome 中,你可以使用开发者工具并在网络标签中选择离线来模拟这一点)。
图 11-6
客户端视图在没有网络连接的情况下仍然可以工作
实现离线功能超出了本书的范围。如果你想了解更多关于这个主题的知识,Vaadin 官方文档有很多关于这个主题的资源(参见 https://vaadin.com/docs/latest/fusion/tutorials/in-depth-course/installing-and-offline-pwa )。
Note
如果你好奇的话,Jussi 是我的一个朋友,他预测我将在今年(2021 年)写一本新书,远在 press 联系我之前。
摘要
在本章中,您学习了 Vaadin Fusion 的基础知识。您学习了如何编译 TypeScript 程序,以及如何使用 Lit 实现 Web 组件。您还了解了如何将客户端视图实现为用 TypeScript 和 Lit 库实现的 Web 组件。
我在这一章的目的是向你提供第一种 Vaadin 融合的方法。这里有许多我们没有涉及的主题。我相信深入解释 Vaadin 融合需要一本完整的书。然而,重要的是,您已经掌握了全局,这样您就可以接手更高级的教程和深入的文档。可以在 vaadin. com/ docs 找到好的学习资源。
下一章回到 Java 和一个令人兴奋的框架:Spring Boot。