这篇文章是 Han Lim 和 Tony Nguyen 的客座文章。 Han 和 Tony 在我们的新加坡 Spring 用户组上做了关于 Spring + Angular JS 的精彩演讲。这个博客是基于他们的介绍。
抽象的
在本文中,我们尝试描述我们从服务器端呈现视图技术(如 JSP、Struts 和 Velocity)转移到使用 AngularJS 的客户端呈现视图技术的经历,AngularJS 是一种流行的现代浏览器 JavaScript 框架。我们将讨论进行此更改时需要注意的一些事项以及您可能遇到的潜在陷阱。如果您在 Spring Web MVC 和 JSP 开发方面有经验,并且想了解 Spring MVC 如何与 AngularJS 等客户端 JavaScript 协同工作,那么本文可能适合您。
还有一个附录提供了一些关于 AngularJS 的额外见解,这些见解对于来自 JSP 世界的人来说可能看起来很奇怪或不熟悉。
样本宠物诊所供参考
我们创建了 Spring Petclinic 应用程序的一个分支,并尝试将其转换为 AngularJS(由 Andrew Abogado 提供的新设计)。我们的叉子可以 在这里 找到。
准备
当您开始从服务器端模板引擎(如 JSP 或 Thymeleaf) 迁移到客户端基于 JavaScript 的模板引擎时,您将需要采用向客户端-服务器架构的范式转变。您必须停止将视图视为 Web 应用程序的一部分,而是将 Web 应用程序视为 2 个独立的客户端和服务器端应用程序。 AngularJS 应用程序因此成为一个独立的应用程序,在您的 Web 浏览器上运行,并与 Spring MVC 提供的后端服务进行通信。 Spring MVC 应用程序和 AngularJS 之间的唯一共同点可能是它们部署在同一个 Java WAR 文件中并且索引文件由 JSP 提供。
下图说明了这一点,显示了 Spring 应用程序如何成为 RESTful Web 服务的提供者,为各种前端应用程序提供服务,包括基于 AngularJS 浏览器的应用程序,以及为平板电脑或智能手机等移动客户端提供服务的可能性.这些服务可能包括 OAuth、身份验证和其他应从公众视野中混淆的业务逻辑服务。应该记住,以 JSON 或 JavaScript 文件形式发布的任何数据或业务逻辑都会暴露给客户端查看。因此,如果有任何不应公开的业务敏感逻辑或工作流,则应仅在后端执行。
使用 AngularJS 而不是 JSP 的另一个需要注意的区别是我们不希望使用 HTML 表单和传统的表单提交来将数据传递到服务器端。相反,我们更愿意将表单提交封装在一个 JSON 对象中,该对象通过 AngularJS HTTP Post 方法调用发送到后端 RESTful 服务。事实上,我们更愿意使用开发 RESTful 服务时鼓励使用的所有 HTTP 动词。
如果您需要对用户输入执行验证,可以使用 AngularJS 的内置验证或您自己的自定义输入验证在前端完成。在将数据发布到服务器之前,您应该始终验证您的数据。同样谨慎的做法是在服务器端验证相同的数据,以确保不检查其数据的客户端不会损害服务器端数据的完整性。
申请结构
现在让我们讨论如何组织 Spring + AngularJS 应用程序。在 WDS(我们公司),我们使用 Maven 作为 Java/Spring 的依赖和包管理工具,这影响了我们决定放置 AngularJS JavaScript 应用程序的方式。 AngularJS 应用程序是在
src/main/webapp
中创建的,主要文件是
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
您可以在下面的 Eclipse 中看到文件夹结构的图像捕获。
此处的资源是根据
feature-grouping
方法组织的。还有一些方法可以根据类型对资源进行分组,例如将所有控制器、服务和视图分组到同名文件夹中。这些选项各有利弊。
还有一些基于 JavaScript 的包管理器,例如 npm 或 bower ,您可能想考虑使用它们来简化外部依赖项的管理。如果您使用的是 bower ,您将创建一个名为 bower_components 的文件夹,其中将安装所有依赖项资源。然后您需要将它们包含在您的模板中,就像您对任何 JavaScript 库所做的那样。至于 npm ,你可以用它来管理你所有的 JavaScript 服务器端系统工具,比如 Grunt(一种类似于 Ant 的任务运行器)
使用 AngularJS 指令与 JSP 自定义标签
如果您在 JSP 中使用 Spring 的自定义表单标记来开发表单,您可能想知道 AngularJS 是否提供了相同类型的便利来将表单输入映射到对象。答案是肯定的!事实上,很容易将任何 HTML 元素绑定到 JavaScript 对象。唯一的区别是现在绑定发生在客户端而不是服务器端。
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
这是 AngularJS 中相同形式的示例
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
AngularJS 中的表单输入增加了额外的功能,例如
ngRequired
指令,该指令根据特定条件强制输入字段。还有用于检查范围、日期、模式等的内置验证。您可以在
此处的
AngularJS 官方文档中找到更多信息,其中提供了所有相关的表单输入指令。
从 JSP 迁移到 AngularJS 时的注意事项
为了成功地将基于 JSP 的应用程序迁移到使用 AngularJS 的应用程序,需要考虑几个因素。
将 Spring 控制器转换为 RESTful 服务
您将需要转换您的控制器,而不是将响应转发给模板引擎以向客户端呈现视图,您将提供将序列化为 JSON 数据的服务。以下是标准 Spring MVC 控制器
RequestMapping
如何使用
ModelAndView
对象来呈现具有所有者的视图的示例,如 url 映射中所述。
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
像这样的控制器 RequestMapping 可以转换为等效的 RESTful 服务,该服务根据 ownerId 返回所有者。然后可以将您的模板移动到 AngularJS 中,然后 AngularJS 会将所有者对象绑定到 AngularJS 模板。
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
为了让 Spring MVC 将返回的对象(需要可序列化)转换为 JSON 对象,您可以使用 Jackson2 序列化库,它是 Spring MVC 依赖项的一部分。在下面的示例中,我们必须通过 Jackson2 自定义日期序列化格式,因此我们在 Spring Context xml 文件中添加了 xml 片段来描述我们的 JSON ObjectMapper Factory 的日期格式,以便它知道 Jackson2 ObjectMapper 需要这样的日期格式。您可以在下面看到执行此 Spring 上下文配置的代码片段。如果没有自定义日期格式(或任何其他序列化要求),您可以使用默认格式,这意味着您甚至不需要包含此部分,因为默认情况下 Spring MVC 将组件扫描 ObjectMapper 并将其注入您的控制器通过自动装配类。
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
一旦将控制器转换为 RESTful 服务,就可以从 AngularJS 应用程序访问这些资源。
在 AngularJS 中访问 RESTful 服务的一种好方法是使用内置的
ngResource
指令,它允许您以优雅简洁的方式访问 RESTful 服务。使用此指令访问 RESTful 服务的 JavaScript 代码示例如下所示:
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
上面的代码片段显示了如何通过声明所有者资源然后将其初始化为所有者服务来创建“资源”。然后,控制器可以使用此服务从 RESTful 端点查询所有者。通过这种方式,您可以轻松创建应用程序所需的资源并将其轻松映射到您的业务领域模型。此声明仅在 app.js 文件中完成一次。您实际上可以 在此处 查看这个正在运行的实际文件。
迁移到 RestAPI 时,请务必记住 RestAPI 是公共接口而不是网站内容。 JSON 模型对用户 完全可见 。
例如,如果我们需要显示用户配置文件,密码屏蔽应该在 JSON 对象上而不是在模板中完成。为了做到这一点,有时我们需要为我们的 RestAPI 创建 DTO 对象。
在后端和 AngularJS 应用程序之间同步状态
在开发客户端-服务器架构时,需要管理同步状态。您需要考虑您的应用程序如何从后端更新其状态或在某些状态更改时刷新其视图。
验证
将您的客户端代码公开给公众使得仔细考虑您希望如何验证您的用户并与您的应用程序保持会话变得更加重要。决定身份验证方法的一个重要考虑因素是根据您的应用程序架构在有状态会话或无状态会话之间进行选择。
您可以 在此处 查看 Dave Syer 关于如何将 AngularJS 与 Spring Security 集成的系列博客。
测试
AngularJS 附带了必要的工具,可帮助您在 JavaScript 开发的所有层执行测试,从单元测试到功能测试。规划如何测试和执行包含这些测试的构建将决定前端客户端的质量。我们使用一个名为
frontend-maven-plugin
的 Maven 插件来帮助我们进行构建测试。
结论
从 JSP 迁移到 AngularJS 可能看起来令人望而生畏,但从长远来看,它可能是非常有益的,因为它使用户界面更易于维护和测试。客户端呈现视图的趋势还鼓励构建响应速度更快的 Web 应用程序,这些应用程序以前受到服务器端呈现设计的阻碍。 HTML 5 和 CSS3 的出现将我们带入了 View 渲染技术的新纪元,出现了不同的竞争框架,如 EmberJs、ReactJs、BackboneJs 等。但是,就势头而言,AngularJS 已经得到了很多关注,并且已经使用它一会儿,我们可以明白为什么。我们希望这篇文章包含对打算冒险的人有用的提示。您可以检查具有一些代码示例的 Spring Petclinic 的分支,看看我们是如何做到的。
附录
AngularJS 简介
AngularJS 是谷歌创建的一个 JavaScript 框架,自称为“超级英雄 Web MVW 框架”(其中“MVW”中的“W”是对所有各种 MVx 架构的 “Whatever”的半开玩笑的引用。如它基于 MVx 架构,AngularJS 为 JavaScript 开发提供了一种结构,因此与传统的 Spring + JSP 应用程序相比,它提升了 JavaScript 的地位,传统的 Spring + JSP 应用程序仅使用 JavaScript 在用户界面上提供一点交互性。
使用 AngularJS,基于 JavaScript 的视图层还继承了诸如依赖注入、HTML 词汇扩展(通过使用自定义指令)、单元测试和功能测试集成以及 JQuery 的 DOM 选择器(使用 jqlite 作为它)等功能仅提供 JQuery 的一个子集,但如果您愿意,也可以轻松使用 JQuery)。 AngularJS 还为您的 JavaScript 代码引入了范围,以便在您的代码中声明的变量仅绑定到所需的范围。这可以防止在 JavaScript 的大小增长时无意中出现的变量污染。
当您使用 JSP 开发 Spring Web MVC 应用程序时,您可能会使用 Spring 提供的表单标签将表单输入绑定到服务器端模型。同样,AngularJS 提供了一种将表单输入绑定到客户端模型的方法。事实上,它提供了从表单输入到 JavaScript 应用程序模型的即时双向数据绑定。这意味着您不仅可以通过 JavaScript 模型内部的更改来更新视图,而且您对 UI 所做的任何更改也会更新 JavaScript 模型(以及因此绑定到该模型的任何其他视图)。看到应用程序上绑定到同一 JS 模型的所有视图自动更新模型几乎是不可思议的。
此外,由于您的模型可以设置为特定范围,因此只有属于同一范围的视图才会受到影响,从而允许您沙盒代码,这些代码应该只在视图的特定部分是本地的。 (这是通过在 HTML 模板中设置的名为
ng-controller
的 AngularJS 属性完成的)。您可以在后面比较 JSP 标记和 AngularJS 指令的部分中看到差异。
双向数据绑定
在 Spring-JSP Web 应用程序中,有一种方法可以将数据从 Spring 模型绑定到 jsp 视图。对模型的任何更改都将反映到 Jsp 视图中,但反之则不然。这是 Web 应用程序的本质。如果我们构建桌面应用程序,则可以使用 Swing UI 进行反向数据绑定。
但是,对于公开 REST 资源的 Web 应用程序,可能没有直接的数据绑定。数据作为 JSON 对象从服务器发送到浏览器。如果没有 AngularJS 等,开发人员需要编写 JavaScript 代码才能将 JavaScript 对象绑定到 html 控件。
由于手动数据绑定是一项乏味的任务,一些开发人员试图通过创建用于数据绑定的 JavaScript 框架来自动执行该任务。值得记住的是,这种数据绑定发生在客户端,数据绑定模型是一个 JavaScript 对象,而不是服务器端模型。
Angular 通过创建双向绑定进一步推动了这个想法。更改 HTML 控件中的值将实时反映在对象中。
如果您需要处理像 AJAX 表这样的复杂 UI 组件,那么绑定是一个有用的概念。
例如:我们需要在 AngularJs 应用程序中呈现用户和角色列表,使用以下 html 模板:
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
添加用户的代码可以这么简单:
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
如果数组
users
多了一个元素,表格将自动多了一行。
AngularJS 模板
使用 AngularJS,可以以有条理和优雅的方式编写相对复杂的用户界面,始终将所需的逻辑封装在您的组件中,并且永远不会冒错误的全局 JavaScript 变量污染您的范围的风险。它也非常可测试,并且有内置机制在单元和功能级别执行测试,确保您的用户界面代码库经过与 Java/Spring 代码相同的严格测试,即使在用户界面上也能确保质量等级。
使用 AngularJS 编写 html 模板的另一个优点是,即使在视图中嵌入了各种前端逻辑,模板在本质上与 html 相似。可以将 AngularJS 逻辑合并到您的模板中,并且仍然执行客户端验证控制。在 JSP 世界中,您可以尝试从浏览器查看 JSP 文件,所有模板逻辑都已就位,但您的浏览器很可能会放弃呈现页面。
你可以看到一个典型的 AngularJS 模板是怎样的:
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
您可能会在模板中发现一些非 HTML 添加项。它包括像
data-ng-click
这样的属性,它将按钮上的点击映射到方法名称调用。还有
data-ng-repeat
循环遍历 JSON 数组并生成必要的 html 代码来为数组中的每个项目呈现相同的视图。然而,有了所有的逻辑,我们仍然能够从浏览器验证和查看 html 模板。
AngularJS 将所有非 html 标签和属性称为“指令”,这些指令的目的是增强 HTML 的功能。 AngularJS 还支持 HTML 4 和 5,因此如果您的模板仍然依赖 HTML 4 DOCTYPE,它应该仍然可以正常工作(尽管 HTML 4 的验证器无法识别 data-ng-x 属性)。
使用 AngularJS 和 JSP 的一大区别是 渲染时间 。如果您使用 JSP,则服务器呈现 html 内容。相比之下,如果您使用 AngularJS,则渲染发生在浏览器中。因此,模板和 JSON 对象都将发送到客户端。值得注意的是,AngularJS 可能会在运行 DOM 操作生成内容之前短暂显示模板。例如,如果 AngularJS 没有完成加载,页面中的出生日期将在显示实际值之前显示为空值。
AngularJS 中的作用域
在 AngularJS 中要掌握的一个重要概念是作用域。过去,每当我不得不为我的 Web 应用程序编写 JavaScript 时,我都必须管理变量名称并构建特殊的名称空间对象以存储我的范围属性。然而,AngularJS 会根据它的 MVx 概念自动为你做这件事。每个指令将从其控制器继承一个作用域(或者,如果您愿意,一个不继承其他作用域属性的独立作用域)。在此作用域中创建的属性和变量不会污染其余作用域或全局上下文。
作用域被用作 AngularJS 应用程序的“粘合剂”。 AngularJS 中的控制器使用作用域与视图进行交互。范围还用于在指令和控制器之间传递模型和属性。这样做的好处是,我们现在被迫以组件自包含的方式设计我们的应用程序,并且必须通过使用可以从父范围原型继承的模型来仔细考虑组件之间的关系。
一个作用域可以原型嵌套在另一个作用域中,就像 JavaScript 通过原型实现其继承模型一样。但是,在子作用域中声明的任何与父作用域相似的属性名称之后将从子作用域中隐藏父属性。下面的代码描述了一个例子:
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/# additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
在范围层次结构的最顶层是 $rootScope,这是一个可以全局访问的范围,可以用作在整个应用程序中共享属性和模型的最后手段。应该尽量减少使用它,因为它引入了一种“全局”变量,当它被过度使用时会造成同样的问题。
可以从 此处 找到的 AngularJS 文档中收集有关范围的更多信息。
AngularJS 中的指令
指令是 AngularJS 中最重要的概念之一。它们在 HTML 元素、属性、类或注释中带来了所有额外的自定义标记。他们是那些赋予标记新功能的人。
以下代码片段演示了一个名为
wdsCustom
的自定义指令,它将用包含有关名为
wds
的模型的信息的标记替换标记元素
<wds-custom company="wds">
。该模型元素在包装指令的控制器范围内声明。您可以查看文件
app.js
、
index.html
和指令模板
wds-custom-directive.html
,了解其在
此处
提供的 plunkr 片段中的工作原理。
由于本文并不试图教你如何编写指令,你可以参考 这里的 官方文档。