使用 Vaadin 8 框架构建 Grails 3 应用程序
了解如何使用 Vaadin 8 框架构建 Grails 3 应用程序
作者:Ben Rhine
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!
2 入门
本指南将指导大家使用 Vaadin 8 框架来构建 Grails 3 应用程序。
2.1 必需条件
要完成本指南,需要以下条件:
-
有空闲时间
-
一个合适的文本编辑器或 IDE
-
已安装 JDK 1.7 或更高版本(并已正确配置了 JAVA_HOME)
2.2 指南完成方法
请执行以下操作以开始:
-
下载资源并解压
或者
-
克隆 Git 代码库
git clone https://github.com/grails-guides/vaadin-grails.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些额外代码,以便帮助你快速入门。 -
complete
完成后的示例。它是按照指南中的步骤并应用于initial
文件夹修改的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/vaadin-grails/initial
然后按照以下部分中的说明进行操作。
进入 grails-guides/vaadin-grails/complete 后,您可以直接进入已完成示例,使用 cd |
2.3 Vaadin 8 Grails 3 配置文件
此项目的 initial
目录使用以下命令创建
grails create-app demo --profile me.przepiora.vaadin-grails:web-vaadin8:0.3
我们向 create-app
命令提供 Vaadin 8 Grails 3 配置文件 的坐标。
通过使用 应用程序配置文件,Grails 允许您构建现代网络应用程序。这些配置文件能方便地构建具有 JavaScript 前端(Angular、REACT)或 Vaadin 应用程序的 REST API 或网络应用程序。
3 关于 Vaadin
Vaadin 是适用于商业应用程序的 Java 网络 UI 框架。
使用 Vaadin Framework,您可以采用一种熟悉且基于组件的做法,比其他任何 UI 框架都快地构建出色的单页面网络应用程序。舍弃复杂的网络技术,直接使用 Java 或任何其他 JVM 语言。只需一个浏览器即可访问您的应用程序,无需其他插件。
Vaadin 8 Grails 配置文件允许您混合使用 Vaadin 端点和传统的 Grails 端点。 |
一方面,我们将拥有由 Grails Controller 处理的端点。它们将使用 GSP 或 Grails Views 呈现 HTML、JSON 或 XML。
另一方面,我们将拥有 Vaadin 端点。我们将使用 Java 或 Groovy 开发 UI,并直接连接到 Grails 服务层。
如果您需要有关 Vaadin 的更多信息,请在此处查看官方文档 here。此外,您可能还会在较早版本的 Vaadin 中找到大量的示例,并且 此 页面很好地解释了其中一些功能如何在 Vaadin 8 中更新。
4 运行该应用程序
此时建议进行测试运行,以确保一切都正常运行。
要运行该应用程序,首先
$ cd initial/
要启动该应用程序,请运行以下命令。
$ ./gradlew bootRun
如果一切正常,这将启动 Grails 应用程序,该应用程序将在 http://localhost:8080
上运行。
要查看 Vaadin 的运行情况,请转至 http://localhost:8080/vaadinUI
。
5 编写该应用程序
5.1 创建域
我们首先创建应用程序的域模型。
$ grails create-domain-class demo.Driver
$ grails create-domain-class demo.Make
$ grails create-domain-class demo.Model
$ grails create-domain-class demo.User
$ grails create-domain-class demo.Vehicle
现在让我们在 grails-app/domain/demo/ 中编辑域类。我们将添加一些属性和以下三个注解。
-
@GrailsCompileStatic
- 标记为GrailsCompileStatic
的代码将全部编译为静态代码,但格雷尔斯特定的交互项除外,这些交互项不能被静态编译,但 GrailsCompileStatic 将其识别为动态分派的允许操作。其中包括调用动态查找器和在约束和映射闭包中配置块中调用 DSL 代码(就像在 domain 类中)。 -
@EqualsAndHashCode
- 自动生成 equals 和 hashCode 方法 -
@ToString
- 自动生成 toString 方法
package demo
import grails.compiler.GrailsCompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@GrailsCompileStatic
@EqualsAndHashCode(includes=['name'])
@ToString(includes=['name'], includeNames=true, includePackage=false)
class Make {
String name
static constraints = {
name nullable: false
}
}
package demo
import grails.compiler.GrailsCompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@GrailsCompileStatic
@EqualsAndHashCode(includes=['name'])
@ToString(includes=['name'], includeNames=true, includePackage=false)
class Model {
String name
static constraints = {
name nullable: false
}
}
package demo
import grails.compiler.GrailsCompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@GrailsCompileStatic
@EqualsAndHashCode(includes=['name', 'make', 'model'])
@ToString(includes=['name', 'make', 'model'], includeNames=true, includePackage=false)
class Vehicle {
String name
Make make
Model model
static belongsTo = [driver: Driver]
static mapping = {
name nullable: false
}
static constraints = {
}
}
与前三个类相比,Driver.groovy
的内容更多。这是因为我们实际上在使用 driver 扩展 User.groovy
类。随着应用程序的不断发展,这将在未来为我们提供一些额外的灵活性。
package demo
import grails.compiler.GrailsCompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
@GrailsCompileStatic
@EqualsAndHashCode(includes=['name'])
@ToString(includes=['name'], includeNames=true, includePackage=false)
class Driver extends User {
String name
static hasMany = [ vehicles: Vehicle ]
static constraints = {
vehicles nullable: true
}
}
package demo
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
static constraints = {
password nullable: false, blank: false, password: true
username nullable: false, blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}
5.2 预加载数据
现在我们的域已经到位,让我们预加载一些数据来使用。
package demo
import groovy.util.logging.Slf4j
@Slf4j
class BootStrap {
def init = { servletContext ->
log.info "Loading database..."
final driver1 = new Driver(name: "Susan", username: "susan", password: "password1").save()
final driver2 = new Driver(name: "Pedro", username: "pedro", password: "password2").save()
final nissan = new Make(name: "Nissan").save()
final ford = new Make(name: "Ford").save()
final titan = new Model(name: "Titan").save()
final leaf = new Model(name: "Leaf").save()
final windstar = new Model(name: "Windstar").save()
new Vehicle(name: "Pickup", driver: driver1, make: nissan, model: titan).save()
new Vehicle(name: "Economy", driver: driver1, make: nissan, model: leaf).save()
new Vehicle(name: "Minivan", driver: driver2, make: ford, model: windstar).save()
}
def destroy = {
}
}
5.3 创建服务层
接下来让我们为应用程序创建服务层,以便 Grails 和 Vaadin 能够共享资源。
$ grails create-service demo.DriverService
$ grails create-service demo.MakeService
$ grails create-service demo.ModelService
$ grails create-service demo.VehicleService
现在,让我们编辑 grails-app/services/demo/ 下的服务类。我们将向所有类中添加一个 listAll()
方法。此方法将拥有以下附加注释。
-
@ReadOnly
- 对于仅返回数据的函数,这种做法十分妥当
package demo
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileStatic
@CompileStatic
@ReadOnly
class DriverService {
@ReadOnly
List<Driver> listAll() {
Driver.where { }.list()
}
}
package demo
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileStatic
@CompileStatic
class MakeService {
@ReadOnly
List<Make> listAll() {
Make.where { }.list()
}
}
package demo
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileStatic
@CompileStatic
class ModelService {
@ReadOnly
List<Model> listAll() {
Model.where { }.list()
}
}
我们的 VehicleService.groovy
具有一个附加 save()
方法,以便可以向应用程序添加新数据。
package demo
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileStatic
@CompileStatic
@ReadOnly
class VehicleService {
def save(final Vehicle vehicle) {
vehicle.save(failOnError: true)
}
@ReadOnly
List<Vehicle> listAll(boolean lazyFetch = true) {
if ( !lazyFetch ) {
return Vehicle.where {}
.join('make')
.join('model')
.join('driver')
.list()
}
Vehicle.where { }.list()
}
}
5.4 创建控制器
尽管对 Vaadin 来说完全没有必要,但我们想要证明 Grails 控制器和 Vaadin 框架之间不存在冲突。
$ grails create-controller demo.GarageController
现在,让我们编辑 grails-app/controllers/demo/ 下的控制器。我们将导入我们的一个服务,更新 index 方法,并添加以下注释。
-
@GrailsCompileStatic
- 标记为GrailsCompileStatic
的代码将全部编译为静态代码,但格雷尔斯特定的交互项除外,这些交互项不能被静态编译,但 GrailsCompileStatic 将其识别为动态分派的允许操作。其中包括调用动态查找器和在约束和映射闭包中配置块中调用 DSL 代码(就像在 domain 类中)。
package demo
import grails.converters.JSON
import groovy.transform.CompileStatic
@CompileStatic
class GarageController {
VehicleService vehicleService (1)
def index() { (2)
final List<Vehicle> vehicleList = vehicleService.listAll()
render vehicleList as JSON
}
}
1 | 声明我们的服务 |
2 | index() 调用我们的服务,并将输出呈现为 JSON |
在此处,让我们确信一切正常运行,并运行 [runningTheApp] 应用程序。
现在,我们可以使用 cURL 或其他 API 工具来演练 API。
对 /garage 发起 GET 请求以获取车辆列表
$ curl -X "GET" "http://localhost:8080/garage"
[{"id":1,"driver":{"id":1},"make":{"id":1},"model":{"id":1},"name":"Pickup"},
{"id":2,"driver":{"id":1},"make":{"id":1},"model":{"id":2},"name":"Economy"},
{"id":3,"driver":{"id":2},"make":{"id":2},"model":{"id":3},"name":"Minivan"}]
如果返回了数据,则表示一切已设置为正常连接,并且我们已经验证了一些测试数据。现在,让我们看看如何将 Vaadin 附加到 Grails
5.5 Vaadin
最后,时候为应用程序添加 Vaadin 代码了!
把 src/main/groovy/demo/DemoGrailsUI.groovy
当作您的 Vaadin 控制器/调度器,因为它会帮助您了解 Vaadin 流程。我们的 init()
方法是应用程序进入 Vaadin 本身的入口点,它基本上是您的顶级视图。从此处,您可以设置导航以及其他整个应用程序视图组件。
我们现在的 DemoGrailsUI.groovy
非常适合单页式 Web 应用程序,但是如果我们希望稍后添加导航组件或其他页面,它并不是最灵活的。考虑到这一点,我们将使用视图使其更灵活。使用视图还有助于保持我们的 Vaadin 前端井井有条。
有关 Vaadin 视图和导航的更多信息,请 单击此处查看。 |
package demo
import com.vaadin.annotations.Title
import com.vaadin.navigator.View
import com.vaadin.navigator.ViewDisplay
import com.vaadin.server.VaadinRequest
import com.vaadin.annotations.Theme
import com.vaadin.spring.annotation.SpringUI
import com.vaadin.spring.annotation.SpringViewDisplay
import com.vaadin.ui.Component
import com.vaadin.ui.Label
import com.vaadin.ui.Panel
import com.vaadin.ui.UI
import com.vaadin.ui.VerticalLayout
import groovy.transform.CompileStatic
@CompileStatic
@SpringUI(path="/vaadinUI")
@Title("Vaadin Grails") (1)
@SpringViewDisplay (2)
class DemoGrailsUI extends UI implements ViewDisplay { (3)
private Panel springViewDisplay (4)
/** Where a line is matters, it can change the position of an element. */
@Override
protected void init(VaadinRequest request) { (5)
final VerticalLayout root = new VerticalLayout()
root.setSizeFull()
setContent(root)
springViewDisplay = new Panel()
springViewDisplay.setSizeFull()
root.addComponent(buildHeader())
root.addComponent(springViewDisplay)
root.setExpandRatio(springViewDisplay, 1.0f)
}
static private Label buildHeader() { (6)
final Label mainTitle = new Label("Welcome to the Garage")
mainTitle
}
@Override
void showView(final View view) { (7)
springViewDisplay.setContent((Component) view)
}
}
1 | 我们添加 @Title 注释,以便给我们的窗口/选项卡起一个好听的名字。 |
2 | 添加 @SpringViewDisplay ,以便我们可以使用视图。 |
3 | 将 implements ViewDisplay 添加到我们的类中。 |
4 | 接下来,为我们的 UI 创建一个附加面板。 |
5 | 进入我们 Vaadin 视图的初始入口点。 |
6 | 建立我们页眉的帮助程序方法。 |
7 | 使用视图需要的其他函数;动态控制设置我们的视图组件。 |
您向布局中添加组件的顺序会决定它们在布局中的位置。 |
init() 可以相当快速地变大,因此最好将 UI 组件分解到它们自己的方法(如 buildHeader() )中,以保持文件的清晰简洁。 |
5.6 添加您的视图
现在添加我们的 Vaadin 代码的大部分内容的视图。创建一个位于 src/main/groovy/demo
中的新文件,名为 GarageView.groovy
。
接下来,进行必要的更新。
package demo
import com.vaadin.data.HasValue
import com.vaadin.data.ValueProvider
import com.vaadin.event.selection.SelectionEvent
import com.vaadin.event.selection.SelectionListener
import com.vaadin.event.selection.SingleSelectionEvent
import com.vaadin.event.selection.SingleSelectionListener
import com.vaadin.navigator.View
import com.vaadin.navigator.ViewChangeListener
import com.vaadin.spring.annotation.SpringView
import com.vaadin.ui.Button
import com.vaadin.ui.ComboBox
import com.vaadin.ui.Grid
import com.vaadin.ui.HorizontalLayout
import com.vaadin.ui.ItemCaptionGenerator
import com.vaadin.ui.Label
import com.vaadin.ui.TextField
import com.vaadin.ui.VerticalLayout
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import javax.annotation.PostConstruct
import groovy.transform.CompileStatic
@Slf4j
@CompileStatic
@SpringView(name = GarageView.VIEW_NAME) (1)
class GarageView extends VerticalLayout implements View { (2)
public static final String VIEW_NAME = "" (3)
@Autowired (4)
private DriverService driverService
@Autowired
private MakeService makeService
@Autowired
private ModelService modelService
@Autowired
private VehicleService vehicleService
private Vehicle vehicle = new Vehicle()
@PostConstruct (5)
void init() {
/** Display Row One: (Add panel title) */
final HorizontalLayout titleRow = new HorizontalLayout()
titleRow.setWidth("100%")
addComponent(titleRow)
final Label title = new Label("Add a Vehicle:")
titleRow.addComponent(title)
titleRow.setExpandRatio(title, 1.0f) // Expand
/** Display Row Two: (Build data input) */
final HorizontalLayout inputRow = new HorizontalLayout()
inputRow.setWidth("100%")
addComponent(inputRow)
// Build data input constructs
final TextField vehicleName = this.buildNewVehicleName()
final ComboBox<Make> vehicleMake = this.buildMakeComponent()
final ComboBox<Model> vehicleModel = this.buildModelComponent()
final ComboBox<Driver> vehicleDriver = this.buildDriverComponent()
final Button submitBtn = this.buildSubmitButton()
// Add listeners to capture data change
//tag::listeners[]
vehicleName.addValueChangeListener(new UpdateVehicleValueChangeListener('NAME'))
vehicleMake.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('MAKE'))
vehicleModel.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('MODEL'))
vehicleDriver.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('DRIVER'))
submitBtn.addClickListener { event ->
this.submit()
}
//end::listeners[]
// Add data constructs to row
[vehicleName, vehicleMake, vehicleModel, vehicleDriver, submitBtn].each {
inputRow.addComponent(it)
}
/** Display Row Three: (Display all vehicles in database) */
final HorizontalLayout dataDisplayRow = new HorizontalLayout()
dataDisplayRow.setWidth("100%")
addComponent(dataDisplayRow)
dataDisplayRow.addComponent(this.buildVehicleComponent())
}
class UpdateVehicleValueChangeListener implements HasValue.ValueChangeListener {
String eventType
UpdateVehicleValueChangeListener(String eventType) {
this.eventType = eventType
}
@Override
void valueChange(HasValue.ValueChangeEvent event) {
updateVehicle(eventType, event.value)
}
}
class UpdateVehicleComboBoxSelectionLister implements SingleSelectionListener {
String eventType
UpdateVehicleComboBoxSelectionLister(String eventType) {
this.eventType = eventType
}
@Override
void selectionChange(SingleSelectionEvent event) {
updateVehicle(eventType, event.firstSelectedItem)
}
}
@Override
void enter(ViewChangeListener.ViewChangeEvent event) {
// This view is constructed in the init() method()
}
/** Private UI component builders ------------------------------------------------------------------------------- */
static private TextField buildNewVehicleName() {
final TextField vehicleName = new TextField()
vehicleName.setPlaceholder("Enter a name...")
vehicleName
}
private ComboBox<Make> buildMakeComponent() {
final List<Make> makes = makeService.listAll()
final ComboBox<Make> select = new ComboBox<>()
select.setEmptySelectionAllowed(false)
select.setPlaceholder("Select a Make")
select.setItemCaptionGenerator(new CustomItemCaptionGenerator())
select.setItems(makes)
select
}
class CustomItemCaptionGenerator implements ItemCaptionGenerator {
@Override
String apply(Object item) {
if (item instanceof Make ) {
return (item as Make).name
}
if ( item instanceof Driver ) {
return (item as Driver).name
}
if ( item instanceof Model ) {
return (item as Model).name
}
null
}
}
private ComboBox<Model> buildModelComponent() {
final List<Model> models = modelService.listAll()
final ComboBox<Model> select = new ComboBox<>()
select.setEmptySelectionAllowed(false)
select.setPlaceholder("Select a Model")
select.setItemCaptionGenerator(new CustomItemCaptionGenerator())
select.setItems(models)
select
}
private ComboBox<Driver> buildDriverComponent() {
final List<Driver> drivers = driverService.listAll()
final ComboBox<Driver> select = new ComboBox<>()
select.setEmptySelectionAllowed(false)
select.setPlaceholder("Select a Driver")
select.setItemCaptionGenerator(new CustomItemCaptionGenerator())
select.setItems(drivers)
select
}
private Grid buildVehicleComponent() {
final List<Vehicle> vehicles = vehicleService.listAll(false) (6)
final Grid grid = new Grid<>()
grid.setSizeFull() // ensures grid fills width
grid.setItems(vehicles)
grid.addColumn(new VehicleValueProvider('id')).setCaption("ID")
grid.addColumn(new VehicleValueProvider('name')).setCaption("Name")
grid.addColumn(new VehicleValueProvider('make.name')).setCaption("Make")
grid.addColumn(new VehicleValueProvider('model.name')).setCaption("Model")
grid.addColumn(new VehicleValueProvider('driver.name')).setCaption("Name")
grid
}
class VehicleValueProvider implements ValueProvider {
String propertyName
VehicleValueProvider(String propertyName) {
this.propertyName = propertyName
}
@Override
Object apply(Object o) {
switch (propertyName) {
case 'id':
if ( o instanceof Vehicle) {
return (o as Vehicle).id
}
break
case 'name':
if ( o instanceof Vehicle) {
return (o as Vehicle).name
}
break
case 'model.name':
if ( o instanceof Vehicle) {
return (o as Vehicle).model.name
}
break
case 'make.name':
if ( o instanceof Vehicle) {
return (o as Vehicle).make.name
}
break
case 'driver.name':
if ( o instanceof Vehicle) {
return (o as Vehicle).driver.name
}
break
}
null
}
}
static private Button buildSubmitButton() {
final Button submitBtn = new Button("Add to Garage")
submitBtn.setStyleName("friendly")
submitBtn
}
private updateVehicle(final String eventType, final eventValue) {
switch (eventType) {
case 'NAME':
if ( eventValue instanceof String ) {
this.vehicle.name = eventValue as String
}
break
case 'MAKE':
if ( eventValue instanceof Optional<Make> ) {
this.vehicle.make = (eventValue as Optional<Make>).get()
}
break
case 'MODEL':
if ( eventValue instanceof Optional<Model> ) {
this.vehicle.model = (eventValue as Optional<Model>).get()
}
break
case 'DRIVER':
if ( eventValue instanceof Optional<Driver> ) {
this.vehicle.driver = (eventValue as Optional<Driver>).get()
}
break
default:
log.error 'updateVehicle invoked with wrong eventType: {}', eventType
}
}
private submit() {
vehicleService.save(this.vehicle)
// tag::navigateTo[]
getUI().getNavigator().navigateTo(VIEW_NAME)
// end::navigateTo[]
}
}
1 | 添加 @SpringView 注释并设置名称,以便找到您的视图。 |
2 | 视图应扩展所需的布局样式。 |
3 | 设置实际的视图名称。 |
4 | 服务不会自动注入到 Vaadin 视图中。您需要使用 @Autowired 注释才能使依赖项注入正常工作。 |
5 | 告诉视图 init() 在主 UI init() 之后执行。 |
6 | 急切地加载车辆及其关联关系。 |
对 vehicleService.listAll(false)
中的急切加载使用需要进一步解释。
当 Vaadin 组件调用 Grails 服务时,一旦服务方法完成,Hibernate 会话将关闭,这意味着查询未加载的任何关联关系都可能导致会话关闭后出现 LazyInitializationException
。
因此,至关重要的是您的查询始终返回呈现视图所需的数据。无论如何,这通常会导致更好的执行查询,实际上将帮助您设计运行性能更高的应用程序。
Grails 自动依赖注入无法检测 Vaadin 中的服务,因此我们需要使用更传统的 Spring 注解 @Autowired,以便正确使用依赖注入。 |
我们的视图试图模仿许多现代 Web 设计的布局,使用“行”,在我们的案例中,我们有 3 行:一个标题、一个数据集和一个数据显示(网格)。当我们开发一种模式时,Vaadin 视图中会开始出现。
-
创建布局
-
创建 UI 组件
-
将 UI 组件添加到布局
-
将布局添加到视图
将布局添加到视图时,您可以仅使用 addComponent()
,因为它知道它正在添加到自身中,这与需要 root.addComponent()
的顶级 UI 不同。
为了保持文件整洁,请继续将每个 UI 组件构建为其自己的私有方法。
一旦构建了 UI 组件,现在我们需要能够与它们交互。为此,我们将侦听器添加到我们的组件,利用 groovy 闭包指定事件发生时将发生什么。在我们的案例中,我们是 updateVehicle()
,然后我们将 submit()
vehicleName.addValueChangeListener(new UpdateVehicleValueChangeListener('NAME'))
vehicleMake.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('MAKE'))
vehicleModel.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('MODEL'))
vehicleDriver.addSelectionListener(new UpdateVehicleComboBoxSelectionLister('DRIVER'))
submitBtn.addClickListener { event ->
this.submit()
}
使用侦听器,我们构建车辆对象,然后在单击该按钮时提交该对象。我们提交方法的最后一行将重新加载我们的视图以刷新新更新的数据。
getUI().getNavigator().navigateTo(VIEW_NAME)
现在一切都已就绪,返回 [runningTheApp] 以运行您的应用程序。如果一切按预期进行,您将看到以下内容。
6 下一步
有很多机会来扩展此应用程序的范围。以下为您自己可以进行改进的一些想法
-
创建一个模态对话框表单以添加新的
Driver
、Makes
和Models
实例。使用 Vaadin 的 子窗口 来为您领先一步。 -
添加对现有
Vehicle
实例进行更新的支持。模态对话框可能也适用于此,或者可能是可编辑表格行 -
目前,
Make
和Model
领域类彼此无关。向它们之间添加关联将允许我们在下拉列表中显示当前选定的Make
的型号。您将需要使用 JavaScript Array.filter 方法。 -
目前,视图包含对服务的直接引用。虽然这对于演示或小型应用程序来说完全没问题,但当我们的代码库增长时,事情往往会失控。诸如 Model-View-Presenter (MVP) 之类的模式可能有助于保持一个有组织的代码库。您可以在 Vaadin 文档 中阅读有关模式和 Vaadin 的更多信息