Vaadin 的体系结构和功能扩展

Vaadin 的体系结构介绍

Vaadin 应用程序实质上是运行在 Java Web 服务器上的 Servlet,专门响应 HTTP 请求,它包含一套 web 应用编程 API,一组 UI 用户界面组件,用来控制外观的主题和一个与用户界面直接绑定的数据模型。此外 Vaadin 还包含一个终端适配器 (terminal adapter), 它的作用是接受 web 浏览器发送的请求并将响应渲染到程序的页面上。终端适配器通过 Web 服务器的 Java Servlet API 接收到客户端的请求,并且把这些请求转换为特定会话 (session) 中的用户事件。 每个用户事件都与一个 Server 端 UI 组件关联在一起并且被交付到应用程序中。当应用程序的逻辑改变了 Server 端 UI 组件时,终端适配器生成一个相应的响应 , 并把它发送到客户端的 Web 浏览器中。位于客户端浏览器中的一个 Javascript 组件 (Client-Side Engine) 负责接收这些响应并且根据响应内容在 Web 页面上渲染程序的 UI 界面。图 1 是 Vaadin 的体系结构示意图 , 展示了该体系结构中几个主要的部分之间的作用关系。下文将介绍这些组成部分的特点和功能。


图 1. Vaadin 的体系结构
Vaadin 的体系结构和功能扩展_第1张图片
  • 用户界面组件 (User Interface Components):Vaadin 的用户界面由各种用户界面 UI 组件组成,这些组件由应用程序创建并布局。每一个 Server 端 UI 组件都与一个直接同用户交互的客户端 UI 组件相对应。通过与客户端的 HTTP 连接,Server 端组件可以使用终端适配器将自身状态传递到客户端。相应的,客户端组件可以将用户交互以事件的形式传递回 server 端组件,server 端组件再将这些事件转送给应用程序的业务逻辑代码。大部分的用户界面组件都和应用程序的数据源绑定在一起。
  • 客户端引擎 (Client-Side Engine):Vaadin 客户端引擎的主要功能是在客户端的 Web 浏览器中管理 UI 界面的渲染。它是一个驻留在 Web 浏览器中的 Javascript 组件,使用 Google Web Toolkit (GWT) 技术来进行渲染操作。客户端引擎使用 UIDL 用户界面定义语言 ( 一种基于 JSON 格式的界面描述语言 ) 来与位于 server 端的终端适配器通信。它接收终端适配器发送的 UI 变化信息,根据这些信息更新 Web 浏览器中的用户界面。并且向端适配器发送从客户端浏览器中接收到的用户界面交互操作信息。客户端引擎与终端适配器使用异步的 HTTP 或 HTTPS 进行通讯。
  • 终端适配器 (Terminal Adapter): 在 Vaadin 中用户界面组件不会直接自动渲染在 Web 页面上。需要通过终端适配器来完成这一工作。终端适配器是一个抽象层,Server 端 UI 组件将他们的变化发送到终端适配器,终端适配器再将这些变化渲染在 Web 浏览器中。当用户在 Web 页面中执行操作后,相应的 UI 事件作为异步 Ajax 请求被传递到终端适配器中,终端适配器再将这些事件转交给 Server 端 UI 组件,进而传递入应用程序的业务逻辑代码中。通过使用这一抽象层,Vaadin 应用程序可以被渲染在任意的浏览器中。例如 IT Mill Toolkit v3/v4 使用一种终端适配器能够支持 HTML 和简单的 Ajax 渲染,而 Vaadin v5/v6 的终端适配器通过使用 Google Web Toolkit(GWT) 能够支持高级的 Ajax 渲染。通过使用不同的终端适配器,Vaadin 能够支持各种不同的渲染技术 ( 甚至不是基于 HTML 的 ),而开发人员却可以始终使用相同的编程 API。
  • 主题 (Themes):Vaadin 中用户界面的外观表现层和业务层的逻辑代码是相互隔离的。UI 组件的业务逻辑使用 Java 语言编写,外观则使用由 CSS 定义的主题来控制。Vaadin 为 UI 组件提供了一个默认的主题,用户也可以通过修改 CSS 样式表,HTML 模版或图片等资源来定义新的主题。
  • 用户界面定义语言 (UIDL): 终端适配器使用一种特殊的用户界面定义语言 UIDL 来在 Web 页面中渲染或更新用户界面。UIDL 是使用 JSON(JavaScript Object Notation, 一种轻量级的,可以在基于 Javascript 的 Ajax 程序中高效使用的数据交换格式 ) 来定义的。
  • 事件 (Events): 当用户在 UI 界面上进行操作时会产生相应的响应事件,这些事件首先会在客户端浏览器中由 JavaScript 处理,之后通过 HTTP 服务器传递到终端适配器,最后转发到应用程序的业务逻辑代码中。
  • 数据模型 (Data Model): 除了用户界面组件,Vaadin 还提供了一个数据模型来在 UI 组件中交互展示数据。通过使用数据模型,用户界面组件可以直接更新应用程序中的数据而无需经过任何控制代码。所有的 UI 组件都在内部使用了这个数据模型,但是它们也可以将数据绑定到一个独立的数据源中,例如可以把一个 SQL 查询的结果直接绑定到一个列表控件中。

Vaadin 应用的运行时机制

在 Vaadin 应用程序中基本上所有的逻辑都是运行在服务器端的 Java Servlet API 上的,如下图 2 Vaadin 的运行时结构图所示,Vaadin 运行时结构主要由服务器端框架 (server-side framework) 和客户端引擎 (client-side engine) 两部分构成。服务器端框架包含了用来与客户端引擎通讯的服务器端集成层 (server-side integration) 以及一系列的 server 端 UI 组件。客户端引擎则由 Google Web toolkit(GWT) 页面渲染模块和客户端集成层 (client-side integration) 两部分组成。


图 2. Vaadin 的运行时结构图
Vaadin 的体系结构和功能扩展_第2张图片

图 3 显示了 Vaadin 运行时服务器端框架和客户端引擎的通讯原理。所有的通讯统一由位于服务器端集成层中的 CommunicationManager类和位于客户端集成层中的 ApplicationConnection类使用用户界面定义语言 (UIDL) 和 JSON 来进行。当包含 Vaadin 应用程序的网页初始化时,首先会在空白页面中加载 Vaadin 客户端引擎的 JavaScript 代码。当这些 JavaScript 代码加载完成后,客户端引擎开始处理与服务器之间的通讯。ApplicationConnection 类负责向 server 端发送 Ajax 请求并且根据 server 端 CommunicationManager 类的响应来渲染用户界面。Vaadin 非常有效的隐藏了客户端 - 服务器通讯的细节,所有的通讯操作对开发人员都是透明的,在开发过程中无须关注。因此可以在应用程序服务器端的 Java 代码中处理所有的用户交互逻辑,只通过在服务器端的开发即可完成全部的应用程序。这种开发模式极大的简化了 Ajax Web 应用程序的结构。


图 3. 运行时服务器 - 客户端通讯原理
Vaadin 的体系结构和功能扩展_第3张图片

Vaadin 的服务器端框架

Vaadin 的服务器端框架运行在 Web 服务器的 Servlet 中。当 Web 容器接受到第一个对 Vaadin 应用程序的请求时,它会创建一个 ApplicationServlet类的实例,ApplicationServlet 是 Vaadin 服务器端框架的应用程序入口,它继承了 Java Servlet API 中的 HttpServlet。ApplicationServlet 会使用 HttpSession接口将应用程序的每个实例都关联到一个会话 (session) 上。在每个 session 的生命周期中,服务器端框架将用户交互信息传播到相应的应用程序实例中供服务器端 UI 组件使用。这种服务器端 UI 组件和客户端引擎之间的信息通讯由服务器端集成层 (server-side integration) 中的 CommunicationManager类来完成。除了服务器端集成层外,在服务器端框架中还包含有一整套 Vaadin 服务器端 UI 组件库,这些服务器端组件提供了创建 Vaadin 应用程序用户界面所需的编程 API。每一个服务器端组件都有两个基本任务:为对应的客户端组件序列化自身的状态变量信息,以及将接受到的客户端已序列化的用户输入信息解序。这两项任务中的大部分操作都由服务器端框架完成。

Vaadin 的客户端引擎

Vaadin 的客户端引擎使用 Google Web Toolkit (GWT) 技术在 Web 浏览器中渲染用户界面并且处理浏览器中用户交互的底层任务。通过使用 GWT,在 Vaadin 中可以统一使用 Java 语言来进行引擎和客户端组件的开发。客户端引擎实际上是一个运行在 Web 浏览器中的 JavaScript 程序,主要功能为根据从服务器端接收到的状态信息来渲染用户界面组件。引擎由 GWT 和客户端集成层 (client-side integration) 两部分组成。每一个 server 端的 UI 组件在客户端浏览器中都有一个对应的客户端小部件 (widget),这个小部件会在 web 页面中渲染 UI 组件的实际内容。Vaadin 的客户端引擎和所有的内置客户端小部件都通过使用 GTW 技术用 Java 语言编写,再使用 GWT 编译器编译成 JavaScript 代码。Vaadin 只是在其框架内部的实现中使用了 GWT,在标准的 Vaadin 程序开发中,对 GWT 的使用是不可见的。客户端集成层的主要功能为从 server 端接收组件的状态信息、当发生用户界面交互时向 server 端发送组件的状态变化,以及管理 Vaadin 应用使用的 CSS 样式表。

用户界面定义语言 UIDL (User Interface Definition Language)

用户界面定义语言 UIDL(User Interface Definition Language) 是一种用来定义用户界面内容以及用户界面变化的语言。它按照特定的格式将这些界面内容和变化相关的信息序列化。这些序列化后的 UIDL 消息会作为响应从 Web 服务器发送到浏览器中。在 Vaadin 应用程序运行时,服务器端组件首先会将渲染自身所需的信息和参数生成为 UIDL 消息,之后这些消息会通过服务器端框架传入浏览器中的客户端引擎。最终在浏览器中 UIDL 消息被解析并转换为 GWT 小组件 (widgets)。UIDL 支持渲染全部的用户界面或只渲染界面的局部,当从一个空白的 Web 页面启动 Vaadin 应用时,全部的用户界面都会被通过 UIDL 渲染。而当只有某一个 UI 组件发生变化时,UIDL 只会重画变化的部分。从 Vaadin 版本 5 开始,UIDL 使用 JSON(JavaScript Object Notation) 来作为通讯的负载消息格式,在客户端引擎中,vaadin 使用 GWT 内置的 JSON 工具库来解析、封装 UIDL 消息。对于终端用户和开发者,JSON 的使用是透明的,无须关注。
清单 1. Button UI 组件的 JSON 格式 UIDL 消息示例

					
 ["button", 
	 {"id": "PID", 
	 "immediate":true, 
	 "caption": "My Button", 
	 "focusid":1, 
	 "v":{"state":false} 
	 } 
 ] 

清单 1 是一个 Button UI 组件的 UIDL 消息示例,UI 组件通过使用 id 属性中的 PID 值来唯一的标识自己,每一个组件的实例都有一个唯一的 PID 值。PID 值通常是由服务器端框架自动生成的字符串,也可以通过使用 setDebugId()方法来手动的设定它。

使用 Vaadin 的 Add-On 插件扩展系统功能

在 Vaadin 中内置了大量的 UI 组件,界面布局,主题和数据源实现。除了这些开箱即用的资源外,还可以通过使用 Add-On 插件来扩展 Vaadin 框架的功能。Vaadin 有大量的 Add-On 插件,可以访问 Vaadin 的 Add-On 插件目录网页 Vaadin Directory来下载使用。图 4 是 Vaadin Directory 页面的截图。在这个页面上列出了大量可以下载使用的 Add-On 插件,其中有商用付费插件,也有免费的插件。


图 4. Vaadin 的 Add-On 下载页面 directory
Vaadin 的体系结构和功能扩展_第4张图片

安装主题、数据源和其他只使用了服务器端构件的 Add-On 插件的方法非常简单,只需将 Add-On 插件的 jar 文件放入应用程序的类路径即可。安装包含客户端组件的 Add-On 时,还需要编译插件包含的部件集 (widget set)。以下为获取并安装一个 Add-On 插件的通用步骤:

  1. 在 Vaadin Directory 页面中点击需要的 Add-On 插件的名称进入详细页面。在详细页面中选择需要的版本 ( 某些插件包含多个不同的版本,Vaadin Directory 默认显示最新的版本。也可以在详细页面中 Version 信息右侧的版本下拉菜单中选择需要的其他版本 )
  2. 选定需要的版本后,点击详细页面右上部的蓝色 Download Now按钮,将包含 Add-On 的 JARZip文件下载到本地计算机中。
  3. 如果 Add-On 包含在 Zip 文件中,需要解压 Zip 压缩包并按照其中提供的操作指南来安装 Add-On。如果下载的 Add-ON 是 Jar 文件,只需要将它放入应用程序项目的 WEB-INF/lib目录即可。
  4. 更新并重新编译应用程序项目工程。如果 Add-On 包含客户端组件 , 还需要编译客户端实现的 widget set。当升级更新一个已经安装过的 Add-On 时,也需要重新编译 widget set。
  5. 更新 Web 服务器中的应用程序代码并根据需要重新启动服务器。

在使用了一个 Add-On 插件之后,可以通过评星级的方式给 Add-On 的作者发送反馈,也可以直接添加文字评论。Add-On 插件的开发独立于 Vaadin 的开发之外 , 所以不同的 Add-On 插件可能使用不用的开源或商业许可证。多数的商用 Add-On 插件也可以直接的下载,使用这些 Add-On 之前需要注意它们使用的许可证和其他附加条件。还有很多的商用 Add-On 插件使用双许可证,这些插件可以在开源的项目中免费使用。当 Add-On 插件安装完成后,就可以在开发环境中编程使用插件提供的新功能了,下文以一个真实的 Add-On 插件 Vaadin Timeline为例演示在下载完成后如何安装、编译 widget set 并编程使用它。

编译 Vaadin Add-On 插件的 Widget Set

Vaadin timeline 是一个用来展示事件和时间趋势的图形组件,它可以将同时间相关的统计数据图形化的显示在用户界面上。在下载完成后会得到一个名为 vaadin-timeline-1.0.2.zip(Vaadin timeline 1.02 版 ) 的压缩文件。将这个文件解压缩后可以获得 Add-On 的 Jar 文件 (vaadin-timeline-1.0.2.jar),用户手册 (Vaadin Timeline Manual.pdf) 以及许可证说明文件。Vaadin timeline 是一个含有客户端 UI 组件的 Add-On,所以需要编译 widget set。Vaadin 的客户端引擎和自定义 widget set 都使用 GWT Compiler来将 Java 代码编译成可在浏览器中执行的 JavaScript 程序,在编译 widget set 之前需要在项目中导入 Google Web Toolkit 的类库。在 Vaadin SDK 的 gwt子目录中包含了一个 GWT 的完全安装版本,可以将整个 gwt 目录或其中包含的 GWT Jar 库文件导入 Vaadin 项目工程。Vaadin 框架提供了两种方式来编译 widget set:使用 Ant 构建脚本或使用 Eclipse plug-in 开发插件。

使用 Ant 构建脚本:使用 Vaadin SDK 中的 Ant 脚本编译 widget set 需要如下几个步骤。

  1. 将 vaadin-timeline-1.0.2.jar 拷贝到 Vaadin SDK 安装路径下的 WebContent/WEB-INF/lib目录中,这样 GWT Compiler 在编译时可以获得 Add-On 组件服务器端的 Java 类。
  2. 使用解压缩工具打开 vaadin-timeline-1.0.2.jar,逐层展开 jar 文件内部的 java 包,直到发现 GWT 的 widget set 配置 XML 文件 ( 这个配置文件名称一般以 .gwt.xml 结尾 )。在本例中该文件名称为 TimelineWidgetSet.gwt.xml。将这个 XML 文件在包中的路径与文件名结合构造出 Widget set 的名称,例如在本例中 TimelineWidgetSet.gwt.xml 位于包路径 com\vaadin\addon\timeline\gwt 下,Widget set 的名称即为 com.vaadin.addon.timeline.gwt.TimelineWidgetSet
  3. 在 Vaadin SDK 的 WebContent/docs/example-source/目录中包含了用来编译 widget set 的 Ant 脚本 build-widgetset.xml。使用文本编辑器编辑这个 ant 文件中的 configure-widgetset目标。将 widgetset属性修改为步骤 2 中获得的 Widget set 的名称 ( 在本例中为“com.vaadin.addon.timeline.gwt.TimelineWidgetSet”),将 widgetset-path属性修改为需要的 widget set 的类路径名 ( 在本例中设定为”com/vaadin/addon/widgetset“),清单 2 为修改完成的 build-widgetset.xml 参数。

    清单 2. build-widgetset.xml 修改参数
    							
      
        Build Vaadin timeline widget set. 
         
        
         
         
             
         
         
         
         
        
      
    

  4. 使用 ant 命令 ant -f build-widgetset.xml启动 widget set 的编译 , 清单 3 是编译 vaadin-timeline Add-On 的 widget set 的 ant 显示信息示例。

    清单 3. vaadin-timeline 的 widget set 编译信息
    							
     configure: 
     init: 
         [echo] Requirements for classpath: 
         [echo]   ../../../gwt/gwt-dev.jar 
         [echo]   ../../../gwt/gwt-user.jar 
         [echo]   ../../../WebContent/WEB-INF/lib/vaadin-6.4.1.jar 
         [echo]   ../../../WebContent/WEB-INF/src 
         [echo] Output will be written into ../../../WebContent/VAADIN/widgetsets 
     compile-server-side: 
       ...... 
     compile-widgetset: 
         [echo] Compiling com.vaadin.addon.timeline.gwt.TimelineWidgetSet... 
         [java] Compiling module com.vaadin.addon.timeline.gwt.TimelineWidgetSet 
         ...... 
         [java]    Compile of permutations succeeded 
         [java] Linking into 
    	 ../WebContent/VAADIN/widgetsets/com.vaadin.addon.timeline.gwt.TimelineWidgetSet. 
         [java]    Link succeeded 
         [java]    Compilation succeeded -- 201.306s 
     BUILD SUCCESSFUL 
     Total time: 3 minutes 28 seconds 
    

  5. 当 widget set 编译成功结束时,ant 输出 “Compilation succeeded”提示信息,此时可以在 Vaadin SDK 安装目录中的 WebContent/VAADIN/widgetsets路径下获得编译好的 widget set, 它的存储目录名称为在步骤 3 中设定的 widgetset 参数的值 ( 在此例中为 com.vaadin.addon.timeline.gwt.TimelineWidgetSet)。

使用 Eclipse plug-in 插件:Eclipse 的 Vaadin 插件提供了 widget set 编译工具,可以快捷的编译 Add-On 插件。当启动了自动编译功能后,Vaadin 插件能够自动检测项目中的 Add-On,当发现新的 Add-On 插件代码时会自动启动 widget set 的编译。它还在工具栏中提供了专门的编译命令按钮,可以随时手动启动 Widget set 的编译。使用 Vaadin 的 eclipse 插件编译 widget set 的步骤非常简单,只需要把 Add-On 的 jar 文件 ( 本文示例中该文件为 vaadin-timeline-1.0.2.jar) 拷贝到 Eclipse 中的 Vaadin 应用工程中即可。Vaadin plug-in 检测到新的 Add-On 后会自动弹出编译 widgetset 的确认窗口,点击 Yes 按钮确认后可以立即启动编译。编译进程信息会在 Eclipse 的 console 视图中显示。清单 4 是编译成功后中 console 视图中的显示信息。


清单 4. Eclipse console 视图中的 widget set 编译信息
				
 Compiling widgetset com.vaadin.addon.timeline.gwt.TimelineWidgetSet 
 Updating GWT module description file... 
 2010-8-22 20:17:15 com.vaadin.terminal.gwt.widgetsetutils.ClassPathExplorer 
 getAvailableWidgetSets 
信息 : Widgetsets found from classpath: 
	 com.vaadin.terminal.gwt.DefaultWidgetSet in jar:file: 
	 D:/DEV_Workspace/eclipse/Vaadin_DW/WebContent/WEB-INF/lib/vaadin-6.4.1.jar!/ 
	 com.vaadin.addon.timeline.gwt.TimelineWidgetSet in file: 
	 //D/DEV_Workspace/eclipse/Vaadin_DW/src 
 Done. 
 Starting GWT compiler 
 Widgetset compilation completed 

编译完成的 widget set 存储在项目的 WebContent/VAADIN/widgetsets路径下。

在应用中使用 Vaadin Add-On 插件

为了在 Vaadin 应用程序中使用 Add-On 插件,首先需要将 Add-On 的 jar 文件放置在 Web 应用程序的类路径中。在本例中,按照 Java Web 应用程序规范将文件 vaadin-timeline-1.0.2.jar 放置在应用程序的 WEB-INF/lib/路径中即可。如果 Add-On 插件包含了客户端组件,除了放置 jar 文件外,还需要注册已经完成编译的 widget set。Vaadin 应用程序本质上是运行在 Java Web 服务器中的 Servlet 程序,为了在 Vaadin 中使用 widget set,需要将包含 widget set 编译结果的文件夹放置在 Vaadin 的 widget set 搜索目录中 ( 该目录位于 web 应用根路径下的"VAADIN/widgetsets"目录中 ),并且在 Servlet 配置文件 web.xml中配置 widget set 的信息。清单 5 是在 web.xml 中注册 Vaadin Timeline 的 widget set 的配置信息片段。
清单 5. 在 web.xml 中注册 Vaadin Timeline 的 widget set 信息

					
  
	 Vaadintimeline Application 
	  
		 com.vaadin.terminal.gwt.server.ApplicationServlet 
	  
	 Vaadin application class to start 
		 application 
		 com.ibm.vaadinTimelineDemo.VaadintimelineApplication
		  
	  
	  
		 Application widgetset 
		 widgetset 
		  
			 com.vaadin.addon.timeline.gwt.TimelineWidgetSet
		  
	  
  

注册 widget set 时需要在 Vaadin 应用程序的 Servlet 配置中添加名称为"widgetset"的 init-param 参数 , 参数的值为包含 widget set 编译结果的文件夹名称 ( 在本例中这个文件夹名称为"com.vaadin.addon.timeline.gwt.TimelineWidgetSet")。这样当应用程序的 Servlet 启动时 Vaadin 框架会自动从 WebContent/VAADIN/widgetsets目录中寻找并加载包含在目录"com.vaadin.addon.timeline.gwt.TimelineWidgetSet"中的 widget set 文件。

当 Add-On 的 jar 文件以及 widget set 的添加和配置完成后,就可以像使用其他 Vaadin 内置的组件一样在 java 代码中直接调用 Add-On 组件的 Java 类。清单 6 中的代码演示了如何在 java 代码中使用 Vaadin Timeline 的类。图 5 为这个示例程序的运行结果。
清单 6. 使用 Vaadin Timeline 的 java 代码示例

					
 package com.ibm.vaadinTimelineDemo; 
 import com.vaadin.Application; 
 import com.vaadin.ui.*; 
 import java.util.Calendar; 
 import java.util.Date; 
 import java.util.Random; 
 import com.vaadin.Application; 
 import com.vaadin.addon.timeline.Timeline;// 导入 Timeline 的类
 import com.vaadin.data.Container; 
 import com.vaadin.data.Item; 
 import com.vaadin.data.util.IndexedContainer; 
 import com.vaadin.ui.Window; 
 public class VaadintimelineApplication extends Application { 
    @Override 
    public void init() { 
		 Window mainWindow = new Window("Timelinedemo Application"); 
            setMainWindow(mainWindow); 
            // 创建 timeline 组件
            Timeline timeline = new Timeline("My timeline"); 
            timeline.setWidth("600px"); 
            timeline.setHeight("400px"); 
            // 为 timeline 添加数据容器
            timeline.addGraphDataSource(createContainer()); 
            mainWindow.addComponent(timeline); 
    } 
	 /** 
     * 为数据容器添加随机数据
    */ 
    private Container.Indexed createContainer() { 
        // 创建容器
        IndexedContainer container = new IndexedContainer(); 
        container.addContainerProperty(Timeline.PropertyId.TIMESTAMP,Date.class, null); 
        container.addContainerProperty(Timeline.PropertyId.VALUE, Float.class,0); 
        // 添加月份和随机数据
        Calendar cal = Calendar.getInstance(); 
        cal.add(Calendar.MONTH, -1); 
        Date today = new Date(); 
        Random generator = new Random(); 
        while (cal.getTime().before(today)) { 
            Item item = container.addItem(cal.getTime()); 
            item.getItemProperty(Timeline.PropertyId.TIMESTAMP).setValue(cal.getTime()); 
            item.getItemProperty(Timeline.PropertyId.VALUE).setValue(generator 
			 .nextFloat()); 
            cal.add(Calendar.DAY_OF_MONTH, 1); 
            } 
        return container; 
    } 
 } 


图 5. Vaadin timeline 示例程序运行结果
Vaadin 的体系结构和功能扩展_第5张图片

你可能感兴趣的:(Vaadin 的体系结构和功能扩展)