使用 Vaadin 创建 CRUD UI

本指南将引导您完成构建一个应用程序的过程,该应用程序在基于 Spring Data JPA 的后端上使用 基于 Vaadin 的 UI

您将构建什么

您将为一个简单的 JPA 仓库构建一个 Vaadin UI。您将获得一个具有完整 CRUD(创建、读取、更新和删除)功能以及一个使用自定义仓库方法的过滤示例的应用程序。

您可以选择以下两种不同的路径之一

  • 从项目中已有的 initial 项目开始。

  • 全新开始。

这些区别将在本文档稍后讨论。

您需要准备什么

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,或者跳过您已熟悉的基本设置步骤。无论哪种方式,您都会得到可工作的代码。

从头开始,请继续阅读 使用 Spring Initializr 开始

跳过基础部分,请执行以下操作

完成时,您可以对照 gs-crud-with-vaadin/complete 中的代码检查您的结果。

使用 Spring Initializr 开始

您可以使用这个 预配置项目,然后点击 Generate 下载 ZIP 文件。该项目已配置好,适用于本教程中的示例。

您也可以从 Github fork 该项目,并在您的 IDE 或其他编辑器中打开它。

手动初始化(可选)

如果您想手动初始化项目,而不是使用前面显示的链接,请按照以下步骤操作

  1. 导航到 https://start.spring.io。此服务会引入应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您想使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 VaadinSpring Data JPAH2 Database

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,它是一个根据您的选择配置的 Web 应用程序存档。

如果您的 IDE 集成了 Spring Initializr,您可以在 IDE 中完成此过程。

创建后端服务

本指南是 使用 JPA 访问数据 的延续。唯一的区别是实体类具有 getter 和 setter,并且仓库中的自定义搜索方法对最终用户来说更加友好。您无需阅读该指南即可完成本指南,但如果您愿意,也可以阅读。

如果您从一个新项目开始,您需要添加实体和仓库对象。如果您从 initial 项目开始,这些对象已经存在。

以下列表(来自 src/main/java/com/example/crudwithvaadin/Customer.java)定义了客户实体

package com.example.crudwithvaadin;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Customer {

	@Id
	@GeneratedValue
	private Long id;

	private String firstName;

	private String lastName;

	protected Customer() {
	}

	public Customer(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public Long getId() {
		return id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	@Override
	public String toString() {
		return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id,
				firstName, lastName);
	}

}

以下列表(来自 src/main/java/com/example/crudwithvaadin/CustomerRepository.java)定义了客户仓库

package com.example.crudwithvaadin;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CustomerRepository extends JpaRepository<Customer, Long> {

	List<Customer> findByLastNameStartsWithIgnoreCase(String lastName);
}

以下列表(来自 src/main/java/com/example/crudwithvaadin/CrudWithVaadinApplication.java)显示了应用程序类,它为您创建了一些数据

package com.example.crudwithvaadin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class CrudWithVaadinApplication {

	private static final Logger log = LoggerFactory.getLogger(CrudWithVaadinApplication.class);

	public static void main(String[] args) {
		SpringApplication.run(CrudWithVaadinApplication.class);
	}

	@Bean
	public CommandLineRunner loadData(CustomerRepository repository) {
		return (args) -> {
			// save a couple of customers
			repository.save(new Customer("Jack", "Bauer"));
			repository.save(new Customer("Chloe", "O'Brian"));
			repository.save(new Customer("Kim", "Bauer"));
			repository.save(new Customer("David", "Palmer"));
			repository.save(new Customer("Michelle", "Dessler"));

			// fetch all customers
			log.info("Customers found with findAll():");
			log.info("-------------------------------");
			for (Customer customer : repository.findAll()) {
				log.info(customer.toString());
			}
			log.info("");

			// fetch an individual customer by ID
			Customer customer = repository.findById(1L).get();
			log.info("Customer found with findOne(1L):");
			log.info("--------------------------------");
			log.info(customer.toString());
			log.info("");

			// fetch customers by last name
			log.info("Customer found with findByLastNameStartsWithIgnoreCase('Bauer'):");
			log.info("--------------------------------------------");
			for (Customer bauer : repository
					.findByLastNameStartsWithIgnoreCase("Bauer")) {
				log.info(bauer.toString());
			}
			log.info("");
		};
	}

}

Vaadin 依赖项

如果您检出了 initial 项目,或者您使用 initializr 创建了项目,您已经设置好了所有必要的依赖项。然而,本节其余部分描述了如何向新的 Spring 项目添加 Vaadin 支持。Spring 的 Vaadin 集成包含 Spring Boot starter 依赖项集合,因此您只需添加以下 Maven 代码片段(或相应的 Gradle 配置)

<dependency>
	<groupId>com.vaadin</groupId>
	<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

本示例使用了比 starter 模块引入的默认版本更新的 Vaadin 版本。要使用更新的版本,请按如下方式定义 Vaadin Bill of Materials (BOM)

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>com.vaadin</groupId>
			<artifactId>vaadin-bom</artifactId>
			<version>${vaadin.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

在开发模式下,依赖项就足够了,但当您为生产构建时,需要为应用程序启用 生产构建

默认情况下,Gradle 不支持 BOM,但有一个方便的 插件可以解决这个问题。请查看 build.gradle 构建文件,了解如何实现相同功能的示例

定义 Main View 类

主视图类(在本指南中称为 MainView)是 Vaadin UI 逻辑的入口点。在 Spring Boot 应用程序中,如果您使用 @Route 注解它,它将自动被识别并在 Web 应用程序的根路径显示。您可以通过向 @Route 注解提供参数来自定义视图显示的 URL。以下列表(来自 initial 项目中的 src/main/java/com/example/crudwithvaadin/MainView.java)显示了一个简单的“Hello, World”视图

package com.example.crudwithvaadin;

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
public class MainView extends VerticalLayout {

	public MainView() {
		add(new Button("Click me", e -> Notification.show("Hello, Spring+Vaadin user!")));
	}
}

在 Data Grid 中列出实体

为了获得一个美观的布局,您可以使用 Grid 组件。您可以使用 setItems 方法将通过构造函数注入的 CustomerRepository 中的实体列表传递给 Grid。然后您的 MainView 主体将如下所示

@Route
public class MainView extends VerticalLayout {

	private final CustomerRepository repo;
	final Grid<Customer> grid;

	public MainView(CustomerRepository repo) {
		this.repo = repo;
		this.grid = new Grid<>(Customer.class);
		add(grid);
		listCustomers();
	}

	private void listCustomers() {
		grid.setItems(repo.findAll());
	}

}
如果您有大型表或大量并发用户,您很可能不想将整个数据集绑定到您的 UI 组件。
虽然 Vaadin Grid 会将数据从服务器延迟加载到浏览器,但前面的方法会将整个数据列表保存在服务器内存中。为了节省一些内存,您可以通过分页或使用延迟加载来仅显示最上面的结果,例如使用 grid.setItems(VaadinSpringDataHelpers.fromPagingRepository(repo)) 方法。

过滤数据

在庞大的数据集成为服务器的问题之前,它很可能会让用户头疼,因为他们试图找到相关的行进行编辑。您可以使用 TextField 组件创建一个过滤输入框。为此,首先修改 listCustomer() 方法以支持过滤。以下示例(来自 complete 项目中的 src/main/java/com/example/crudwithvaadin/MainView.java)展示了如何实现

void listCustomers(String filterText) {
	if (StringUtils.hasText(filterText)) {
		grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
	} else {
		grid.setItems(repo.findAll());
	}
}
这就是 Spring Data 声明式查询的用武之地。在 CustomerRepository 接口中,编写 findByLastNameStartsWithIgnoringCase 只需要一行定义。

您可以为 TextField 组件添加一个监听器,并将其值作为参数传递给过滤方法。由于您在过滤文本字段上定义了 ValueChangeMode.LAZY,当用户输入时,ValueChangeListener 会自动调用。以下示例展示了如何设置此类监听器

TextField filter = new TextField();
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.LAZY);
filter.addValueChangeListener(e -> listCustomers(e.getValue()));
add(filter, grid);

定义编辑器组件

由于 Vaadin UI 是纯 Java 代码,您可以从一开始就编写可重用的代码。为此,为您的 Customer 实体定义一个编辑器组件。您可以将其设为 Spring 管理的 bean,这样您就可以直接将 CustomerRepository 注入到编辑器中,并处理 CRUD 功能的创建、更新和删除部分。以下示例(来自 src/main/java/com/example/crudwithvaadin/CustomerEditor.java)展示了如何实现

package com.example.crudwithvaadin;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * A simple example to introduce building forms. As your real application is probably much
 * more complicated than this example, you could re-use this form in multiple places. This
 * example component is only used in MainView.
 * <p>
 * In a real world application you'll most likely using a common super class for all your
 * forms - less code, better UX.
 */
@SpringComponent
@UIScope
public class CustomerEditor extends VerticalLayout implements KeyNotifier {

	private final CustomerRepository repository;

	/**
	 * The currently edited customer
	 */
	private Customer customer;

	/* Fields to edit properties in Customer entity */
	TextField firstName = new TextField("First name");
	TextField lastName = new TextField("Last name");

	/* Action buttons */
	Button save = new Button("Save", VaadinIcon.CHECK.create());
	Button cancel = new Button("Cancel");
	Button delete = new Button("Delete", VaadinIcon.TRASH.create());
	HorizontalLayout actions = new HorizontalLayout(save, cancel, delete);

	Binder<Customer> binder = new Binder<>(Customer.class);
	private ChangeHandler changeHandler;

	@Autowired
	public CustomerEditor(CustomerRepository repository) {
		this.repository = repository;

		add(firstName, lastName, actions);

		// bind using naming convention
		binder.bindInstanceFields(this);

		// Configure and style components
		setSpacing(true);

		save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
		delete.addThemeVariants(ButtonVariant.LUMO_ERROR);

		addKeyPressListener(Key.ENTER, e -> save());

		// wire action buttons to save, delete and reset
		save.addClickListener(e -> save());
		delete.addClickListener(e -> delete());
		cancel.addClickListener(e -> editCustomer(customer));
		setVisible(false);
	}

	void delete() {
		repository.delete(customer);
		changeHandler.onChange();
	}

	void save() {
		repository.save(customer);
		changeHandler.onChange();
	}

	public interface ChangeHandler {
		void onChange();
	}

	public final void editCustomer(Customer c) {
		if (c == null) {
			setVisible(false);
			return;
		}
		final boolean persisted = c.getId() != null;
		if (persisted) {
			// Find fresh entity for editing
			// In a more complex app, you might want to load
			// the entity/DTO with lazy loaded relations for editing
			customer = repository.findById(c.getId()).get();
		}
		else {
			customer = c;
		}
		cancel.setVisible(persisted);

		// Bind customer properties to similarly named fields
		// Could also use annotation or "manual binding" or programmatically
		// moving values from fields to entities before saving
		binder.setBean(customer);

		setVisible(true);

		// Focus first name initially
		firstName.focus();
	}

	public void setChangeHandler(ChangeHandler h) {
		// ChangeHandler is notified when either save or delete
		// is clicked
		changeHandler = h;
	}

}

在较大的应用程序中,您可以在多个地方使用此编辑器组件。另请注意,在大型应用程序中,您可能希望应用一些常见模式(例如 MVP)来组织您的 UI 代码。

连接编辑器

在前面的步骤中,您已经了解了组件化编程的一些基础知识。通过使用 Button 并向 Grid 添加选择监听器,您可以将编辑器完全集成到主视图中。以下列表(来自 src/main/java/com/example/crudwithvaadin/MainView.java)显示了 MainView 类的最终版本

package com.example.crudwithvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import org.springframework.util.StringUtils;

@Route
public class MainView extends VerticalLayout {

	private final CustomerRepository repo;

	private final CustomerEditor editor;

	final Grid<Customer> grid;

	final TextField filter;

	private final Button addNewBtn;

	public MainView(CustomerRepository repo, CustomerEditor editor) {
		this.repo = repo;
		this.editor = editor;
		this.grid = new Grid<>(Customer.class);
		this.filter = new TextField();
		this.addNewBtn = new Button("New customer", VaadinIcon.PLUS.create());

		// build layout
		HorizontalLayout actions = new HorizontalLayout(filter, addNewBtn);
		add(actions, grid, editor);

		grid.setHeight("300px");
		grid.setColumns("id", "firstName", "lastName");
		grid.getColumnByKey("id").setWidth("50px").setFlexGrow(0);

		filter.setPlaceholder("Filter by last name");

		// Hook logic to components

		// Replace listing with filtered content when user changes filter
		filter.setValueChangeMode(ValueChangeMode.LAZY);
		filter.addValueChangeListener(e -> listCustomers(e.getValue()));

		// Connect selected Customer to editor or hide if none is selected
		grid.asSingleSelect().addValueChangeListener(e -> {
			editor.editCustomer(e.getValue());
		});

		// Instantiate and edit new Customer the new button is clicked
		addNewBtn.addClickListener(e -> editor.editCustomer(new Customer("", "")));

		// Listen changes made by the editor, refresh data from backend
		editor.setChangeHandler(() -> {
			editor.setVisible(false);
			listCustomers(filter.getValue());
		});

		// Initialize listing
		listCustomers(null);
	}

	// tag::listCustomers[]
	void listCustomers(String filterText) {
		if (StringUtils.hasText(filterText)) {
			grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
		} else {
			grid.setItems(repo.findAll());
		}
	}
	// end::listCustomers[]

}

构建可执行 JAR

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的单一可执行 JAR 文件并运行它。构建可执行 JAR 使服务在整个开发生命周期中,跨不同环境等轻松发布、版本控制和部署为应用程序。

如果您使用 Gradle,可以使用 ./gradlew bootRun 运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后按如下方式运行 JAR 文件

java -jar build/libs/gs-crud-with-vaadin-0.1.0.jar

如果您使用 Maven,可以使用 ./mvnw spring-boot:run 运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行 JAR 文件

java -jar target/gs-crud-with-vaadin-0.1.0.jar
此处描述的步骤创建了一个可运行的 JAR。您也可以构建经典的 WAR 文件

您可以在 http://localhost:8080 查看您的 Vaadin 应用程序

总结

恭喜!您已经使用 Spring Data JPA 编写了一个功能齐全的 CRUD UI 应用程序。而且您没有暴露任何 REST 服务,也无需编写一行 JavaScript 或 HTML 代码。

另请参阅

以下指南可能也有帮助

想撰写新指南或为现有指南做贡献?请查看我们的贡献指南

所有指南的代码均采用 ASLv2 许可发布,文字内容采用署名-禁止演绎创作共用许可发布。

获取代码