vlambda博客
学习文章列表

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

Chapter 8. AngularJS and Java RESTful Services

 

“慢 - 任何快于 50 毫秒的速度对人类来说是无法察觉的,因此可以被视为“即时”。

 
  --Misko Hevery, co-creator of AngularJS

对于本章,我们将跳出 JSF 的舒适区,探索一种不同的 Web 应用模式。你们中的大多数人都会熟悉流行的社交媒体,如 Google Mail、Facebook 和 Twitter 及其网络用户界面。这些 Web 应用程序具有特殊的用户体验和信息架构,给人一种交互发生在一个网页上的错觉。然而,在幕后,这些应用程序依赖于标准技术:HTML5、CSS 和客户端 JavaScript。它们都使用 AJAX 调用通过 HTTP 与后端服务器进行通信。当服务器端应用程序向 Web 客户端发送数据时,只有页面的一部分被更新。在当代使用中,许多数字站点利用应用程序后端的 RESTful 服务端点。一些复杂的企业应用程序可能会使用 服务器发送事件向工作的多个用户发送通知span> (SSE),而更前沿的则依赖于新出现的 HTML5 WebSocket 规范来提供客户端之间的全双工通信和服务器。顺便提一下,Java Community Process 的完整 Java EE 7 规范支持 JAX-RS、SSE 和 WebSocket。

Single-page applications


在单个页面上构建应用程序以使其类似于桌面应用程序的设计理念与 JavaServer Faces 之间的导航链接的原始设计形成鲜明对比页。 JSF 1.0 早在 2005 年重新发现 XMLHttpRequest JavaScript 对象和 Google 地图之前就创建了,因此历史记录不应该是 一个惊喜(http://en.wikipedia .org/wiki/JavaServer_Faces)。完全可以将 JSF 编写为单页应用程序,但我不建议将方形钉子强制插入圆孔! JSF 适用于本质上和设计上非常有状态的应用程序,其中客户旅程基于页面到页面的导航。在前面的章节中,我们已经介绍了很多关于带有 JSF、流范围、对话和视图范围 bean 的有状态 Web 应用程序。如果您对这些概念不彻底,那么我强烈建议您再次修改该材料。我们现在将继续使用替代设计模式。

让我们列出单页应用程序的有益特性:

  • SPA 通常具有适合单个页面的网站或 Web 应用程序。

  • 它们依赖于现代数字 JavaScript 技术,包括 AJAX、HTML5 和 CSS。

  • 这种类型的应用程序不是在导航期间加载整个页面,而是操纵 文档对象模型 (DOM< /strong>) 以提供页面更新。

  • 这些 应用程序通常使用 HTML 模板引擎在客户端本地呈现内容。客户端的表示逻辑和服务器端的业务逻辑之间存在关注点分离。

  • SPA 与 Web 服务器动态通信,通常使用 RESTful 服务,使用 JSON 作为流行的有效负载类型。

单页应用程序有一些缺点,内容策略师、技术主管开发人员,显然还有利益相关者业务人员应该意识到:

  • 可能很难将搜索引擎优化应用于 SPA。

  • 在浏览器中使用后退按钮可能会导致数据条目丢失; SPA 不能很好地处理 Web 浏览器历史记录。

  • SPA 需要更高程度的应用程序开发知识来处理反应式编程和概念。值得注意的是,工程师应该意识到与权衡回合可扩展性、弹性、事件驱动处理和通知有关的因素,并做出响应。

最后,请允许我给你一个忠告。业内的数字界面开发人员拥有 JavaScript、HTML5 和 CSS 技能。在本章中,您将认识到 JavaScript 编程能力与 Java 服务器端要求同样重要。换句话说,使用 AngularJS 和类似的客户端框架往往是一种全栈参与。

The caseworker application


对于本章节,我们将研究一种特殊类型的国际政府单页应用程序,称为个案工作者系统。个案工作者的业务用户将坐在办公桌上,并且在他们一天的大部分时间里,通过申请产品的各个阶段来处理申请人。

以下是该应用程序的屏幕截图:

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

案例工作者应用程序的屏幕截图,xen 国家力量

该应用程序名为 xen-national-force,旨在通过微型 工作流程。它远不能满足真正的业务应用程序的需求。例如,为了使事情尽可能简单,没有实施用户输入安全措施。它仅适用于一名案例工作者,并且从用户体验方面来看存在一个非常明显的设计缺陷。但是,xen-national-force 应用程序演示了如何使用 AngularJS 构建具有主从记录和 CRUD 操作的系统,并且它具有基本的有限状态机实现。

我们现在将继续学习流行的AngularJS 框架。

AngularJS


近年来,一个 特定的JavaScript 客户端框架已成为构建企业业务单页应用程序的有力竞争者。它被称为 AngularJS (http:// angularjs.org) 并由 Google 支持和许可。可以在 GitHub 上的 https://github.com/angular/angular 找到软件存储库.js。应该说它不是唯一提供 DOM 双向绑定、Model-View Controller、模板、模块、服务和工厂的框架。

在本书中,我们只关注 AngularJS,但您应该知道 JavaScript 客户端世界中的两个主要竞争对手,即 Backbone.jsEmber.js。由于任务的范围,我们开始使用 AngularJS 框架,本章将介绍框架的初学者。我们将介绍针对 Java EE 7 运行的 AngularJS 版本 1.3.15。

Tip

要从头开始全面讨论 AngularJS 和客户端 JavaScript,我们建议阅读 Packt Publishing 的另一本书,Mastering Single Application Development with AngularJS 科兹洛夫斯基和达尔文

使用 AngularJS 进行编程意味着您很少需要深入研究低级 W3C HTML DOM API。事实上,需要数十行 JavaScript 自定义绑定代码来进行完整性检查的操作现在变成了单行代码。

假设我们有一个简单的 HTML 表单,它实现了经典的 Hello, World!片段。我们希望用户在文本字段中输入他/她的姓名,并在现实世界中的问候语中使用该姓名。我们的 HTML 内容可能如下所示:

<form id="helloForm">
  <input class="greeting-name" type="text" ></input>
  <div class="greeting-name">message </div>
</form>

我们如何使用 jQuery 将文本输入与 div 元素中的消息区域连接起来?一种可行的方法是编写事件处理程序和回调函数,如 JavaScript 模块的以下片段:

$('#helloForm input.greeting-name').on('value', function() {
  $('#helloForm div.greeting-name').text('Hello ' + this.val() + '!');
});

前面的 代码片段,没有 JavaScript 对象模块的样板文件和依赖注入,就可以解决问题。当用户在 CSS 类选择器 input.greeting-name 标识的文本字段中键入内容时,jQuery 会调用回调函数,该回调函数会更新 div 元素层,用 CSS 类 div.greeting.name 标识。我们可以扩展这段代码并编写一个带有参数的通用解决方案,特别是如果我们的应用程序中有更多这样的情况,但迟早,这种低级别的编程会引入复杂性和错误。

AngularJS 的设计者意识到存在改进的机会。同样的例子可以用AngularJS重写,如下:

<!DOCTYPE HTML>
<html>
<head>
  <script src="http://ajax.googleapis.com/ajax/lib/angularjs/1.3.15/angular.js"></script>
</head>
<body ng-app ng-init="greeting-name = 'Mr. Anderson'">
  <form>
    <input ng-model="customer-name" type="text" />
    <div class="greeting-name">Hello {{customer-name}}!</div>
  </form>
</body>
</html>

前面的片段完全是 HTML。它包括来自远程服务器的 AngularJS 框架,Content Delivery Network (CDN< /跨度>)。主体 HTML 元素使用 非标准属性 ng-app 进行注释,以声明这个 DOM 节点是整个模板的一部分。另一个属性 ng-init 在模板呈现在客户端之前声明一个数据模型。 AngularJS 需要知道从哪里开始模板化或动态修改 DOM;因此,每个页面都以 ng-app 属性开始。通常,ng-app 属性应用于 HTML body 元素。如果没有访问数据模型,AngularJS 模板将毫无用处,这就是 ng-init 属性的目的。它设置了一个名为 greeting-name 的作用域变量,并为其分配字符串字面值,Mr.安德森

请注意附加属性类型 ng-model 和特殊的双花括号语法:{{customer-name}}。该属性是 AngularJS 框架提供的一个特殊扩展,用于识别内联数据模型,大括号表示一个特殊的 HTML 模板 语法称为指令。在这里,我们将 ng-model 属性应用于输入字段元素。加载页面时,输入文本字段会显示文本 Mr Anderson。该代码还允许用户在输入字段中输入文本并同时更新消息区域。这个简单的案例不需要编程;实际上它是声明性的。那么秘方是什么呢?以下代码显示了双向绑定的一种形式。让我们扩展它来演示完整的双向绑定:

<form>
  <input ng-model="customer-name" type="text" />
  <div class="greeting-name">Hello {{customer-name}}!</div>
  <p>
   <button class="btn-large" ng-click="user-model = 'Karen'">
   Karen </button> </p>
  <p>
   <button class="btn-large" ng-click="user-model = 'Albert'">
   Albert </button> </p>
</form>

我们引入了具有新属性 ng-click 的 HTML button 元素。该属性的值是一个 AngularJS JavaScript 表达式。每个按钮都会使用新名称更新数据模型。实际上,他们重置了输入字段和消息区域中的名称。多么酷啊?那里根本没有 jQuery 编程。 AngularJS 有许多特殊的自定义属性,例如 ng-repeatng-switchng -option,我们将在本章后面遇到。

您可能想知道这些绑定和模板非常聪明;那么它在客户端是如何工作的呢?

How does AngularJS work?

AngularJS 作为 HTML 页面内容的一部分加载到 Web 浏览器中。该框架最强大的部分是它鼓励关注点分离。表示视图应该故意与业务逻辑和数据模型混合。这有几个原因。当 Angular JS 框架被加载时,页面被触发,框架在 DOM 中上下移动并寻找某些称为指令的非标准属性。它使用编译器解析和处理这个标记。 AngularJS 有效地转换静态加载的 DOM 并生成渲染视图。框架采用这些指令并创建关联、绑定和额外的行为。

ng-app 属性链接到初始化应用程序的指令。 ng-init 链接到允许程序员设置数据模型的指令。它可用于为变量赋值。 ng-model 与指令访问相关联或存储与 HTML Input 元素相关联的值。 AngularJS 允许开发人员编写自定义指令。您可能希望将来编写一个来访问 DOM。

AngularJS 致力于模板视图中嵌套范围的想法。范围是表达式的执行上下文。范围可以以分层方式组织,以便它们模仿 DOM 模型。

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

AngularJS 的工作原理

AngularJS 依赖于定义控制器和其他逻辑的 JavaScript 模块。模块可以依赖于其他模块;然而,与 RequireJS 不同的是,作为不同 JavaScript 文件一部分的模块不会自动加载到应用程序中。范围是绑定表示和数据模型的粘合剂。作用域是 AngularJS 中定义 观察者和监听者的地方。大多数时候,框架会自动处理表达式处理和数据绑定,并处理 JavaScript 模块和 DOM 元素组件之间的通知。在编译阶段之后,AngularJS 进入链接阶段并将表达式与模块控制器方法和其他资源相关联。

让我们总结一下这些步骤:

  1. AngularJS 框架自举。特别是,它在 DOM 中搜索具有 ng-app 属性的 HTML 元素。这是框架的触发点。

  2. 一旦找到 ng-app 元素,AngularJS 就会创建一个依赖注入器。

  3. 然后它将静态 DOM 编译为呈现中间视图,并在执行过程中收集指令。

  4. 然后 AngularJS 开始链接和组合指令及其相关范围。这是一种算法和分层操作。在执行链接阶段之前,框架会创建一个称为根范围的初始范围。

  5. 最后,AngularJS 使用根范围调用 apply 调用,在这个阶段,视图被渲染。

让我们看看个案工作者的观点。在本书的源代码中,您会找到名为 xen-force-angularjs 的 Gradle 项目。它遵循 Java EE 项目的 Maven 约定。我们的 讨论将分为两个部分。我们将查看由 HTML5、JavaScript 和一些 CSS 组成的前端代码。之后,我们将深入研究 Java 服务器端的后端。让我们看一下下图:

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

AngularJS中指令与业务逻辑的关系

Caseworker overview


案例工作者项目显示了一个主从应用程序。我们的工作人员开始申请,看到一个案例记录列表,其中包含每个申请人的姓名和护照详细信息。这是主记录。每个案例记录可以附加零个或多个任务记录。这些是主人的详细记录。每个主记录还包含一个状态属性,显示每个申请人在流程中的位置。我们的用户被允许访问所有案例记录并将当前状态从开始移动到结束。

Caseworker main view

在 caseworker 示例中只有 一个 HTML 文件,它作为 src/main/webapp/ 中的模板index.xhtml 文件。请记住,这是一个单页应用程序!

<!DOCTYPE html>
<html ng-app="app">
  <head>
    ...
    <link href="styles/bootstrap.css" rel="stylesheet">
    <link href="styles/main.css" rel="stylesheet">
  </head>

  <body ng-controller="CaseRecordController">
    ...
    <div id="mainContent">
      ...
      <div class="case-record-view" >
        ...
        <div class="actionBar" ng-controller="NewCaseRecordModalController" >
          <button class="btn btn-primary" ng-click="openCreateCaseRecordDialog()" >Add New Case Record</button>
          <div ng-show="selected">Selection from a modal: {{ selected }}</div>
        </div>

        <h2 class="case-record-headline">Case Records</h2>
        <table class="table table-bordered" >
          <tr>
            <th>Id</th>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Sex</th>
            <th>Country</th>
            <th>Passport No</th>
            <th>D.o.B</th>
            <th>Expiration Date</th>
            <th>Status</th>
          </tr>
          ...
        </table>
      </div>
    </div>
  </body>
</html>

HTML 标记 元素由 AngularJS 指令 ng-app 赋予属性,该指令指定用作应用程序的作用域值。我们有通常的 headbody 元素。我们包括 CSS 文件 Bootstrap (bootstrap.css) 和应用程序的样式文件 main.css。在我们到达 Body 标记之前没有太大区别,该标记是用 ng-controller 属性声明的。 ng-controller 指令将控制器附加到视图。控制器是属于 MVC 模式的 JavaScript 对象。因此,DOM 中的整个 body 标签元素都绑定到名为 CaseRecordController 的 JavaScript 对象。我们稍后会看到它的代码,但首先,让我们深入一点。

当您进一步检查代码时,您会注意到 div 元素上的另一个控制器指令,其 CSS 选择器名为 action-bar。此元素与名为 NewCaseRecordModalController 的不同控制器相关联。每次 ng-controller 指令被赋予属性时,AngularJS 都会创建一个全新的作用域。因此范围可以相互嵌套。这是 AngularJS 框架中的关键概念。范围存在于与其他嵌套范围相关联并包含它们的元素上,如果它们存在的话。

主视图呈现案例记录表。前面的代码显示了申请人的姓名、性别、出生日期、ISO 国家代码、护照号码和护照有效期。

以下是呈现主表行的内容的下一部分:

    <tr ng-repeat-start="caseRecord in caseRecords">
      <td>
        <div ng-controller="NewCaseRecordModalController" style="display: inline;">
          <a class="btn" href="#" ng-click="showOrHideTasks($parent.caseRecord)">
          <i class="glyphicon" ng-class="getIconClass($parent.caseRecord)" ></i>
          </a>
        </div>
      </td>
      <td>{{caseRecord.lastName}}</td>
      <td>{{caseRecord.firstName}}</td>
      <td>{{caseRecord.sex}}</td>
      <td>{{caseRecord.country}}</td>
      <td>{{caseRecord.passportNo}}</td>
      <td>{{caseRecord.dateOfBirth}}</td>
      <td>{{caseRecord.expirationDate}}</td>
      <td>{{caseRecord.currentState}}</td>
    </tr>

此代码内容有几个部分。 ng-repeat-start 是一个特殊指令,允许使用表达式迭代 内容。该表达式是 AngularJS 动态评估的表单选择查询。因此,<"caseRecord in caseRecords"> 表达式表示在名为 caseRecords 的范围内的对象的整体迭代,并将每个元素分配为一个名为 caseRecord 的对象。我们使用 AngularJS 绑定指令表达式在适当的表格单元格元素中呈现每个案例记录的信息。我们对单元格 {{caseRecord.lastName}} 执行此操作,然后冲洗并重复。

第一个数据单元格是特殊的,因为它呈现嵌入的 div 元素。它说明了如何关联布尔值,并为案例记录提供扩展和折叠关联。我们必须在 div 上创建一个范围,并将适当的控制器 NewCaseRecordModalControllerng-控制器 属性。我们利用 ng-click 指令来调用控制器上名为 showOrHideTasks() 的方法。请注意,我们传递了范围的父级,其中包含当前的 CaseRecord ,因为表格正在呈现。还有另一个指令,ng-class,它通过设置 CSS 选择器将图标元素与来自 Bootstrap 的相应字形图标相关联。此代码打开和关闭表格视图中的第二行,该行呈现任务视图。它还会根据任务视图是打开还是关闭来正确更新字形图标。

此表视图内容的第三部分现在如下:

<tr ng-repeat-end ng-if="caseRecord.showTasks" >
  <td colspan="9">
    <div class="case-record-task-view">
      <div ng-controller="NewCaseRecordModalController">
        <button class="btn btn-info" ng-click="openEditCaseRecordDialog($parent.caseRecord)" >Edit Case Record Details</button>
        <button class="btn btn-info" ng-click="changeStateCaseRecordDialog($parent.caseRecord)" >Change State</button>
      </div>
      <br />

      <div ng-controller="NewTaskModalController">
        <p>
          <button class="btn btn-primary" ng-click="openNewTaskDialog(caseRecord.id)">Add New Task</button>
        </p>
      </div>

      <table class="case-record-task-table">
        <tr>
          <td> Item </td>
          <td> Description </td>
          <td> Completed </td>
          <td> Due Date </td>
          <td> Control </td>
        </tr>

        <tr ng-repeat="task in caseRecord.tasks">
          ...
        </tr><!-- ng-repeat-end ## tasks in caseRecords.tasks -->
      </table>
    </div>
  </td>
</tr><!-- ng-repeat-end ## caseRecord in caseRecords -->

主表中的第二个 行有一个ng-repeat-end 指令,它通知AngularJS 哪个DOM element 完成每个 CaseRecord 元素的循环迭代。实际上还有另一个指令叫做 ng-repeat 它结合了 ng-repeat-startng -repeat-end 用于单个 DOM 元素。该指令通常用于呈现表中的简单行。

ng-if 指令有条件地从 DOM 中添加或删除内容。我们使用这个 ng-if 来显示和隐藏每个案例记录元素的任务视图区域。 AngularJS 提供了其他类似的指令,称为 ng-showng-hide,但它们不会从 DOM 中动态添加或删除内容.

Tip

为什么我们会选择 ng-if 而不是 ng-show?假设您的数据库中有数百个案例记录元素,我们是否希望在 Web 前端呈现所有这些案例及其任务历史记录?

我们有一个 div-layer 元素,专门用于显示与案例记录相关的任务。查看 CSS 选择器,case-record-task-view。我们添加内容以将每个 task 元素显示为表格。 caseRecord.tasks 中有一个使用具有表达式任务的 ng-repeat 的示例。

还有另外两个内部 div 层。第一个元素绑定到编辑当前案例记录的逻辑并引用名为 NewCaseRecordModalController 的控制器。第二个元素允许用户创建一个新任务,它引用一个名为 NewTaskModalController 的新控制器。稍后我们将看到这些控制器的 JavaScript 代码。

以下屏幕截图说明了 show 任务的扩展和收缩:

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

此屏幕截图描述了使用 ng-if 的辅助行元素的扩展和收缩。

为了完成表格视图的 内容,我们编写表格数据行来显示任务的属性元素:

<tr ng-repeat="task in caseRecord.tasks">
  <td> {{task.id}} </td>
  <td>
    <span class="done-{{task.completed}}"> {{task.name}} </span>
  </td>
  <td>
    <label class="checkbox">
      <input type="checkbox" ng-model="task.completed" ng-change="updateProjectTaskCompleted(task)">
      Done
    </label>
  </td>
  <td>
    {{task.targetDate}}
  </td>
  <td>
    <div ng-controller="NewTaskModalController">
      <a class="btn" href="#"ng-click="openEditTaskDialog($parent.task)" >
        <i class="glyphicon glyphicon-edit"></i></a>
      <a class="btn" href="#"ng-click="openDeleteTaskDialog($parent.task)" >
        <i class="glyphicon glyphicon-trash"></i></a>
    </div>
  </td>
</tr><!-- ng-repeat-end ## tasks in caseRecords.tasks -->

在视图的第四部分,我们充分利用 AngularJS 双向绑定来渲染 HTML checkbox 元素并将其与布尔属性 caseRecord.completed。使用 CSS 选择器,我们使用 类选择器表达式 class="done- 动态更改任务名称的文本{{task.completed}}”。当用户更改复选框时,将选择以下 CSS:

.done-true {
  text-decoration: line-through; color: #52101d;
}

任务完成后,文字划线!我们向复选框元素添加了一个 ng-change 指令,AngularJS 将它与更改事件相关联。 AngularJS 在控制器 NewTaskModalController 上调用方法 updateProjectTaskCompleted()。此方法调用 WebSocket 调用。我们将很快解释它背后的代码!请注意,方法调用传递了当前的 task 元素,因为我们仍在渲染范围内。

为了完成任务视图,我们有一个与控制器 NewTaskModalController 关联的 div 层,其中带有字形图标按钮来编辑和删除一个任务。如您所见,我们需要传入 $parent.task 以引用元素循环变量。

是时候看看项目组织,然后是各个 JavaScript 模块、控制器和工厂了。

Project organization


该项目被组织成一个Java EE Web 应用程序。我们将所有 JavaScript 代码放入遵循 AngularJS 约定的文件夹中,因为我们很可能在全栈环境中专业地工作,并以混合技能共享代码库。 AngularJS 的控制器放在 app/controllers 下,而工厂和服务放在 app/service 下,如图以下结构:

src/main/webapp/app/controllers

src/main/webapp/app/controllers/main.js

src/main/webapp/app/controllers/newcaserecord-modal.js

src/main/webapp/app/controllers/newtask-modal.js

src/main/webapp/app/services

src/main/webapp/app/services/iso-countries.js

src/main/webapp/app/services/shared-services.js

接下来,我们将第三方 JavaScript 库放到他们指定的区域:

src/main/webapp/javascripts

src/main/webapp/javascripts/angular.js

src/main/webapp/javascripts/bootstrap.js

src/main/webapp/javascripts/jquery-2.1.3.js

src/main/webapp/javascripts/ui-bootstrap-0.12.1.js

src/main/webapp/javascripts/ui-bootstrap-tpl-0.12.1.js

请注意,我们的 caseworker 应用程序还依赖于 Bootstrap、jQuery 和扩展库 Bootstrap UI for AngularJS。我们在主视图 index.html 的最后一部分内容中明确包含所有这些库,如下所示:

<html ng-app="app">  ...
  <body> ...
    <script src="javascripts/jquery-2.1.3.js"></script>
    <script src="javascripts/angular.js"></script>
    <script src="javascripts/bootstrap.js"></script>
    <script src="javascripts/ui-bootstrap-tpls-0.12.1.js"></script>
    <script src="app/controllers/main.js"></script>
    <script src="app/controllers/newcaserecord-modal.js"></script>
    <script src="app/controllers/newtask-modal.js"></script>
    <script src="app/services/shared-service.js"></script>
    <script src="app/services/iso-countries.js"></script>
  </body>
</html>

正如我之前所说,为了演示的目的,我们使代码库更简单,但我们可以使用 RequireJS 来处理依赖项加载。

Tip

如果你没有在 AngularJS 之前显式加载 jQuery,那么它会加载自己的小版本的 jQuery 称为 jq-lite。所以如果你的应用依赖于完整版的 jQuery 库,请确保在 AngularJS 之前加载它。

最后一步是将 CSS 放在自己的特殊区域中:

src/main/webapp/styles

src/main/webapp/styles/bootstrap.css

src/main/webapp/styles/bootstrap-theme.css

src/main/webapp/styles/main.css

前面的文件加载在主视图的顶部,在通常的 head HTML 元素内。

Application main controller


我们的 AngularJS 应用程序中的第一个模块声明了应用程序的名称。以下是文件中的声明:src/main/webapp/app/controllers/main.js

var myApp = angular.module('app', ['ui.bootstrap', 'newcaserecord','newtask', 'sharedService', 'isoCountries']);

框架导出了一个名为angular的函数对象,它有一个名为module 定义一个模块。第一个参数是模块的名称,第二个参数是依赖模块名称的数组。 module() 方法返回一个 AngularJS 模块对象给调用者。从那里,我们声明初始控制器。

ui.bootstrap 模块包含 AngularJS 和 Bootstrap 集成。 newcaserecord 模块是个案工作者应用程序的一部分,它定义了一个控制器,用于插入和修改主记录。 newtask 模块定义了一个控制器,用于插入、修改和删除详细信息记录。 sharedService 定义了一个为应用程序执行实用功能的工厂提供程序,最后,isoCountries 定义了另一个保存列表的提供程序ISO护照国家。

AngularJS 框架有一个流畅的 API 用于定义模块、控制器和提供者;因此,我们可以编写一个几乎是声明性的 JavaScript,如下面的代码摘录所示:

angular.module('myApp', [ 'depend1', 'depend2'])
  .controller( 'controller1', function( depend1, depend2 ) {
      /* ... */
  })
  .controller( 'controller2', function( depend1 ) {
      /* ... */   
  })
  .filter('greet', function() {
   return function(name) {
      return 'Hello, ' + name + '!';
    };
  }) 
  .service( 'our-factory', function( ... ) {
      /* ... */   
  })
  .directive( 'my-directive', function( ... ) {
      /* ... */   
});

前面的编码风格是一种口味问题,缺点是所有模块都集中在一起。许多专业开发人员更喜欢将实际的 Angular 模块对象分配给全局模块变量。

视图中的 body 标签元素定义了一个控制器:

<body ng-controller="CaseRecordController">

以下 摘录显示了将用户界面绑定到客户端数据模型的控制器CaseRecordController

myApp.controller('CaseRecordController',  function ($scope, $http, $log, UpdateTaskStatusFactory, sharedService, isoCountries ) {
  var self = this;
  $scope.caseRecords = [{sex: "F", firstName: "Angela", lastName: "Devonshire", dateOfBirth: "1982-04-15", expirationDate: "2018-11-21", country: "Australia", passportNo: "123456789012", currentState: "Start"},];

  $scope.isoCountries = isoCountries;

  $scope.getCaseRecords = function () {
    $http.get('rest/caseworker/list').success(function(data) {
      $scope.caseRecords = data;
    });
  }

  $scope.$on('handleBroadcastMessage', function() {
    var message = sharedService.getBroadcastMessage();
    if ( message !== "showTasksCaseRecord")  {
      $scope.getCaseRecords();
    }
  })

  // Retrieve the initial list of case records
  $scope.getCaseRecords();

  $scope.connect = function() {
    UpdateTaskStatusFactory.connect();
  }

  $scope.send = function( msg ) {
    UpdateTaskStatusFactory.send(msg);
  }

  $scope.updateProjectTaskCompleted = function( task ) {
    var message = { 'caseRecordId': task.caseRecordId, 'taskId': task.id, 'completed': task.completed }
    $scope.connect()
    var jsonMessage = JSON.stringify(message)
    $scope.send(jsonMessage)
  }
});

AngularJS 对象中的控制器方法接受第一个参数作为名称。第二个参数是函数对象,按照惯例,我们传入一个带有参数的匿名 JavaScript 函数。

function ($scope, $http, $log, UpdateTaskStatusFactory, sharedService, isoCountries ) { /* ... */ }

参数 都是 AngularJS 注入控制器的对象模块。 AngularJS 定义了以美元字符 ($) 开头的标准模块。模块 $scope 是一个特殊参数,表示当前作用域。模块 $http 代表核心 AngularJS 服务,其方法与远程 HTTP 服务器通信。 $log 模块是另一个用于登录控制台的核心服务。其他参数 UpdateTaskStatusFactorysharedServiceisoCountries 是工厂和服务我们的应用程序提供。 AngularJS 与许多 JavaScript 现代数字框架一样,鼓励模块化编程并尽可能避免污染全局范围。

那么这个控制器有什么作用呢?首先,出于演示目的,控制器初始化一个虚拟 JSON 记录 $scope.caseRecord,以防在页面视图加载时服务器不可用。接下来,我们为记录列表定义一个属性,$scope.caseRecords。是的,将自定义属性添加到 AngularJS $scope 是从数据模型到用户界面的通信方式。

我们为控制器定义属性,$scope.isoCountries

我们定义我们的第一个函数,getCaseRecords(),如下:

$scope.getCaseRecords = function () {
  $http.get('rest/caseworker/list').success(function(data) {
    $scope.caseRecords = data;
  });
}

此函数从为页面视图提供服务的同一主机向远程服务器发出 RESTful GET 请求。 URL 应该是这样的:http://localhost:8080/xen-national-force/rest/caseworker/list

一旦服务器返回 JSON 结果,我们就利用 fluent API 执行操作。匿名函数用最新数据覆盖 $scope.caseRecords 属性。

顺便说一句,当我们构造函数对象 CaseRecordController 时,我们调用方法 getCaseRecords() 来启动应用程序。

在 AngularJS 中,我们可以使用我们的应用程序创建的工厂服务或通过向服务器发出 HTTP 请求,将信息从一个控制器传递到另一个控制器。也可以监听 AngularJS 在广播频道上发布的事件。

CaseRecordController 中的以下 代码演示了如何更新除一条消息之外的所有消息的用户界面:

  $scope.$on('handleBroadcastMessage', function() {
    var message = sharedService.getBroadcastMessage();
    if ( message !== "showTasksCaseRecord")  {
      $scope.getCaseRecords();
    }
  })

在这里,我们在 AngularJS 范围内注册一个事件处理程序,以便从我们的 SharedService 提供程序中检索通知。 $on() 方法在特定事件类型上注册一个监听器。第一个参数是消息类型,第二个参数是回调。在函数回调中,如果消息以及自定义事件不是 showTasksCaseRecord,我们会发出 HTTP 请求以从服务器端检索整个案例记录集。

Tip

在处理程序回调中,我们读取了整个数据集,这可能是真实企业应用程序中的数千个案例记录。因此,我们可以提高 REST 调用和响应代码的性能。然而,我们应该抵制过早优化的冲动。您应该更喜欢让用户故事正常工作。

控制器中的其他方法 connect()send() 建立到服务器的 WebSocket 通道并发送 JSON分别向服务器发送消息。我们将在后面的部分中检查 UpdateTaskStatusFactory 模块和最终方法 updateProjectTaskCompleted()

如果您以前从未专业地开发过任何 JavaScript,那么这一章一开始可能会让人望而生畏。但是,请坚持下去,因为这实际上只是要有足够的耐心才能成功。在这方面,我准备了一个简单的 AngularJS 范围图,它们出现在我们的案例工作者应用程序中。

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

案例工作者应用程序中的 AngularJS 范围

前面的 图表描绘了进度的历程,并帮助我们了解我们的目标。它还建立了 AngularJS 如何以类似于 DOM 本身的分层方式绑定作用域的概念。在幕后,AngularJS 创建了内部作用域来处理呈现 HTML table 元素的可重复 DOM 元素,该元素是案例记录的列表。开发人员只能通过表达式编程才能访问这些内部数据,我们应该将它们视为不透明对象。

Tip

在写的时候,有一个谷歌浏览器插件叫做Batarang(< a class="ulink" href="https://chrome.google.com/webstore/detail/angularjs-batarang-stable/" target="_blank">https://chrome.google.com/webstore/detail/ angularjs-batarang-stable/),我强烈建议检查浏览器内的 AngularJS 范围。可悲的是,该工具似乎不再维护。仍然值得检查是否有人采用了它。

New case record controller


我们将用于创建和编辑案例记录的代码放在一个名为 newcaserecord-modal.js 的单独文件中,其中包含用户定义的 AngularJS 模块 newcaserecord。该模块依赖于其他模块,其中一些之前提到过。 ui.bootstrap.modal 是 AngularJS UI Bootstrap 第三方框架的一个特殊模块。该模块定义了 AngularJS 团队编写的 Bootstrap 组件。特别是,它有一个有用的模态对话框扩展,我们在整个案例工作者应用程序中都使用它。

以下是 newcaserecord 模块和 NewCaseRecordModalController 的缩短代码:

var newcaserecord = angular.module('newcaserecord', ['ui.bootstrap.modal', 'sharedService','isoCountries'])

newcaserecord.controller('NewCaseRecordModalController', function($scope, $modal, $http, $log, sharedService, isoCountries ) {
  $scope.caseRecord = {
    sex: "F", firstName: "", lastName: "", country: "", passportNo: "", dateOfBirth: "", expirationDate: "", country: "", currentState: "", showTasks: false};
  $scope.returnedData = null;
  $scope.isoCountries = isoCountries;

  $scope.openCreateCaseRecordDialog = function () {
    var modalInstance = $modal.open({
      templateUrl: 'newCaseRecordContent.html', controller: newCaseRecordModalInstanceController, isoCountries: isoCountries, resolve: {
            caseRecord: function () {
              return $scope.caseRecord;
            }
      }
    });

    modalInstance.result.then(function (data) {
      $scope.selected = data;
      $http.post('rest/caseworker/item', $scope.caseRecord).success(function(data) {
        $scope.returnedData = data;
        sharedService.setBroadcastMessage("newCaseRecord");
      });

    }, function () {
      $log.info('Modal dismissed at: ' + new Date());
    });
  };
  // . . .
);

控制器 函数对象接受注入参数如$http, $日志sharedService。我们还注入了 $modal 实例,它允许我们在控制器中打开模式对话框。

由于每个控制器都有自己的作用域注入其中,我们需要提供数据模型的元素以便视图可以访问。因此,我们在范围内创建一个空的案例记录作为 $scope.caseRecord。我们还设置了退货数据和 ISO 国家/地区列表。

$scope.openCreateCaseRecordDialog() 函数生成一个模态对话框,因此允许用户输入一个主案例记录。

Tip

允许用户创建任意应用程序通行证记录可能会被禁止并仅限于管理员和经理以外的任何员工。我们的演示应用程序根本没有角色和权限的概念。开发人员应小心避免在其数字应用程序中引入零日漏洞。

UI Bootstrap 扩展接受几个参数。第一个参数是对 HTML 模板指令的引用。第二个参数引用另一个控制器,名为 newCaseRecordModalInstanceController,它负责处理与对话框的交互。第三个参数是一个解析器,它允许库代码在封闭范围内的用户模式中找到参考数据:

var modalInstance = $modal.open({
  templateUrl: 'newCaseRecordContent.html',
    controller: newCaseRecordModalInstanceController,
      resolve: {
        caseRecord: function () {
          return $scope.caseRecord;
        }
      }
});

控制器的下一部分,NewCaseRecordModalController 处理模态对话框成功完成后的回调,因为用户输入了数据并按下了确认按钮。我们在名为 then 的对象上注册两个函数对象作为参数。

    modalInstance.result.then(function (data) {...},
       function () { /* modal dismissed */ });

第一个函数是回调处理程序,其中包含向 带有案例记录数据的服务器发出 REST POST 请求的代码。第二个函数是为对话框关闭时保留的。你会注意到 AngularJS 使用了流畅的接口。即使您碰巧不了解 JavaScript 和框架的所有内容,代码也应该相当容易理解。

那么我们来看看模态对话框实例的代码,即对象newCaseRecordModalInstanceController

var newCaseRecordModalInstanceController = function ($scope, $modalInstance, caseRecord ) {
  caseRecord.showTasks = true; // Convenience for the user
  $scope.caseRecord = caseRecord;

  $scope.ok = function () {
    $modalInstance.close(true);
  };

  $scope.cancel = function () {
    $modalInstance.dismiss('cancel');
  };
};

如果你注意到,这个变量并不是 JavaScript 中的一个封装模块;相反, newCaseRecordModalInstanceController 函数是在全局范围内声明的。我想规则总是有例外的。 UI Bootstrap 代码通过 $modalInstance.open() 调用调用此控制器函数。该框架提供三个参数,范围 $scope、模态实例 $modalInstance 和案例记录 caseRecord 到函数。我们将案例记录分配给提供的范围,以便从模式对话框中写回数据。在那里,函数对象实现了两个方法,ok()cancel(),用于处理对话框的确认和取消分别。

我们只需要为对话框编写 HTML 指令。

The case record modal view template

众所周知,网站的所有内容都在一个单页应用程序中。 HTML 指令也可以在视图 index.html 中找到。您如何将指令写入页面内容而不出现在视图中?秘诀与CSS有关吗?

虽然 样式是个好主意,但这不是正确的答案。 AngularJS 设计者利用 HTML Script 标签的正式定义,它是嵌入或引用可执行脚本的元素。

以下是用于将新案例记录插入应用程序的 HTML 指令:

<script type="text/ng-template" id="newCaseRecordContent.html"> <div class="modal-header"> <h3>New Case Record </h3> </div> <div class="modal-body"> <form name="newCaseRecordForm" class="css-form" novalidate> Sex:<br /> <select ng-model="caseRecord.sex" required> <option value="F" ng-option="selected caseRecord.sex === 'F'">Female</option> <option value="M" ng-option="selected caseRecord.sex === 'M'">Male</option> </select> <br/> First Name:<br /> <input type="text" ng-model="caseRecord.firstName" required /><br /> Last Name:<br /> <input type="text" ng-model="caseRecord.lastName" required /><br /> Date of Birth:<br /> <input type="text" ng-model="caseRecord.dateOfBirth" datepicker-popup="yyyy-MM-dd" required /><br /> Country:<br /> <select ng-model="caseRecord.country" required ng-options="item.code as item.country for item in isoCountries.countryToCodeArrayMap"> </select> <br /> Passport Number:<br /> <input type="text" ng-model="caseRecord.passportNo" required /><br /> Expiration Date:<br /> <input type="text" ng-model="caseRecord.expirationDate" datepicker-popup="yyyy-MM-dd" required /><br /> </form> </div> <div class="modal-footer"> <button class="btn btn-primary" ng-click="ok()" ng-disabled="newCaseRecordForm.$invalid" >OK</button> <button class="btn btn-warning" ng-click="cancel()">Cancel</button> </div> </script>

前面的 HTML 指令定义了一个 UI Bootstrap 模态对话框,因为 HTML script 标签用 text/ng-template 。所有 AngularJS 指令都需要一个标识符。正如我们从 CSS 中看到的 所示,该指令包含页眉、页脚和 main。主要的 div 层是一个 HTML 表单。

表单中的每个输入字段都绑定到 newCaseRecordModalInstanceController 实例中的数据模型。 UI Bootstrap 调用函数对象后,案例记录就被分配给范围。因此,ng-model 数据模型 $scope.caseRecord.firstName 可用于为 first 保留的 HTML 文本输入元素名字。

AngularJS 有一个优雅的附加标记来验证表单输入元素。您可以在几乎所有输入上看到额外的必需属性。不幸的是,由于本书无法深入研究验证检查的更深入细节,我想提请您注意两个微妙的验证检查。

数据输入利用 UI Bootstrap 日期选择器组件允许案例工作者轻松输入日期:

<input type="text" ng-model="caseRecord.dateOfBirth" datepicker-popup="yyyy-MM-dd" required />

日期的格式由属性 datepicker-popup 定义。

最后,我们在 HTML select 元素中显示 ISO 护照国家名称的下拉列表。这部分的代码如下:

<select ng-model="caseRecord.country" required ng-options="item.code as item.country for item in isoCountries.countryToCodeArrayMap">
</select>

isoCountries 是一个服务实例,我们稍后会看到。由于该模块被注入到 NewCaseRecordModalController 模块中,而后者的范围恰好包含了模态实例范围,AngularJS 允许我们访问该服务。 isoCountries 实例包含键值字典中的护照国家列表。该代码允许我们将 ISO 代码 AUS 与国家名称澳大利亚相关联。 ng-option 属性接受类似于 SQL 查询的表达式。我们以声明方式告知 AngularJS 如何为每个 HTML option 元素。

以下 是带有日期选择器的创建案例记录模式对话框的屏幕截图:

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

带有完全生效的日期选择器的创建案例记录模式对话框的屏幕截图

让我们转到类似于案例记录控制器的任务记录控制器。

New task record controller


当一个 案例工作者使用该系统时,他或她能够展开和折叠与案例记录关联的任务记录。用户可以创建、编辑和修改任务,还可以更改案例的状态。

AngularJS 模块 newtask 是这样定义的:

var newtask = angular.module('newtask', ['ui.bootstrap.modal', 'sharedService'])
newtask.config(function($httpProvider) {
  $httpProvider.defaults.headers["delete"] = {
    'Content-Type': 'application/json;charset=utf-8'
  };
})

我们围绕 HTTP 远程处理向 AngularJS 添加了配置更改。 HTTP DELETE 请求有一个微妙的错误。 GlassFish 和 Payara 应用服务器中的 JAX-RS 参考实现 Jersey 引发 HTTP 错误,响应代码为 415:Unsupported Media Type。这迫使 AngularJS 发送 MIME 类型,因为 DELETE 请求上的 JSON 解决了​​这个问题。

由于任务控制器的代码非常相似,因此本书只介绍 CRUD 的创建部分。有关其他方法,请参阅来源。以下是NewTaskModalController的源码:

newtask.controller('NewTaskModalController', function($scope, $modal, $http, $log, sharedService ) {
  $scope.selected = false;
  $scope.task = {
      id: 0, name: '', targetDate: null, completed: false, caseRecordId: 0
  };
  $scope.returnedData = null;
  $scope.openNewTaskDialog = function(caseRecordId) {
    var modalInstance = $modal.open({
      templateUrl: 'newTaskContent.html',
      controller: newTaskModalInstanceController,
      resolve: {
        task: function () {
          return $scope.task;
        }
      }
    });

    modalInstance.result.then(function (data) {
      $scope.selected = data;
      $http.post('rest/caseworker/item/'+caseRecordId+'/task', $scope.task).success(function(data) {
        $scope.returnedData = data;
        sharedService.setBroadcastMessage("newTask");
        // Reset Task in this scope for better UX affordance.
        $scope.task = {
          id: 0, name: '', targetDate: null, completed: false, caseRecordId: 0
        };
      });
    }, function () {
        $log.info('Modal dismissed at: ' + new Date());
    });
  };

  $scope.openEditTaskDialog = function(taskItem) {
    // ...
  };

  $scope.openDeleteTaskDialog = function(taskItem) {
    // ...
  };
});

在这个 控制器中,我们有一个空的默认 $scope.caseRecord "literal">$scope.task 对象。每个 Task 对象都有一个通过属性 caseRecordId 对父对象的引用。

openNewTaskDialog() 函数打开一个 UI Bootstrap 模式对话框,允许用户输入品牌任务。该方法将模态对话框与当前 Task 对象的 AngularJS 范围连接起来。最大的不同是 REST URL 端点,它的形式是 rest/caseworker/item/'+caseRecordId+'/task

我们使用 UI Bootstrap $modal 对象并像以前一样创建一个模态对话框实例,只是我们现在传递不同的参数。参数是 HTML 指令 ID,即 newTaskContent.html;控制器被称为 newTaskModalInstanceController,以及解析器函数。 AngularJS 调用定义为匿名函数的解析器函数,以引用封闭的 Task 对象。

modalInstance 对象的回调函数中,我们方便地重置 Task 对象,这样用户就不会对陈旧的表单数据感到惊讶当对话框再次出现时。我们在sharedService中设置广播消息。

在任务对话框中处理模态实例的代码几乎相同:

var newTaskModalInstanceController = function ($scope, $modalInstance, task) {
  $scope.task = task;

  $scope.ok = function () {
      $modalInstance.close(true);
  };

  $scope.cancel = function () {
    $modalInstance.dismiss('cancel');
  };
};

函数 newTaskModalInstanceController 接受三个参数:绑定模态实例对话框的 $scope$ modalInstance 本身,以及 Task 对象。最后一个参数 Task 对象已解析,我们将其设置为范围的属性,以便在模板中轻松呈现视图。

The task modal view template

AngularJS 指令 newTaskContent.html 呈现模式对话框的视图,让 用户输入新任务.只有四个属性,所以这个视图比案例记录要短。

该视图的定义如下:

<script type="text/ng-template" id="newTaskContent.html"> <div class="modal-header"> <h3>New Task</h3> </div> <div class="modal-body"> <form name="newTaskForm" class="css-form" novalidate> Task Name:<br /> <textarea ng-model="task.name" rows="3" required /><br /> Target Date: <br /> <input type="text" datepicker-popup="yyyy-MM-dd" ng-model="task.targetDate" required /><br /> Task Completed: <br /> Done <input type="checkbox" ng-model="task.completed" /> <br /> </form> </div> <div class="modal-footer"> <button class="btn btn-primary" ng-click="ok()" ng-disabled="newTaskForm.$invalid" >OK</button> <button class="btn btn-warning" ng-click="cancel()">Cancel</button> </div> </script>

此视图 还遵循用于模式对话框的 UI Bootstrap CSS 样式。我们演示了一个与数据模型关联的 HTML text area 元素,即 Task 对象。每个表单域都有一个 ng-model 关联。对于目标日期,我们重用了日期选择器,并说明了如何使用 HTML checkbox 元素。

编辑和删除任务记录的代码看起来大致相同。但是,对于编辑,我们不会在用户确认模态对话框后重置任务记录,而对于删除,我们只显示任务记录的只读视图;模态对话框只是一个确认。

让我们看看我们如何处理状态变化。

State change


一个case记录存在以下状态:

状态

描述

开始

系统中的每个新申请人都从这个初始状态开始

结尾

在流程结束时,申请人的案件在此结束状态下结束

审查

个案工作者正在审查申请人的记录

决定

案件已审查,企业正在作出决定

公认

案件已受理,正在通知申请人

被拒绝

案件已被驳回,申请人正在被驳回

所有这些业务需求都在有限状态机中捕获。

Controller code

到目前为止,您应该熟悉 代码。 NewTaskModalController中的控制器方法changeStateCaseRecordDialog()如下:

$scope.changeStateCaseRecordDialog = function (caseRecordItem) {
    /* Copy  */
  $scope.caseRecord = {
    id: caseRecordItem.id,
    firstName: caseRecordItem.firstName,
    lastName: caseRecordItem.lastName,
    dateOfBirth: caseRecordItem.dateOfBirth,
    country: caseRecordItem.country,
    passportNo: caseRecordItem.passportNo,
    expirationDate: caseRecordItem.expirationDate,
    currentState: caseRecordItem.currentState,
    nextStates: caseRecordItem.nextStates,
    showTask: caseRecordItem.showTasks
  };

  $scope.caseRecord.nextStates.push( caseRecordItem.currentState );
  $scope.saveCurrentState = caseRecordItem.currentState;

  var modalInstance = $modal.open({
    templateUrl: 'changeStateCaseRecordContent.html', controller: moveStateRecordModalInstanceController, resolve: {
          caseRecord: function () {
            return $scope.caseRecord;
          }
      }
  });

  modalInstance.result.then(function (data) {
      $scope.selected = data;
      if ( $scope.saveCurrentState !== $scope.caseRecord.currentState ) {
          $http.put('rest/caseworker/state/'+$scope.caseRecord.id, $scope.caseRecord).success(function(data) {
            $scope.returnedData = data;
            sharedService.setBroadcastMessage("editCaseRecord");
          });
      }
  }, function () { $log.info('Modal dismissed."); } );
};

由于我们 只是编辑现有的案例记录,我们将 CaseRecord 的属性从封闭范围复制到控制器范围。请记住,外部范围是主模块。

服务器发送的每个 JSON 案例记录(我们将在后面看到)都有一个名为 nextStates 的属性,它是用户可以将记录移动到的下一个可能状态的列表.举个例子,Start状态只有一个可能的下一个状态,称为Reviewing

每个 case 记录对象都有一个 currentState 属性。我们将当前状态推送到存储在当前范围内的后续状态列表中。这个数组 $scope.nextStates 允许对话框 HTML 指令在视图中呈现下拉菜单。

您可以看到这个函数 changeStateCaseRecordDialog() 打开了一个 UI Bootstrap 模态对话框。

The template view code

所以让我们检查状态变化的HTML指令:

<script type="text/ng-template" id="changeStateCaseRecordContent.html"> <div class="modal-header"> <h3>Change State of Case Record</h3> </div> <div class="modal-body"> <p> <table class="table table-bordered"> <tr> <th> Field </th> <th> Value </th> </tr> <tr> <td> Case Record Id</td> <td> {{caseRecord.id }}</td> </tr> ... </table> </p> <form name="moveStateCaseRecordForm" class="css-form" novalidate> Next States:<br /> <select ng-model="caseRecord.currentState" ng-options="state for state in caseRecord.nextStates"> </select> </form> </div> <div class="modal-footer"> <button class="btn btn-primary" ng-click="ok()" ng-disabled="moveStateCaseRecordForm.$invalid" >OK</button> <button class="btn btn-warning" ng-click="cancel()">Cancel</button> </div> </script>

前面的 指令,标识为changeStateCaseRecordContent.html,本质上是整个案例的只读视图记录。唯一可修改的部分是 HTML select 元素,它显示案例记录的下一个可能状态。为了生成 HTML option 元素,属性 ng-options 有不同的表达形式,它被声明作为 caseRecord.nextStates 中状态的状态。这个表达式意味着数组String的选项名称和值相同,如下:

读书笔记《digital-java-ee-7-web-application-development》AngularJS和Java RESTful服务

更改案例记录的状态

模态实例代码基本相同。与对话框关联的相应函数称为 moveStateRecordModalInstanceController()

var moveStateRecordModalInstanceController = function ($scope, $modalInstance, caseRecord) {
  $scope.caseRecord = caseRecord;
  $scope.ok = function () { $modalInstance.close(true); };
  $scope.cancel = function () { $modalInstance.dismiss('cancel'); };
};

在我们 结束这个长长的AngularJS 和客户端示例之前,我们将介绍更多功能。这些函数是定义 NewCaseRecordModalController 的模块的一部分。

Toggling the task display state

第一个函数showOrHideTasks(),切换显示属性showTasks< /code> 在案例记录中。它还向服务器调用带有案例记录 JSON 数据的 HTTP PUT 请求。代码如下:

$scope.showOrHideTasks = function(caseRecord) {
  caseRecord.showTasks = !caseRecord.showTasks;
  $http.put('rest/caseworker/showtasks/'+caseRecord.id, caseRecord).success(function(data) {
    sharedService.setBroadcastMessage("showTasksCaseRecord");
  });
}

第二个函数 getIconClass() 有点作弊模式。它根据显示状态返回 Bootstrap CSS 字形选择器。 AngularJS 确实有 ng-class 的条件表达式;但是,在撰写本文时,作者无法使其适用于案例记录元素数组。因此,此函数作为一种解决方法存在于代码库中。

$scope.getIconClass = function(caseRecord) {
  if ( caseRecord.showTasks)
    return "glyphicon-minus"
  else
    return "glyphicon-plus"
}

如果您有兴趣,应该工作的客户端的正确代码如下:

<i class="glyphicon" ng-class="{true: 'glyphicon-minus', false: 'glyphicon-plus'}[caseRecord.showTasks]">

我们现在将跳转到服务器端。

Server-side Java


我们为案例工作者提供的 Java EE 应用程序 系统是围绕 RESTful 服务、Java WebSocket 构建的、JSON-P 和 Java 持久性。

Tip

本书的这一部分依赖于对 Java EE 开发的初级理解。我建议您阅读姊妹书Java EE 7 开发手册,尤其是当您发现其中一些主题难以理解时。

Entity objects

如果没有几个域对象,服务器端将一事无成。毫不奇怪 这些被称为 CaseRecordTask< /代码>。

下面是提取出来的带有完整注解的CaseRecord实体对象:

@NamedQueries({
  @NamedQuery(name="CaseRecord.findAllCases",
    query = "select c from CaseRecord c order by c.lastName, c.firstName"),
    /* ... */
})
@Entity
@Table(name = "CASE_RECORD")
public class CaseRecord {
  @Id @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;
  @NotEmpty @Size(max=64) private String lastName;
  @NotEmpty @Size(max=64) private String firstName;
  @NotEmpty @Size(max=1) private String sex;
  @NotEmpty @Size(max=16) private String passportNo;
  @NotEmpty @Size(max=32) private String country;
  @Past @NotNull @Temporal(TemporalType.DATE) private Date dateOfBirth;
  @Future @NotNull @Temporal(TemporalType.DATE) private Date expirationDate;
  @NotEmpty private String currentState;
  private boolean showTasks;
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "caseRecord", fetch = FetchType.EAGER)
  private List<Task> tasks = new ArrayList<>();

  // Required by JPA
  public CaseRecord() {}
  /*  ... */
}

对于这些实体,我们 利用流行的 Hibernate Validator 注解来确保信息正确保存到数据库中。详细实体Task如下:

@Entity
public class Task {
  @Id @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name="TASK_ID") private Integer id;
  @NotEmpty @Size(max=256) private String name;
  @Temporal(TemporalType.DATE)
  @Column(name="TARGET_NAME") @Future
  private Date targetDate;
  private boolean completed;
  @ManyToOne(cascade = CascadeType.ALL)
  @JoinColumn(name="CASE_RECORD_ID")
  private CaseRecord caseRecord;

  public Task() { /* Required by JPA */ }
  /*  ... */
}

实体与我们在客户端看到的 JavaScript 对象非常紧密地映射。在实践中,不同域中的业务应用程序可能会选择另一种设计,例如数据模型的外观、聚合或投影。

当然,这些实体有一个持久层,以便检索信息并将其存储到数据库中。在源代码中,有一个 CaseRecordTaskService 负责持久化 CaseRecord任务记录。

RESTful communication

无状态 session EJB 类 CaseWorkerRESTServerEndpoint 作为我们的 RESTful 端点:

package uk.co.xenonique.nationalforce.control;
/* ... */
import javax.json.*;
import javax.json.stream.*;
import javax.ws.rs.*;
import javax.ws.rs.container.*;
import javax.ws.rs.core.*;
import static javax.ws.rs.core.MediaType.*;

@Path("/caseworker/")
@Stateless
public class CaseWorkerRESTServerEndpoint {
  static JsonGeneratorFactory jsonGeneratorFactory = Json.createGeneratorFactory(new HashMap<String, Object>() {{
        put(JsonGenerator.PRETTY_PRINTING, true);
      }});

  @Inject
  CaseRecordTaskService service;
/* ... */
}

此类@Path< 注释 /code> 带有此端点的初始 URI。这个相对 URI /caseworker/ 匹配 AngularJS 客户端。我们将持久有状态会话 EJB CaseRecordTaskService 注入此端点,我们还设置了一个 JSON 生成器工厂来打印 JSON 输出。我们始终使用标准的 Java EE 7 JSON 生成器工厂。

Retrieval of case records

为了处理案例工作者记录的检索,我将演示如何使用 JAX-RS 处理异步 操作。我们需要来自应用服务器的托管执行器,同时确保 Web 应用在部署后支持 async 操作。

对于 Java EE 7,在 Web XML 部署描述符 (src/main/web-app/WEB/web.xml) 中启用异步支持至关重要。该文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" ... version="3.1" ... >
  <servlet>
    <servlet-name>javax.ws.rs.core.Application</servlet-name>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>
  <servlet-mapping>
    <servlet-name>javax.ws.rs.core.Application</servlet-name>
    <url-pattern>/rest/*</url-pattern>
  </servlet-mapping>
  <resource-env-ref>
    <resource-env-ref-name>
      concurrent/LongRunningTasksExecutor
    </resource-env-ref-name>
    <resource-env-ref-type>
      javax.enterprise.concurrent.ManagedExecutorService
    </resource-env-ref-type>
  </resource-env-ref>
</web-app>

重要的 XML 元素是 <async-supported>,我们将其正文内容设置为 true。我们还 将用于接收整个应用程序的 REST 查询的 URI 设置为 /rest。因此,将类 CaseWorkerRESTServerEndpoint 放在一起,到目前为止,完整的相对 URI 是 /rest/caseworker。最后,我们向 Java EE 7 应用程序服务器声明,我们的应用程序需要一个托管执行器,并在 <resource-env-ref> 周围添加 XML 元素。这个托管执行器的名称是 concurrent/LongRunningTasksExecutor(JNDI 查找名称)。

我们现在将在第一个 REST 查询方法中使用它:

  @Resource(name="concurrent/LongRunningTasksExecutor")
  ManagedExecutorService executor;

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Path("/list")
  public void getCaseRecordList(
    @Suspended final AsyncResponse asyncResponse) {
    executor.submit(new Runnable() {
      @Override
      public void run() {
        final List<CaseRecord> caseRecords = service.findAllCases();
        final StringWriter swriter = new StringWriter();
        final JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
          CaseRecordHelper.generateCaseRecordAsJson(generator, caseRecords).close();
          final Response response = Response.ok(swriter.toString()).build();
          asyncResponse.resume(response);
      }
    });
  }

我们用 @GET 注释方法 getCaseRecordList() 来处理来自完整相对 URI 的 HTTP GET 请求,/rest/caseworker/list。此方法异步工作。它依赖于注入的 ManagedExecutorService 实例,该实例是 Java EE 7 管理的线程池 执行器。为了参与服务,我们提供了一个方法参数,即 AsyncResponse 对象,它使用 @Suspended 进行了注释。

我们的 getCaseRecordList() 方法的主体将工作实例 (java.lang.Runnable) 提交到托管执行器服务。工作人员从持久性服务中检索案例记录列表并将其转换为 JSON 输出。输出被转换为字符串,我们通过 resume() 方法请求 AsyncResponse 实例开始向下发送数据客户端的输出通道。我们使用 JAX RS @Produces 对方法 getCaseRecordList() 进行注释,以声明 MIME 类型 application.json 的输出内容。

Tip

顺便提一下,Java EE 7 中有两个 @Produces 注释。一个是 JAX-RS 的一部分,另一个是 CDI。

我们还有一个 REST 端点,用于通过 ID 检索特定案例记录。让我们看看我们如何实现这一点:

  @GET
  @Path("/item/{id}")
  @Produces(APPLICATION_JSON)
  public String retrieveCase(
      @PathParam("id") int caseId ) {
    List<CaseRecord> caseRecords = service.findCaseById( caseId );
    StringWriter swriter = new StringWriter();
    JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
    CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecords.get(0)).close();
    return swriter.toString();
  }

对于 HTTP GET 请求,方法 retrieveCase() 使用 @GET 注释。它具有 /rest/caseworker/item/{id} 的相对 URI。该方法按 ID 搜索案例记录并创建它的 JSON 表示。它将输出同步发送到客户端。简单说明一下:我们删除了这些摘录中的健全性检查代码以节省空间。

Creating a case record

我们已经介绍了检索方面,现在我们转向创建 REST 端点。在我们的系统中,Web 客户端可以使用 REST 调用创建案例记录。以下代码将新案例记录插入到应用程序中。创建新案例记录的相对 URI 是 /rest/caseworker/item

  @POST
  @Path("/item")
  @Consumes(APPLICATION_JSON)
  @Produces(APPLICATION_JSON)
  public String createCase( JsonObject json )
    throws Exception {
    CaseRecord caseRecord = new CaseRecord();
    caseRecord.setSex(json.getString("sex"));
    caseRecord.setFirstName(json.getString("firstName"));
    caseRecord.setLastName(json.getString("lastName"));
    caseRecord.setCountry(json.getString("country"));
    caseRecord.setPassportNo(json.getString("passportNo"));
    caseRecord.setDateOfBirth( CaseRecordHelper.FMT2.parse(json.getString("dateOfBirth")));
    caseRecord.setExpirationDate( CaseRecordHelper.FMT2.parse(json.getString("expirationDate")));
    caseRecord.setCurrentState( BasicStateMachine.FSM_START.toString());
    caseRecord.setShowTasks(json.getBoolean("showTasks", false));

    JsonArray tasksArray = json.getJsonArray("tasks");
    if ( tasksArray != null ) {
      for ( int j=0; j<tasksArray.size(); ++j ) {
        JsonObject taskObject = tasksArray.getJsonObject(j);
        Task task = new Task(taskObject.getString("name"), ( taskObject.containsKey("targetDate") ?
              CaseRecordHelper.FMT.parse(taskObject.getString("targetDate")) : null ), taskObject.getBoolean("completed"));
            caseRecord.addTask(task);
            task.setCaseRecord(caseRecord);
        }
    }

    service.saveCaseRecord(caseRecord);
    StringWriter swriter = new StringWriter();
    JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
    CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
    return swriter.toString();
}

createCase() 方法更长,因为它将 JSON-P 对象 实例中的数据传输到CaseRecord 实体。我们用 @POST 注释该方法以表示此端点处理 HTTP POST 请求。这是一个冗长的样板文件,通过 其他非 Java EE 7 框架如 GSON (https://code.google.com/p/google-gson/) 或更快JSON 的 Jackson 处理 API (http://wiki.fasterxml.com/JacksonInFiveMinutes),但我必须在这里演示标准方法。我们必须等到规范主体提供 JSON-B(Java JSON Binding API)之后才能简化此代码。

Updating a case record

更新一个案例记录,和创建一个新记录非常相似,只是我们先通过ID搜索记录,然后逐字段更新记录从 JSON 输入。

updateCase()方法如下:

@PUT
@Path("/item/{caseId}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String updateCase(
  @PathParam("caseId") int caseId, JsonObject json ) throws Exception {
  final List<CaseRecord> caseRecords = service.findCaseById(caseId);
  CaseRecord caseRecord = caseRecords.get(0);
  caseRecord.setSex(json.getString("sex"));
  /* ... omitted */
  caseRecord.setDateOfBirth( FMT2.parse( json.getString("dateOfBirth")));
  caseRecord.setExpirationDate( FMT2.parse(json.getString("expirationDate")));
  caseRecord.setCurrentState( BasicStateMachine.retrieveCurrentState( json.getString("currentState", BasicStateMachine.FSM_START.toString())).toString());
  caseRecord.setShowTasks(json.getBoolean("showTasks", false));
  service.saveCaseRecord(caseRecord);
  final StringWriter swriter = new StringWriter();
  final JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
  CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
  return swriter.toString();
}

这个 RESTful 端点使用 @PUT 进行注释,以便处理 HTTP PUT 请求。这一次,相对 URI 是 /rest/caseworker/item/{id},表示客户端必须提供案例记录 ID。同样,我们从 JSON 对象复制值并覆盖从持久性中检索到的 CaseRecord 中的属性;然后我们保存记录。我们生成记录的 JSON 表示并将其设置为 JAX-RS 将发送回客户端的响应。

静态实例 FMT2 是一个 java.text.SimpleDateFormat,它在过期日期和出生日期字符串与 java.util .Date 实例。模式格式为 yyyy-MM-dd。 BasicStateMachine 实例是有限状态机的实现。 FSM_START 是其中一种可能状态的单例实例。请参阅本书的源代码以了解它是如何实现的。

Creating a task record

我们现在将快速连续检查任务记录的创建、更新和删除端点。检索已经解决,因为每个 CaseRecord 实例都有一个由零个或多个 Task 实体组成的集合,它们满足了主从安排。

创建和更新任务记录是非常相似的操作。那么我们先来研究一下create方法:

@POST
@Path("/item/{caseId}/task")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String createNewTaskOnCase(
  @PathParam("caseId") int caseId, JsonObject taskObject ) throws Exception
{
  final List<CaseRecord> caseRecords =
    service.findCaseById(caseId);
  final CaseRecord caseRecord = caseRecords.get(0);
  final Task task = new Task(
    taskObject.getString("name"),
    ( taskObject.containsKey("targetDate") ?
      CaseRecordHelper.convertToDate(
      taskObject.getString("targetDate")) :
      null ),
    ( taskObject.containsKey("completed")) ?
        taskObject.getBoolean("completed") : false );
  caseRecord.addTask(task);
  service.saveCaseRecord(caseRecord);
  final StringWriter swriter = new StringWriter();
  JsonGenerator generator =
    jsonGeneratorFactory.createGenerator(swriter);
  CaseRecordHelper.writeCaseRecordAsJson(
    generator, caseRecord).close();
  return swriter.toString();
}

我们用 @POST 方法 createNewTaskOnCase() >。相对 URI 是 /rest/caseworker/item/{caseId}/task。客户端提交父案例记录,该方法使用此 ID 检索适当的 CaseRecord。从新的任务记录控制器与 AngularJS 客户端交叉引用可能是个好主意。在 createNewTaskOnCase() 中,我们再次删除了健全性检查代码,以便专注于实质内容。代码的下一部分是将 JSON 映射到 Java 实体。之后,我们将 Task 实体添加到 CaseRecord 中,然后持久化主记录。一旦我们编写了响应,该方法就完成了。

Updating a task record

updateTaskOnCase() 方法执行任务的更新。我们用 @PUT 和两个 RESTful 参数来注释这个方法 。相对 URI 是 /rest/caseworker/item/{caseId}/task/{taskId}。更新任务记录的代码如下:

@PUT
@Path("/item/{caseRecordId}/task/{taskId}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String updateTaskOnCase(
  @PathParam("caseRecordId") int caseRecordId,
  @PathParam("taskId") int taskId,
  JsonObject taskObject ) throws Exception
{
  final List<CaseRecord> caseRecords =
    service.findCaseById(caseRecordId);
  final CaseRecord caseRecord = caseRecords.get(0);
  caseRecord.getTasks().stream().filter(
    task -> task.getId().equals(taskId)).forEach(
      task -> {
        task.setName( taskObject.getString("name") );
        task.setTargetDate(
          taskObject.containsKey("targetDate") ?
          CaseRecordHelper.convertToDate(
            taskObject.getString("targetDate")) : null );
        task.setCompleted(taskObject.containsKey("completed") ?
            taskObject.getBoolean("completed") : false );
    }); 
  service.saveCaseRecord(caseRecord);
  final StringWriter swriter = new StringWriter();
  final JsonGenerator generator =
    jsonGeneratorFactory.createGenerator(swriter);
  CaseRecordHelper.writeCaseRecordAsJson(
    generator, caseRecord).close();
  return swriter.toString();
}

有了我们的两个坐标caseRecordIdTaskId,我们找到适当的 Task 实体,然后从 JSON 输入更新属性。在这里,我们利用 Java 8 Lambda 和 Stream API 来实现函数式方法。我们保存实体并呈现来自当前 CaseRecord 实体的 JSON 响应。

Deleting a task record

最后,但并非最不重要的,我们为客户端前端提供了一种从案例记录中删除任务记录的方法。它的代码如下:

@DELETE
@Path("/item/{caseRecordId}/task/{taskId}")
@Consumes( { APPLICATION_JSON, APPLICATION_XML, TEXT_PLAIN })
@Produces(APPLICATION_JSON)
public String removeTaskFromCase(
  @PathParam("caseRecordId") int caseRecordId,
  @PathParam("taskId") int taskId,
  JsonObject taskObject )
  throws Exception
{
  final List<CaseRecord> caseRecords =
    service.findCaseById(caseRecordId);
  final CaseRecord caseRecord = caseRecords.get(0);
  caseRecord.getTasks().stream().filter(
    task -> task.getId().equals(taskId))
    .forEach( task -> caseRecord.removeTask(task) );
  service.saveCaseRecord(caseRecord);
  final StringWriter swriter = new StringWriter();
  final JsonGenerator generator =
          jsonGeneratorFactory.createGenerator(swriter);
  CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
  return swriter.toString();
}

对于 HTTP DELETE 请求,我们使用 @DELETE 注释方法 deleteTaskFromCase()。此方法的相对 URI 是 /rest/caseworker/item/{caseId}/task/{taskId} 的严格 RESTful 服务端点。

在这个 方法中,棘手的部分是搜索实际的Task 记录。在这里,Java 8 Lambda 和流函数使这成为一项非常全面和愉快的任务。正确识别 Task 实体后,我们将其从父 CaseRecord 中删除,然后持久保存主记录。在消息的最后,我们发送 CaseRecord 的 JSON 响应。

这涵盖了应用程序的 JAX-RS 方面;我们现在将继续讨论 Java EE WebSocket 支持。

WebSocket communication

WebSocket 是 一个 HTML 协议扩展,它允许客户端和服务器完全参与 跨网络的双工异步通信。在切换到更快的 TCP/IP 流之前,它通过向后兼容的 HTTP 的两个端点之间的初始握手来工作。 WebSocket 规范 (RFC 6455) 是 HTML5 技术集合的一部分 Web 超文本应用技术工作组WHATWG)推动( https://whatwg.org) 和 互联网工程工作组 (IETF) (https://www.ietf.org)。

WebSocket 支持从 Java EE 7 版本开始提供,相关的 JSCP 规范是 JSR 356 (https://jcp.org/en/jsr/detail?id=356)。我们可以使用注释或直接针对 API 开发 JavaEE WebSocket。正如我们将看到的,使用注释更容易编写。

AngularJS client side

围绕新的任务记录控制器和应用程序主控制器再次查看 AngularJS 客户端会很有帮助。让我们检查控制器 CaseRecordController 中的方法 updateProjectTaskCompleted()。每当用户通过选择或取消选择 HTML checkbox 元素来决定任务完成时,我们连接前端以通过 send 发送 WebSocket 消息() 方法:

  $scope.updateProjectTaskCompleted = function( task ) {
      var message = { 'caseRecordId': task.caseRecordId, 'taskId': task.id, 'completed': task.completed }
      $scope.connect()
      var jsonMessage = JSON.stringify(message)
      $scope.send(jsonMessage)
  }

整个本地 JavaScript 任务记录以 JSON 形式发送。

为了向客户端的其他模块提供 WebSocket 通信,AngularJS 建议我们定义一个工厂或服务。工厂通常只初始化一次。另一方面,服务添加功能并根据调用上下文返回不同的实例。

以下是缺失的工厂:

myApp.factory('UpdateTaskStatusFactory', function( $log ) {
  var service = {};

  service.connect = function() {
    if (service.ws) { return; }
    var ws = new WebSocket("ws://localhost:8080/
      xen-force-angularjs-1.0-SNAPSHOT/update-task-status");
    ws.onopen = function() {
      $log.log("WebSocket connect was opened"); };
    ws.onclose = function() {
      $log.log("WebSocket connection was closed"); }
    ws.onerror = function() {
      $log.log("WebSocket connection failure"); }
    ws.onmessage = function(message) {
      $log.log("message received ["+message+"]"); };
    service.ws = ws;
  }

  service.send = function(message) {
      service.ws.send(message);
  }

  return service;
});

定义 AngularJS 工厂的习惯用法非常类似于定义控制器或模块。在主模块 myApp 中,我们使用两个参数调用库 factory() 方法:工厂名称和函数定义服务的回调。工厂对默认的日志记录模块只有一个依赖项,$log

connect() 方法通过实例化 WebSocket 实例来使用 URL 初始化 HTML5 WebSocket。使用句柄,我们注册可选的回调来处理事件:当 WebSocket 打开、关闭、接收到消息或出现错误时。每个 回调将消息转储到网络浏览器的控制台日志。

我们定义了几个 send() 方法,它们将消息体内容通过 WebSocket 发送到对等方。在 WebSocket 的说法中,远程端点被称为对等点,因为客户端和服务器端点之间没有区别。双方可以开始与对方建立连接并开始通信;因此术语全双工。

Server-side WebSocket endpoints

如前所述,在 Java EE 7 中,我们可以使用标准的 注释快速开发 WebSocket 端点。 Java WebSocket API 严格遵循 IETF 规范,您将认识到与 Web 浏览器中的许多 JavaScript 实现的相似之处。在这本数字 Java EE 书籍中可以合理压缩的配置以及直接对库进行注释和编程的不同方法太多了。

尽管如此,基本的 Java 类实际上被注释为无状态会话 EJB 以及 WebSocket。这应该不足为奇,因为 Java EE 规范在某些情况下允许这种注释混合。

以下是 CaseRecordUpdateTaskWebSocketEndpoint 中的端点:

package uk.co.xenonique.nationalforce.control;
/* ... */
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/update-task-status")
@Stateless
public class CaseRecordUpdateTaskWebSocketEndpoint {
  @Inject
  CaseRecordTaskService service;

  static JsonGeneratorFactory jsonGeneratorFactory =
    Json.createGeneratorFactory(...);

  @OnMessage
  public String updateTaskStatus(String message) {
    final StringReader stringReader = new StringReader(message);
    final JsonReader reader = Json.createReader(stringReader);
    final JsonObject obj = reader.readObject();
    final int projectId = obj.getInt("caseRecordId");
    final int taskId = obj.getInt("taskId");
    final boolean completed = obj.getBoolean("completed");
    final List<CaseRecord> projects =
      service.findCaseById(projectId);
    if ( !projects.isEmpty()) {
      projects.get(0).getTasks().stream()
        .filter(task -> task.getId() == taskId).
        forEach(task -> {
            task.setCompleted(completed);
            service.saveCaseRecord(task.getCaseRecord());
        });
      return "OK";
    }
    return "NOT FOUND";
  }

  @OnOpen
  public void open( Session session ) { ... }
  @OnClose
  public void close( Session session ) { ... }
  @OnError
  public void error( Session session, Throwable err ){
      err.printStackTrace(System.err);
  }
}

我们 @ServerEndpoint 注释 bean 以表示服务器端端点。 server-side 的概念本质上是一种 Java EE 命名法,用于声明此端点存在于应用程序服务器上的意图。还有诸如 @ClientEndpoint 连接之类的东西。

Java EE 使用带注释的方法来处理 WebSocket 周围的打开、关闭和失败事件,而不是回调,使用 @OnOpen, @OnClose @OnError 分别。

要正确处理 WebSocket 上的消息接收,POJO 或 bean 必须只有一个用 @Message 注释的方法。在幕后,库框架将消息转换为字符串,用于我们这里最简单的情况。可以通过线路向下和跨线发送二进制和复杂数据类型。

updateTaskStatus() 方法中,我们利用 JSON-P API 将文本解析为显着任务的属性。从输入的文本消息中,我们需要案例记录 ID、任务 ID 和任务的完成属性。我们从持久化中检索匹配的 CaseRecord 实体,并过滤 Task 对象的集合以获得正确的项目。一旦我们有了它,我们设置完成的属性并将整个记录持久化回持久化。

允许 WebSocket 向对等方返回响应。我们同步发送响应,就像我们在这里所做的那样,使用 OKNOT FOUND 之类的文本消息。读者应该知道,也可以异步发送响应。

我们已经到了服务器端讨论的结尾。

Consider your design requirements


AngularJS 是一个强大的客户端 JavaScript MVC 框架。开发人员、设计师和数字经理一直在寻找下一个令人兴奋的技术。人们通常倾向于衡量新框架的影响。可以说 AngularJS 改变了游戏规则,因为绑定到模型的开发组件很容易。默认情况下,不使用 jQuery 实现这种潜在的容易出错的代码的努力是巨大的!

我们知道 AngularJS 适合单页应用程序。这是否意味着您的下一个企业应用程序必须是 SPA?好吧,执业顾问的回答是,它总是取决于你的目标。 SPA 适用于有限的客户旅程以及体验主要发生在一个地方的情况。个案工作者申请属于这种类型,因为该人正在逐案评估护照申请人,因此,他们在工作日的大部分时间都呆在一个网页上工作。

案例工作者应用程序演示了主从关系。您的下一个企业应用程序可能需要涉及一组更复杂的使用案例。您的域可能需要大量实体。单个 SPA 可能无法涵盖所有​​领域。一方面,您将需要更多的 JavaScript 模块、控制器和工厂以及 HTML 指令来完全包围系统的有界上下文。

那么如何处理这些复杂的要求呢?一种方法是将客户端脚本上的所有 JavaScript 逻辑捆绑到一个下载中。为此,我们在上一章简要介绍过的 GruntJS 等工具可以合并、压缩和优化文件。我们可以通过多个网页和导航来利用 Java EE 的优势。

Array collection of single-page applications

我们可以将 SPA 构造成线性序列,以便系统的客户旅程几乎 遵循工作流程。在仓库订单管理、工程和金融交易等领域,这种方法可能是有意义的。在这样的领域中,业务用户在一系列复杂的步骤中工作,以便处理从 A 到 B 的大量工作单元。如果 SPA 数组有一个短的线性序列,可能由三个或四个步骤,但如果链的长度大于或等于七,则会丢失。

Hierarchical collection of single-page applications

另一种方法是从线性序列完全下降到 SPA 的分层树结构中。这种方法非常专业,建议寻求一些架构保证这条路径对您的业务来说是可持续的。为什么企业要以这种方式组织 SPA?您的利益相关者可能希望以准确反映领域的方式维持组织功能。设计方法是一个冒险的过程,因为它在整个模型中引入了僵化,并且在我看来,它似乎是由管理层领导的权威而不是有机的。如果层次结构树是按广度而不是按深度的方式组织的,请问自己为什么?

在这些时代,工程师和架构师正在研究微服务以便在一个优雅的盒子中扩展并拥有单一的业务功能,HSPA 可能确实有用。树结构大小应在 10 个节点左右。

Summary


在这一长章中,我们介绍了使用案例工作者应用程序进行客户端 AngularJS 开发。我们了解了 AngularJS 框架如何操作 DOM 以及它如何提供数据之间的绑定以及使用 MVC 的元素呈现。我们还学习了一些 AngularJS 概念,例如作用域、模块、控制器和工厂。

使用研究示例,我们说明了 AngularJS 如何使用来自客户端的 RESTful 服务调用与远程服务器通信。我们还简要研究了 WebSocket 交互。在 JavaScript 的客户端,我们在 caseworker 应用程序中遍历了整个 CRUD 习惯用法。

在服务器端,我们看到了使用 JAX-RS 实现的 RESTful 服务,它涵盖了四种标准的 HTTP 方法请求。我们还了解了 Java WebSocket 的实现。

AngularJS 适合单页应用程序所需的应用程序模式。但是,这可能适合也可能不适合您的业务需求。采用 AngularJS 需要 JavaScript 编程和 Java EE 开发的完整堆栈知识。迁移到像 AngularJS 这样的框架会使您的企业面临雇用、保留和学习更多技术的风险。

三角形的另一面也需要考虑:组织动态。著名的美国苹果公司将他们当时从事在线购物商店的敏捷团队划分为纯服务器和客户端部门。他们之间唯一允许的通信是商定的编程接口。这种划分发生在 iPhone 开发时(大约 2005-2007 年),这显然早于 AngularJS。您的团队可能会以不同的方式运作,但按合同设计的概念仍然很重要,因为它展示了可以实现的目标,尤其是在 RESTful 服务方面。

我将用 AngularJS 的共同创建者 Misko Hevery 的第二句话留给你。他说:

"有限——你不能在一个页面上向一个人显示超过 2000 条信息。任何超过这个都是非常糟糕的 UI,人类无法处理这个无论如何。”

Exercises


  1. 下载 xen-national-force 案例工作者应用程序的源代码,并花几个小时研究实施。你注意到了什么?编译代码并将生成的 WAR 部署到您选择的应用程序服务器。

  2. 使用本章的材料,用一个简单的实体 EMPLOYEE 创建您的 CRUD AngularJS 应用程序。该实体应具有员工 ID、姓名和社会保险号。使用 AngularJS 构建客户端,使用 JAX-RS 构建服务器端。 (在本书的源代码中,有一个空白项目可以帮助您入门。)

  3. 在 AngularJS 和 JavaEE 中的上一个问题中构建 EMPLOYEE CRUD 时,您是否使用了 UI Bootstrap 中的模态对话框?如果不是,请研究其他呈现视图以插入、更新和删除记录的方法。 (提示:一种可能的方法是动态显示和隐藏不同的 DIV 元素。)

  4. 个案工作者应用程序存在明显的设计缺陷。你找到了吗?当案例工作者显示和隐藏任务视图时,它会更新持久性数据库;解释为什么这是一个问题?

  5. 想象一下,从今天开始,您已成为整个 xen-national-force 团队的项目负责人,突然间,企业决定他们希望在状态发生变化时向其他案例工作者广播即时通知。从技术层面解释如何实现这个用户故事。想想 AngularJS 客户端的挑战。您将如何构建 Java EE 服务器端?

  6. 研究 xen-national-force 中负责维护 ISO 护照国家名称及其代码的集合的工厂模块 (iso-countries.js)。这个模块在前端是如何使用的?它在哪里使用?

  7. 不要在 CaseRecord JPA 实体中使用专用的布尔属性来表示是否显示任务视图,而是编写一个 AngularJS 工厂模块,在本地存储所有案例记录的此信息在客户端。

  8. 示例案例工作者应用程序检索数据库中的每条记录并将其返回给用户。假设真实系统有 1,000 个案例记录。此功能可能有什么问题?你会怎么解决?如果个案工作者无法查看所有记录,请解释您如何确保他们能够查看相关案例?您需要在 AngularJS 客户端和 Java EE 服务器端实现什么?