1. 概述
Vaadin Flow 是一款用於創建 Web 用户界面的 基於 Java 的服務器端框架。
在本教程中,我們將探索如何為基於 Spring Boot 的後端構建一個 基於 Vaadin Flow 的 CRUD UI。有關 Vaadin Flow 的介紹,請參考本教程。
2. 部署準備
讓我們從在標準 Spring Boot 應用程序中添加 Maven 依賴項開始:
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>Vaadin 也是 Spring Initializr 認可的依賴項。
如果未使用 Spring Initializr,我們也可以手動將 Vaadin 的 Bill of Materials 添加到項目中。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>24.3.8</version> <!-- check latest version -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. 後端服務
我們將使用一個包含 firstName 和 lastName 屬性的 Employee 實體,對其執行 CRUD 操作。我們還定義了這些屬性的驗證規則,以便在構建的 UI 中強制執行它們:
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
@Size(min = 2, message = "Must have at least 2 characters")
private String firstName;
@Size(min = 2, message = "Must have at least 2 characters")
private String lastName;
public Employee() {
}
// Getters and setters
}
以下是用於管理 CRUD 操作的簡單 Spring Data 存儲庫:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByLastNameStartsWithIgnoreCase(String lastName);
}我們聲明在 EmployeeRepository 接口上定義一個查詢方法 findByLastNameStartsWithIgnoreCase。它將返回給定姓氏的員工列表。
我們還應該使用幾個樣本員工預填充數據庫:
@Bean
public CommandLineRunner loadData(EmployeeRepository repository) {
return (args) -> {
repository.save(new Employee("Bill", "Gates"));
repository.save(new Employee("Mark", "Zuckerberg"));
repository.save(new Employee("Sundar", "Pichai"));
repository.save(new Employee("Jeff", "Bezos"));
};
}
4. Vaadin Flow UI
該應用程序將包含一個可過濾的數據網格,用於顯示員工信息,以及一個用於編輯和創建員工的表單。我們首先將創建表單作為自定義組件,然後使用標準 Vaadin 組件和我們的自定義表單組件創建主佈局:
4.1. EmployeeEditor 表單組件
EmployeeEditor 是一個自定義組件,通過組合現有的 Vaadin 組件並定義自定義 API 創建的。
EmployeeForm 使用 VerticalLayout 作為其基礎。 VerticalLayout 是一個 Layout,它以添加的順序顯示子組件 (垂直)。 通過擴展 Composite<VerticalLayout> 而不是 VerticalLayout,我們不暴露所有 VerticalLayout 的方法,從而完全控制我們組件的 API。
讓我們首先定義我們組件的 API,以便用户可以設置要編輯的員工,並訂閲保存、取消和刪除事件:
public class EmployeeEditor extends Composite<VerticalLayout> {
public interface SaveListener {
void onSave(Employee employee);
}
public interface DeleteListener {
void onDelete(Employee employee);
}
public interface CancelListener {
void onCancel();
}
private Employee employee;
private SaveListener saveListener;
private DeleteListener deleteListener;
private CancelListener cancelListener;
private final Binder<Employee> binder = new BeanValidationBinder<>(Employee.class);
public void setEmployee(Employee employee) {
this.employee = employee;
binder.readBean(employee);
}
// Getters and setters
}setEmployee 方法保存當前員工的引用,並將 Bean 讀取到 Vaadin 的 BeanValidationBinder 中,以便將其綁定到我們表單的輸入字段並進行驗證。
接下來,我們使用 TextField 和 Button 組件構建組件的 UI。我們使用 binder 將輸入字段映射到模型中的字段。將 Bean 讀取到 binder 中意味着,當我們調用 setEmployee 時,輸入字段的值會得到更新。
我們將所有組件添加到 VerticalLayout 中,它是我們的 Composition 的根佈局:
public EmployeeEditor() {
var firstName = new TextField("First name");
var lastName = new TextField("Last name");
var save = new Button("Save", VaadinIcon.CHECK.create());
var cancel = new Button("Cancel");
var delete = new Button("Delete", VaadinIcon.TRASH.create());
binder.forField(firstName).bind("firstName");
binder.forField(lastName).bind("lastName");
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
save.addClickListener(e -> save());
save.addClickShortcut(Key.ENTER);
delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
delete.addClickListener(e -> deleteListener.onDelete(employee));
cancel.addClickListener(e -> cancelListener.onCancel());
getContent().add(firstName, lastName, new HorizontalLayout(save, cancel, delete));
}
最後,我們通過讀取輸入字段的值,創建一個新的 Employee 對象,前提是字段驗證通過。我們避免將這些值保存到原始對象中,因為該對象與 UI 的其他部分綁定,並且我們希望在不產生副作用的情況下使用我們的組件。一旦我們獲得了更新後的員工對象,我們就會調用 saveListener 來通知父組件。
4.2. 主視圖
<em>EmployeesView</em> 類是我們的應用程序的入口點。<strong>@Route(“”)` 註解告訴 Vaadin Flow 組件在應用程序啓動時將其映射到根路徑。
我們以 <em>VerticalLayout</em> 為基礎構建我們的視圖,然後使用標準 Vaadin 組件和我們創建的自定義 <em>EmployeeEditor</em> 構建 UI。
Vaadin Flow 視圖是 Spring 豆,這意味着我們可以通過構造函數將 <em>EmployeeRepository</em> 自動注入到視圖中。
@Route("")
public class EmployeesView extends VerticalLayout {
private final EmployeeRepository employeeRepository;
private final TextField filter;
private final Grid<Employee> grid;
private final EmployeeEditor editor;
public EmployeesView(EmployeeRepository repo) {
employeeRepository = repo;
// Create components
var addButton = new Button("New employee", VaadinIcon.PLUS.create());
filter = new TextField();
grid = new Grid<>(Employee.class);
editor = new EmployeeEditor();
// Compose layout
var actionsLayout = new HorizontalLayout(filter, addButton);
add(actionsLayout, grid, editor);
}
}
4.3. 配置組件和數據
接下來,我們創建兩個輔助方法:一個用於根據搜索字符串更新網格,另一個用於處理員工選擇:
private void updateEmployees(String filterText) {
if (filterText.isEmpty()) {
grid.setItems(employeeRepository.findAll());
} else {
grid.setItems(employeeRepository.findByLastNameStartsWithIgnoreCase(filterText));
}
}
private void editEmployee(Employee employee) {
editor.setEmployee(employee);
if (employee != null) {
editor.setVisible(true);
} else {
// Deselect grid
grid.asSingleSelect().setValue(null);
editor.setVisible(false);
}
}
最後,我們配置組件:
- 我們配置 EmployeeEditor 組件使其初始時隱藏,並定義處理保存、刪除和取消事件的監聽器。
- 我們將 ValueChangeMode 設置為 LAZY,應用於過濾器 TextField,以便在用户停止輸入時調用 updateEmployees 方法。
- 我們為網格設置固定 200px 高度,並在選定行發生變化時調用 editEmployee 方法。
public EmployeesView(EmployeeRepository repo) {
// Component creation code from above
// Configure components
configureEditor();
addButton.addClickListener(e -> editEmployee(new Employee()));
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.EAGER);
filter.addValueChangeListener(e -> updateEmployees(e.getValue()));
grid.setHeight("200px");
grid.asSingleSelect().addValueChangeListener(e -> editEmployee(e.getValue()));
// List customers
updateEmployees("");
}
private void configureEditor() {
editor.setVisible(false);
editor.setSaveListener(employee -> {
var saved = employeeRepository.save(employee);
updateEmployees(filter.getValue());
editor.setEmployee(null);
grid.asSingleSelect().setValue(saved);
});
editor.setDeleteListener(employee -> {
employeeRepository.delete(employee);
updateEmployees(filter.getValue());
editEmployee(null);
});
editor.setCancelListener(() -> {
editEmployee(null);
});
}
4.4. 運行應用程序
我們可以使用 Maven 啓動應用程序:
mvn spring-boot:run應用程序現在正在 localhost:8080 上運行:
5. 結論
在本文中,我們使用 Spring Boot 和 Spring Data JPA 構建了一個功能齊全的 CRUD UI 應用程序,用於持久化。