领先一步
VMware 提供培训和认证,助您加速进步。
了解更多本文是 Han Lim 和 Tony Nguyen 的客座文章。Han 和 Tony 在我们的新加坡 Spring 用户组做了一个关于 Spring + Angular JS 的精彩演示。这篇博客就是基于他们的演示。
在本文中,我们将尝试描述从服务器端渲染视图技术(如 JSP、Struts 和 Velocity)迁移到使用 AngularJS(一个流行的现代浏览器 JavaScript 框架)的客户端渲染视图技术的经验。我们将讨论在进行此更改时需要注意的一些事项以及可能遇到的潜在陷阱。如果您对 Spring Web MVC 和 JSP 开发有经验,并希望了解 Spring MVC 如何与 AngularJS 这样的客户端 JavaScript 配合使用,那么本文可能适合您。
还有一个附录,提供了一些关于 AngularJS 的额外见解,这些见解对于来自 JSP 世界的人来说可能显得陌生或不熟悉。
我们创建了 Spring Petclinic 应用程序的一个分支,并尝试将其转换为 AngularJS(由 Andrew Abogado 重新设计)。我们的分支可以在此处找到。
当您开始从 JSP 或 Thymeleaf 等服务器端模板引擎迁移到客户端基于 Javascript 的模板引擎时,您需要转向客户端-服务器架构的范例转变。您必须停止将视图视为 Web 应用程序的一部分,而是将 Web 应用程序视为两个独立的客户端和服务器端应用程序。因此,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 的任务运行器)。
如果您在 JSP 中使用过 Spring 的自定义表单标签来开发表单,您可能想知道 AngularJS 是否提供了将表单输入映射到对象的相同便利性。答案是肯定的!事实上,将任何 HTML 元素绑定到 Javascript 对象非常容易。唯一的区别是,现在绑定发生在客户端,而不是服务器端。
<form:form method="POST" commandName="user">
<table>
<tr>
<td>User Name :</td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td>Password :</td>
<td><form:password path="password" /></td>
</tr>
<tr>
<td>Country :</td>
<td>
<form:select path="country">
<form:option value="0" label="Select" />
<form:options items="${countryList}" itemValue="countryId" itemLabel="countryName" />
</form:select>
</td>
</tr>
</table>
</form:form>
以下是 AngularJS 中相同表单的示例
<form name="UserForm" data-ng-controller="ExampleUserController">
<table>
<tr>
<td>User Name :</td>
<td><input data-ng-model="user.name" /></td>
</tr>
<tr>
<td>Password :</td>
<td><input type="password" data-ng-model="user.password" /></td>
</tr>
<tr>
<td>Country :</td>
<td>
<select data-ng-model="user.country" data-ng-options="country as country.label for country in countries">
<option value="">Select<option />
</select>
</td>
</tr>
</table>
</form>
AngularJS 中的表单输入通过附加功能进行了增强,例如 ngRequired
指令,该指令根据特定条件使字段成为必需字段。还有内置的验证功能,用于检查范围、日期、模式等。您可以在此处找到 AngularJS 官方文档中的更多信息,该文档提供了所有相关的表单输入指令。
为了成功地将基于 JSP 的应用程序迁移到使用 AngularJS 的应用程序,有几个因素需要考虑。
您需要转换您的控制器,使其不再是将响应转发到模板引擎以向客户端呈现视图,而是提供将序列化为 JSON 数据的服务。以下是标准 Spring MVC 控制器 RequestMapping
如何使用 ModelAndView
对象根据 url 映射呈现 Owner 的示例。
@RequestMapping("/api/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
mav.addObject(this.clinicService.findOwnerById(ownerId));
return mav;
}
像这样的控制器 RequestMapping 可以转换为等效的 RESTful 服务,该服务根据 ownerId 返回 owner。然后可以将您的模板移动到 AngularJS 中,AngularJS 会将 owner 对象绑定到 AngularJS 模板。
@RequestMapping(value = "/api/owners/{id}", method = RequestMethod.GET)
public @ResponseBody Owner find(@PathVariable Integer id) {
return this.clinicService.findOwnerById(id);
}
为了让 Spring MVC 将您返回的对象(需要是 Serializable)转换为 JSON 对象,您可以使用 Jackson2 序列化库,它是 Spring MVC 依赖项的一部分。在下面的示例中,我们必须自定义 Jackson2 的日期序列化格式,因此我们在 Spring Context xml 文件中添加了 xml 片段,以描述 JSON ObjectMapper 工厂的日期格式,以便它知道 Jackson2 ObjectMapper 需要这种格式的日期。您可以在下面看到执行此 Spring Context 配置的片段。如果不需要自定义日期格式(或任何其他序列化要求),您可以使用默认格式,这意味着您甚至不需要包含此部分,因为 Spring MVC 默认会组件扫描 ObjectMapper 并通过自动装配将其注入您的控制器类中。
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean" p:indentOutput="true" p:simpleDateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ"></bean>
<mvc:annotation-driven conversion-service="conversionService" >
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" >
<property name="objectMapper" ref="objectMapper" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
将控制器转换为 RESTful 服务后,您就可以从 AngularJS 应用程序访问这些资源。
在 AngularJS 中访问 RESTful 服务的一个不错的方法是使用内置的 ngResource
指令,该指令允许您以优雅简洁的方式访问 RESTful 服务。使用此指令访问 RESTful 服务的 Javascript 代码示例如下:
var Owner = ['$resource','context', function($resource, context) {
return $resource(context + '/api/owners/:id');
}];
app.factory('Owner', Owner);
var OwnerController = ['$scope','$state','Owner',function($scope,$state,Owner) {
$scope.$on('$viewContentLoaded', function(event){
$('html, body').animate({
scrollTop: $("#owners").offset().top
}, 1000);
});
$scope.owners = Owner.query();
}];
上面的代码片段展示了如何通过声明一个 Owner 资源,然后将其初始化为 Owner 服务来创建一个“资源”。然后控制器可以使用此服务从 RESTful 端点查询 Owner。通过这种方式,您可以轻松创建应用程序所需的资源,并轻松地将其映射到您的业务领域模型。此声明仅在 app.js 文件中执行一次。您实际上可以此处查看正在运行的实际文件。
迁移到 RestAPI 时,重要的是要记住 RestAPI 是公共接口而不是网站内容。JSON 模型对用户是**完全可见**的。例如,如果我们需要显示用户资料,密码屏蔽应该在 JSON 对象上完成,而不是在模板中。为了做到这一点,有时我们需要为 RestAPI 创建 DTO 对象。
当您开发客户端-服务器架构时,状态同步是需要管理的。您需要仔细考虑您的应用程序如何从后端更新其状态或在状态发生变化时刷新其视图。
将客户端代码暴露给公众使得考虑如何验证用户身份并维护与应用程序的会话变得更加重要。在决定身份验证方法时,一个重要的考虑因素是根据您的应用程序架构选择有状态会话还是无状态会话。
您可以在 Dave Syer 的一系列博客文章中查看如何将 AngularJS 与 Spring Security 集成,此处。
AngularJS 提供了必要的工具,可帮助您在 Javascript 开发的各个层面执行测试,从单元测试到功能测试。规划如何进行测试并执行包含这些测试的构建将决定前端客户端的质量。我们使用一个名为 frontend-maven-plugin
的 Maven 插件来协助我们的构建测试。
从 JSP 迁移到 AngularJS 可能看起来令人生畏,但从长远来看非常有益,因为它使得用户界面更易于维护和测试。客户端渲染视图的趋势也鼓励构建响应性更强的 Web 应用程序,而这些应用程序以前受到服务器端渲染设计的限制。HTML 5 和 CSS3 的出现将我们带入了视图渲染技术的新时代,涌现出各种竞争框架,如 EmberJs、ReactJs、BackboneJs 等。然而,就发展势头而言,AngularJS 一直备受关注,并且使用了一段时间后,我们理解了原因。我们希望本文能为打算迈出这一步的人们提供有用的建议。您可以查看 Spring Petclinic 的分支,其中包含一些代码示例,了解我们是如何做到的。
AngularJS 是一个由 Google 创建的 Javascript 框架,自诩为“超级英雄般的 Web MVW 框架”(其中“MVW”中的“W”是对所有各种 MVx 架构的幽默指代,“Whatever”)。由于它基于 MVx 架构,AngularJS 为 Javascript 开发提供了一个结构,从而提升了 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 模板
<tr ng-repeat="user in users">
<td>{{user.username}}</td>
<td>{{user.role}}</td>
</tr>
...
<a ng-click="addUser()">Add new user</a>
添加用户的代码可以如此简单
$scope.addUser = function(){
newUser = {}
$scope.users.push(newUser );
}
如果数组 users
多了一个元素,表格就会自动多一行。
使用 AngularJS,可以以有组织且优雅的方式编写相对复杂的 User Interfaces,始终将所需的逻辑封装在您的组件中,并且绝不会冒 errant global Javascript 变量污染您的作用域的风险。它也非常易于测试,并且内置了在单元和功能级别执行测试的机制,确保您的 User Interface 代码库经过与您的 Java/Spring 代码相同的严格测试,即使在用户界面级别也能确保质量。
使用 AngularJS 编写 html 模板的另一个优点是,即使在视图中嵌入了各种前端逻辑,模板本质上仍然与 html 相似。可以将 AngularJS 逻辑整合到您的模板中,同时仍然进行客户端验证控制。在 JSP 世界中,您可以尝试从浏览器中查看一个包含所有模板逻辑的 JSP 文件,您的浏览器很可能无法渲染该页面。您可以此处看到一个典型的 AngularJS 模板是什么样子。
<div class="row thumbnail-wrapper">
<div data-ng-repeat="pet in currentOwner.pets" class="col-md-3">
<div class="thumbnail">
<img data-ng-src="images/pets/pet{{pet.id % 10 + 1}}.jpg"
class="img-circle" alt="My Pet Image">
<div class="caption">
<h3 class="caption-heading" data-ng-bind="pet.name"></h3>
<p class="caption-meta" data-ng-bind="pet.birthdate"></p>
<p class="caption-meta"><span class="caption-label"
data-ng-bind="pet.type.name"></span></p>
</div>
<div class="action-bar">
<a class="btn btn-default" data-toggle="modal" data-target="#petModal"
data-ng-click="editPet(pet.id)">
<span class="glyphicon glyphicon-edit"></span> Edit Pet
</a>
<a class="btn btn-default">
<span></span> Add Visit
</a>
</div>
</div>
</div>
</div>
您可能会注意到模板中添加了一些非 HTML 内容。它包含诸如 data-ng-click
等属性,这些属性将按钮上的点击映射到方法名称调用。还有 data-ng-repeat
,它遍历 JSON 数组并生成必要的 html 代码以渲染数组中每个项目的相同视图。即使所有逻辑都已就位,我们仍然可以从浏览器验证和查看 html 模板。AngularJS 将所有非 html 标签和属性称为“指令”,这些指令的目的是增强 HTML 的功能。AngularJS 还支持 HTML 4 和 5,因此如果您仍依赖 HTML 4 DOCTYPEs 的模板,它应该仍然可以正常工作(尽管 HTML 4 的验证器不会识别 data-ng-x 属性)。
使用 AngularJS 和 JSP 的一个巨大区别是**渲染时间**。如果您使用 JSP,服务器会渲染 html 内容。相比之下,如果您使用 AngularJS,渲染发生在浏览器中。因此,模板和 JSON 对象都需要发送到客户端。值得注意的是,AngularJS 在运行 DOM 操作以生成内容之前可能会短暂地显示模板。例如,如果 AngularJS 尚未完全加载,页面中的出生日期将显示一个空值,然后才显示真实值。
在 AngularJS 中理解一个重要概念是作用域。过去,每当我在 Web 应用程序中编写 Javascript 时,我不得不管理变量名并构建特殊的命名空间对象来存储我的作用域属性。然而,AngularJS 根据其 MVx 概念自动为您完成此操作。每个指令都会从其控制器继承一个作用域(或者如果您愿意,一个不继承其他作用域属性的隔离作用域)。在此作用域中创建的属性和变量不会污染其余作用域或全局上下文。
作用域是 AngularJS 应用程序的“粘合剂”。AngularJS 中的控制器使用作用域与视图交互。作用域也用于在指令和控制器之间传递模型和属性。这样做的好处是,我们现在被迫以一种组件是自包含的方式设计我们的应用程序,并且必须通过使用可以从父作用域原型继承的模型来仔细考虑组件之间的关系。
一个作用域可以以原型的方式嵌套在另一个作用域中,这与 Javascript 通过原型实现其继承模型的方式类似。然而,在子作用域中声明的任何与父作用域中同名的属性,此后都将从子作用域中隐藏父属性。以下代码描述了这一示例:
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@*" data-semver="1.4.0-rc.0" src="https://code.angularjs.org/1.4.0-rc.0/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body data-ng-app="demo">
<h1>Scopes in AngularJS</h1>
<div data-ng-controller="parentController">
<div data-ng-controller="childController">
<span>This is a demonstration of scopes</span>
<div>
Parent model: <span data-ng-bind="$parent.model.name"></span>
</div>
<div>
Current model: <span data-ng-bind="model.name"></span>
</div>
<div>
<button data-ng-click="updateModel()">Click me</button>
</div>
</div>
</div>
</body>
</html>
在作用域层级的最顶层是 $rootScope,这是一个全局可访问的作用域,可以作为在整个应用程序中共享属性和模型的最后手段。应尽量减少使用它,因为它引入了一种“全局”变量,过度使用时会带来同样的问题。
有关作用域的更多信息,请参阅 AngularJS 文档,请点击此处查看。
指令是 AngularJS 中最重要的概念之一。它们在 HTML 元素、属性、类或注释中带来了所有额外的定制标记。它们是为标记赋予新功能的组件。
以下代码片段演示了一个名为 wdsCustom
的定制指令,它将把标记元素 <wds-custom company="wds">
替换为包含有关名为 wds
的模型信息的标记。该模型元素是在包裹指令的控制器作用域中声明的。您可以查看 app.js
、index.html
文件和指令模板 wds-custom-directive.html
,以了解在可用的 plunkr 代码片段中这是如何工作的,请点击此处查看。
由于本文不打算教您如何编写指令,您可以参考官方文档,请点击此处查看。