Skip to content

3. Angular客户端 JS

3.1. Angular 框架参考资料 JS

本文开头已提供了两本关于 Angular 框架的参考书目 JS。现再次列出如下:

  • [ref1]:由 Adam Freeman 撰写、Apress 出版社出版的《Pro AngularJS》一书。这是一本非常优秀的书籍。 该书示例的源代码可在 URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/] 上免费获取;
  • [ref2]:Angular的官方文档 JS [https://docs.angularjs.org/guide];

Angular JS 值得单独写一本书。亚当·弗里曼(Adam Freeman)的这本书有600多页,每一页都物有所值。我们将描述一个 Angular 应用程序,并在描述过程中探讨该框架的基础知识。不过,我们仅会提供理解所提解决方案所需的必要解释。 Angular 是一个功能极其丰富的框架,实现相同结果的方案不胜枚举。这确实是一个难题,因为初学者往往无法判断自己采用的方案是优于还是劣于其他方案。本文提出的解决方案亦是如此。它本可以采用不同的写法,甚至可能遵循更优的实践规范。

3.2. Angular客户端架构

Angular客户端的架构与经典Web应用MVC的架构相似,但存在一些差异。例如,一个Spring Web应用MVC的架构如下:

客户端请求的处理流程如下:

  1. 请求 - 请求的 URL 采用 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... 格式[Dispatcher Servlet] 是 Spring 中的类,负责处理传入的 URL。 它将 URL “路由”到负责处理它的操作。这些操作是名为 [Contrôleurs] 的特定类的方法。此处的 MVC 的 C 部分是字符串 [Dispatcher Servlet, Contrôleur, Action]。 如果未配置任何操作来处理传入的 URL,则 [Dispatcher Servlet] Servlet 将返回“未找到请求的 URL” (404 错误 NOT FOUND);
  1. 处理
  • 所选操作可利用 parami 参数,这些参数由 [Dispatcher Servlet] Servlet 传递而来。这些参数可能来自多个来源:
    • [/param1/param2/...]路径中的URL,
    • [p1=v1&p2=v2] 中的参数(源自 URL,)
    • 浏览器随请求发送的参数;
  • 在处理用户请求时,该操作可能需要 [metier] 和 [2b] 层。一旦处理了客户端的请求,该操作可能会触发各种响应。一个典型的例子是:
    • 如果请求无法正确处理,则返回错误页面
    • 否则显示确认页面
  • 该操作会要求显示某个视图 [3]。该视图将显示被称为视图模型的数据。这就是 MVC 中的 M。 该操作将创建视图模型 M [2c],并要求显示视图 V [3];
  1. 响应——选定的视图 V 使用操作构建的模型 M 来初始化响应 HTML 的动态部分(该响应需发送给客户端),随后发送此响应。

我们的 Angular 客户端架构与此类似,只是术语略有不同。首先,Angular 应用程序通常是单页 Web 应用程序(APU)或单页应用程序(SPA):

Image

  • 用户以以下形式请求应用程序的初始页面:URL。浏览器将向Web服务器发出请求以获取该文档。 该文档是一个由 CSS 进行样式设置、并通过 JavaScript 实现动态化的页面;
  • 随后,用户将与呈现给他的视图进行交互。交互可分为以下几种类型:
    • 无需与外部交互的操作,例如隐藏/显示视图中的元素。这些操作由嵌入的JavaScript处理;
    • 需要从远程Web服务获取数据的交互。这些数据将通过调用AJAX(异步JavaScript和XML)获取,随后构建模型并显示视图;
    • 需要初始视图以外的其他视图的情况。将通过Ajax调用向提供初始页面的服务器请求该视图。随后上述流程将重复进行。获取到的页面将被缓存于浏览器中。下次调用时,将不再向远程服务器HTML请求该页面;

最终,浏览器仅发起一次 HTTP 请求,即获取初始页面的请求。后续针对页面服务器 HTML 或远程 Web 服务的 HTTP 请求,均由页面中嵌入的 JavaScript 完成。

现在我们介绍浏览器内的应用程序架构。我们暂不考虑提供应用程序页面 HTML 的服务器 HTML。为便于说明,我们可以假设这些页面都已存在于浏览器的缓存中。

首先,我们需要明确这一架构的位置:

  • 在 [1] 中,我们处于浏览器环境;
  • 在 [2] 中,用户与浏览器显示的视图进行交互;
  • 在 [3] 中,数据从网络上获取,通常来自 Web 服务;

用户与视图进行交互:填写表单并提交。让我们以上面的视图 V1 为例来阐明这一过程。假设这是应用程序的初始视图。它是通过以下方式获得的:

  • 用户以 http://machine:port/contexte 的形式请求应用程序的初始视图 URL
  • 浏览器请求了与该 URL 关联的文档。 浏览器从视图 V1 接收到了页面 HTML / CSS / JS;
  • 页面中嵌入的JavaScript随后接管了控制权,并将控制权移交给了控制器C1 [5];
  • 该控制器构建了视图 V1 的模型 M1 [8] [9]。 构建该模型可能需要调用内部服务 [6] 并查询外部服务 [7];

用户现在面前显示的是视图 V1。假设这是一个表单。用户填写后提交:

  • 在 [4] 中,用户提交表单;
  • 在 [5] 中,该事件将由控制器 C1 的某个方法处理;

如果该事件仅导致视图 V1 发生简单变化(隐藏/显示字段), 控制器 C1 将修改视图 V1 的模板 M1,然后重新显示视图 V1。 为此,它可能需要调用 [services] 或 [6] 层中的某项服务。

如果该事件需要外部数据:

  • 在 [6] 中,控制器 C1 将请求 [DAO] 层获取这些数据;
  • 在 [7] 中,该层将通过一次或多次 AJAX 调用获取数据;
  • 在 [8] 和 [9] 中,M1 模型将被修改,并显示 V1 视图;

如果该事件导致视图发生变化,在前两种情况下,控制器C1将不再显示视图V1,而是请求新的URL和[10]。 这是一个浏览器内部的 URL。它不会立即转化为对页面服务器 HTML 的 HTTP 调用。 该 URL 的变更由路由器处理,该路由器配置为:每个内部 URL 都对应一个视图 V 及其控制器 C。随后,路由器会显示新的视图 Vn。 在显示之前,其控制器 Cn 接管控制,构建模型 Mn,然后显示视图 Vn [11]。 如果视图 Vn 的页面 HTML 未缓存于浏览器中,则会向页面服务器 HTML 请求该页面。

该架构的 [Présentation] 层与 JSF(Java Server Faces)架构相似:

  • 视图 V 对应于 JSF 的 Facelet 类型视图;
  • 控制器 C 对应于 Bean JSF,这是一个同时包含视图 V 的模型 M 及其事件处理程序的 Java 类;

[Services] 层与我们惯常使用的 [Services] 层有所不同。在 Web 开发中,服务器端通常采用以下分层架构:

在上图中,[web]层仅通过[métier]层与[DAO]层进行通信。 虽然我们可以向 [web] 层注入对 [DAO] 层的引用,从而实现这种通信,但我们不允许这样做。

使用 Angular 时,我们并不禁止这样做。因此,架构变为如下所示:

  • 在 [1] 中,[présentation] 层可直接与任意服务通信;
  • 在 [2] 中,服务之间相互认识。一个服务可以使用一个或多个其他服务。

3.3. Angular客户端视图

Angular客户端视图已在第1.3.3节中介绍过。为了便于阅读本章,我们在此再次列出。第一个视图如下:

  • 在 [6] 中,这是应用程序的登录页面。这是一个为医生提供预约服务的应用程序;
  • 在 [7] 中,一个复选框用于控制是否处于 [debug] 模式。该模式的特征是存在 [8] 框架,用于显示当前视图的模板;
  • 在 [9] 中,是一个以毫秒为单位的人为等待时间。其默认值为 0(不等待)。若 N 为该等待时间的数值,则用户的任何操作都将在等待 N 毫秒后执行。这有助于观察应用程序所实现的等待管理机制;
  • 在 [10] 中,是 Spring 4 服务器的 URL。根据前文所述,该值为 [http://localhost:8080];
  • 在 [11] 和 [12] 中,分别表示希望使用该应用程序的用户名和密码。共有两个用户:admin/admin(登录名/密码)拥有角色 ADMIN,以及 user/user 拥有角色 (USER)。仅角色 ADMIN 拥有使用该应用程序的权限。角色 USER 仅用于展示在此使用场景下服务器的响应;
  • 在 [13] 中,是用于连接服务器的按钮;
  • 在 [14] 中,应用程序的语言。共有两种:默认的法语和英语。
  • 在 [1] 中,进行连接;
  • 登录后,可选择希望预约的医生 [2] 及预约日期 [3];
  • 在 [4] 中,查询所选医生在所选日期的日程安排;
  • 获取医生日程表后,即可预订时段 [5];
  • 在 [6] 步骤中,选择该预约的患者,并在 [7] 步骤中确认该选择;

预约确认后,系统会自动返回日程表,新预约已记录其中。该预约日后可通过 [7] 进行删除。

主要功能已介绍完毕,操作简单。未提及的功能均为导航功能,用于返回上一视图。最后介绍语言管理:

  • 在 [1] 中,可切换为英语;

2

Image

  • 输入 [2],视图将切换为英语,包括日历;

3.4. Angular 项目配置

我们将逐步构建我们的 Angular 客户端。我们使用 Webstorm

创建一个空文件夹 [rdvmedecins-angular-v1],然后用 Webstorm 打开它:

  • 在 [1] 中,我们创建一个文件夹;
  • 在 [2] 中,选择我们创建的文件夹;
  • 在 [3] 中,我们得到一个空的 WebStorm 项目;
  • 在 [4] 中,通过选项 [File / Settings] 进行项目配置;
  • 在 [5] 和 [6] 中,需配置负责拼写检查的属性 [Spelling]。默认情况下,该功能处于启用状态。 由于下载的软件为英文版,我们用法语撰写的程序注释会被标记为可能的拼写错误。因此,我们需要禁用该拼写检查功能 [7];
  • 在 [8] 中,创建一个新文件;
  • 在 [9] 中,选择创建文件 [package.json],该文件使用语法 JSON 描述应用程序;
  • 在 [10] 中,对生成的文件进行修改,如 [11] 所示;
  • 在 [12] 中,将该文件同时保存到 [package.json] 和 [bower.json] 中;
  • 在 [13] 中,重新配置项目;
  • 在 [14] 中,配置 [Javascript / Bower] 属性,该属性将允许我们声明所需的 JavaScript 库;
  • 在 [15] 中,指定我们刚刚创建的文件 [bower.json];
  • 在 [16] 中,添加一个 JavaScript 库;
  • 在 [17] 中,显示所有可下载的 JavaScript 库;
  • 在 [18] 中,我们可以设置条件来筛选 [17] 中的列表。这里,我们指定要选择 [Angular JS] 库;
  • 在 [19] 中,显示了该库的详细信息。这里可以看到将下载 Angular 1.2.18 版本;
  • 在 [20] 中,正在下载该库;
  • 在 [21] 中,可见该文件已下载;
  • 在 [22] 中,可以看到已下载的版本。实际上这是 1.2.19 版;
  • 在 [23] 中,可以看到最新可用版本;
  • 在 [24] 中,按照与之前相同的步骤,下载以下库:
angular-base64
用于将字符串“user:password”编码为Base64;
angular-i18n
用于日历的国际化
angular-route
用于将应用程序内部的 URL 路由到正确的控制器和视图;
angular-translate
用于视图的国际化。这是一个独立于 Angular 的项目。此处将使用两种语言:法语和英语;
angular-ui-bootstrap-bower
提供兼容 Bootstrap 的视觉组件。此处将使用其日历组件;
bootstrap
CSS Bootstrap 框架。将用于构建视图;
footable
提供了一个“表格”类型的可视化组件。它具有“响应式”特性,能够适应屏幕尺寸;
bootstrap-select
提供了一个“下拉列表”类型的组件;
  • 在 [25] 中,下载的库已安装在 [bower_components] 文件夹中;
  • 在 [26] 中,可以看到库 JQuery 已被下载。这是因为 Bootstrap 使用了它。 项目中 JavaScript 依赖项的安装机制与 Java 领域的 Maven 类似:如果下载的库本身存在依赖项,这些依赖项会自动被下载;

文件 [bower.json] 已更新:

{
  "name": "rdvmedecins-angular",
  "version": "0.0.1",
  "dependencies": {
    "angular": "~1.2.18",
    "angular-base64": "~2.0.2",
    "angular-route": "~1.2.18",
    "angular-translate": "~2.2.0",
    "bootstrap": "~3.1.1",
    "footable": "~2.0.1",
    "angular-ui-bootstrap-bower": "~0.11.0",
    "bootstrap-select": "~1.5.2"
  }
}

所有已下载的依赖项均已记录在该文件中。

3.5. Angular 客户端的首页

我们创建 Angular 客户端首页的初始版本:

  • 文件名为 [1] 和 [2], 我们创建了一个名为 HTML 的文件,其子文件分别为 [app-01]、[3] 和 [4];

文件 [app-01.html] 将暂时作为我们的主页面。我们将在此配置应用程序所需的 CSS 和 JS 文件的导入:


<!DOCTYPE html>
<html>
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- CSS -->
  <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap 核心 JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
  • 第 11-12 行:用于 Bootstrap 的 CSS 文件;
  • 第 13 行:用于组件 [boostrap-select] 的文件 CSS;
  • 第 14 行:用于组件 [footable] 的文件 CSS;
  • 第 21-24 行:Bootstrap 组件的 JS 文件;
  • 第 21 行:Bootstrap 组件由 JQuery 驱动;
  • 第 22 行:Bootstrap 的 JS 文件;
  • 第 23 行:用于组件 [boostrap-select] 的文件 JS;
  • 第 24 行:用于组件 [footable] 的文件 JS;
  • 第 26-30 行:Angular 及其相关项目的 JS 文件;
  • 第 26 行:Angular 的 JS 文件。如果使用了该库,则必须在 JQuery 之后加载;
  • 第 27 行:项目 [angular-ui-bootstrap] 的文件 JS;
  • 第 28 行:路由器 [angular-route] 的文件 JS;
  • 第 29 行:Angular 应用程序国际化模块的文件 JS;
  • 第30行:模块[angular-base64]的文件JS;

可以验证文件 [app-01.html] 的有效性:

  • 在 [1] 中,请求检查代码;
  • 在 [2] 中,显示一切正常时的结果;

建议在代码执行前进行这种系统性的检查。在此,该检测可发现文件 CSS 和 JS 的任何引用错误。如果路径不正确,代码检查器会发出提示。

  • 在 [3] 中,可以通过调试器将页面加载到浏览器中。在浏览器中会得到以下结果:
  • 在 [4] 中,页面 [app-01.html] 由 WebStorm 内部服务器提供,该服务器在此运行于 63342 端口;
  • 在 [5] 处,是调试器的控制台。如果发生错误,它们会显示在这里。JavaScript 指令 [console.log(expression)] 生成的屏幕输出也会显示在此处。我们将大量使用这一功能;

调试模式允许我们在 WebStorm 中修改页面,并在浏览器中查看修改结果,而无需重新加载页面。因此,如果我们在下面添加第 3 行:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <h2>Version 1</h2>
</div>

然后返回浏览器,我们会发现页面已经发生了变化:

 

3.6. 探索 Bootstrap

接下来我们将演示该应用中使用的部分 Bootstrap 特性。我对这个框架的了解仅限于从网上复制粘贴的代码,因此仅限于此。我将解释那些我认为理解的类(如 CSS)的作用,其余部分我将不予评论。

3.6.1. 示例 1

在 Angular 中,从外部获取信息的操作都是异步的。这意味着操作启动后,视图会立即返回,用户可以继续与之交互。应用程序通过一个事件获知操作已完成。 该事件由函数 JS 处理,该函数可据此更新当前视图或切换视图。如果操作可能耗时较长,为用户提供取消操作的选项会很有帮助。我们将系统地提供这一选项。为此,我们将使用一个 Bootstrap 横幅:

Image

为了实现这一效果,我们将 [app-01.html] 复制为 [app-02.html],并修改以下代码行:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="alert alert-warning">
    <h1>Opération en cours. Veuillez patienter...
      <button class="btn btn-primary pull-right">Annuler</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
</div>
  • 第 1 行:类 CSS [container] 定义了浏览器内的显示区域;
  • 第 3 行:类 CSS [alert] 显示一个彩色区域。类 [alert-warning] 使用预定义的颜色;
  • 第 5 行:类 [btn] 用于装饰按钮。类 [btn-primary] 为其赋予特定颜色。类 [pull-right] 将其定位在警报横幅的右侧;
  • 第 6 行:一张加载动画图片;

3.6.2. 示例 2

应用程序的不同视图将有一个共同的标题:

Image

为实现此效果,我们将 [app-01.html] 复制为 [app-03.html],并修改以下行:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <!-- Bootstrap Jumbotron -->
  <div class="jumbotron">
    <div class="row">
      <div class="col-md-2">
        <img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
      </div>
      <div class="col-md-10">
        <h1>Les Médecins associés</h1>
      </div>
    </div>
  </div>
</div>
  • 第 4 行中的类 [jumbotron] 用于生成彩色区域;
  • 第 5 行:类 [row] 定义了一行 12 列的布局;
  • 第6行:类[col-md-2]定义了该行中的两列区域;
  • 第7行:在这两列中放置一张图片;
  • 第9-11行:在其余10列中放置文本;

3.6.3. 示例 3

视图将包含一个顶部控制栏。其中包含控制选项、链接或按钮。此外还包含表单控件。例如:

要获得此效果,我们将 [app-01.html] 复制到 [app-04.html] 中,并修改以下行:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
       <div class="navbar-collapse collapse">
        <form class="navbar-form navbar-right">
          <!-- 调试模式 -->
          <label style="width: 100px">
            <input type="checkbox">
            <span style="color: white">Debug</span>
          </label>
          <!-- 登录表单 -->
          <div class="form-group">
            <input type="text" class="form-control" placeholder="Temps d'attente"
                   style="width: 150px"/>
            <input type="text" class="form-control" placeholder="URL du service web"
                   style="width: 200px"/>
            <input type="text" class="form-control" placeholder="Login"
                   style="width: 100px"/>
            <input type="password" class="form-control" placeholder="Mot de passe"
                   style="width: 100px"/>
          </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
    </div>
  </div>
</div>
  • 第 4 行:类 [navbar] 将为导航栏设置样式。 类 [navbar-inverse] 为其设置黑色背景。类 [navbar-fixed-top] 将确保当用户在浏览器中滚动页面时,导航栏始终保持在屏幕顶部;
  • 第 6-14 行:定义了 [1] 区域。这通常是一系列我无法理解的类。我直接使用该组件;
  • 第15行:定义了控制栏中的一个“响应式”区域。在智能手机上,该区域会隐藏在菜单区域中;
  • 第16行:类[navbar-form]为命令栏中的表单提供样式。类[navbar-right]将其置于该表单的右侧;
  • 第23-32行:第17行表单的四个输入区域 [3]。 它们位于类 [form-group] 内部,该类负责包装表单元素,且每个输入框都具有类 [form-control];
  • 第33行:我们之前已经见过的[btn]类,该类通过[btn-success]类进行了扩展,使其呈现绿色;

3.6.4. 示例 4

控制栏将通过下拉列表实现语言切换:

Image

为了实现这一效果,我们将 [app-01.html] 复制为 [app-05.html],并在控制栏中添加以下代码行:


          <button class="btn btn-success">
            Connexion
          </button>
          <!-- 语言 -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">
              Langues
            </button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span>
              <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li>
                <a href="">Français</a>
              </li>
              <li>
                <a href="">English</a>
              </li>
            </ul>
          </div>
</form>

新增的行是第 4 至 21 行。

  • 第 5 行:类 [btn-group] 用于修饰一组按钮。第 6 行和第 9 行各有一个;
  • 第 6-8 行:第一个按钮定义下拉列表的标签。类 [btn-danger] 将其颜色设为红色;
  • 第 9-12 行:第二个按钮是下拉列表按钮。它紧贴在第一个按钮旁边,给人一种单一组件的感觉;
  • 第10行:显示向下箭头,表明该按钮是一个下拉列表;
  • 第11行:用于屏幕阅读器;
  • 第13-20行:下拉列表中的项目属于无序列表;

3.6.5. 示例 5

为了提交表单或进行导航,用户可在控制栏中使用如下选项或按钮:

菜单选项已安装在 [1] 中。为实现此效果,我们将 [app-01.html] 复制到 [app-06.html] 中,并添加以下行:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
...
      </div>
      <!-- 菜单选项 -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span>Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Annuler</span>
            </a>
          </li>
        </ul>
        <!-- 右侧按钮 -->
        <form class="navbar-form navbar-right" role="form">
...
        </form>
      </div>
    </div>
  </div>
</div>
  • 菜单选项由第 8-29 行生成。这些仍是 <ul> 列表中的元素。类 [active] 使文本呈现高亮效果,以此表明该选项可点击。

3.6.6. 示例 6

我们将像下面这样在下拉列表中展示医生和客户:

 

所使用的下拉列表并非 Bootstrap 的原生组件,而是 [bootstrap-select] 组件(http://silviomoreto.github.io/bootstrap-select/)。 为了实现这一效果,我们将 [app-01.html] 复制为 [app-07.html],并添加以下代码行:


<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>

</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <h2><label for="medecins">Médecins</label></h2>
  <select id="medecins" data-style="btn btn-primary" class="selectpicker">
    <option value="1">Mme Marie PELISSIER</option>
    <option value="1">Mr Jacques BROMARD</option>
    <option value="1">Mr Philippe JANDOT</option>
    <option value="1">Mme Justine JACQUEMOT</option>
  </select>
</div>
<!-- Bootstrap 核心 JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- 本地脚本 -->
<script>
  $('.selectpicker').selectpicker();
</script>
</body>
</html>
  • 第 5 行:需导入 [bootstrap-select] 的样式表;
  • 第13行:[data-style]属性由[bootstrap-select]调用。该属性用于为下拉列表设置样式。此处将其设置为蓝色按钮样式[btn-primary];
  • 第13行:属性[class]在第23行被调用。该属性可任意设置;
  • 第14-17行:下拉列表的元素。这里使用了经典的HTML标签;
  • 第22行:需从[bootstrap-select]导入JS;
  • 第24-26行:页面加载完成后执行的JS脚本;
  • 第25行:一条JQuery指令。对所有具有[selectpicker]类($('.selectpicker'))的元素应用[selectpicker]方法(selectpicker())。 仅有一个,即第13行的<select>标签。方法[selectpicker]来自第22行引用的文件JS;

3.6.7. 示例 7

为了显示医生的日程表,我们将使用由库 JS [footable] 提供的“响应式”表格:

  • 在 [1] 中:表格的正常显示效果;
  • 在 [2] 中:当缩小浏览器窗口时表格的显示效果。[Action] 列会自动换行。这就是所谓的“响应式”组件,或简称为自适应组件。

我们将 [app-01.html] 复制到 [app-08.html] 中,并添加以下代码行:


...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="row alert alert-warning">
    <div class="col-md-6">
      <table id="creneaux" class="table">
        <thead>
        <tr>
          <th data-toggle="true">
            <span>Créneau horaire</span>
          </th>
          <th>
            <span>Client</span>
          </th>
          <th data-hide="phone">
            <span>Action</span>
          </th>
        </thead>
        <tbody>
        <tr>
          <td>
            <span class='status-metro status-active'>
              9h00-9h20
            </span>
          </td>
          <td>
            <span></span>
          </td>
          <td>
            <a href="" class="status-metro status-active">
              Réserver
            </a>
          </td>
        </tr>
        <tr>
          <td>
            <span class='status-metro status-suspended'>
              9h20-9h40
            </span>
          </td>
          <td>
            <span>Mme Paule MARTIN</span>
          </td>
          <td>
            <a href="" class="status-metro status-suspended">
              Supprimer
            </a>
          </td>
        </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
  • 第 2 行和第 60 行已在 [app-01.html] 中存在。它们是 [footable] 库提供的 CSS 和 JS 文件;
  • 第3行引用了以下文件CSS:

@CHARSET "UTF-8";

#th 间距 {
    text-align: center;
}

#td 间距 {
    text-align: center;
    font-weight: bold;
}

.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}

.status-metro.status-active {
  background: #43c83c;
}

.status-metro.status-suspended {
  background: #fa3031;
}

样式 [status-*] 源自图书馆网站上找到的表格 [footable] 的使用示例。

  • 第 8 行:将表格放置在一行 [row] 和一个彩色边框 [alert alert-warning] 中;
  • 第 9 行:该表格将占用 6 列 [col-md-6];
  • 第10行:表格HTML采用Bootstrap样式进行格式化[class='table'];
  • 第13行:[data-toggle]属性指定了包含折叠/展开行控件的列([+/-]);
  • 第 19 行:属性 [data-hide='phone'] 指定当屏幕尺寸为手机屏幕时,该列应被隐藏。也可以使用值 'tablet';

3.6.8. 示例 8

为了帮助用户,我们将在视图的主要组件周围创建工具提示(tooltip):

要实现此效果,我们将 [app-01.html] 复制为 [app-09.html],并添加以下代码行:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <!-- 菜单选项 -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap 核心JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- 本地脚本 -->
<script>
  // --------------------- Angular 模块
  angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>

帮助气泡由库 [angular-ui-bootstrap] 提供,该库本身依赖于库 [angular]。第 50 行导入了库 [angular-ui-bootstrap]。 要实现 [angular-ui-bootstrap] 库的组件,我们需要创建一个 Angular 模块。这在第 52-55 行完成。这些行定义了一个名为 [rdvmedecins] 的 Angular 模块(第一个参数)。 一个 Angular 模块可以使用其他 Angular 模块。这被称为模块依赖。它们作为 [angular.module] 函数的第二个参数以数组形式提供。 在此,名为 [ui.bootstrap] 的模块由库 [angular-ui-bootstrap] 提供。正是该模块将为我们提供帮助气泡。

第 54 行定义了一个 Angular 模块。默认情况下,这不会对页面产生任何影响。通过将页面关联到一个 Angular 模块,我们指定该页面应由 Angular 管理。第 2 行正是这样做的。[ng-app='rdvmedecins'] 属性将页面关联到了第 54 行创建的模块。 随后,Angular 将对该页面进行解析。[tooltip] 属性将被 [ui.bootstrap] 模块识别并处理。

提示框的语法如下:


 <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>

在上文中,我们为文本 [Home] 添加了一个工具提示:

  • [tooltip]:定义工具提示的文本;
  • [tooltip-placement]:定义其位置(bottom、top、left、right);

Angular JS 允许在 HTML 语言中已有的标签或属性基础上添加新的标签或属性。对 HTML 语言的这一扩展是通过 Angular 指令实现的。 此处,[tooltip] 和 [tooltip-placement] 属性是由 [angular-ui-bootstrap] 创建的。

3.6.9. 示例 9

为了帮助用户选择约会的日期,我们将提供一个日历:

Image

与帮助气泡一样,此日历由库 [angular-ui-bootstrap] 提供。要实现此效果,我们将 [app-01.html] 复制到 [app-10.html] 中,并添加以下行:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
        </div>

        </div>
      </div>
    </div>
  </div>
</div>
...
<!-- 本地脚本 -->
<script>
  // --------------------- Angular 模块
  angular.module("rdvmedecins", ['ui.bootstrap'])
</script>

</body>
</html>

与之前一样,该页面关联了一个 Angular 模块(第 2 行和第 28 行)。日历由第 16 行中的 <datepicker> 标签定义,该标签由库 [angular-ui-bootstrap] 提供:

  • [show-weeks='true']:用于显示周数;
  • [class='well']:用于在日历周围添加一个圆角灰色区域;
  • [ng-model='jour']:[ng-*] 属性是 Angular 属性。[ng-model] 属性指定将要放入视图模板中的数据。 当用户点击某个日期时,该日期将被放入模型的 [jour] 变量中。 该变量在第 10 行被使用。语法 {{expression}} 用于求解由模型元素组成的表达式。此处 {{jour}} 将显示模型中变量 [jour] 的值。 Angular的一个显著特点是视图会自动跟踪变量 [jour] 的变化。因此,当用户更改日期时,这些更改将立即显示在第 10 行。一般而言,其工作原理如下:
    • 视图 V 与模型 M 相关联;
    • Angular 监听模型 M,并在模型 M 发生变化时自动更新视图 V

语法 {{jour|date}} 被称为过滤器。显示的并非 [jour] 的原始值,而是经过名为 [date] 的过滤器处理后的 [jour] 的值。 该过滤器在 Angular 中是预定义的。它用于格式化日期,并支持指定所需格式的参数。 因此,表达式 {{jour | date:'fullDate'}} 表示需要完整的日期格式,此处为 [Friday, June 20, 2014],因为日历默认使用英语。我们将在后续内容中探讨其国际化问题。

3.6.10. 结论

我们介绍了CSS Bootstrap框架中我们将要使用的组件。这些都是被动组件:它们的事件并未被处理。因此,点击按钮或链接不会产生任何效果。 这些事件将通过 JavaScript 进行处理。虽然可以不借助框架直接使用该语言,但正如服务器端的情况一样,客户端也存在一些不可或缺的框架。Angular JS 框架便是如此,它为浏览器执行的 JavaScript 应用程序开发带来了全新的思路。接下来我们将对此进行介绍。

3.7. 探索 Angular JS

接下来我们将演示该应用中使用的 Angular JS 框架的部分特性。其中一些我们已经接触过:

  • 如果为页面关联一个模块,该页面便由 Angular JS 驱动:

<html ng-app="rdvmedecins">
  • Angular 允许通过指令创建新的标签和属性 HTML:
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
  • Angular 允许创建过滤器
{{jour|date:'fullDate'}}
  • 视图 V 显示模型 M。当模型 M 发生变化时,Angular 会监听模型 M,并自动更新视图 V。模型 M 中某个变量的值可通过以下方式在视图 V 中显示:
{{variable}}

我们将首先深入探讨 Angular 中模型-视图-控制器(MVC)设计模式的实现。让我们从架构角度回顾一下它们之间的关系:

  • 视图 V1 显示由控制器 C1 构建的模型 M1。 该控制器不仅包含模型 M1,还包含视图 V1 的事件处理程序。当前处于循环的第 5、8、9 步:
    • [5]:视图 V1 中发生了一个事件。该事件由控制器 C1 处理;
    • 该控制器执行其任务 [6-7],随后构建视图 M1 和 [8];
    • [9]:视图 V1 显示新的模型 M1。如前所述,最后这一步是自动完成的。 与其他框架不同,这里没有显式的推送(MVC,即 C1 将模型 M1 推入 V1)或显式的拉取 (视图 V1 从 C1 中获取模型 M1)。这里存在一个开发者无法察觉的隐式推送
    • 随后循环 5、8、9 再次开始;

3.7.1. 示例 1:Angular 的 MVC 模板

我们将继续使用日历示例。我们已经看到了生成它的指令:


          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>

该指令支持除上述以外的其他属性,其中包括 [min-date] 属性,该属性用于设定日历中可选的最小日期。这对我们很有用。当用户选择预约日期时,该日期必须等于或晚于当前日期。因此,我们将编写如下代码:


<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>

其中 [dateMin] 是页面模板中的一个变量,其值将设为当前日期。这样生成的页面如下:

  • 在 [1] 处,当前日期为 2014 年 6 月 19 日。光标表示可以选中 6 月 19 日;
  • 在 [2] 中,光标表示无法选择 6 月 18 日;

我们将[app-10.html]复制到[app-11.html],并进行以下修改:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap 核心JavaScript ================================================== -->
...
<!-- 本地脚本 -->
<script>
  // --------------------- Angular 模块
  angular.module("rdvmedecins", ['ui.bootstrap']);
  // 控制器
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // 最小日期
        $scope.minDate = new Date();
      }]);

</script>

</body>
</html>

首先查看第26至37行的本地脚本:

  • 第28行:创建模块[rdvmedecins],该模块依赖于提供日历功能的模块[ui.bootstrap];
  • 第30-35行:创建一个控制器。它将持有我们页面的模板。此处不会有事件处理程序;
  • 第30-31行:控制器[rdvMedecinsCtrl]属于模块[rdvmedecins]。我们可以向一个模块添加任意数量的控制器。在我们的应用程序中,我们将拥有:
    • 一个应用程序管理模块;
    • 每个视图对应一个控制器;
  • 函数 [controller] 的第二个参数是一个形式为 ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] 的数组。最后一个参数是实现该控制器的函数。其参数是 Angular JS 将提供给该函数的对象

让我们回到 Angular 应用程序的架构:

在上图中,控制器 C1 包含视图 V1 的所有事件处理程序,以及该视图的模型 M1。 事件处理程序可能需要一个或多个 [6] 服务来完成其工作。我们将所有这些服务作为参数传递给控制器构造函数:

['S1', 'S2', ..., 'Sn', function(S1, S2, ..., Sn)]

Si服务是单例。Angular会创建它们的唯一实例。它们通过Si名称进行标识。为什么它们在上表中出现了两次?在运行时,JS脚本会被压缩。在压缩过程中,上表变为:

['S1', 'S2', ..., 'Sn', function(a1, a2, ..., an)]

参数会丢失其名称。而这些名称正是服务名称。因此保留这些名称至关重要。这就是为什么它们作为字符串参数传递给函数。在压缩过程中,字符串不会被修改。 当 Angular 使用新数组构建控制器时,它会将 a1 替换为 S1,将 a2 替换为 S2,……因此参数的顺序至关重要。它必须与函数定义前面的服务顺序一致。

让我们回到控制器 [rdvMedecinsCtrl] 的定义:


  // 控制器
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // 最小日期
        $scope.minDate = new Date();
}]);
  • 第 3-4 行:注入到控制器中的唯一对象是 $scope 对象。这是一个预定义对象,代表与控制器关联的视图的 M 模型。要丰富视图的模型,只需向 $scope 对象添加字段即可;
  • 第 6 行即执行了此操作。创建字段 [minDate],其值为当日日期;

视图 V 通过以下方式利用该模型 M:


<body ng-controller="rdvMedecinsCtrl">
<div class="container">
 ...
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
...
</div>
...
  • 第 1 行:通过属性 [ng-controller],将页面主体与控制器 [rdvMedecinsCtrl] 关联。 这意味着<body>标签内的所有内容都将使用控制器[rdvMedecinsCtrl]来管理其事件并获取其M模型。一个HTML页面可能依赖于多个控制器,这些控制器之间可能相互嵌套,也可能不相互嵌套:
<div id='div1' ng-controller='c1'>
    ...
    <div id='div11' ng-controller='c11'>
    ...
    </div>
    ...
    <div id='div12' ng-controller='c12'>
    ...
    </div>
</div>

上文:

  • [div1] 的内容(第 1-10 行)会显示由控制器 c1 管理的模板 M1。该区域内的标签可以引用控制器 c1 的事件处理程序;
  • [div11] 的内容(第 3-4 行)不仅显示由控制器 c11 管理的模板 M11,还显示模板 M1。 此处存在模板继承关系。该区域的标签既可引用控制器 c11 的事件处理程序,也可引用控制器 c1 的事件处理程序但它们既不能引用控制器 c12 的模板 M12,也不能引用该控制器的事件处理程序。 因为在第3至5行之间,控制器c12尚未被定义;
  • 第7-9行:我们可以采用与之前类似的推理方式;

让我们回到日历代码:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>

属性 [min-date] 被初始化为模板中的值 [minDate]。隐含地,即 [$scope.minDate]。该字段始终在对象 $scope 中查找。

3.7.2. 示例 2:日期本地化

目前该日历对我们几乎没有用处,因为它是英式日历。可以对其进行本地化:

  • 在 [1] 中,我们得到了法语日历;
  • 通过 [2],将其切换为英语;
  • 通过 [3],可切换回英文日历;

我们将页面 [app-11.html] 复制为 [app-12.html],然后对后者进行如下修改:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- 日历-->
    <div class="col-md-4">
      <h4>Calendrier</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- 语言 -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>

修改内容不多。仅在第21至31行添加了语言下拉列表。在第27至28行,我们首次遇到事件处理程序:

  • 第27行:[ng-click]属性是Angular属性,用于指定点击带有该属性的元素时应执行的事件处理程序。在此处,将执行[$scope.setLang('fr')]函数,该函数将日历设置为法语;
  • 第28行:此处将日历切换为英语;
  • 第 35 行:由于控制器中的 JavaScript 代码量较大,我们将它放在一个名为 [rdvmedecins.js] 的文件中;

Angular 通过名为 [ngLocale] 的模块管理视图的本地化。因此,我们的 [rdvmedecins] 模块定义如下:


  // --------------------- Angular 模块
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);

第 2 行,切勿遗漏依赖项,因为 Angular 的错误信息有时不够明确。遗漏依赖项的情况往往特别难以察觉。此处新增了对模块 [ngLocale] 的依赖。

默认情况下,Angular 仅支持日期、数字等具有本地变体的元素的本地化,而不支持文本的国际化。为此,我们将使用 [angular-translate] 库。 本地化管理由 [angular-i18n] 库负责。该库包含的文件数量与日期、数字等元素的语言变体数量相同。

  

对于法语日历,我们将使用文件 [angular-locale_fr-fr.js];对于英语日历,则使用文件 [angular-locale_en-us.js]。让我们以文件 [angular-locale_fr-fr.js] 为例,看看其中包含的内容:


'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
  "DATETIME_FORMATS": {
    "AMPMS": [
      "AM",
      "PM"
    ],
    "DAY": [
      "dimanche",
      "lundi",
      "mardi",
      "mercredi",
      "jeudi",
      "vendredi",
      "samedi"
    ],
    "MONTH": [
      "janvier",
      "f\u00e9vrier",
      "mars",
      "avril",
      "mai",
      "juin",
      "juillet",
      "ao\u00fbt",
      "septembre",
      "octobre",
      "novembre",
      "d\u00e9cembre"
    ],
    "SHORTDAY": [
      "dim.",
      "lun.",
      "mar.",
      "mer.",
      "jeu.",
      "ven.",
      "sam."
    ],
    "SHORTMONTH": [
      "janv.",
      "f\u00e9vr.",
      "mars",
      "avr.",
      "mai",
      "juin",
      "juil.",
      "ao\u00fbt",
      "sept.",
      "oct.",
      "nov.",
      "d\u00e9c."
    ],
    "fullDate": "EEEE d MMMM y",
    "longDate": "d MMMM y",
    "medium": "d MMM y HH:mm:ss",
    "mediumDate": "d MMM y",
    "mediumTime": "HH:mm:ss",
    "short": "dd/MM/yy HH:mm",
    "shortDate": "dd/MM/yy",
    "shortTime": "HH:mm"
  },
  "NUMBER_FORMATS": {
    "CURRENCY_SYM": "\u20ac",
    "DECIMAL_SEP": ",",
    "GROUP_SEP": "\u00a0",
    "PATTERNS": [
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 3,
        "minFrac": 0,
        "minInt": 1,
        "negPre": "-",
        "negSuf": "",
        "posPre": "",
        "posSuf": ""
      },
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 2,
        "minFrac": 2,
        "minInt": 1,
        "negPre": "(",
        "negSuf": "\u00a0\u00a4)",
        "posPre": "",
        "posSuf": "\u00a0\u00a4"
      }
    ]
  },
  "id": "fr-fr",
  "pluralCat": function (n) {  if (n >= 0 && n <= 2 && n != 2) {   return PLURAL_CATEGORY.ONE;  }  return PLURAL_CATEGORY.OTHER;}
});
}]);

其中包含用于生成法语日历的要素:

  • 第 10-18 行:星期表;
  • 第19-32行:月份表;
  • 第33-41行:星期缩写表;
  • 第42-55行:月份缩写表;
  • 第56-63行:日期和时间格式。在第62行可以看到法语日期格式“jj/mm/aa”;
  • 第65-95行:数字格式化信息。此处我们不关注;
  • 第96行:文件的区域设置标识符“fr-fr”(fr-fr:法国法语,fr-ca:加拿大法语,……)

在文件 [angular-locale_en-us.js] 中,内容完全相同,只不过这次针对的是 USA 的英语版本(en-us)。

上面的代码不太容易理解。仔细阅读后,我们会发现这段代码的全部作用是定义第4行中的变量 [$locale]。 通过更改该变量的值,我们可以实现日期、数字、货币等的国际化。有趣的是,Angular并未设计在运行时更改 [$locale] 变量。而是通过导入所需的区域设置文件,一次性将其定义好:


<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>

导入所有所需区域设置文件是毫无意义的,因为正如我们所见,每个文件只做一件事:定义变量 [$locale]。最后导入的文件将生效,此后便无法再更改区域设置。

我在网上搜索过解决此问题的方案,但未果。在此我提出一个方案:[https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]。其思路是将我们需要的不同语言环境放入一个字典中。当需要切换时,我们便可从中获取。 [rdvmedecins.js]的JavaScript代码架构如下:

 

如果去除占用200行的语言环境定义(即上文第15-215行),代码结构其实很简单:

  • 第 6 行:定义模块 [rdvmedecins] 及其依赖项;
  • 第 8-10 行:定义页面的控制器 [rdvMedecinsCtrl];
  • 第 9 行:控制器构造函数接收两个参数:
    • $scope:用于创建视图模板;
    • $locale:用于管理日历本地化的变量。切换语言时需修改此变量;
  • 第13行:模板中的变量[minDate]被初始化为当前日期;
  • 第15行:定义字典[locales]。请注意,这里未写入[$scope.locales]。因为变量[locales]实际上并不属于向视图暴露的模型;
  • 第15-215行:定义了一个字典 {'fr':locale-fr-fr, 'en':locale-en-us}。 值 [locale-fr-fr] 和 [locale-en-us] 分别取自文件 JS、[angular-locale_fr-fr.js] 和 [angular-locale_en-us.js]。 最难的是在这个词典中数不胜数的括号中不要出错……
  • 第 217 行:将变量 $locale 初始化为 locales['fr'],即法语版本的区域设置。 不能简单地写成 [$locale=locales['fr']],因为这会将 locales['fr'] 的地址赋值给 $locale。 必须进行值复制。这可以通过预定义函数 [angular.copy] 来实现;
  • 第219行:模板中的变量[jour]被初始化为当前日期。这将导致日历显示时默认定位于该日期;
  • 第223-230行:定义了在切换语言时调用的事件处理程序。请注意以下语法:
$scope.nom_fonction=function(param1, param2, ...){...}

用于定义一个名为 [nom_fonction] 的事件处理程序,该处理程序接受参数 [param1, param2, ...];

回顾下拉列表中的代码 HTML:


    <!-- 语言 -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
</div>
  • 第 8 行:选择法语将调用 [setLang('fr')];
  • 第 9 行:选择英语将调用 [setLang('en')];
  • 第 3 行:属性 [is-open] 是一个布尔值,用于控制下拉列表的展开(true)或收起(false)。它由视图模型中的变量 [isopen] 初始化;

回到 [rdvmedecins.js] 的代码:

  • 第 225 行:将变量 [$locale] 的值替换为字典 [locales] 中相应的值;
  • 第 227 行:我们提到,当视图 V 的模型 M 发生变化时,视图 V 会自动使用新模型进行刷新。 第225行,我们修改了变量[$locale]的值,但该变量不属于视图V所显示的模型M。必须找到一种方法来修改该模型M,以便日历能够刷新并使用新的区域设置。 在此,我们修改了日历模型中的变量 [jour]。 我们使用一个指向与当前显示日期相同的新指针(new)对其进行初始化。[$scope.jour.getTime()] 表示从 1970 年 1 月 1 日到日历显示日期之间经过的毫秒数。利用这个数值,我们可以重建一个新的日期。 当然,我们会得到相同的日期,日历也将保持在显示的日期上。但 [$scope.jour] 的值(实际上是一个指针)已经发生了变化,日历将自动刷新;
  • 第229行:将模板中变量[isopen]的值设置为false。该变量控制下拉列表的某个属性:

<div class="btn-group" dropdown is-open="isopen">
    <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
    </button>
...
</div>

如上文第1行所示,属性 [is-open] 将变为 false,这将导致下拉列表关闭。

3.7.3. 示例 3:文本国际化

让我们回到日历的本地化问题:

在 [3] 中,我们可以看到日历显示为英文,但文本 [Calendrier, Langues] 并非如此。默认情况下,Angular 并未提供用于消息国际化的工具。 这里我们将使用 [angular-translate] 库(https://github.com/angular-translate/angular-translate)。

我们将开发以下示例:

  • 在 [1] 中,法语视图;
  • [2],即英文视图;

让我们看看国际化所需的配置。脚本 [rdvmedecins.js] 修改如下:


  // --------------------- Angular 模块
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// i18n 配置
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // 法语消息
    $translateProvider.translations("fr", {
      'msg_header': '诊所<br/>Les Médecins Associés',
      'msg_langues': '语言',
      'msg_agenda': '{{titre}} {{prenom}} {{nom}}的日程安排<br/>{{jour}}',
      'msg_calendrier': '日历',
      'msg_jour': '所选日期:',
      'msg_meteo': "今天会下雨..."
    });
    // 英文消息
    $translateProvider.translations("en", {
      'msg_header': 'The Associated Doctors',
      'msg_langues': 'Languages',
      'msg_agenda': "{{标题}} {{名字}} {{姓氏}}的日记<br/> 发布于 {{日期}}",
      'msg_calendrier': '日历',
      'msg_jour': '选定日期:',
      'msg_meteo': '今天会下雨……'
    });
    // 默认语言
    $translateProvider.preferredLanguage("fr");
}]);
  • 第 2 行:第一个修改是添加了一个新的依赖项。应用程序的国际化需要 Angular 模块 [pascalprecht.translate];
  • 第 5-26 行:定义了 [rdvmedecins] 模块中的 [config] 函数。 在启动 Angular 应用程序时,框架会实例化应用程序所需的所有服务,包括 Angular 的预定义服务和用户定义的服务。目前,我们尚未定义任何服务。 应用程序模块中的 [config] 函数会在任何服务实例化之前执行。它可用于定义后续将被实例化的服务的配置信息。在此,将使用 [config] 函数来定义应用程序的国际化消息;
  • 第5行:函数[config]的参数是一个数组['O1', 'O2', ..., 'On', function(O1, O2, ..., On)],其中Oi是一个已知的、由Angular提供的对象。 此处,对象 [$translateProvider] 由模块 [pascalprecht.translate] 提供。[function] 是用于配置应用程序的函数;
  • 第 7-14 行:函数 [$translateProvider.translations] 接受两个参数:
    • 第一个参数是语言键。可以设置任意值。此处,我们为法语翻译设置了 'fr'(第 7 行),为英语翻译设置了 'en'(第 16 行),
    • 第二个参数是翻译列表,采用字典形式 {'cle1':'msg1', 'cle2':'msg2', ...};
  • 第7-14行:法语消息;
  • 第16-23行:英语消息;
  • 第25行:方法[preferredLanguage]用于设置默认语言。其参数即函数[$translateProvider.translations]中作为第一个参数使用的值,因此此处为'fr'(第7行)或'en'(第16行);
  • 请注意,消息分为三类:
    • 不包含参数或 HTML 元素的消息(第 9、11、12 行等),
    • 包含 HTML 元素的消息(第 8、10 行等),
    • 包含参数的消息(第10、19行);

现在我们将 [app-11.html] 复制为 [app-12.html],并进行以下修改:


<div class="container">
  <!-- 包含 HTML 元素的第一个文本 -->
  <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
  <!-- 包含参数的第二段文本 -->
  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
  <!-- 由控制器翻译的第三段文本 -->
  <h3 class="alert alert-danger">{{msg2}}</h3>

  <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- 日历-->
    <div class="col-md-4">
      <h4>{{'msg_calendrier'|translate}}</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- 语言 -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          {{'msg_langues'|translate}}<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
  • 翻译内容位于第 3、5、9、13、23 行;
  • 可以区分三种语法:
    • [translate={{'msg_key'}}]语法(第3行),其中[msg_key]是翻译词典中的一个键。该语法适用于包含或不包含HTML元素的消息,但不适用于包含参数的消息;
    • 语法 [translate={{'msg_key'}} translate-values={{dictionnaire]}}](第 5 行),适用于包含或不包含 HTML 元素且带有参数的消息;
    • 语法 [{{'msg_key'|translate}}](第 9、13、23 行)适用于不包含参数且不包含 HTML 元素的消息;

让我们看看此视图中的不同消息:

ligne
français
anglais
3
诊所<br/>联合医生
联合医生
13
日历
日历
23
语言
Languages
9
所选日期:
所选日期:

现在我们来看第5行:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

请注意,[msg.text] 和 [msg.model] 没有被单引号包围。它们不是字符串,而是模板元素:

  • msg.text:定义了要使用的参数化消息的键;
  • msg.model:是提供参数值的字典;

字段名称 [text, model] 可以是任意名称。在视图的控制器 [rdvMedecinsCtrl] 中,对象 [msg] 定义如下:

Image

  • 第 245 行:对象 [msg] 的定义;
  • 第 245 行:字段 [text] 的值为键 [msg_agenda],该键关联两个值:
    • {{titre}} {{prenom}} {{nom}}的日程<br/>于{{jour}}在法语词典中;
    • {{titre}} {{prenom}} {{nom}}的日记<br/> 于 {{jour}} 在英语词典中;

因此,要显示的消息有四个参数 [titre, prenom, nom, jour];

  • 第245行:字段[model]是一个字典,为这四个参数提供对应值。其中参数[jour]存在处理难题。 我们希望显示该日的完整名称。法语和英语中的名称不同。因此,我们使用已在视图中以 {{ jour | date:'fullDate'}} 形式使用的过滤器 [date]。 在 JavaScript 代码中,可以使用任何过滤器,形式为 $filter('filter')(value, options),其中 $filter 是 Angular 的预定义对象,'filter' 是过滤器的名称;
  • 第 33-34 行:预定义对象 $filter 作为参数传递给控制器,从而可在第 245 行使用它;

让我们回到视图中的另一行代码:


  <!-- 由控制器翻译的第三段文本 -->
<h3 class="alert alert-danger">{{msg2}}</h3>

之前的翻译均通过 [pascalprecht.translate] 模块的属性在视图中完成。我们也可以选择在服务器端进行翻译。此处即采用了这种方式。在控制器中(如上图所示的第 247 行),有以下代码:


$scope.msg2 = $filter('translate')('msg_meteo');

由于“translate”同样是一个过滤器,因此这里使用了与“date”过滤器相同的语法。此处请求的键值消息为“msg_meteo”。

让我们来分析一下语言切换的机制。我们看到,[rdvmedecins]模块的配置函数[config]已将法语设为默认语言(如下第9行):


// i18n 配置
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // 法语消息
    $translateProvider.translations("fr", {...});
    // 英语消息
    $translateProvider.translations("en", {...});
    // 默认语言
    $translateProvider.preferredLanguage("fr");
}]);

此外,需注意默认区域设置同样为法语。在控制器 [rdvmedecins] 的初始化中,我们写道:


// 将区域设置设为法语
angular.copy(locales['fr'], $locale);
  • 第2行:[locales]是我们构建的一个字典;

[pascalprecht.translate] 模块带来的消息国际化与我们实现的日期本地化之间没有任何关联。后者使用了一个名为 $locale 的变量,而该变量并未被 [pascalprecht.translate] 模块所使用。 这两个过程互不干涉。

现在,让我们看看当用户切换语言时会发生什么:

Image

  • 第 251 行:在切换语言时,会调用 [setLang] 函数,并传入两个参数中的一个 ['fr','en'];
  • 第 252-257 行:已作过说明——它们更改了日历中的 [$locale] 变量。这不会影响翻译的语言;
  • 第 259 行:更改翻译语言。使用由模块 [pascalprecht.translate] 提供的对象 [$translate]。为此,需要将其注入控制器:

// 控制器
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {

在上述第 3 和第 4 行中,注入对象 $translate;

  • 函数 [$translate.use(lang)] 的 lang 参数值必须是配置中作为函数 [$translateProvider.translations] 第一参数使用的键之一,即 'fr' 或 'en'。当前情况正是如此;
  • 第 261 行:重新计算 msg2 的值。为什么?在视图中,经过第 259 行执行的语言切换后,所有存在的 [translate] 属性都将被重新评估。 但表达式 {{msg2}} 没有该属性,因此不会被重新计算。所以我们在控制器中计算其新值。这必须在第259行完成语言切换之后进行,以便计算 [msg2] 时使用新语言;

如果仅此而已,我们会发现两个异常:

  1. 在 [1] 中,日期仍显示为法语,而视图的其余部分已切换为英语;
  2. 在 [2] 和 [3] 中,所选日期为 6 月 24 日,而在 [1] 中,日期仍固定为 6 月 20 日;

在寻找解决方案之前,让我们先尝试进行解释。消息 [1] 是通过控制器使用以下代码生成的:


      $scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};

并在视图中通过以下代码显示:


  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

异常情况 [1](日期仍显示为法语,而视图其余部分为英语)似乎表明,虽然在切换语言时会重新评估属性 [translate], 而 [translate-values] 属性却未被重新评估。因此,我们可以在控制器中强制执行该评估:


      // ------------------- 事件管理器
      // 更改语言
      $scope.setLang = function (lang) {
...
        // 更新 msg2
        $scope.msg2 = $filter('translate')('msg_meteo');
        // 并更新消息日期
        $scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};

每次切换语言时,上文第 8 行都会重新计算显示的日期。这确实解决了第一个问题,但未能解决第二个问题(即在日历中选择其他日期时,消息中显示的日期不会改变)。出现这种行为的原因如下。消息是在视图中通过以下代码显示的:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

视图 V 仅在模板 M 发生变化时才会更新。然而在此处,日历中选择新日期会触发一个未被处理的事件,导致模板 [msg] 保持不变,视图因此也不会更新。我们需要在视图中更新日历的定义:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>

在上文中,我们指定点击日历应由函数 [$scope.calendarClick] 处理。该函数如下:

Image

  • 第 267 行:日历点击处理程序;
  • 第269行:通过消息[msg]强制更新显示的日期;

3.7.4. 示例 4:配置服务

让我们回顾一下 Angular 应用程序 JS 的架构:

这里我们将重点探讨“服务”这一概念。这是一个相当宽泛的概念。虽然上文中的 [DAO] 层显然是一个服务,但任何 Angular 对象都可以成为服务:

  • 服务遵循特定的语法规范。它拥有一个名称,Angular 通过该名称识别它;
  • Angular 可以将服务注入到控制器和其他服务中;

我们在 [rdvmedecins] 模块中将要配置的部分服务需要进行配置。由于一个服务可以被注入到另一个服务中,因此我们倾向于在一个名为 [config] 的服务中进行配置,并将该服务注入到需要配置的服务和控制器中。 下面我们将详细说明这一过程。

我们将 [app-13.html] 复制为 [app-14.html],并进行以下修改:


<div class="container">
  <!-- 检查待处理消息 -->
  <label>
    <input type="checkbox" ng-model="waiting.visible">
    <span>Voir le message d'attente</span>
  </label>

  <!-- 待处理消息 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
            {{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
  • 第 3-6 行:一个复选框,用于控制是否显示第 9-15 行的等待提示信息。 该复选框的值被存入视图 V 的模型 M 中的变量 [waiting.visible]。若复选框被选中,该值为 true;否则为 false。这种关联是双向的。 如果我们将变量 [waiting.visible] 设为 true,复选框将被选中。视图 V 与其模型 M 之间存在双向关联;
  • 第9-15行:一个等待提示框,带有取消等待的按钮(第11行);
  • 第9行:只有当变量[waiting.visible]的值为true时,该消息才可见。因此,当勾选第4行的复选框时:
    • 变量 [waiting.visible] 将被赋值为 true(ng-model,第 4 行);
    • 由于模型 M 发生了变化,视图 V 会自动重新计算。此时等待提示信息将被显示出来(ng-show,第 9 行);
    • 当取消勾选第 4 行复选框时,其原理与此类似:等待提示信息被隐藏;
  • 第10行:等待提示信息经过翻译(过滤器 translate);
  • 第 11 行:点击按钮时,将执行方法 [waiting.cancel()](ng-click 属性);
  • 第 12 行:按钮的标签文字已进行翻译;
  • 第 19 行:将应用程序的 JavaScript 代码放入新文件 JS [rdvmedecins-02] 中,以免丢失已编写的代码(这些代码现在需要重新组织);

这将生成以下视图:

  • 在 [1] 中,未勾选该复选框;
  • 在 [2] 中,勾选该复选框;

脚本 [rdvmedecins-02] 是对脚本 [rdvmedecins] 的重组:

Image

  • 第 6 行:应用程序的 [rdvmedecins] 模块;
  • 第 9-10 行:应用程序的配置函数;
  • 第 38-39 行:[config] 服务;
  • 第283-284行:控制器[rdvMedecinsCtrl];

此前,我们在控制器中定义了长度达200行的字典 locales={'fr':..., 'en': ...}。 该字典显然属于配置项,因此将其迁移至第38-39行中的服务 [config]。该服务定义如下:

Image

  • 第 38-39 行:通过 [angular.module] 对象的 [factory] 函数创建了一个服务。 该函数的语法与前面的类似:factory('nom_service', ['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]),其中 Oi 是 Angular 已知的对象名称(预定义或由开发者创建),Angular 将其作为 factory 函数的参数注入。 由于此处函数没有参数,因此使用了同样被接受的更简短语法 factory('nom_service', function (){...}])
  • 第 40 行:函数 [factory] 必须通过返回一个对象来实现该服务。该对象即为服务本身。这就是为什么该函数被称为 factory(对象工厂);

通常,服务代码的形式为:


Angular.module('nom_module')
  .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
     // 服务准备
    ...
     // 返回实现该服务的对象
    return {
         // 字段
        ...
         // 方法
        ...
        }
});
  • 第 6 行:返回一个 JS 对象,该对象可同时包含字段和方法。正是这些方法确保了服务功能;

此处的 [config] 服务仅定义了字段,未定义任何方法。我们将在此放置应用程序中所有可配置的内容:

  • 第 42-47 行:待翻译消息的键;
  • 第 59-62 行:应用程序的 URL;
  • 第 64-69 行:远程 Web 服务的 URL;
  • 第 71 行:向未响应的 Web 服务发起 HTTP 调用,可能需要较长时间。 此处将 Web 服务的最大响应等待时间设为 1 秒。超过此时间,HTTP 调用将失败并抛出 JS 异常;
  • 第73行:在每次调用服务器之前,将模拟一次等待,其持续时间在此以毫秒为单位设定。 等待时间为0表示不进行等待。该应用程序将设计为允许用户取消已发起的操作。为了能够取消操作,该操作必须至少持续几秒钟。我们将利用这种人工等待来模拟耗时较长的操作;
  • 第75行:在[debug=true]模式下,当前视图中会显示补充信息。默认情况下,此模式处于启用状态。在生产环境中,应将此字段设置为false
  • 第77-278行:'fr'和'en'两种语言环境的词典。该词典此前位于控制器[rdvMedecinsCtrl]中;

随着该服务的引入,控制器 [rdvMedecinsCtrl] 的结构如下所示:

Image

  • 第 284-285 行:服务 [config] 被注入到控制器中;
  • 第 290 行:字典 [locales] 现位于服务 [config] 中,而非控制器中;
  • 第 294 行:控制等待消息显示的 [waiting] 对象。 等待消息的键位于服务 [config] 中(text 字段)。默认情况下,等待消息处于隐藏状态(visible 字段)。cancel 字段的值为第 316 行函数的名称。因此,该字段是一个方法或函数;
  • 第 316 行:函数 [cancel] 为私有(未编写 $scope.cancel=function(){})。让我们回到取消按钮的代码:

<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">

当用户点击取消按钮时,会调用方法 [$scope.waiting.cancel()]。 最终执行的是第 316 行中的私有函数 cancel。它仅通过将模型变量 [waiting.visible] 设为 false(第 318 行)来隐藏等待提示;

3.7.5. 示例 5:异步编程

现在我们将介绍一项新服务,并引入一个新概念——异步编程。

我们的应用程序将包含三个服务:

  • [config]:即我们刚刚介绍过的配置服务;
  • [utils]:一个提供实用方法的服务。我们将介绍其中的两个;
  • [dao]:用于访问预约 Web 服务的服务。我们将在后续介绍;

我们将编写以下应用程序:

  • 该应用程序的作用是在由 [1] 设定的时长内显示 [2] 横幅。等待状态可通过 [3] 取消。

我们将 [app-01.html] 复制到 [app-15.html] 中,并按以下方式修改代码:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">

  <!-- 待处理消息 -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- 表单 -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">Exécuter</button>
  </div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
  • 第11行:[ng-cloak]属性会在该区域的Angular表达式计算完成之前阻止其显示。这可以避免在评估[ng-show]属性(该属性实际上会导致该区域被隐藏)之前,该区域短暂显示;
  • 第 22 行:用户的输入(等待时间)将被存储在 [waiting.time] 模型中(ng-model 属性);
  • 第 28 行:页面使用了一个新的脚本 [rdvmedecins-03];

脚本 [rdvmedecins-03] 内容如下:

Image

  • 第 6 行:管理该应用程序的 Angular 模块;
  • 第 10 行:用于消息国际化的 [config] 函数;
  • 第 41 行:我们之前描述过的 [config] 服务;
  • 第 286 行:我们将要构建的 [utils] 服务;
  • 第 315 行:我们将要构建的控制器 [rdvmedecinsCtrl];

我们在函数 [config] 中添加了一个新的消息键(第 6、11 行):


angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // 法语消息
    $translateProvider.translations("fr", {
...
      'msg_waiting_time_text': "等待时间:"
    });
    // 英语消息
    $translateProvider.translations("en", {
...
      'msg_waiting_time_text': "等待时间:"
    });
    // 默认语言
    $translateProvider.preferredLanguage("fr");
}]);

我们在服务 [config] 中为该消息键添加了一行(第 6 行):


angular.module("rdvmedecins")
  .factory('config', function () {
    return {
      // 待国际化的消息
      ...
waitingTimeText: 'msg_waiting_time_text',

服务 [utils] 包含两个方法(第 4、12 行):


angular.module("rdvmedecins")
  .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
    // 显示对象的 JSON 表示形式
    function debug(message, data) {
      if (config.debug) {
        var text = data ? message + " : " + angular.toJson(data) : message;
        console.log(text);
      }
    }

    // 等待
    function waitForSomeTime(milliseconds) {
      // 以毫秒为单位的异步等待
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // 返回任务
      return task;
    };

    // 服务实例
    return {
      debug: debug,
      waitForSomeTime: waitForSomeTime
    }
}]);
  • 第 2 行:该服务名为 [utils](第 1 个参数)。它依赖于三个服务,包括两个预定义的 Angular 服务 $timeout、$q 以及 config 服务。 服务 [$timeout] 用于在经过一定时间后执行一个函数。服务 [$q] 用于创建异步任务;
  • 第 4 行:本地函数 [debug];
  • 第 12 行:一个本地函数 [waitForSomeTime]
  • 第 23-26 行:服务 [utils] 的实例。这是一个提供两个方法(即第 4 行和第 12 行中的方法)的对象。请注意,该对象的字段可以命名为任意名称。为了保持一致性,我们将其命名为它们所引用的函数名称;
  • 第 4-9 行:方法 [debug] 将消息 [message] 写入控制台,并可能输出对象 [data] 的表示形式 JSON。 这使得可以显示任意复杂度的对象;
  • 第 12-20 行:方法 [waitForSomeTime] 创建了一个持续 [milliseconds] 毫秒的异步任务;
  • 第 14 行:使用预定义对象 [$q](https://docs.angularjs.org/api/ng/service/$q)创建任务。 以下是 Angular 文档中名为 [deferred] 的任务对应的 API:

Image

  • 通过指令 [$q.defer()] 创建了一个异步任务 [task];
  • 可通过以下两种方法之一将其终止:
    • [task.resolve(value)]:成功结束任务,并将值 [value] 返回给等待任务结束的调用方;
    • [task.reject(value)]:该指令将任务终止并返回错误,同时向等待任务结束的对象返回值 [value];

任务 [task] 可以定期向等待其结束的用户提供信息:

    • [task.notify(value)]:向等待任务结束的对象发送值 [value]。任务继续执行;

希望等待任务结束的用户可使用该任务的 [promise] 字段:

var promise=[task].promise ;

对象 [promise] 具有以下 API(http://www.frangular.com/2012/12/api-promise-angularjs.html):

Image

为了同时处理任务的成功和失败,应编写如下代码:

1
2
3
var promise=[task].promise;
promise.then(successCallback, errorCallBack);
promise['finally'](finallyCallback);
  • 第1行:获取任务的Promise;
  • 第 2 行:定义成功或失败时要执行的函数。可以不设置失败函数。 函数 [successCallback] 仅在任务 [task] 成功完成(即 [task.resolve()] 成功执行)后才会被执行。 函数 [errorCallBack] 仅在任务 [task] 执行失败(即 [task.reject()] 失败)后才会执行。
  • 第3行:定义在前两个函数中的任一个执行完毕后需执行的函数。此处放置两个函数共有的代码[successCallback, errorCallBack]。

让我们回到函数 [waitForSomeTime] 的代码:


    // 等待
    function waitForSomeTime(milliseconds) {
      // 异步等待(毫秒)
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // 返回任务
      return task;
};
  • 第 4 行:创建一个任务;
  • 第5-7行:[$timeout]对象用于定义一个函数(第一个参数),该函数将在以毫秒为单位的指定延迟时间(第二个参数)后执行。 此处,函数 [$timeout] 的第二个参数即为方法的参数(第 1 行);
  • 第6行:在[milliseconds]的延迟时间结束后,任务成功完成;
  • 第 9 行:返回任务 [task]。需注意,第 9 行是在定义对象 [$timeout] 之后立即执行的。 不会等待 [milliseconds] 超时结束。因此,第 2-10 行的代码将在两个不同时间点执行:
    • 第一次是在定义对象 [$timeout] 时;
    • 第二次是在延迟 [milliseconds] 结束时;

这便形成了一个异步函数:其结果是在执行之后才获得的。

调用服务 [config] 的控制器代码如下:


// 控制器
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
    function ($scope, utils, config, $filter) {
      // ------------------- 模型初始化
      // 等待消息
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      // 等待任务
      var task;
      // 日志
      utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
      utils.debug("locales['fr']=", config.locales['fr']);

      // 操作执行
      $scope.execute = function () {
        // 日志
        utils.debug('début', new Date());
        // 显示等待消息
        $scope.waiting.visible = true;
        // 模拟等待
        task = utils.waitForSomeTime($scope.waiting.time);
        // 等待结束
        task.promise.then(function () {
          // 成功
          utils.debug('fin', new Date());
        }, function () {
          // 失败
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // 所有情况下的等待结束
          $scope.waiting.visible = false;
        });

      };

      // 取消等待
      function cancel() {
        // 任务结束
        task.reject();
      }
    }]);
  • 第3行:控制器使用服务[config];
  • 第7行:在对象[$scope.waiting]中添加了字段[time]。对象[$scope.waiting.time]接收用户设定的超时值;
  • 第 8 行:视图显示的等待消息的键被放置在 [$scope.waitingTimeText] 模板中。通常,视图 V 显示的所有内容都应放置在 [$scope] 对象中;
  • 第 10 行:一个局部变量。该变量不向视图 V 公开;
  • 第12-13行:调用服务[config]中的方法[debug]。控制台输出如下结果:
libellé temps d'attente : "Temps d'attente : "
locales['fr']= : {"DATETIME_FORMATS":{"AMPMS":["AM","PM"],"DAY":["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],"MONTH":["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],"SHORTDAY":["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],"SHORTMONTH":["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],"fullDate":"EEEE d MMMM y","longDate":"d MMMM y","medium":"d MMM y HH:mm:ss","mediumDate":"d MMM y","mediumTime":"HH:mm:ss","short":"dd/MM/yy HH:mm","shortDate":"dd/MM/yy","shortTime":"HH:mm"},"NUMBER_FORMATS":{"CURRENCY_SYM":"","DECIMAL_SEP":",","GROUP_SEP":" ","PATTERNS":[{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":3,"minFrac":0,"minInt":1,"negPre":"-","negSuf":"","posPre":"","posSuf":""},{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":2,"minFrac":2,"minInt":1,"negPre":"(","negSuf":" ¤)","posPre":"","posSuf":" ¤"}]},"id":"fr-fr"}

第 2 行,获取对象 *locales[&#x27;fr&#x27;]* 的标识符 JSON

  • 第 16 行:用户点击按钮时执行的方法 [Executer];
  • 第18行:显示方法的开始执行时间;
  • 第 22 行:启动任务 [waitForSomeTime]。不等待其结束。执行继续进行到接下来的第 24 行;
  • 第24-30行:定义任务成功结束时(第26行)和发生错误时(第29行)需执行的函数;
  • 第 26 行:显示方法执行结束的时间;
  • 第29行:显示操作已被取消。这仅在用户点击按钮[Annuler]时触发。随后第41行的语句将以失败代码终止该异步任务;
  • 第31-34行:定义在前两个函数中的任一函数执行完毕后需执行的函数;

理解该代码的执行顺序至关重要。若用户设置了3秒延迟且未取消等待:

  • 当用户点击按钮 [Exécuter] 时,函数 [$scope.execute] 开始执行。 第16至34行代码会在不等待3秒的情况下立即执行。执行完毕后,视图V与模型M同步。此时显示等待提示(ng-show=$scope.waiting.visible=true,第20行),表单被隐藏(ng-hide=$scope.waiting.visible=true,第20行);
  • 此时用户可以再次与视图进行交互。特别是,用户可以点击按钮 [Annuler];
  • 若用户未进行操作,3秒后将执行[$timeout]函数(参见下文第5-7行):

    // 等待
    function waitForSomeTime(milliseconds) {
      // 异步等待 毫秒 毫秒
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // 返回任务
      return task;
};
  • 因此,3秒后代码将被执行。该代码将任务[task]以成功代码(resolve)结束。这将触发所有等待该任务结束的代码的执行(见下文第4行):

        // 模拟等待
        task = utils.waitForSomeTime($scope.waiting.time);
        // 等待结束
        task.promise.then(function () {
          // 成功
          utils.debug('fin', new Date());
        }, function () {
          // 失败
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // 所有情况下的等待结束
          $scope.waiting.visible = false;
        });

  • 因此,上文第6行(成功结束)将被执行。随后将执行第11-14行。该代码执行完毕后,将返回视图V,并将其与模型M同步。等待提示被隐藏(ng-show=$scope.waiting.visible=false,第13行),表单被显示(ng-hide=$scope.waiting.visible=false,第13行);

此时屏幕显示如下:

début : "2014-06-23T15:05:58.480Z"
fin : "2014-06-23T15:06:01.481Z"

如上所示,等待开始与结束之间有3秒的延迟(06:01-05:58)。反之,如果用户在3秒内取消等待,则显示如下:

début : "2014-06-23T15:08:09.564Z"
Opération annulée

最后,需要明确的是,在任何时刻都只有一个执行线程,即UI(用户界面)线程。异步任务的结束会通过事件触发,这与点击按钮的情况完全相同。该事件不会被立即处理。 它会被放入等待执行的事件队列中。轮到它时,才会被处理。该处理使用UI线程,因此在此期间,界面会冻结。它不会响应用户的操作。因此,事件处理必须快速。 由于每个事件均由 UI 线程处理,因此无需解决同时运行的线程之间的同步问题。在任何时刻,只有 UI 线程在运行。

3.7.6. 示例 6:HTTP 服务

现在我们介绍与 Web 服务器通信的 [dao] 服务:

3.7.6.1. 视图 V

我们将编写一个表单来查询医生列表:

Image

我们将 [app-01.html] 复制为 [app-16.html],然后按以下方式进行修改:


<div class="container" ng-cloak="">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待消息 -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- 请求 -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <div class="form-group">
      <label for="urlServer">{{urlServerLabel | translate}}</label>
      <input type="text" id="urlServer" ng-model="server.url"/>
    </div>
    <div class="form-group">
      <label for="login">{{loginLabel | translate}}</label>
      <input type="text" id="login" ng-model="server.login"/>
    </div>
    <div class="form-group">
      <label for="password">{{passwordLabel | translate}}</label>
      <input type="password" id="password" ng-model="server.password"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
  </div>

  <!-- 医生列表 -->
  <div class="alert alert-success" ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
  </div>

</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
  • 第 13-31 行:实现表单。当显示等待信息时,该表单不可见(ng-hide="waiting.visible")。 需注意,这四个输入字段的数据存储在(ng-model 属性)[waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)] 中;
  • 第34-39行:显示医生列表。该列表并非始终可见(ng-show="medecins.show")。
  • 第35行:对之前出现的语法 的一种替代方案;
  • 第 36 行:一个无序列表;
  • 第 37 行:医生列表将来自 [medecins.data] 模板。Angular 指令 [ng-repeat] 用于遍历列表。 语法 ng-repeat="medecin in medecins.data" 要求针对列表 [medecins.data] 的每个元素重复 <li> 标签。列表中的当前元素称为 [medecin];
  • 第 37 行:对于每个 <li>,写入由变量 [medecin] 指定的当前医生的头衔、名字和姓氏;
  • 第42-47行:显示错误列表。该列表并非总是可见(ng-show="errors.show")。其显示方式与医生列表的显示方式相同。 通常,要显示对象列表,会使用 Angular 指令 [ng-repeat];
  • 第 51 行:JavaScript 代码现已移至 [rdvmedecins-04] 文件中

3.7.6.2. C控制器和M模型

JavaScript 代码的演变如下:

Image

  • 第 6-9 行:模块 [rdvmedecins] 声明依赖于由库 [angular-base64] 提供的模块 [base64],而该库是项目的依赖项之一。 该模块用于将发送至 Web 服务用于身份验证的字符串 [login:password] 编码为 Base64;
  • 第 12-13 行:初始化函数,其中包含我们的国际化消息。出现了新的消息,此处不再赘述;
  • 第 69-70 行:[config] 服务,用于配置我们的应用程序。其中添加了新的消息键。此处不再赘述;
  • 第 318-319 行:[utils] 服务,其中包含实用方法。其中添加了新的方法。我们将介绍它们;
  • 第 385-386 行:负责与 Web 服务交互的 [dao] 服务。我们将重点关注该服务;
  • 第 467-468 行:我们刚才介绍的 V 视图的 C 控制器。我们将现在介绍它,因为它是响应用户请求的“指挥家”;

3.7.6.3. C控制器

控制器的代码如下:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- 模板初始化
      // 模板
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // 异步任务
      var task;

      // 执行操作
      $scope.execute = function () {
        // 正在更新 UI
        $scope.waiting.visible = true;
        $scope.medecins.show = false;
        $scope.errors.show = false;
        // 模拟等待
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // 等待
        promise = promise.then(function () {
          // 请求医生列表;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
        });
        // 分析前一次调用的结果
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // 结果={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // 将获取的数据放入模型中
            $scope.medecins.data = result.data;
            // 更新 UI
            $scope.medecins.show = true;
            $scope.waiting.visible = false;
          } else {
            // 获取医生列表时出现错误
            $scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // 更新 UI
            $scope.waiting.visible = false;
          }
        });
      };

      // 等待取消
      function cancel() {
        // 任务已完成
        task.reject();
        // 正在更新 UI
        $scope.waiting.visible = false;
        $scope.medecins.show = false;
        $scope.errors.show = false;
      }

    }
  ])
;
  • 第 2 行:控制器新增了一项依赖,即对服务 [dao] 的依赖;
  • 第6-13行:视图V的模型M被初始化,以供首次显示;
  • 第 8 行:将使用 [$scope.server] 获取表单 V 的四项信息中的三项,第四项存储在 [$scope.waiting.time] 中(第 6 行);
  • 第9行:[$scope.medecins]将收集显示医生列表所需的信息:

  <!-- 医生列表 -->
  <div class="alert alert-success"  ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
</div>

[medecins.title] 属性将作为横幅的标题。该属性在 [config] 服务中定义。 属性 [medecins.show] 将控制横幅是否显示(属性 ng-show="medecins.show")。 [medecins.model] 属性是一个空字典,且将保持为空。它仅用于说明第 3 行中使用的翻译变体。[medecins.data] 属性尚未定义,该属性将包含医生列表(第 5 行)。

  • 第10行:[$scope.errors]将收集显示错误列表所需的信息:

  <!-- 错误列表 -->
  <div class="alert alert-danger"  ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
</div>

属性 [errors.title] 将作为横幅的标题。它在服务 [config] 中定义。属性 [errors.show] 将控制横幅是否显示(属性 ng-show="errors.show")。 属性 [errors.model] 是一个空字典,且将保持为空。它仅用于说明第 3 行中使用的翻译变体。尚未定义的属性 [errors.messages] 将包含待显示的错误消息列表(第 5 行)。

  • 第 16 行:异步任务。控制器将依次启动两个异步任务。这些连续任务的引用将存入变量 [task] 中。这将允许在第 55 行取消它们;
  • 第 19 行:用户点击按钮时执行的方法 [Liste des médecins]:

    <button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
  • 第21-23行:更新视觉界面:显示等待提示,隐藏其余所有内容;
  • 第25行:创建等待的异步任务。在用户在表单中输入的时间结束时,将收到一个信号(任务完成);
  • 第26行:获取异步任务的Promise。启动任务的程序将通过该Promise进行操作。但必须保留任务本身的引用,以便后续取消任务(第55行);
  • 第28-32行:定义等待结束后的处理逻辑;
  • 第30行:使用方法[dao.getData]来启动一个新的异步任务。向其传递所需的信息:
    • Web 服务 [$scope.server.url] 的根任务 URL,例如 [http://localhost:8080];
    • 用于身份验证的登录名 [$scope.server.login],例如 [admin];
    • 用于登录的密码 [$scope.server.password],例如 [admin];
    • URL 用于提供所请求的服务 [config.urlSvrMedecins],此处为 [/getAllMedecins]。因此完整的 URL 将为 [http://localhost:8080/getAllMedecins];

方法 [dao.getData] 的返回结果可能有两种形式:

  • (续)
    • {err: 0, data: [med1, med2, ...]} 其中 [medi] 是一个表示医生的对象(头衔、名字、姓氏),
    • {err: n, messages: [msg1, msg2, ...]} 其中 [msgi] 是一个错误消息,且 n 不为 0;
  • 第 31 行:返回任务的 Promise。这里有几点需要理解。我们有两个 Promise:
    • promise.then():返回第一个 Promise [promise1];
    • return task.promise:返回第二个 Promise [promise2];
    • 最终,promise=promise.then(...; return task.promise) 是一个由两个 [promise2.promise1] 组成的承诺链。 [promise1] 仅在获取到 [promise2] 承诺时才会被求值,即当任务 [dao.getData] 完成时。 [promise1] 不依赖于任何异步任务。因此它将立即被获取;
  • 第 34-50 行:根据前面的解释,这些行只有在任务 [dao.getData] 完成后才会执行。 传递给第 34 行函数的参数 [result] 由方法 [dao.getData] 构建,并通过操作 [task.resolve(result)] 传递给调用方,其中 [result] 具有以下形式:
    • {err: 0, data: [med1, med2, ...]},其中 [medi] 是一个表示医生的对象(头衔、名字、姓氏),
    • {err: n, messages: [msg1, msg2, ...]},其中 [msgi] 是一个错误消息,且 n 不为 0;
  • 第37行:检查错误代码 [result.err];
  • 第38-42行:如果没有错误(result.err==0),则获取医生列表并显示;
  • 第44-47行:反之,若出现错误(result.err != 0),则获取错误消息列表并显示;
  • 第53-56行:只要这两项异步操作尚未完成,带有取消按钮的等待提示框就会一直显示。让我们看看根据取消操作发生的时间点,会发生什么情况:
    • 首先需要理解的是,第19-50行代码会一次性执行完毕。此时仅启动了一个异步任务,即第25行的任务,
    • 首次执行后,视图 V 被更新,因此等待横幅及其取消按钮可见。如果用户在第 25 行任务完成前取消等待,则执行第 53 行的方法,并以失败状态取消该任务(第 55 行);
    • 第56-59行:界面被更新:表单重新显示,其余内容均被隐藏,
    • 随后返回 V 视图,浏览器将处理下一个事件。由于任务已完成,获取了该任务的 Promise,从而触发了一个事件。该事件随后被处理;
    • 随后执行第28-32行。由于未定义失败情况的处理函数,因此没有代码被执行。此时获得一个新的 Promise,该 Promise 依然由 [promise.then] 返回且始终可获取,
    • 事件处理完毕后,程序返回视图 V,浏览器将处理下一个事件。由于第 28 行的 [promise] 已被处理,第 34 行的 [promise] 将被解析,从而触发一个新事件。该事件随后被处理;
    • 随后第34至49行将依次执行,因为第34行使用的 Promise 已成功获取。同样,由于未定义失败情况的处理函数,因此没有代码被执行,
    • 于是到达第50行。此时不再有任务等待,新的视图V被显示出来;
    • 现在假设在第二个异步任务 [dao.getData] 执行期间发生了取消。上述推理同样适用。任务结束将导致第 34-50 行以任务失败的方式执行。 我们很快会发现,方法 [dao.getData] 会向 Web 服务发起一次异步调用 HTTP。该调用不会被取消,但其结果也不会被利用。

理解视图 V 的显示与浏览器事件处理之间这种持续的往返过程至关重要。事件由用户(点击)或系统操作(如异步操作结束)触发。 浏览器的静止状态即视图 V 的显示。当某个事件发生并被处理时,浏览器便会从该静止状态中被唤醒。一旦事件处理完毕,浏览器便会恢复到静止状态。如果被处理的事件修改了其模型 M,视图 V 就会随之更新。随后,下一个事件又会将浏览器从静止状态中唤醒。

所有操作均在单一线程中进行。两个事件绝不会同时处理,其执行是顺序进行的。只有当前一个事件完全处理完毕(通常是已处理完毕)并释放控制权后,浏览器才会转到下一个事件。

还有一点需要说明。为了显示错误消息,我们编写如下代码:


$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};

消息列表由 [utils] 服务中定义的 [utils.getErrors] 方法提供。该方法如下:


// 分析服务器响应中的错误JSON
    function getErrors(data) {
      // 数据 {err:n, messages:[]}, err!=0
      // 错误
      var errors = [];
      // 错误代码
      var err = data.err;
      switch (err) {
        case 2 :
          // 未授权
          errors.push('not_authorized');
          break;
        case 3 :
          // 禁止
          errors.push('forbidden');
          break;
        case 4 :
          // 本地错误
          errors.push('not_http_error');
          break;
        case 6 :
          // 文档未找到
          errors.push('not_found');
          break;
        default :
          // 其他情况
          errors = data.messages;
          break;

      }
      // 如果没有消息,则添加一条
      if (! errors || errors.length == 0) {
        errors=['error_unknown'];
      }
      // 返回错误列表
      return errors;
    }
  • 第 2-3 行:接收到的 [data] 参数是一个具有两个属性的对象:
    • [err]:一个错误代码;
    • [messages]:一条消息列表;
  • 第 5 行:我们将构建一个错误消息数组。这些消息已实现国际化。 因此,除第27行外,我们放入数组的不是消息本身,而是其国际化键。在第27行,我们使用参数[data]的属性[messages]。这些消息是真正的消息,而非消息键。 然而,视图 V 会将其视为消息键,因此无法找到这些键。在此情况下,模块 [translate] 会显示其未找到的消息键,即此处的真实消息。这是预期的结果;
  • 第 32-34 行:处理 [data.messages] 第 27 行等于 null 的情况。这种情况在编写的 Web 服务中会出现。本应避免这种情况。

3.7.6.4. 服务 [dao]

服务 [dao] 负责与 Web 服务 / JSON 进行 HTTP 协议的交互。其代码如下:


angular.module("rdvmedecins")
  .factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
    function ($http, $q, config, $base64, utils) {

      // 日志
      utils.debug("[dao] init");

      // ----------------------------------私有方法
      // 从Web服务获取数据
      function getData(serverUrl, username, password, urlAction, info) {
        // 异步操作
        var task = $q.defer();
        // URL 请求HTTP
        var url = serverUrl + urlAction;
        // 基本身份验证
        var basic = "Basic " + $base64.encode(username + ":" + password);
        // 响应
        var réponse;
        // 所有 HTTP 请求都必须经过身份验证
        var headers = $http.defaults.headers.common;
        headers.Authorization = basic;
        // 执行请求 HTTP
        var promise;
        if (info) {
          promise = $http.post(url, info, {timeout: config.timeout});
        } else {
          promise = $http.get(url, {timeout: config.timeout});
        }
        promise.then(success, failure);
        // 返回任务本身以便将其取消
        return task;

        // 成功
        function success(response) {
          // response.data={status:0, data:[med1, med2, ...]} 或 {status:x, data:[msg1, msg2, ...]
          utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
          // 响应
          var payLoad = response.data;
          réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
          // 返回响应
          task.resolve(réponse);
        }

        // 失败
        function failure(response) {
          utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
          // 分析状态
          var status = response.status;
          var error;
          switch (status) {
            case 401 :
              // 未授权
              error = 2;
              break;
            case 403:
              // 禁止
              error = 3;
              break;
            case 404:
              // 未找到
              error = 6;
              break;
            case 0:
              // 本地错误
              error = 4;
              break;
            default:
              // 其他
              error = 5;
          }
          // 正在返回响应
          task.resolve({err: error, messages: [response.statusText]});
        }
      }

      // --------------------- 服务实例 [dao]
      return {
        getData: getData
      }
}]);
  • 第 77-79 行:该服务仅有一个字段:方法 [getData],用于从 Web 服务 / JSON 获取信息;
  • 第2行:出现了一个我们尚未见过的依赖项[$http]。这是Angular的一个预定义服务,用于与远程实体进行HTTP通信;
  • 第 6 行:一条日志,用于查看代码在应用程序生命周期的哪个阶段被执行;
  • 第 10 行:方法 [getData] 接受五个参数:
    • [serverUrl]:Web 服务的根路径(http://localhost:8080);
    • [urlAction]:所请求的特定服务的路径(/getAllMedecins);
    • [username]:用户登录名;
    • [password]:其密码;
    • [info]:当通过 POST 操作请求所请求的特定服务的 URL 时,用于收集补充信息的对象。 对于 URL(/getAllMedecins),该参数未被传递。因此,它即为 [undefined];
  • 第12行:创建一个异步任务;
  • 第14行:请求的服务(http://localhost:8080/getAllMedecins)的完整URL;
  • 第 16 行:通过发送以下 HTTP 头部进行身份验证:
Authorization:Basic code

其中 [code] 是字符串 [username:password] 的 Base64 编码;

第 16 行构建了 HTTP 标头中的 [Basic code] 部分;

  • 第 18 行:Web 服务的响应;
  • 第 20 行:Angular 在 HTTP 请求中默认发送的 HTTP 头部在 [$http.defaults.headers.common] 对象中定义。 [Authorization:Basic code] 头部不在此列;
  • 第 21 行:将其添加到需系统性发送的 HTTP 头部中。赋值表达式的左侧是待初始化的 [Authorization] 头部,右侧是该头部的值,此处即第 16 行定义的值。因此,如果写为:
headers.Authorization = 'x';

Angular 将发送 HTTP 头部:

Authorization : x
  • 第 23 行:[$http] 服务的方法返回 Promise。该 Promise 将存储在变量 [promise] 中;
  • 第 27 行:由于此处的参数 [info] 的值为 [undefined],因此执行的是第 27 行。 通过 GET 请求 URL(http://localhost:8080/getAllMedecins)。 为避免等待过久,需设置获取服务器响应的最大等待时间(超时)。默认超时时间为一秒;
  • 第29行:定义在获得承诺时要执行的两个方法:
    • [success]:定义在第34行,是任务成功时获取承诺后应执行的方法;
    • [failure]:定义在第 45 行,是任务失败时获取 Promise 后需执行的方法;
    • 这两个方法(更准确地说应为函数)定义在 [getData] 函数内部。这在 JavaScript 中是可行的。在 [getData] 中定义的变量在两个内部函数 [success, failure] 中均可访问;
  • 第 31 行:返回第 12 行创建的任务。此处需注意调用代码:

        promise = promise.then(function () {
          // 请求医生列表;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
});

上文第3行,确实获取到了一个任务。

  • 第34行:函数[success]将在稍后执行,即当调用HTTP成功结束时。这种“成功”的概念与响应HTTP的第一行相关。该响应的形式为:
HTTP/1.1 code texte

代码是一个三位数字,用于指示调用是否成功。大致来说,2xx 和 3xx 代码表示成功,其余代码则表示失败。文本是一段简短的说明文字。以下是两种可能的响应,一种表示成功,另一种表示失败:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
  • 第36行:在控制台显示服务器的响应。在错误[404 Not Found]中,会得到类似以下内容:

[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}

在此响应中,我们仅使用字段 [data]、[status] 和 [statusText]。

  • 第 38 行:从响应中提取字段 [data]。该字段将呈现以下其中一种形式:
    • {status: 0, data: [med1, med2, ...]},其中 [medi] 是一个表示医生的对象(头衔、名字、姓氏),
    • {status: n, data: [msg1, msg2, ...]},其中 [msgi] 是一条错误消息,且 n 不为 0;

Image

  • 第 39 行:构建响应 {0,data} 或 {n,messages}。第一个响应包含 [data] 字段中的医生信息。 第二个响应报告了服务器端发生的错误。服务器处理了该错误,在 [err] 中生成错误代码,并在 [data] 中生成错误消息列表。 在这两种情况下,它都会返回状态码 HTTP 200,表示任务 HTTP 已完全处理完毕。因此,这两种情况都在同一个函数 [success] 中处理;
  • 第41行:任务已完成([task.resolve]),并返回以下两种响应之一:
    • {err: 0, data: [med1, med2, ...]},其中 [medi] 是一个表示医生的对象(头衔、名字、姓氏),
    • {err: n, messages: [msg1, msg2, ...]},其中 [msgi] 是一个错误消息,且 n 不为 0;

必须将此代码与控制器调用方代码中获取该响应的方式关联起来:


        // 分析前一次调用的结果
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          ...
          }

[task.resolve(réponse)] 的响应在上面已存储在变量 [result] 中。

  • 第 45 行:当异步任务以失败告终时,将调用函数 [failure]。可能出现两种情况:
    • 服务器通过返回非 2xx 或 3xx 的状态码来报告此失败,
    • Angular 会取消调用 HTTP。此时不会发生调用。 存在 Angular 异常,但服务器未返回 HTTP 错误代码。例如,当提供一个无法调用的无效 URL 时即属此情况;
  • 第 46 行:将响应显示在控制台上;
  • 第 48 行:回顾服务器响应的格式:

{"data":"...","status":404,"config":{...},"statusText":"Not Found"}

第 48 行,提取上述的 [status] 属性;

  • 第50-70行:基于错误代码HTTP,生成一个新的错误代码,以向调用方隐藏方法[dao.getData]的真实性质HTTP。 可以验证,在使用该方法的控制器中,没有任何迹象表明该方法中存在对 HTTP 的调用;
    • 第 51 行:错误 [401] 表示身份验证失败(例如密码错误),
    • 第55行:错误代码[403]表示调用未获授权。用户虽已正确认证,但其权限不足以请求其所请求的URL。 此情况将发生在用户 [user / user] 身上。该用户虽存在于数据库中,但无权使用该应用程序。仅用户 [admin / admin] 拥有此权限;
    • 第59行:错误[404]对应于未找到的URL。该错误可能由多种原因引起:
      • 用户在服务 URL 的输入中出现错误;
      • Web 服务未启动;
      • Web 服务响应速度过慢(默认超时时间为 1 秒);
    • 第 63 行:错误代码 HTTP 0 不存在。 当前情况是,由于用户输入的 URL 无效且无法调用,因此 Angular 未执行所请求的 HTTP 调用。 接下来我们将遇到其他情况,Angular 也会因此不执行所请求的 HTTP 调用;
  • 第 72 行:任务成功完成(task.resolve),并返回 {err, messages} 类型的响应,其中数组 [messages] 仅包含一条消息 [response.statusText]。 如果 Angular 未执行请求的 HTTP 调用,则返回空字符串;

现在我们已经对应用程序有了全面且详细的了解,可以开始进行测试了。

3.7.6.5. 应用程序测试 - 1

让我们从有效的输入开始:

Image

  • 在 [1] 中,输入 0 以避免等待;
  • 在 [2] 中,尽管输入正确,却出现了错误信息。我们尚未展示各种错误信息。[2] 显示的是一条与错误代码 0 相关的通用信息,该错误对应于 Angular 异常。 Angular 遇到问题,导致无法执行 HTTP 调用。遇到这种情况时,需要查看 JavaScript 控制台日志。有两种方法可以做到这一点:
    • 在 Chrome 浏览器中执行 [F12];
    • 使用 WebStorm 的控制台;

在 WebStorm 控制台中,我们会发现各种消息,其中包括以下内容:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins。请求的资源上未包含 'Access-Control-Allow-Origin' 标头。因此,源 'http://localhost:63342' 被拒绝访问。
[dao] getData[/getAllMedecins] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
  • 第 1 行:Angular 报告了一个错误,我们稍后会再讨论;
  • 第 2 行:方法 [dao.getData] 的日志。其中包含一些值得注意的信息:
    • [status] 的值为 0,这表明并未调用 HTTP。因此 [statusText] 为空,
    • [url] 等于 [http://localhost:8080/getAllMedecins],这是正确的;
    • HTTP 作为 [Authorization":"Basic YWRtaW46YWRtaW4=] 的认证头也是正确的;

那么,为什么没能成功呢?日志中的关键信息是 [No 'Access-Control-Allow-Origin' header is present]。要理解这一点,需要进行一番详细说明。首先让我们回顾一下客户端/服务器应用程序的总体架构:

Image

  • Angular 应用程序中的页面 HTML / CSS / JS 来自服务器 [1];
  • 在 [2] 中,服务 [dao] 向另一台服务器 [2] 发起请求。然而,运行 Angular 应用程序的浏览器会禁止这种操作,因为这属于安全漏洞。 该应用只能向其来源服务器发起请求,即服务器 [1];

实际上,说浏览器禁止 Angular 应用程序向 [2] 服务器发起请求并不准确。它实际上是向该服务器发送请求,询问其是否允许非本源的客户端向其发起请求。 这种共享技术被称为 CORS(跨源资源共享)。[2] 服务器通过发送特定的 HTTP 头部来表示同意。 正因为我们的服务器[2]未发送这些头部,浏览器才拒绝执行应用程序请求的HTTP调用。

现在让我们深入探讨细节。 让我们分析 HTTP 调用过程中发生的网络通信。为此,在 Chrome 浏览器中,我们按 [F12] 调出开发者工具,并选择 [Network] 选项卡查看网络通信:

  • 在 [1] 页面中,我们选择 [network] 标签页;
  • 在 [2] 中,我们查询医生列表;

在 [network] 选项卡中,我们获得以下信息:

  • 在 [1] 中,显示发送至服务器的信息;
  • 在 [2] 中,显示服务器的响应;

在 [1] 中可以看到,浏览器针对所请求的 URL 发送了 HTTP [OPTIONS] 请求。 [OPTIONS]是HTTP可能的命令之一,其中[GET]和[POST]更为常见。 该命令用于向服务器查询信息,特别是其支持的 HTTP 选项,因此得名。 服务器以 [2] 格式进行响应。为了表明其接受来自非本域客户端的请求,它必须返回一个名为 [Access-Control-Allow-Origin] 的特定标头。 正是由于服务器未返回该标头,Angular 才未执行所请求的 HTTP 调用,并返回了以下错误:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins。请求的资源上未包含 'Access-Control-Allow-Origin' 头。因此,源 'http://localhost:63342' 被拒绝访问。

因此,我们需要修改服务器,使其发送预期的 HTTP 头部。

3.7.6.6. 修改 Web 服务器 / JSON

我们回到 Eclipse 环境。为了保留现有成果,我们将当前的 Web 服务器版本 / JSON [rdvmedecins-webapi-v2] 复制为 [rdvmedecins-webapi-v3] [1]:

我们在 [ApplicationModel] 中进行首次修改,该文件是 Web 服务配置文件之一:


package rdvmedecins.web.models;

...

@Component
public class ApplicationModel implements IMetier {

    // [métier] 层
    @Autowired
    private IMetier métier;

    // 来自 [métier] 层的数据
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // 配置数据
    private boolean CORSneeded = true;

...

    public boolean isCORSneeded() {
        return CORSneeded;
    }

}
  • 第 17 行:我们创建一个布尔值,用于指示是否接受服务器域外的客户端;
  • 第21-23行:访问该信息的处理方法;

随后,我们创建了一个新的 Spring 控制器 MVC [3]:

[RdvMedecinsCorsController] 类的定义如下:


package rdvmedecins.web.controllers;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import rdvmedecins.web.models.ApplicationModel;

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // 向客户端发送选项
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 设置头信息 CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
        }

    }

    // 医生列表
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • 第 28-31 行:定义了一个控制器,用于在通过命令 HTTP [OPTIONS] 调用时处理 URL [/getAllMedecins];
  • 第29行:方法[getAllMedecins]接受[HttpServletResponse]对象作为参数,该对象将被发送给发起请求的客户端。该对象由Spring注入;
  • 第 30 行:将请求的处理委托给第 19-25 行中的私有方法;
  • 第 15-16 行:注入对象 [ApplicationModel];
  • 第20-23行:如果服务器配置为接受其域外的客户端,则发送HTTP头部:

Access-Control-Allow-Origin: *

这意味着服务器接受来自任何域(*)的客户端。

现在我们可以进行新的测试了。我们启动了Web服务的新版本,发现问题依然存在。没有任何改变。如果在上文第30行添加控制台输出,该输出从未显示,这表明第29行的[getAllMedecins]方法从未被调用。

经过一番调查,我们发现Spring MVC会自行处理HTTP和[OPTIONS]命令,并采用默认处理机制。 因此,响应始终由 Spring 处理,第 29 行中的 [getAllMedecins] 方法从未被调用。Spring MVC 的这种默认行为是可以更改的。我们引入一个新的配置类,用于配置新的行为:

  

新的配置类 [WebConfig] 如下所示:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    // 为标头配置dispatcherservlet CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
}
  • 第 8 行:该类是一个 Spring 配置类。它声明了将被放置在 Spring 上下文中的 Bean;
  • 第 12 行:Bean [dispatcherServlet] 用于定义处理客户端请求的 Servlet。其类型为 [DispatcherServlet]。该 Servlet 通常默认创建。如果由我们自行创建,则可以对其进行配置;
  • 第 14 行:创建一个类型为 [DispatcherServlet] 的实例;
  • 第 15 行:要求该 Servlet 将 HTTP 和 [OPTIONS] 命令转发给应用程序;
  • 第 16 行:将 Servlet 配置为上述状态;

接下来我们需要修改 [AppConfig] 类:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {

}
  • 第 11 行:导入新的配置类 [WebConfig];

3.7.6.7. 应用程序测试 - 2

我们启动新版 Web 服务 / JSON,并尝试使用 Angular 客户端获取医生列表。我们在 [Network] 选项卡中查看网络通信:

  • 在 [1] 中,可以看到服务器响应中现已包含 HTTP [Access-Control-Allow-Origin: *] 头部。然而,它仍然无法正常工作。 我们在 [2] 中查看控制台日志,发现以下日志:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins。请求头字段 Authorization 未被 Access-Control-Allow-Headers 允许

可以看出,浏览器正在等待一个新的标头 HTTP [Access-Control-Allow-Headers],该标头将告知其有权接收身份验证标头:

Authorization:Basic code

这可能是个好兆头。Angular 可能本想发送指令 HTTP GET。但由于该指令附带了身份验证头,因此它询问服务器是否接受该头。

我们将 Web 服务器 / JSON 进行修改以发送该标头。[RdvMedecinsCorsController] 类演变为如下形式:


    // 将选项发送给客户端
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 设置 CORS 头部
            response.addHeader("Access-Control-Allow-Origin", "*");
            // 允许 [Authorization] 标头
            response.addHeader("Access-Control-Allow-Headers", "Authorization");            
}
  • 第 6-7 行添加了缺失的头部。

我们重启服务器,并使用 Angular 客户端再次请求医生列表:

 

这次成功了。控制台日志显示了通过 [dao.getData] 方法收到的响应:


[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}

可以看出:

  • 服务器返回了错误代码 [status=200],附带消息 [statusText=OK]。这就是我们当前位于函数 [success] 中的原因;
  • 服务器返回了一个名为 [data] 的对象,其中包含两个字段:
    • [status]:(请勿与错误代码 HTTP [status] 混淆)。 此处的 [status=0] 表示 URL 和 [/getAllMedecins] 已成功处理;
    • [data]:其中包含医生列表 JSON;

现在我们来看其他一些有趣的案例:

[login, password] 的标识符有误:

使用 [user / user] 身份登录,该用户无权访问该应用程序(仅 [admin] 有权访问):

这次,错误不再是 [Erreur d'authentification],而是 [Accès refusé]。

3.7.7. 示例 7:客户列表

我们沿用前一个应用程序,这次将客户列表显示在 [Bootstrap select] 类型的下拉列表中(参见第 3.6.6 节)。

3.7.7.1. 视图 V

初始视图如下:

 

为了生成视图 V,我们将代码 [app-16.html] 复制为 [app-17.html],并按以下方式进行修改:


<div class="container" >
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待消息 -->
  <div class="alert alert-warning" ng-show="waiting.visible" >
...
  </div>

  <!-- 请求 -->
  <div class="alert alert-info" ng-hide="waiting.visible" >
...
    <button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
  </div>

  <!-- 客户列表 -->
  <div class="row" style="margin-top: 20px" ng-show="clients.show">
    <div class="col-md-3">
      <h2 translate="{{clients.title}}"></h2>
      <select data-style="btn-primary" class="selectpicker">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
      </select>
    </div>
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger"  ng-show="errors.show">
   ...
  </div>

</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
  • 第 5-7 行:等待条保持不变;
  • 第10-13行:表单保持不变,仅按钮文字(第12行)有所调整;
  • 第 28-30 行:错误提示条保持不变;
  • 第 16-25 行:客户列表通过 [Bootstrap-selectpicker] 组件进行样式设置(data-styleclass 属性,第 19 行);
  • 第20行:使用指令[ng-repeat]生成下拉列表的各项选项。 需注意,选项的标签类型为 [Mme Julienne Tatou],而选项的值类型为 [100],其中 100 是所显示客户的 id 标识符;
  • 第 34 行:JavaScript 代码迁移至新文件 [rdvmedecins-05];

3.7.7.2. C控制器与M模型

文件 [rdvmedecins-05] 中的 JavaScript 代码通过复制文件 [rdvmedecins-04] 获得:

Image

几乎没有任何变化,只有控制器进行了调整,现在用于提供客户列表:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- 模板初始化
      // 模板
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.clients = {title: config.listClients, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // 异步任务
      var task;

      // 执行操作
      $scope.execute = function () {
        // 正在更新 UI
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // 模拟等待
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // 等待
        promise = promise.then(function () {
          // 请求客户列表;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
          return task.promise;
        });
        // 分析前一次调用的结果
        promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // 将获取的数据放入模型
            $scope.clients.data = result.data;
            // 更新 UI
            $scope.clients.show = true;
            $scope.waiting.visible = false;
            // 对下拉列表进行样式设置
            $('.selectpicker').selectpicker();
          } else {
            // 获取客户列表时出现错误
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // 更新 UI
            $scope.waiting.visible = false;
          }
        });
      };

      // 等待取消
      function cancel() {
        // 任务已完成
        task.reject();
        // 正在更新 UI
        $scope.waiting.visible = false;
        $scope.clients.show = false;
        $scope.errors.show = false;
      }
    }
  ])
;
  • 控制器中的改动非常少。它原先提供医生列表,现在则提供客户列表;
  • 第 9 行:[$scope.clients] 将作为视图 V 中客户横幅的模板;
  • 第 30 行:现在使用的是 URL 和 [/getAllClients];
  • 第35-36行:[dao.getData]方法返回的两种响应形式。现在显示的是客户而非医生;
  • 第44行:在Angular代码中较为罕见的语句。此处直接操作DOM(文档对象模型)。此处旨在将方法[selectpicker] (属于 [bootstrap-select.min.js])应用于 DOM 中具有类 [selectpicker] [$('.selectpicker')] 的元素。只有一个,即下拉列表:

      <select data-style="btn-primary" class="selectpicker" select-enable="">
....
      </select>

在第 3.6.6 节中,已展示这会将下拉列表样式化为如下形式:

与针对医生的处理方式一样,我们也需要修改该Web服务。

3.7.7.3. Web 服务修改 - 1

  

类 [RdvMedecinsController] 新增了一个方法:


package rdvmedecins.web.controllers;

...

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // 向客户端发送选项
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 设置标头 CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // 授权该标头 [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");
        }

    }

    // 医生列表
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }

    // 客户列表
    @RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
    public void getAllClients(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • 第29-32行:方法[getAllClients]将处理浏览器发送的请求HTTP和[OPTIONS];

3.7.7.4. 应用程序测试 – 1

现在我们可以进行测试了。启动Web服务器,然后在Angular表单中输入有效值。我们得到以下响应:

Image

当 Angular 无法执行请求的 HTTP 请求时,会显示此错误信息。此时需在控制台日志中查找原因。日志中会显示以下信息:

XMLHttpRequest cannot load http://localhost:8080/getAllClients。请求的资源上未包含 'Access-Control-Allow-Origin' 标头。因此,源 'http://localhost:63342' 被禁止访问。

一个原本以为已解决的问题。接下来我们将查看发生的网络通信:

Image

可见,采用方法 HTTP 和 [OPTIONS] 的操作 [getAllClients]已成功执行,但操作 [getAllClients] 及其方法 HTTP、[GET] 已被取消。对请求 [OPTIONS] 的响应如下:

Image

CORS 的 HTTP 报头已存在。现在让我们查看 GET 期间的 HTTP 交换:

Image

HTTP 请求看起来是正确的。特别是可以看到身份验证头。

除了之前的错误信息外,控制台日志中还出现了以下信息:


[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}

这是方法 [dao.getData] 在收到其请求 HTTP 的响应时系统生成的日志。可以注意到两点:

  • [status=0]:这表示是 Angular 取消了请求 HTTP;
  • [method=GET]:表示被取消的是请求 GET;

结合第一条消息来看,这意味着对于请求 GET,Angular 同样在此处等待请求头 CORS。 但目前,我们的Web服务仅针对请求HTTP和[OPTIONS]发送这些头部信息。现在出现这个错误,而医生列表却没有出现,这非常奇怪。我无法解释。

因此必须再次修改Web服务。

3.7.7.5. Web服务修改 – 2

  

方法 [GET] 和 [POST] 在类 [RdvMedecinsController] 中处理。我们需要修改该类,以便这些方法发送 CORS 头部。具体操作如下:


@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;

    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;

...

    // 客户列表
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients(HttpServletResponse response) {
        // 标头CORS
        rdvMedecinsCorsController.getAllClients(response);
        // 应用程序状态
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // 客户列表
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
    }
...
  • 第 8 行:我们希望复用之前放置在控制器 [RdvMedecinsCorsController] 中的代码。因此,我们将该代码注入此处;
  • 第 14 行:处理请求的方法 [GET /getAllClients]。我们进行了两处修改:
    • 第 14 行:我们将对象 [HttpServletResponse] 注入到方法参数中,
    • 第 16 行:使用 [RdvMedecinsCorsController] 类的方法,将 CORS 的头部信息填入该对象中;

3.7.7.6. 应用程序测试 – 2

我们启动新版Web服务,并再次请求客户列表。我们得到以下响应:

  • 在 [1] 中,确实有响应但内容为空 [2];
  • 在 [3] 中:网络通信顺利完成;

在控制台日志中,方法 [dao.getData] 显示了其收到的响应:


[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"} 

因此该方法确实收到了客户端列表。在验证代码后,我们开始怀疑下一条指令,因为我们对其掌握得不太透彻:


// 下拉列表样式
$('.selectpicker').selectpicker();

我们将第 2 行注释掉并重新尝试。随后得到以下响应:

因此我们已定位到问题所在。问题出在将方法 [selectpicker] 应用于下拉列表时。查看出错页面的源代码,可见如下内容:

  • 发现,在 [1] 中,下拉列表及其选项确实存在,但并未显示 [style='display:none'];
  • 而在 [2] 中,可以看到 [bootstrap select] 按钮已显示。 下拉列表的选项本应出现在 <ul role='menu'> 列表中。但它们并未出现在该列表中,因此列表为空。看来在将方法 [selectpicker] 应用于下拉列表时,其内容当时是空的;

在网上搜索解决方案时,找到了这个方法。我们将代码:


// 为下拉列表设置样式
$('.selectpicker').selectpicker();

替换为以下代码:


            // 为下拉列表设置样式
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

样式 [bootstrap-select] 是通过函数 [$timeout] 应用的。 我们之前已经遇到过这个函数,它允许在经过一定延迟后执行某个函数。在此处,未指定延迟即视为延迟为零。前面的几行代码将一个事件放入浏览器的事件队列中。当当前事件(点击按钮 [Liste des clients])的处理即将结束时,视图 V 将会显示。 紧接着,浏览器将检查其事件队列。由于该事件的延迟为零,事件 [$timeout] 将位于队列首位并被处理。随后,样式 [bootstrap-select] 将应用于一个已填充的下拉列表。让我们看看结果:

如果再次查看显示页面的源代码,我们会看到以下内容:

之前为空的按钮 [bootstrap-select] 现在已包含客户列表。

3.7.7.7. 使用指令

我们在视图 V 的 C 控制器中发现了以下代码:


            // 为下拉列表设置样式
            $('.selectpicker').selectpicker();

这里操作了一个 DOM 对象。许多 Angular 开发者对在控制器代码中操作 DOM 感到不适。在他们看来,此类操作应在指令中进行。 Angular 指令可视为 HTML 语言的扩展。因此,可以创建新的 HTML 元素或属性。让我们来看一个简单的示例:

我们创建如下 JS [selectEnable] 文件:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        var selectpicker = $('.selectpicker');
        selectpicker.selectpicker();
      });
    }
  };
}]);
  • 该指令遵循我们已熟悉的控制器语法:

angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)

该指令属于模块 [rvmedecins]。这是一个接受两个参数的函数:

  • (续)
    • 第一个是指令名称 [selectEnable];
    • 第二个参数是一个数组 ['obj1','obj2',..., function(obj1, obj2,...)],其中 [obj] 表示要注入到该函数中的对象。此处唯一被注入的对象是预定义对象 [$timeout];
  • 函数 [directive] 返回一个可能具有多种属性的对象。此处唯一的属性是 [link](第 3 行)。其值是一个接受三个参数的函数:
    • scope:使用该指令的视图模板;
    • element:视图中的元素,即指令的作用对象;
    • attrs:该元素的属性;

举个例子。指令 [selectEnable] 可在以下上下文中使用:

<div select-enable="data"></div>

在上文中,[select-enable] 属性将指令 [selectEnable] 应用于元素 HTML <div>。 可以通过添加 [do-something] 属性,将 [doSomething] 指令应用于任何 HTML 元素。 需注意指令名称与其关联属性的写法差异。写法将从 [camelCase] 变为 [camel-case]。

指令 [selectEnable] 也可按以下方式使用:

<select-enable attr1='val1' attr2='val2' ...>...</select-enable>

在此,指令 [doSomething] 以标签 HTML <do-something> 的形式应用。

让我们回到以下写法

<div select-enable="data"></div>

以及该指令中 [link] 函数的三个参数:

  • scope:是包含 <div> 的视图模板;
  • element:即 <div> 标签本身;
  • attrs:是 <div> 的属性数组。这些属性可用于向指令传递信息。在上文中,我们将写成 attrs['selectEnable'] 以获取信息 [data]。 请注意,为了表示属性 [select-enable],写法已更改为 [selectEnable];

让我们回到指令的代码:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
    }
  };
}]);
  • 第 14-16 行:这里出现了我们之前放置在控制器中的代码。当显示视图 V 时,若遇到指令 [select-enable](以元素或属性形式存在),该代码即被执行。

要实现该指令,我们将文件 [app-17.html] 复制为 [app-17B.html],并按以下方式进行修改:


      <select data-style="btn-primary" class="selectpicker" select-enable="">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>
  • 第 1 行:将指令 [selectEnable] 应用于元素 HTML [select]。 由于无需向该指令传递任何信息,我们只需写入 [select-enable=""];

我们还通过将文件 JS [rdvmedecins-05.js] 复制为 [rdvmedecins-05B.js] 来修改控制器,并在指令文件 [selectEnable.js] 中引用新文件 JS002519ZQX 文件中引用新文件 JS,并在指令文件 [selectEnable.js] 中进行引用。请务必注意这一点。如果指令文件缺失,[select-enable=""] 属性将无法被处理,但 Angular 不会报错。


<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>

在文件 JS [rdvmedecins-05B.js] 中,我们从控制器中删除以下几行:


            // 设置下拉列表样式
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

因为该操作现在由指令负责。

3.7.7.8. 应用程序测试 – 3

测试新应用程序 [app-17B.html] 时,得到以下结果:

  • 在 [1] 中,得到的是一个空列表。

控制台日志显示如下内容:

1
2
3
[dao] init
directive selectEnable
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • 第 1 行:初始化 [dao] 服务;
  • 第 2 行:在首次显示视图 V 时,执行指令 [selectEnable];
  • 第 3 行:当用户点击按钮 [Liste des clients] 时,此行出现。由此可见,指令 [selectEnable] 并未被第二次执行。最终,该指令是在客户列表为空时执行的,因此下拉列表为空;

换言之, 了以下操作:


$('.selectpicker').selectpicker();

并未在正确的时间点执行。我们可以尝试通过多种方式解决此问题。经过多次测试未果后,我们意识到上述操作仅需执行一次,且仅在下拉列表已填充时执行。为实现这一结果,我们将 <select> 标签重写如下:


      <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>

第 1 行,只有当 [clients.data] 存在时,才会生成 <select> 标签。而在视图 V 的初始显示时,该条件并不成立。因此,<select> 标签不会被生成,[selectEnable] 指令也不会被评估。 当用户点击按钮 [Liste des clients] 时,[clients.data] 在模板 M 中将获得新值。由于模板 M 已发生变化,标签 <select> 将被重新评估并在此处生成。 因此,指令 [selectEnable] 也将被评估。在评估该指令时,<select> 标签的第 2-4 行尚未被评估。因此,客户列表为空。如果将指令 [selectEnable] 编写如下:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $('.selectpicker').selectpicker();
    }
  }
}]);

第 5 行将使用空列表执行,因此显示的下拉列表也将为空。因此应写为:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $timeout(function () {
        $('.selectpicker').selectpicker();
      })
    }
  }
}]);

才能获得预期结果。由于第5行中的[$timeout],第6行将在视图V完全评估后才执行,即当<select>标签包含所有元素时。

3.7.8. 示例 8:医生的日程表

现在我们介绍一个显示医生日程表的应用程序。

3.7.8.1. 应用程序的视图 V

我们将展示以下表单:

  • 在 [1] 中,查询 PELISSIER 女士的日程表,时间是 2014 年 6 月 25 日 [2];

得到以下结果:

我们将分别研究这两个视图。

3.7.8.2. 表单

我们将文件 [app-17.html] 复制为 [app-18.html],然后按以下方式修改代码:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- 请求 -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="row" style="margin-bottom: 20px">
      <div class="col-md-3">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
      <div class="col-md-3">
        <h2 translate="{{calendar.title}}"></h2>
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
                      class="well well-sm"></datepicker>
        </div>
      </div>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!-- 日程表 -->
  <div id="agenda" ng-show="agenda.show">
...
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
  • 第5-7行:等待提示信息保持不变;
  • 第12-19行:[bootstrap select]类型的医生列表;
  • 第20-26行:我们之前介绍过的[ui-bootstrap]日历。请注意,所选日期被放入[calendar.jour]模板中(ng-model属性);
  • 第28行:调用日历的按钮;
  • 第32-34行:错误列表保持不变;
  • 第37-39行:日历(我们将在后续介绍);
  • 第 42 行:通过复制 [rdvmedecins-05.js] 文件,将代码 JS 转移到 [rdvmedecins-06.js] 文件中;

3.7.8.3. 控制器 C

应用程序中的代码 JS 变为如下:

Image

只有服务 [utils] 和控制器 [rdvMedecinsCtrl] 会受到这些更改的影响。

控制器 [rdvMedecinsCtrl] 变为:


// 控制器
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- 模板初始化
      // 模板
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', 登录名: 'admin', 密码: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins};
      $scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
      $scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
      // 为下拉列表设置样式
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
      // 日历的法语区域设置
      angular.copy(config.locales['fr'], $locale);
 ...
    }
  ])
;
  • 第 7 行:在调用 HTTP 之前设置 3 秒的等待时间;
  • 第8行:硬编码了连接HTTP所需的参数;
  • 第10-17行:硬编码定义医生列表;
  • 第18行:模板[agenda]配置视图中的日程表显示;
  • 第19行:模板[calendar]配置视图中的日历显示。将最小日期[minDate]设为今天,并将当前日期也设为今天;
  • 第 21-23 行:下拉列表采用之前介绍的方法进行样式设置;
  • 第25行:将应用程序的区域设置设为'fr'。默认情况下,该设置为'en';

调用日历时执行的方法如下:


// 执行操作
      $scope.execute = function () {
        // 表单信息
        var idMedecin = $('.selectpicker').selectpicker('val');

        // 验证
        utils.debug("[homeCtrl] idMedecin", idMedecin);
        utils.debug("[homeCtrl] jour", $scope.calendar.jour);

        // 将日期格式设置为 yyyy-MM-dd
        var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
        // 更新视图
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        $scope.agenda.show = false;
...
      };
  • 第 4 行:获取所选医生的 [value] 属性。此处再次使用了来自文件 [bootstrap-select.min.js] 的 [selectpicker] 方法。需注意下拉列表选项的格式:

          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}

因此,该选项的值(value属性)即为医生的标识符[id]。

  • 第 11 行:将用户选择的日期转换为 [aaaa-mm-jj] 格式,这是 Web 服务器所期望的日期格式;
  • 第13-15行:当方法[execute]执行完毕后,将显示加载条并隐藏其余所有内容;

代码后续如下:


// 模拟等待
        var task = utils.waitForSomeTime($scope.waiting.time);
        // 查询医生日程
        var promise = task.promise.then(function () {
          // 服务路径
          var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
          // 请求日程表
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
          // 返回任务完成承诺
          return task.promise;
        });
        // 分析服务调用结果 [dao]
        promise.then(function (result) {
          // 等待结束
          $scope.waiting.visible = false;
          // 错误?
          if (result.err == 0) {
            // 准备日程表模板
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // 格式化时间表的显示
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
            // 创建一个事件,用于在视图显示后对表格进行样式设置
            $timeout(function () {
              $("#creneaux").footable();
            });
          } else {
            // 获取日程表时出现错误
            $scope.errors = {
              title: config.getAgendaErrors,
              messages: utils.getErrors(result),
              show: true
            };
}
  • 第 2 行:3 秒的异步等待任务;
  • 第5-10行:该等待结束时将执行的代码;
  • 第6行:构建被调用的URL(即[/getAgendaMedecinJour/1/2014-06-25]);
  • 第8行:查询URL。启动一个异步任务;
  • 第 10 行:返回该异步任务的承诺;
  • 第14-38行:当调用HTTP返回响应时将执行的代码;
  • 第 13 行:[result] 是 [dao.getData] 方法返回的响应。此处需注意 Web 服务器的响应格式:

第19行的参数[result.data]即为上文中的属性[data] [1]。 该属性又包含上文提到的 [creneauxMedecin] [2] 属性。这是一个时间段数组,每个时间段包含以下两项信息:

  • [rv]:该时段内已安排的约会(格式为 JSON),若该时段未安排约会,则为 [null];
  • [hDeb, mDeb, hFin, mFin]:该时段的时间信息;

回到调度员的代码:

  • 第15行:等待状态结束;
  • 第19行:调用控制日程表显示的[$scope.agenda]模板;
  • 第20行:日历被显示出来;
  • 第22-24行:遍历刚才提到的[creneauxMedecin]数组中的每个C元素;
  • 第23行:每个C元素都有一个[creneau]属性,即时间段。该属性被赋予了一个[text]属性,该属性将以[10h20:10h40]的形式呈现时间段的文本描述;
  • 第26-28行:我们将用于显示日程表时段的表格HTML设置为“响应式”。我们在第3.6.7节中已介绍过这一概念;
 
  • 第27行:要使表格具备响应式布局,需对其应用[footable]方法。此处遇到了与组件[bootstrap-select]相同的难题。若仅照搬第17行的代码,会发现表格并未实现响应式布局。 此问题可通过调用函数 [$timeout](第 26 行)以相同方式解决;
  • 第31-34行:当调用HTTP失败时,将显示错误信息;

3.7.8.4. 日历显示

现在我们回到文件 [app-18.html] 中的日程表代码。代码如下:


<!-- 日程表 -->
  <div id="agenda" ng-show="agenda.show">
    <!-- 医生无就诊时段的情况 -->
    <h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
        translate="agenda_medecinsanscreneaux"></h4>
    <!-- 医生日程表 -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table creneaux-table id="creneaux" class="table">
          <thead>
          <tr>
            <th data-toggle="true">
              <span translate="agenda_creneauhoraire"></span>
            </th>
            <th>
              <span translate="agenda_client">Client</span>
            </th>
            <th data-hide="phone">
              <span translate="agenda_action">Action</span>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
            <td>
            <span
              ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
              {{creneauMedecin.creneau.text}}
            </span>
            </td>
            <td>
              <span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
            </td>
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
</div>
  • 第4-5行:回顾一下,[agenda.data]是日程表,[agenda.data.creneauxMedecin]是一个包含[creneauMedecin]类型对象的数组。 后者类型的每个元素都有一个 [creneauMedecin.creneau] 属性,它是一个时间段。每个时间段有两个我们感兴趣的元素:
    • [creneauMedecin.creneau.rv],即该时间段内可能存在的 RV(rv!=null);
    • [creneauMedecin.creneau.text],即该时间段的文本 [début:fin];
  • 第4行:若医生无可用时段,则显示特殊提示。这种情况虽不常见,但因我们的数据库存在缺失,故可能发生。该提示是否生成(HTML)由指令[ng-if]控制;

Image

指令 [ng-if] 与 [ng-show, ng-hide] 指令不同。后者仅将文档中的某个区域隐藏。若为 [ng-if='false'],则该区域将从文档中移除。此处仅将其用作示例;

  • 第 9 行:[id='creneaux'] 属性非常重要。该指令中使用的正是该属性:

$("#creneaux").footable();
  • 第10-22行:显示表[1]的表头;
  • 第 23-45 行:显示表 [2] 的内容;
  • 第 24 行:遍历数组 [agenda.data.creneauxMedecin];
  • 第26-29行:写入文本[3]。使用指令[ng-class]来生成该元素的[class]属性。 在此,若值为 [creneauMedecin.rv==null],则表示该时段有空位,文本背景设为绿色;否则,背景设为红色;
  • 第 32 行:写入已获取 RV 或 [4] 的客户名称。若为 [rv==null],则该信息不存在,但 Angular 会正确处理此情况且不会报错;
  • 第34-39行:显示两个按钮中的一个:[Réserver] 或 [Supprimer]。是否存在预约决定了选择哪个按钮;

3.7.8.5. 修改 Web 服务器

与前面的示例一样,必须修改Web服务器,以便URL和[/getAgendaMedecinJour]发送CORS的头部信息:

  

在类 [RdvMedecinsCorsController] 中添加一个新方法:


    // 医生日程表
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getAgendaMedecinJour(HttpServletResponse response) {
        sendOptions(response);
}

该方法将为请求 HTTP 和 [OPTIONS] 发送头部 CORS。 在类 [RdvMedecinsController] 中,对于请求 HTTP 和 [GET],也需执行相同操作:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
        // 表头CORS
        rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}

3.7.8.6. 使用指令

与之前一样,我们将把对 DOM 的处理移至指令中。我们对 DOM 有两处处理:

  • 在视图初次显示时:

      // 为下拉列表设置样式
      $timeout(function () {
        $('.selectpicker').selectpicker();
});
  • 在显示日程时:

            // 创建一个事件,在视图显示后对表格进行样式设置
            $timeout(function () {
              $("#creneaux").footable();
});

对于第一种情况,我们将使用之前介绍过的 [selectEnable] 指令。对于第二种情况,我们在以下 JS [footable.js] 文件中创建 [footable] 指令:


angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive footable");
      $timeout(function () {
        $("#creneaux").footable();
      })
    }
  }
}]);

因此,我们采用了与 [selectEnable] 指令相同的技术。

代码 HTML [app-18.html] 被复制到 [app-18B.html] 中。然后按以下方式进行修改:


        <select data-style="btn-primary" class="selectpicker" select-enable="">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
</select>
  • 第 1 行:通过属性 [select-enable] 将指令 [selectEnable] 应用于医生列表的 <select> 标签;

    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
          <thead>
<tr>
  • 第 3 行:将指令 [footable](通过属性 [footable])应用于日程表的 HTML 表;

<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- 指令 -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • 第3-4行:引用两条指令中的JS文件;
  • 第1行:[app-18B.html]中的代码JS,是[app-18.html]中的代码JS在文件[rdvmedecins-06B.js]中的重复项;

文件 [rdvmedecins-06B.js] 与文件 [rdvmedecins-06.js] 除两处细节外完全相同。处理 DOM 的行已消失:


      // 设置下拉列表样式
      $timeout(function () {
        $('.selectpicker').selectpicker();
});

            // 创建事件,在视图显示后对表格进行样式设置
            $timeout(function () {
              $("#creneaux").footable();
});

因此,运行应用程序 [app-18B.html] 所得结果与运行 [app-18.html] 完全相同。

3.7.9. 示例 9:创建和取消预订

现在我们介绍一个用于创建和取消预订的应用程序。

3.7.9.1. 应用程序的视图 V

我们将展示以下表单:

  • 在 [1] 中,可以进行预订。所做的预订将针对一位随机客户;
  • 在 [2] 中,可以删除我们已创建的预订;

我们将文件 [app-18.html] 复制为 [app-19.html],然后按以下方式修改代码:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!-- 日程表 -->
  <div id="agenda" ng-show="agenda.show">
..
    <!-- 医生日程表 -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
...
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active"  ng-click="reserver(creneauMedecin.creneau.id)">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • 第 5-7 行:等待提示信息与上一版本相同;
  • 第10-12行:错误提示信息沿用上一版本;
  • 第15-36行:日程表与上一版本相同,仅两处细节不同:
    • 第26行:点击按钮[réserver](ng-click属性)由视图V中模型M的[reserver]方法处理。向该方法传递预订时段的编号;
    • 第26行:点击按钮[supprimer]由视图V的模型M中的方法[reserver]处理。向该方法传递待删除的预约编号;
  • 第39行:管理该应用程序的代码JS位于文件[rdvmedecins-07.js]中;
  • 第40行:第20行应用的指令[footable]中的代码JS;

3.7.9.2. 控制器 C

[rdvmedecins-07.js] 中的代码 JS 首先通过复制文件 [rdvmedecins-06.js] 获得,随后进行修改。其中仍包含惯常的大段代码块。修改主要集中在控制器中:

Image

我们将分几个步骤描述视图 V 的 C 控制器。

3.7.9.3. C 控制器初始化

控制器初始化代码如下:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- 模板初始化
      // 模板
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', 登录名: 'admin', 密码: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins
      };
      var médecin = $scope.medecins.data[0];
      var clients = [
        {id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
        {id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
        {id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
        {id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
      ];
      // 法语日期格式
      angular.copy(config.locales['fr'], $locale);
      var today = new Date();
      var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
      var fullDay = $filter('date')(today, 'fullDate');
      $scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};


      // ---------------------------------------------------------------- 初始日程表
      // 全局异步任务
      var task;
      // 请求日历
      getAgenda();

      // ------------------------------------------------------------------ 预订
      $scope.reserver = function (creneauId) {
....
      };

      // ------------------------------------------------------------ 删除 RV
      $scope.supprimer = function (idRv) {
...
      };

      // 获取日程表
      function getAgenda() {
 ...
      }

      // 取消等待
      function cancel() {
...
      }
} ]);
  • 第6行:配置等待消息。默认情况下,将等待3秒后才进行调用 HTTP;
  • 第7行:HTTP调用所需的必要信息;
  • 第8行:错误消息配置;
  • 第9-17行:固定医生列表;
  • 第18行:一位私人医生。预约就是为了他的诊疗时段;
  • 第19-24行:固定客户;
  • 第26行:需要处理法国日期;
  • 第27行:预约将以今天日期为准;
  • 第28行:在线预约系统要求日期格式为“yyyy-mm-dd”;
  • 第29行:以[jeudi 26 juin 2014]格式表示的今天日期;
  • 第30行:日历配置。[model]属性承载了即将显示的国际化消息的参数:

        agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
  • 第 35 行:全局变量 [task] 在特定时刻代表正在执行的异步任务;
  • 第 37 行:请求初始日程表;

页面初始加载时仅执行上述操作。若一切顺利,视图将显示 PELISSIER 女士的当日日程。

Image

3.7.9.4. 获取日程表

日程表通过以下方法 [getAgenda] 获取:


      // 获取日程表
      function getAgenda() {
        // 服务路径 URL
        var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
        // 查询日程
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
        // 等待消息
        $scope.waiting.visible = true;
        // 分析调用服务 [dao] 的结果
        task.promise.then(function (result) {
          // 等待结束
          $scope.waiting.visible = false;
          // 错误?
          if (result.err == 0) {
            // 正在准备日程表模板
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // 格式化时间表显示
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
          } else {
            // 获取日程表时出现错误
            $scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
          }
        });
}

该代码即前一个应用程序中研究过的代码。有两处改动:

  • 在调用 HTTP 之前,不再进行模拟等待;
  • 第 4 行:使用了在控制器初始化时创建的医生以及已构建的格式化日期;

该代码已被提取为一个函数,因为函数 [reserver] 和 [supprimer] 也会使用它。

3.7.9.5. 预约时段

需注意,客户是随机选取的。

预订代码如下:


$scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // 正在创建一个 RV,其中包含一个随机客户,该客户位于由 [id] 标识的时间段内
        var idClient = clients[Math.floor(Math.random() * clients.length)].id;
        utils.debug("réservation du créneau pour le client", idClient);
        // 模拟等待
        $scope.waiting.visible = true;
        var task = utils.waitForSomeTime($scope.waiting.time);
        // 添加该时段
        var promise = task.promise.then(function () {
          // 服务路径 URL
          var path = config.urlSvrResaAdd;
          // 要传输给服务的数据
          var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
          // 启动异步任务
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // 返回任务完成承诺
          return task.promise;
        });

        // 分析任务结果
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // 验证结果时出现错误
            $scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
          } else {
            // 请求新的日程表
            getAgenda();
          }
        });

      };
  • 第1行:需注意,函数[reserver]的参数是时段编号(属性id);
  • 第4行:从初始化代码中硬编码定义的客户列表中随机选取一名客户。记录其标识符[id];
  • 第7-8行:等待3秒;
  • 第11-18行:这些行仅在3秒结束后执行;
  • 第12行:预订服务[/ajouterRv]中的URL。该URL与此前遇到的那些有所不同。它在Web服务中定义如下:

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
  • (续)
    • 第 1 行:URL 没有参数,且通过 POST 进行调用;
    • 第2行:提交的参数以JSON对象的形式发送。该对象将被反序列化到[post]参数中(@RequestBody);

我们在第2.12.2节中已经看到过POST的一个示例:

  • 在 [0] 中,即 Web 服务的 URL;
  • 在 [1] 中,使用了 POST 方法;
  • 在 [2] 中,以 {day, JSON, idClient, idCreneau} 形式传输给 Web 服务的文本为 JSON;
  • 在 [3] 中,客户端向 Web 服务明确表示将向其发送 JSON 信息;

让我们回到函数 [reserver] 中的代码 JS:

  • 第 14 行:创建待提交的值,其形式为 JS 对象。当该值被提交时,Angular 会将其序列化为 JSON;
  • 第 16 行:调用 HTTP 函数。待提交的值是 [dao.getData] 函数的最后一个参数。 当该参数存在时,函数 [dao.getData] 会调用 POST 而不是 GET(参见第 3.7.6.4 节的代码);
  • 第 18 行:返回 HTTP 调用的承诺;
  • 第23-29行:仅在HTTP调用返回响应后才会执行;
  • 第23行:参数[result]的形式为[err,data]或[err,messages],其中[err]为错误代码;
  • 第23-26行:若出现错误,则显示错误信息;
  • 第28行:若预订成功,则重新显示新的日程表;

3.7.9.6. 服务器修改

  

在类 [RdvMedecinsCorsController] 中,我们添加以下方法:


    // 向客户发送选项
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 设置标头 CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // 已授权该标头 [authorization]
            response.addHeader("Access-Control-Allow-Headers", "authorization");
        }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
    public void ajouterRv(HttpServletResponse response) {
        sendOptions(response);
}

该方法添加在第10至13行。 第2至8行的头部信息将发送至URL和[/ajouterRv](第10行),以及方法HTTP和[OPTIONS](第10行)。

[RdvMedecinsController]类是否按以下方式修改:


    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
        // CORS 头部
        rdvMedecinsCorsController.ajouterRv(response);
...

对于方法 [POST](第 1 行)以及 URL 和 [/ajouterRv](第 1 行), 将调用我们刚刚在 [RdvMedecinsCorsController] 中添加的方法(第 4 行),因此返回的 HTTP 头部与方法 HTTP [OPTIONS] 相同。

3.7.9.7. Tests

让我们进行一个初步测试,预订一个任意时段:

 

像往常一样,这种情况下需要查看控制台日志:


[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}

方法 [dao.getData] 因 [status=0] 失败,这意味着是 Angular 取消了该请求。日志中显示了错误原因:

XMLHttpRequest cannot load http://localhost:8080/ajouterRv。请求头字段 Content-Type 未被 Access-Control-Allow-Headers 允许。

查看网络流量,发现如下情况:

  • 在 [1] 和 [2] 中:仅有一个请求 HTTP,请求 [OPTIONS];
  • 在 [3] 中,Angular 客户端请求两项授权:
    • 发送请求头 HTTP 和 [accept, authorization, content-type] 的权限;
    • 发送命令 POST 的权限;
  • 在 [4] 中:服务器授权了 [authorization] 头部。需要提醒的是,在服务器端,是我们自己发送了该授权;

因此,新变化在于:在 POST 操作中,Angular 客户端向服务器请求了更多授权。因此需要修改服务器端以授予这些授权:

  

在类 [RdvMedecinsCorsController] 中,我们修改了生成 HTTP 请求头的方法,该请求头用于处理 OPTIONS、 GET 和 POST 命令发送的:


    // 向客户端发送选项
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 设置 CORS 头部
            response.addHeader("Access-Control-Allow-Origin", "*");
            // 允许某些标头
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // 允许 POST
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
}
  • 第 7 行:已为 HTTP 和 [accept, content-type] 头部添加了授权;
  • 第 9 行:已为方法 POST 添加授权;

重启服务器后重新进行测试:

 

这次预订成功了。

3.7.9.8. 删除预约

函数代码 [supprimer] 如下:


$scope.supprimer = function (idRv) {
        utils.debug("suppression rv n°", idRv);
        // 模拟等待
        $scope.waiting.visible = true;
        task = utils.waitForSomeTime($scope.waiting.time);
        // 添加时段
        var promise = task.promise.then(function () {
          // 服务路径
          var path = config.urlSvrResaRemove;
          // 要传输给服务的数据
          var post = {idRv: idRv};
          // 启动异步任务
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // 返回任务完成承诺
          return task.promise;
        });

        // 分析任务结果
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // 删除 rv 时发生错误
            $scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
            // 更新 UI
            $scope.waiting.visible = false;
          } else {
            // 请求新的日程表
            getAgenda();
          }
        });
      };
  • 第 1 行:请注意,该函数的参数是待删除预约的编号。此代码与预订代码非常相似。我们仅对差异部分进行说明;
  • 第9行:该服务中的URL在此处为[/supprimerRV],且同样是通过POST调用的:

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {

该参数在此处仍以 JSON 的形式传输。在第 2.12.17 节中,我们展示了手动生成的 POST 的性质:

  • 在 [1] 中,即 Web 服务的 URL;
  • 转换为 [2],此处采用的是 POST 方法;
  • 在 [3] 中,以 {idRv} 形式传输给 Web 服务的 JSON 文本;
  • 在 [4] 中,客户端向 Web 服务明确表示将向其发送 JSON 信息;

让我们回到函数 [supprimer] 中的代码 JS:

  • 第 11 行:创建 POST 对象。Angular 会自动将其序列化为 JSON;

其余代码与预订功能类似。

3.7.9.9. 服务器端修改

在服务器端,我们进行以下修改:

  

在类 [RdvMedecinsCorsController] 中,我们添加以下方法:


    // 向客户发送选项
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // 正在修复标头CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // 授权某些标头
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // 允许 POST
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
    }
...
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(HttpServletResponse response) {
        sendOptions(response);
}

该方法添加在第 13-16 行。 第2至10行的头部信息将发送至URL [/supprimerRv](第13行)和方法HTTP [OPTIONS](第13行)。

[RdvMedecinsController]类则按以下方式修改:


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
        // CORS 标头
        rdvMedecinsCorsController.supprimerRv(response);
...

对于方法 [POST](第 1 行)以及 URL 和 [/supprimerRv](第 1 行), 将调用我们刚刚在 [RdvMedecinsCorsController] 中添加的方法(第 4 行),因此返回的 HTTP 头部与 HTTP 和 [OPTIONS] 方法相同。

3.7.10. 示例 10:创建和取消预订 - 2

现在我们介绍与之前相同的应用程序,但不再为随机客户预订,而是从下拉列表中选择客户。

3.7.10.1. 应用程序的 V 视图

我们将展示以下表单:

客户将在 [1] 中进行选择。

代码与前一个应用程序类似,因此我们仅介绍主要差异。

我们将文件 [app-19.html] 复制为 [app-20.html],然后编写客户下拉列表的代码 [1]:


<!-- 客户列表 -->
  <div class="alert alert-info">
    <h3>{{agenda.title|translate:agenda.model}}</h3>

    <div class="row" ng-show="clients.show">
      <div class="col-md-3">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  • 第 8-12 行:下拉列表将通过组件 [bootstrap-select] 实现;
  • 第 1 行:通过属性 [select-enable] 应用指令 [selectEnable];
  • 第1行:仅当[clients.data]存在时(# null, undefined)才会生成<select>标签。这一点非常重要,已在第3.7.7.8节中进行过说明;

此外,我们还导入了新的 JS 文件:


<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- 指令 -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • 第 1 行:文件 [rdvmedecins-08.js] 是通过复制文件 [rdvmedecins-0.js] 获得的;
  • 第3-4行:导入两条指令对应的文件;

3.7.10.2. C 控制器

C 控制器代码的演变如下:


// 控制器
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- 模板初始化
...
      // 客户
      $scope.clients = {title: config.listClients, show: false, model: {}};

      //------------------------------------------- 视图初始化
      // 全局异步任务
      var task;
      // 先获取客户,然后获取日程
      getClients().then(function () {
        getAgenda();
      });
...

      // 执行操作
      function getClients() {
....
      };
} ]);
  • 第 8 行:对象 [$scope.clients] 在视图 V 中配置了客户下拉列表;
  • 第14-16行:以异步方式,首先请求客户列表,然后在获取该列表后,请求PELISSIER女士今天的日程安排。 此处的语法之所以能正常工作,是因为函数 [getClients] 返回了一个 Promise;

[getClients]方法用于获取客户列表:


function getClients() {
        // 更新UI
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // 查询客户列表;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // 分析前一次调用的结果
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // 将获取的数据放入模型
            $scope.clients.data = result.data;
            // 更新 UI
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // 获取客户列表时出现错误
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // 更新 UI
            $scope.waiting.visible = false;
          }
        });
        // 履行承诺
        return promise;
      };

这是我们之前已经遇到并讨论过的代码。需要重点注意的是第 31 行:

  • 第27行:返回第10行的 Promise,即代码中获取到的最后一个 Promise。该 Promise 仅在调用 HTTP 并返回响应后才会获得;

[reserver]方法略有变化:


      $scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // 为所选客户创建 RV
        var idClient = $(".selectpicker").selectpicker('val');
        ...
        });
  • 第 4 行:不再为随机客户预留,而是为客户列表中选定的客户预留。

3.7.11. 示例 11:一条 [selectEnable2] 指令

本示例回顾了指令。

3.7.11.1. 视图 V

应用程序显示以下视图:

 

3.7.11.2. 视图的代码 HTML

视图 [app-21.html] 的代码 HTML 如下:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 显示等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
   ...
  </div>

  <!-- 客户列表 -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

  <!-- 医生列表 -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- 说明 -->
<script type="text/javascript" src="selectEnable2.js"></script>
  • 第 19-23 行:客户下拉列表;
  • 第 19 行:应用指令 [selectEnable2](属性 [select-enable2]);
  • 第 19 行:仅当 [clients.data] 不为空时;
  • 第 19 行:下拉列表由属性 [id="selectpickerClients"] 标识;
  • 第 33-37 行:医生下拉列表;
  • 第 33 行:应用指令 [selectEnable2](属性 [select-enable2]);
  • 第33行:仅当[medecins.data]不为空时;
  • 第33行:下拉列表由属性[id="selectpickerMedecins"]标识;
  • 第 43 行:导入新文件 JS [rdvmedecins-09.js];
  • 第 45 行:导入新指令中的文件 JS;

3.7.11.3. 指令 [selectEnable2]

指令 [selectEnable2] 的代码如下:


angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable2 attrs", attrs);
      $timeout(function () {
        $('#' + attrs['id']).selectpicker();
      })
    }
  }
}]);
  • 第4行:显示参数[attrs]的值,以便理解代码的工作原理。我们将发现,对于客户列表,attrs['id']='selectpickerClients';
  • 第 6 行:要在 DOM 中定位 [id='x'] 的某个元素,应写为 [$('#x')]。 因此,要定位客户列表,应写为 [$('#selectpickerClients')]。这可通过语法 [$('#' + attrs['id'])] 实现;

因此,指令 [selectEnable2] 使用了其所应用的 HTML 元素的某个属性所携带的信息。

3.7.11.4. 控制器 C

C 控制器位于文件 JS [rdvmedecins-09.js] 中,其结构如下:


// 控制器
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- 模型初始化
      // 等待消息
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      // 登录信息
      $scope.server = {url: 'http://localhost:8080', 登录名: 'admin', 密码: 'admin'};
      // 错误
      $scope.errors = {show: false, model: {}};
      // 医生
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      // 客户
      $scope.clients = {title: config.listClients, show: false, model: {}};

      // 全局异步任务
      var task;
      // ---------------------------------------------------- 视图初始化
      // 更新 UI
      $scope.waiting.visible = true;
      $scope.clients.show = false;
      $scope.medecins.show = false;
      $scope.errors.show = false;
      // 查询客户,然后查询医生
      getClients().then(function () {
        getMedecins();
      });

      // 客户名单
      function getClients() {
        ...
      }

      // 医生名单
      function getMedecins() {
...
      }

      // 预约取消
      function cancel() {
...
      }
    } ]);
  • 第 26-28 行:首先查询客户,然后查询医生;

3.7.11.5. 测试

请测试此新版本。

3.7.12. 示例 12:[list] 指令

我们沿用之前的示例,但希望通过使用指令来简化代码 HTML。实际上,我们目前有以下代码 HTML:


<!-- 客户名单 -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  <!-- 医生名单 -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

第 14-26 行与第 1-13 行完全相同,只是将“客户”替换为“医生”。我们希望能够写成如下形式:


  <!-- 客户名单 -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- 医生列表 -->
<list model="medecins" ng-if="medecins.show"></list>

该代码涉及一个新的指令 [list],我们将立即创建该指令。

3.7.12.1. 指令 [list]

指令 [list] 位于文件 JS [list.js] 中。其代码如下:


angular.module("rdvmedecins")
  .directive("list", ['utils', '$timeout', function (utils, $timeout) {
    // 返回的指令实例
    return {
      // HTML 元素
      restrict: "E",
      // 片段网址
      templateUrl: "list.html",
      // 每个指令实例独有的作用域
      scope: true,
      // 与文档的链接函数
      link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }
    }
}]);
  • 第 2 行:定义了一个名为“list”的指令;
  • 第 6 行:[restrict] 属性规定了该指令的使用方式。[restrict: "E"] 表示指令 [list] 可用作 HTML <list ...>...</list>。[restrict: "A"]表示指令[list]可作为属性使用,例如<div ... list='...'>。 [restrict: "AE"] 表示指令 [list] 既可作为属性,也可作为元素使用;
  • 第 8 行:属性 [templateUrl] 指定了在遇到该标签时要使用的片段 HTML。该片段将作为标签的主体;
  • 第 10 行:属性 [scope] 确定了指令模板的作用域。[scope: true] 表示两个 <list> 类型的元素将各自拥有独立的模板。默认情况下(作用域未初始化),它们共享模板;
  • 第 12 行:我们已经多次使用过的函数 [link];

要理解上述代码,需回顾该指令的实际用途:


  <!-- 客户列表 -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- 医生列表 -->
<list model="medecins" ng-if="medecins.show"></list>

指令 [list] 用作 HTML <list> 元素。该元素有两个属性:

  • [model]:其取值为包含指令 [list] 的视图 V 中模型 M 的元素。该元素将为指令的模型提供数据;
  • [ng-if]:该属性将确保在无内容可显示时,不生成指令中的 HTML 代码;

让我们回到指令中 [link] 函数的代码:


link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }

将此代码 JS 与使用该指令的代码 HTML 关联起来:


  <list model="clients" ng-if="clients.show"></list>
  • 第 3 行:attrs['model'] 在此处的值为 'clients';
  • 第 3 行:scope[attrs['model']] 的值为 scope['clients'],因此代表 [$scope.clients],即视图模型中的字段 [clients]。 该字段的值为 {id:'...', data:[client1, client2, ...], show:..., title:'...'}
  • 第 3 行:在指令模型中添加字段 [model]。该指令继承了其所在视图的模型。因此必须避免与视图中可能存在的 [model] 字段发生冲突。此处不会发生冲突;
  • 第 4 行:显示 [scope.model] 以更好地理解代码;
  • 第5-7行:这里出现了一段之前见过的代码。区别在于,组件的id之前是在attrs['id']属性中获取的。而现在它将从[scope.model.id]中获取;

现在,让我们看看该指令生成的代码 HTML。由于指令的 [templateUrl: "list.html"] 属性,我们需要在文件 [list.html] 中查找它:


<!-- 客户或医生列表 -->
<div class="alert alert-info" ng-show="model.show">
  <div class="row">
    <div class="col-md-4">
      <h2 translate="{{model.title}}"></h2>
      <select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
        <option ng-repeat="element in model.data" value="{{element.id}}">
          {{element.titre}} {{element.prenom}} {{element.nom}}
        </option>
      </select>
    </div>
  </div>
</div>
  • 阅读此代码时首先需要记住的是,该指令创建了一个形式为 [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}] 的 [scope.model] 对象。 该 [model] 对象(scope 在代码 HTML 中是隐含的)由指令中的代码 HTML 使用;
  • 第 2 行:使用 [model.show] 来显示/隐藏由该指令生成的视图;
  • 第 5 行:使用 [model.title] 设置标题;
  • 第 6 行:使用 [model.id] 为 <select> 标签设置 ID。该 id 由指令中的代码 JS 调用;
  • 第 6 行:仅当有数据需要显示时,才使用 [model.data] 生成 <select> 标签;
  • 第 7-9 行:使用 [model.data] 生成下拉列表的元素;

3.7.12.2. 代码 HTML

应用程序 [app-22.html] 中的代码 HTML 如下:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- 客户列表 -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- 医生列表 -->
  <list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- 说明 -->
<script type="text/javascript" src="list.js"></script>
  • 第 22 行:切勿忘记包含指令中的代码 JS;

3.7.12.3. C控制器

C 控制器变化极小:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- 模板初始化
...
      // 医生
      $scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
      // 客户
      $scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
  • 第7行和第9行,我们在医生和客户模板中添加了[id]属性;

3.7.12.4. 测试

测试结果与前一个示例相同。

3.7.13. 示例 13:更新指令的模板

我们继续研究指令,并沿用下拉列表的示例。此处旨在探讨当下拉列表内容发生变化时,指令 [list] 的行为表现。

3.7.13.1. 视图 V

各视图如下:

  • 在 [1] 中,首次请求客户列表;
  • 在 [2] 中,第二次请求客户列表。该列表随后与第一次的 [3] 合并。本例中要研究的是组件 [Bootstrap select] 的更新。

3.7.13.2. 页面 HTML

页面 HTML [app-23.html] 是通过复制 [app-22.html] 获得的,随后按以下方式进行了修改:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- 按钮 -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- 客户列表 -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- 指令 -->
<script type="text/javascript" src="list2.js"></script>

与前一个应用程序相比,修改内容如下:

  • 第 15-17 行:添加了一个按钮;
  • 第 20 行:使用新的指令 [list2];
  • 第 23 行:使用新文件 JS;
  • 第 25 行:从指令 [list2] 导入文件 JS;

3.7.13.3. 指令 [list2]

[list2.js]中的指令[list2]如下:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // 返回的指令实例
    return {
      // 元素HTML
      restrict: "E",
      // 片段网址
      templateUrl: "list.html",
      // 每个指令实例独有的作用域
      scope: true,
      // 与文档的链接函数
      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        scope.model = scope[attrs['model']];
        $timeout(function () {
          $('#' + scope.model.id).selectpicker('refresh');
        })
      }
    }
}]);

与指令 [list] 的唯一区别在于第 16 行:通过方法 [selectpicker('refresh')],要求组件 [Bootstrap-select] 进行刷新。 其背后的思路是:每当用户请求新的客户列表时,下拉列表就会被刷新。虽然实际效果可能不理想,但这是基本思路。

3.7.13.4. C 控制器

控制器位于文件 [rdvmedecins-11.js] 中,该文件是通过复制文件 [rdvmedecins-10.js] 生成的:


      // 客户
      $scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
      // 客户列表
      $scope.getClients = function getClients() {
        // 更新 UI
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        // 请求客户列表;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // 分析前一次调用的结果
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
             // 将获取的数据放入新模型中,强制视图刷新
            $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
            // 更新 UI
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // 获取客户列表时出现错误
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // 更新 UI
            $scope.waiting.visible = false;
          }
        });
}
  • 第1行:为了能在[clients.data]中进行数组拼接,该对象被初始化为一个空数组;
  • 第 18 行:将新的客户列表与 [clients.data] 数组中已有的列表进行拼接;

之前我们写的是:

// 将获取的数据放入模型
$scope.clients.data = result.data;

现在写为:

// 将获取的数据放入新模板以强制视图刷新
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

要理解这段代码,需要回顾在指令 [list2] 的情况下,视图 V 中是如何使用模型 M 的:


  <!-- 客户列表 -->
<list2 model="clients" ng-if="clients.show"></list2>

指令 [list2] 使用的模板是 [clients]。只有当视图 V 的模板 M 中 [clients] 发生变化时,该指令才会被重新评估。关于修改,首先想到的写法是:

$scope.clients.data=$scope.clients.data.concat(result.data) ;

以体现新客户列表需追加到原有列表中的事实。这样做会修改 [clients.data],但不会修改 [clients]。 虽然我不深谙JavaScript的奥秘,但[clients]和[clients.data]很可能是指针,这并不令人意外。 当修改指针 [clients.data] 时,指针 [clients] 保持不变。 因此,指令 [list2] 不会被重新评估。这在调试应用程序时确实可以观察到(Chrome 中的 F12)。

通过编写:

$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

可确保 [$scope.clients] 确实接收到了新值。 指针 [$scope.clients] 指向了一个新对象。此时,指令 [list2] 本应被重新评估。然而,我们并未得到预期的结果。让我们查看两次请求客户列表时的屏幕截图:

  • 在 [1] 中,只有四个元素,而不是八个;
  • 在 [2] 中,这四个元素其实属于 [select],但后者被隐藏了(style='display : none');
  • 在 [3] 中,这四个客户端位于另一个架构 HTML 中,用户点击下拉列表时看到的就是这个;

最后,控制台日志显示如下内容:

1
2
3
4
[dao] init
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
directive list2
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • 第 1 行:服务 [dao] 被实例化;
  • 第 2 行:服务 [dao] 获取了第一份客户端列表;
  • 第 3 行:执行指令 [list2];
  • 第 4 行:服务 [dao] 获取第二份客户列表;

第 2 行的显示来自指令中的以下代码:


      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        ...
}

让我们来分析指令 [list2] 的生命周期:

  • 在第 1 行和第 2 行之间,该指令尚未被激活,尽管视图已首次显示。这是由于视图 V 中其 [ng-if="clients.show"] 属性所致:

<list2 model="clients" ng-if="clients.show"></list2>
  • 第 3 行:获取第一份医生列表后,[clients.show] 变为 true,指令被激活;
  • 在获取第二份客户列表后,可以看到指令 [list2] 的代码未被调用。这就是为什么看不到第二份列表的原因;

为解决此问题,我们将指令 [list2] 修改如下:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // 返回的指令实例
    return {
      // HTML 元素
      restrict: "E",
      // 片段 URL
      templateUrl: "list.html",
      // 每个指令实例独有的作用域
      scope: true,
      // 与文档的链接函数
      link: function (scope, element, attrs) {
        // 每当 attrs["model"] 发生变化时,指令的模板也必须随之改变
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("directive list2 newValue", newValue);
          // 更新指令的模板
          scope.model = newValue;
          $timeout(function () {
            $('#' + scope.model.id).selectpicker('refresh');
          })
        });
      }
    }
}]);
  • 第14行:函数[scope.$watch]用于监视模型中的某个值。其语法为[scope.$watch('var'), f],其中[var]是模型中某个变量的标识符,f则是当该变量值发生变化时需要执行的函数。 在此,我们希望监视变量 [clients]。因此,应编写 [scope.$watch('clients')]。 由于 attrs['model']='clients',因此写为 [scope.$watch(attrs["model"], function (newValue)] ;
  • 第14行:函数[scope.$watch]的第二个参数是当被观察变量值发生变化时需要执行的函数。 参数 [newValue] 是该变量的最新值,因此对我们而言,即模型中变量 [clients] 的最新值;
  • 第17行:该新值被赋给指令模型中的[model]字段;

完成此修改后,日志内容随之改变:

上文可见,在获取第二份客户列表后,指令 [list2] 确实再次被执行,结果 [2] 证实了这一点。

3.7.14. 示例 14:指令 [waiting] 和 [errors]

让我们回到前一个应用程序中的代码 HTML:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- 错误列表 -->
  <div class="alert alert-danger" ng-show="errors.show">
  ...
  </div>

  <!-- 按钮 -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- 客户列表 -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
  • 第 5-7 行:等待消息;
  • 第 10-12 行:错误消息;

我们决定将这两个消息的代码 HTML 放入指令中。

3.7.14.1. 新代码 HTML

新代码 HTML [app-24.html] 如下:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- 等待提示 -->
  <waiting model="waiting"></waiting>

  <!-- 错误列表 -->
  <errors model="errors"></errors>

  <!-- 按钮 -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- 客户列表 -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- 指令 -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
  • 第 5 行:等待消息的指令;
  • 第 8 行:错误消息的指令;
  • 第19行:与应用程序关联的新文件JS;
  • 第 21-23 行:三个指令对应的 JS 文件;

3.7.14.2. [waiting]指令

指令 [waiting] 的代码 JS 位于以下文件 [waiting.js] 中:


angular.module("rdvmedecins")
  .directive("waiting", ['utils', function (utils) {
    // 返回的指令实例
    return {
      // 元素 HTML
      restrict: "E",
      // 片段 URL
      templateUrl: "waiting.html",
      // 每个指令实例独有的作用域
      scope: true,
      // 与文档的链接函数
      link: function (scope, element, attrs) {
        // 每当 attr["model"] 发生变化时,页面模板也必须随之改变
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[waiting] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
  }]);

该代码遵循与先前已研究的指令 [list2] 相同的逻辑。

第 8 行引用了以下文件 [waiting.html]:


<div class="alert alert-warning" ng-show="model.show">
  <h1>{{ model.title.text | translate:model.title.values}}
    <button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
    <img src="assets/images/waiting.gif" alt=""/>
  </h1>
</div>

在应用程序的 JS 代码中,该 HTML 代码的 [$scope.waiting] 模板将按以下方式定义:


// 等待消息
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};

3.7.14.3. 指令 [errors]

指令 [errors] 的代码 JS 位于以下文件 [errors.js] 中:


angular.module("rdvmedecins")
  .directive("errors", ['utils', function (utils) {
    // 返回的指令实例
    return {
      // 元素 HTML
      restrict: "E",
      // 片段 URL
      templateUrl: "errors.html",
      // 每个指令实例独有的作用域
      scope: true,
      // 与文档的链接函数
      link: function (scope, element, attrs) {
        // 每当 attr["model"] 发生变化时,页面模板也必须随之改变
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[errors] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
}]);

该代码遵循与先前研究的 [list2] 指令相同的逻辑。

第 8 行引用了以下文件 [errors.html]:


<div class="alert alert-danger" ng-show="model.show">
  {{model.title.text|translate:model.title.values}}
  <ul>
    <li ng-repeat="message in model.messages">{{message|translate}}</li>
  </ul>
</div>

在应用程序的 JS 代码中,该 HTML 代码的 [$scope.errors] 模板将按以下方式定义:


// 获取客户列表时发生错误
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};

3.7.15. 示例 15:导航

到目前为止,我们一直使用单页应用程序。在本示例中,我们将探讨多页应用程序以及页面之间的导航。

3.7.15.1. 应用程序的 V 视图

  • 在 [1] 中,视图 1 的 URL;
  • 在 [2] 中,显示其内容;
  • 在 [3] 中,跳转至第 2 页;
  • 在 [4] 中,显示视图 2;
  • 在 [5] 中,跳转至第 3 页;
  • 在 [6] 中,显示第 3 视图;
  • 在 [7] 中,跳转至第 1 页;
  • 在 [8] 中,返回视图 1;

3.7.15.2. 代码组织

我们开始对代码进行新的组织:

  
  • 应用程序的视图将放置在文件夹 [views] 中;
  • 应用程序模块将放置在 [modules] 文件夹中;
  • 应用程序的控制器将放置在 [controllers] 文件夹中;

同样,在最终版本中:

  • 服务将放置在 [services] 文件夹中;
  • 指令将放置在文件夹 [directives] 中;

3.7.15.3. 视图容器

[views]文件夹中的视图将显示在以下容器[app-25.html]中:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body>
    <div class="container" ng-controller="mainCtrl">
        <!-- 导航栏 -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- 当前视图 -->
        <ng-view></ng-view>
    </div>

...
<!-- 模块 -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- 控制器 -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
  • 第 7 行:容器的主体由 [mainCtrl] 控制;
  • 第 9 行:指令 [ng-include] 用于包含外部文件 HTML,此处为导航栏;
  • 第12行:容器显示的各种视图均位于[ng-view]指令内部。最终,该容器显示的内容为:
    • 始终显示相同的导航栏(第 9 行);
    • 第 12 行中的不同视图;
  • 第 16-22 行:导入应用程序模块 [rdvmedecins-13.js] 及其控制器中的 JS 文件;

3.7.15.4. 应用程序模块

文件 [rdvmedecins-13.js] 定义了应用程序模块以及视图之间的路由:


// --------------------- Angular 模块
angular.module("rdvmedecins", [ 'ngRoute' ]);

angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ 路由
  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
    });
  $routeProvider.when("/page2",
    {
      templateUrl: "views/page2.html",
      controller: 'page2Ctrl'
    });
  $routeProvider.when("/page3",
    {
      templateUrl: "views/page3.html",
      controller: 'page3Ctrl'
    });
  $routeProvider.otherwise(
    {
      redirectTo: "/page1"
    });
}]);
  • 第 1 行:定义模块 [rdvmedecins]。该模块依赖于由库 [angular-route.min.js] 提供的模块 [ngRoute]。正是该模块实现了第 6-24 行中定义的视图路由;
  • 第4行:定义了模块[rdvmedecins]中的函数[config]。 需要提醒的是,该函数在任何服务实例化之前执行。这是一个模块配置函数。此处配置的是其路由。这是通过模块 [ngRoute] 提供的对象 [$routeProvider] 实现的;
  • 第6-10行:定义了当用户请求URL时要显示的视图。这是应用程序内部的路由。 URL实际上是[/rdvmedecins-angular-v1/app-21.html#/page1]。 可以看出,实际使用的仍是容器 [/rdvmedecins-angular-v1/app-21.html] 中的 URL,只是在 # 字符后附加了额外信息。Angular 路由机制正是通过处理这些附加信息来实现路由的;
  • 第 8 行:指示将片段 HTML 插入到容器的 [ng-view] 指令中:
  • 第 9 行:指定该片段的控制器名称;
  • 第 11-15 行:定义了当用户请求 URL [/page2] 时要显示的视图;
  • 第 16-20 行:定义当用户请求 URL 时应显示的视图;
  • 第21-24行:定义当请求的URL不属于前三个视图时应执行的路由(否则,第21行);
  • 第23行:重定向至URL [/page1],即重定向至第6-10行定义的视图;

3.7.15.5. 视图容器的控制器

我们看到视图容器声明了一个控制器:


<div class="container" ng-controller="mainCtrl">

控制器 [mainCtrl] 在文件 [mainController.js] 中定义:


// 控制器
angular.module("rdvmedecins")
  .controller('mainCtrl', ['$scope', '$location',
    function ($scope, $location) {

      // 页面模板
      $scope.page1 = {};
      $scope.page2 = {};
      $scope.page3 = {};
      // 全局模板
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // 视图公开的方法
      main.showPage1 = function () {
        $location.path("/page1");
      };
      main.showPage2 = function () {
        $location.path("/page2");
      };
      main.showPage3 = function () {
        $location.path("/page3");
      }
}]);
  • 第 3 行:控制器 [mainCtrl] 需要路由模块 [ngRoute] 提供的对象 [$location]。该对象用于切换视图(第 16、19、22 行);

让我们回到容器的代码:


    <div class="container" ng-controller="mainCtrl">
        <!-- 导航栏 -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- 当前视图 -->
        <ng-view></ng-view>
</div>
  • 控制器 [mainCtrl] 构建了区域 1-7 的模型;
  • 第 6 行中包含的视图也有一个控制器。例如,视图 [page1] 对应的控制器是 [page1Ctrl]。该控制器构建了第 6 行显示区域的模型。因此,该区域中包含两个模型:
    • 由控制器 [mainCtrl] 构建的模型;
    • 由控制器 [page1Ctrl] 构建的模型;

存在模型继承的情况。在第6行的视图中,可以同时看到控制器模型[mainCtrl]和[pagexCtrl]。如果这两个模型中有两个变量名称相同,其中一个会覆盖另一个。为避免这种名称冲突,我们创建了四个分别命名为:

page
contrôleur
modèle
ligne du code
容器
mainCtrl
11
第1页
page1Ctrl
第1页
7
第2页
page2Ctrl
第2页
8
第3页
page3Ctrl
第3页
9
  • 第12行:在[main]模型中定义了一个[text]元素;

第7至11行具有非常特殊的含义:它们定义了控制器[mainCtrl]中的[$scope],并在其中创建了四个变量[main, page1, page2, page3]。 这四个变量将分别用作容器及其将依次包含的三个视图的模板。

3.7.15.6. 导航栏

导航栏在容器中定义如下:


    <div class="container" ng-controller="mainCtrl">
        <!-- 导航栏 -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- 当前视图 -->
        <ng-view></ng-view>
</div>

导航栏在第 3 行被定义。这意味着它仅识别 [main] 这一模板。其代码如下:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">RdvMedecins</a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li class="active">
          <a href="">
            <span ng-click="main.showPage1()">Page 1</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage2()">Page 2</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage3()">Page 3</span>
          </a>
        </li>
      </ul>
    </div>
  </div>
</div>
  • 第16、21、26行调用了[main]模型的方法;
  • 第16行:点击链接[Page1]将触发方法[$scope.main.showPage1]的执行。该方法在控制器[mainCtrl]中定义如下:

      // 全局模型
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // 视图公开的方法
      main.showPage1 = function () {
        $location.path("/page1");
};
  • 第 6 行:从上述代码中可以看出,方法 [main.showPage1] 实际上是方法 [$scope.main.showPage1]。因此,真正执行的正是该方法;
  • 第7行:将应用程序中的URL修改为[/page1]。让我们回到主模块中定义的路由:

  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
});

可以看到,片段 [views/page1.html] 将被插入到容器中,且其控制器为 [page1Ctrl]。

3.7.15.7. 视图 [/page1] 及其控制器

片段 [views/page1.html] 如下所示:


<h1>Page 1</h1>
<div class="alert alert-info">
  <ul>
    <li>Modèle global : {{main.text}}</li>
    <li>Modèle local : {{page1.text}}</li>
  </ul>
</div>

需要注意的是,在插入容器的视图中,[main] 模型是可见的。 这就是我们要验证的第 4 行内容。此外,片段 [views/page1.html] 的控制器 [page1Ctrl] 定义了一个模板 [page1]。第 5 行使用的是该模板。

控制器 [page1Ctrl] 的代码如下:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope',
    function ($scope) {

      // 第 1 页的模板
      var page1=$scope.page1;
      page1.text="[Modèle local dans page 1]";
}]);
  • 第2行:此处注入的[$scope]并非空值。由于控制器[page1Ctrl]控制的区域被插入到由[mainCtrl]控制的容器中, 因此第2行的[$scope]包含由控制器[mainCtrl]定义的[$scope]的元素。这一点非常重要。 由控制器 [mainCtrl] 定义的 [$scope] 包含以下 [main, page1, page2, page3] 元素。这意味着我们可以访问所有视图的模型。这未必是理想的情况,但在此处确实如此。 在 Angular 客户端的最终版本中,我们将利用这一特性,在 [main] 模型中存储需要在视图之间共享的信息。这将实现一个类似于服务器端“会话”的概念;
  • 第 6 行:从 [$scope] 中获取第 1 页的 [page1] 模型,随后对其进行操作(第 7 行)。此时将显示如下内容:
 

视图 [/page2] 和 [/page3] 基于与视图 [/page1] 相同的模板构建(参见第 240 页的屏幕截图)。

3.7.15.8. 导航控制

现在,我们希望按以下方式控制导航:[page1 --> page2 --> page3 --> page1]。 因此,如果用户位于第 1 页 [/page1],并在浏览器中输入 URL 或 [/page3],则不应接受此导航操作,用户应保持在第 1 页。

为实现此效果,我们按以下方式修改页面控制器:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope', '$location',
    function ($scope, $location) {
      // 允许导航吗?
      var main = $scope.main;
      if (main.lastUrl && main.lastUrl != '/page3') {
        // 返回上一个 URL
        $location.path(main.lastUrl);
        return;
      }
      // 保存该页面的URL
      main.lastUrl = '/page1';
      // 页面模板
      var page1 = $scope.page1;
      page1.text = "[Modèle local dans page 1]";
    }]);
  • 第 12 行:当页面显示时,我们将它的 URL 存储在 [main.lastUrl] 模型中。 此处运用了前文提及的概念:使用模板 [main] 来存储所有视图共用的信息。这里指的是最近访问的 URL;
  • 第4至12行的代码被复制并适配给三个视图。当前位于视图[/page1]中;
  • 第 5 行:获取 [main] 模板;
  • 第6行:如果[main.lastUrl]模板存在且与[/page3]不同,则禁止导航(最后访问的URL存在且不是/page3);
  • 第8行:此时返回上次访问的URL;

让我们试一下:

  • 在[1]中,当前位于第1页,输入第3页的URL后跳转至[2];
  • 在 [3] 时,未进行导航,返回了第 1 页的 URL;

3.7.16. 结论

我们已经梳理了Angular客户端最终版本中将遇到的所有用例。在正式发布时,我们将更多地讲解应用程序的功能,而非其实现细节。对于后者,我们将仅通过示例来展示当时所研究的用例。

3.8. 最终的 Angular 客户端

3.8.1. 项目结构

最终项目结构如下:

  • [1] 包含整个项目。[app.html] 是应用程序的主页;
  • [2] 包含控制器;
  • 在 [3] 中,包含指令;
  • [4] 包含应用程序的服务和 Angular 模块 [main.js];
  • 在 [5] 中,各种视图,这些视图将嵌入到主页面 [app.html] 中;

3.8.2. 项目的依赖项

项目的依赖项如下:

 

这些不同元素的作用已在第 3.4 节(第 134 页)中进行说明。

3.8.3. 主页面 [app.html]

主页面如下:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- CSS -->
  <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
  <link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- 控制器 [appCtrl],模板 [app] -->
<body ng-controller="appCtrl">
<div class="container">
 ...
</div>
<!-- Bootstrap 核心 JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- 模块 -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- 服务 -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- 指令 -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- 控制器 -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
  • 第18行:请注意,[appCtrl]是主页的控制器;
  • 第19-21行:母版页的内容;

该内容如下:


<div class="container">
  <!-- 导航栏 -->
  <ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
  <ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
  <!-- 巨型横幅 -->
  <ng-include src="'views/jumbotron.html'"></ng-include>
  <!-- 页面标题 -->
  <div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
       translate-values="{{app.titre.model}}"></div>
  <!-- 页面错误 -->
  <errors model="app.errors" ng-show="app.errors.show"></errors>
  <!-- 等待提示 -->
  <waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
  <!-- 当前视图 -->
  <ng-view></ng-view>
  <!-- 调试 -->
  <debug model="app" ng-show="app.debug.on"></debug>
</div>

无论显示哪个视图,它都将始终包含以下元素:

  • 第3-4行:一个控制栏。第3行和第4行的两个控制栏互斥;

Image

Image

  • 第6行:应用程序的徽标/文本:

Image

  • 第8行:标题

Image

  • 第11行:一条错误信息:

Image

  • 第13行:一条等待提示:

Image

  • 第 17 行:一条调试信息:

Image

上述所有元素均受指令 [ng-show / ng-hide] 控制,这意味着即使它们确实存在,也不一定可见。

3.8.4. 应用程序的视图

在主页面代码中,我们有:


<div class="container">
  ...
  <!-- 当前视图 -->
  <ng-view></ng-view>
  ...
</div>

第 4 行接收应用程序的各个视图。这些视图在模块 [main.js] 中定义:

Image

关于不同路由配置的作用,已在第 3.7.15.4 节(第 242 页)中进行说明。

视图 [login.html] 为空,即它不会向主页面中已有的元素添加任何新内容。

视图 [home.html] 在主页面中添加了以下元素:

Image

视图 [agenda.html] 在母版页中添加了以下元素:

Image

视图 [resa.html] 在母版页中添加了以下元素:

Image

3.8.5. 应用程序功能

Angular 客户端视图已在第 1.3.3 节(第 7 页)中介绍过。为了便于阅读本新章节,我们在此再次列出。第一个视图如下:

  • [6],即应用程序的登录页面。这是一个为医生提供预约服务的应用程序;
  • [7],一个复选框,用于控制是否处于 [debug] 模式。该模式的特征是存在 [8] 框架,用于显示当前视图的模板;
  • 在 [9] 中,是一个以毫秒为单位的人为等待时间。其默认值为 0(不等待)。若 N 是该等待时间的数值,则用户的任何操作都将在等待 N 毫秒后执行。这有助于观察应用程序所实现的等待管理机制;
  • 在 [10] 中,是 Spring 4 服务器的 URL。根据前文所述,该值为 [http://localhost:8080];
  • 在 [11] 和 [12] 中,分别表示希望使用该应用程序的用户名和密码。共有两个用户:admin/admin(登录名/密码)拥有角色 ADMIN,以及 user/user 拥有角色 (USER)。仅角色 ADMIN 拥有使用该应用程序的权限。角色 USER 仅用于展示服务器在此使用场景下的响应;
  • 在 [13] 中,用于连接服务器的按钮;
  • 在 [14] 中,即应用程序的语言。共有两种语言:默认的法语和英语。
  • 在 [1] 中,进行登录;
  • 登录后,可选择希望预约的医生 [2] 及预约日期 [3];
  • 在 [4] 中,查询所选医生在所选日期的日程安排;
  • 获取医生日程表后,即可预订时段 [5];
  • 在 [6] 步骤中,选择该预约的患者,并在 [7] 步骤中确认该选择;

预约确认后,系统将自动返回日程表,新预约已记录其中。该预约日后可通过 [7] 进行删除。

主要功能已介绍完毕,操作简单。未提及的功能均为导航功能,用于返回上一视图。最后介绍语言管理:

  • 在 [1] 中,可切换为英语;
  • 在 [2] 中,视图切换为英语,包括日历;

3.8.6. 模块 [main.js]

模块 [main.js] 定义了将控制应用程序的 Angular 模块:

 
  • 第 4 行:该模块名为 [rdvmedecins];
  • 第 5 行:模块 [ngRoute] 用于路由 URL;
  • 第 6 行:模块 [translate] 用于文本的国际化处理;
  • 第 7 行:模块 [base64] 用于将字符串 'login:password' 编码为 Base64;
  • 第 8 行:模块 [ngLocale] 用于日历的国际化;
  • 第 9 行:模块 [ui.bootstrap] 用于日历;
  • 第 12 行:路由配置;
  • 第 40 行:消息的国际化;

3.8.7. 主页控制器

回顾主页面 [app.html] 中的代码 HTML:


<body ng-controller="appCtrl">
<div class="container">
...

第1行,主页面的整个主体(body)由控制器[appCtrl]控制。基于其位置,这使其成为应用程序的通用且主要的控制器。正如第3.7.15节所解释的,由该控制器构建的模型将被所有插入主页面的视图继承。

其代码如下:


angular.module("rdvmedecins")
  .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
    function ($scope, config, utils, $location, $locale) {

      // 调试
      utils.debug("[app] init");

      // ----------------------------------------页面初始化
      // # 页面的模板
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // 当前页面的模板
      var app = $scope.app;
      ...

      // ---------------------------------- 方法

      // 取消当前任务
      app.cancel = function () {
...
      };

      // 注销
      app.deconnecter = function () {
        ...
      };

      // 此代码必须保留在此处,因为它引用了前面的函数 [cancel]
      app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
    }])
;

第10-14行定义了应用程序中使用的五个模板:

Modèle
Vue
Contrôleur
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.resa
resa.html
resaCtrl
$scope.agenda
agenda.html
agendaCtrl

需要理解的是,对象 [$scope] 作为主页面的控制器模板,会被所有视图和控制器继承。因此,控制器 [loginCtrl] 可以访问 [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda] 的元素。 换言之,一个控制器可以访问其他控制器的模型。本示例应用刻意避免使用这一特性。例如,控制器 [loginCtrl] 仅与两个模型交互:

  • 其自身的 [$scope.login];
  • 以及父控制器 [$scope.app] 的模板;

其他所有控制器亦是如此。[$scope.app] 模型将作为不同控制器之间的共享内存。当控制器 C1 需要向控制器 C2 传输信息时,操作流程如下:

在 [C1] 中:

$scope.app.info=value ;

在 [C2] 中:

var value=$scope.app.info ;

在这两种情况下,$scope 都是从控制器 [appCtrl] 继承而来的,因此在 [C1] 和 [C2] 中是相同的(因为它是一个指针)。 作为控制器间共享内存的对象 [$scope.app],在注释中通常被称为 session,这是为了模仿传统 Web 应用中用于表示连续请求 HTTP 之间共享内存的会话概念。

让我们回到控制器 [appCtrl] 的代码:


      // # 页面的模板
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // 当前页面的模板
      var app = $scope.app;
      // [app.debug] 和 [utils.verbose] 必须始终保持同步
      app.debug = utils.verbose;
      app.debug.on = config.debug;
      // 目前没有页面标题
      app.titre = {show: false};
      // 没有导航栏
      app.navbarrun = {show: false};
      app.navbarstart = {show: false};
      // 无错误
      app.errors = {show: false};
      // 默认语言
      angular.copy(config.locales['fr'], $locale);
      // 当前视图
      app.view = {url: undefined, model: {}, done: false};
      // 当前任务
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
  • 第 8 行:[$scope.app] 将作为主页模板。这也将是不同控制器之间共享的内存。 与其在各处都写上 [$scope.app.champ=value],不如将指针 [$scope.app] 赋值给变量 [app],然后写成 [app.champ=value]。 只需记住,[app]是主页面中显示的模板;
  • 第11行:[app.debug.on] 是一个布尔值,用于控制应用程序的 debug 模式。 默认值为 true。该值与导航栏中的复选框 [debug] 相关联;
  • 第 15 行:[app.navbarrun.show] 控制以下导航栏的显示:

Image

  • 第 16 行:[app.navbarstart.show] 控制以下导航栏的显示:

Image

  • 第 18 行:[app.errors] 是错误横幅的模板;

Image

  • 第 22 行:[app.view] 将包含当前视图的信息,即主页面中由标签 [ng-view] 当前显示的视图。我们将在此记录以下信息:
    • [url]:当前视图的 URL,例如 [/agenda];
    • [model]:当前视图的模板,例如 [$scope.agenda];
    • [done]:向 vrai 指示当前视图已完成工作,且正在切换至另一个视图;

这些 信息用于导航控制。

  • 第24行:启动一个异步任务,即模拟等待。该异步任务由两个指针[app.view.model.task.action]和[app.task]引用;

在控制器 [appCtrl] 中提取了两个方法:


      // 取消当前任务
      app.cancel = function () {
...
      };

      // 注销
      app.deconnecter = function () {
        ...
};
  • 第 2 行:函数 [app.cancel] 用于取消当前显示等待消息的任务。所有视图均会显示该消息,因此任务的取消操作将在这里进行;
  • 第 7 行:函数 [app.deconnecter] 将用户重定向至身份验证页面。除视图 [/login] 外,所有视图均提供此功能;

函数 [app.deconnecter] 如下:


      // 注销
      app.deconnecter = function () {
        // 返回登录页面
        $location.path(config.urlLogin);
};
  • 第 4 行:返回 URL [/login] 的登录页面;

3.8.8. 异步任务管理

在我们的应用程序中,任何时刻只会有一项异步任务正在执行。但理论上可以同时运行多项任务。例如,在应用程序启动时,它会通过两个连续的 HTTP 请求,依次向 Web 服务查询医生列表和客户列表。 我们也可以通过两个同时发起的 HTTP 请求来实现相同的功能。Angular 提供了相应的管理工具。但在此,我们并未采用这种方案。

通过在控制器 [appCtrl] 中添加以下代码,可取消正在执行的任务:


      // 取消当前任务
      app.cancel = function () {
        utils.debug("[app] cancel task");
        // 取消当前视图的异步任务
        var task = app.view.model.task;
        task.isFinished = true;
        task.action.reject();

        ...
};
  • 第 5 行:在 [app.view.model.task] 中查找任务。此外,所有控制器都会确保其异步任务都引用该对象
  • 第 6 行:用于标记任务已完成;
  • 第 7 行:用于以失败状态结束任务。此语法与所研究的 Angular 示例中使用的语法不同:
    • 在示例中,对象 [task] 是一个可以被终止的 [$q.defer()] 对象;
    • 在最终版本中,对象 [task] 是一个包含字段 [action, isFinished] 的对象,其中 [action] 是可终止的对象 [$q.defer()];,而 [isFinished] 是一个布尔值,用于指示操作是否已完成;

让我们通过一个示例来探讨对象 [task] 的生命周期。 在启动时,经过控制器 [appCtrl] 之后,由控制器 [loginCtrl] 接管并显示视图 [views/login.html]。其初始化代码如下:


      // 获取父视图
      var login = $scope.login;
      var app = $scope.app;
      // 当前视图
app.view = {url: config.urlLogin, model: login, done: false};

第5行显示为[model=login]。这意味着当修改对象[login]时,会同时修改对象[app.view.model],进而影响[$scope.app.view.model]。 当在控制器 [loginCtrl] 中需要进行模拟等待时,应编写:


// 模拟等待
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};

将字段 [task] 添加到对象 [login] 时,实际上是添加到了对象 [$scope.app.view.model] 上。 如果用户取消等待,则 [appCtrl.cancel] 中的代码:


// 当前页面的模型
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();

将正常结束模拟的等待(第4-6行)。

3.8.9. 导航控制

应用程序中使用的导航规则如下:

URL cible
URL précédente
Navigation autorisée
/login
任意
/home
/登录
是,如果检查员 [loginCtrl] 已报告工作完成

/home

/日程表
/agenda
/home
是,如果控制器 [homeCtrl] 已报告工作完成

/resa

/日程
/resa
/日程
是,如果检查员 [homeCtrl] 已表示其工作已完成

/预订

这是通过以下代码实现的:

对于 [agendaCtrl]:

Image

  • 第 11-20 行:导航规则的实现;
  • 第 26 行:新的当前视图;

对于 [resaCtrl]:

Image

  • 第 12-20 行:实现导航规则:
  • 第 27 行:新的当前视图;

对于 [loginCtrl]:

Image

  • 此处没有导航控制,因为规则规定可以从任何地方跳转到 URL [/login]。因此,如果用户在浏览器中输入此 URL,无论当前视图为何,都会生效;
  • 第 16 行:新的当前视图;

[homeCtrl] 控制器代码已在 3.8.7给出。

最后,对于如下规则:

/agenda
/home
是的,如果控制器 [homeCtrl] 已报告其工作完成

以下是一个示例代码,用于将任务从 URL [/home] 转移至 URL [/agenda]:

 

上文代码位于控制器 [homeCtrl] 的方法 [afficherAgenda] 中。用户请求了某位医生的日程表。

  • 第107行:任务HTTP的承诺;
  • 第109行:变量[app]被初始化为[$scope.app]。如前所述,后者作为视图[app.html]的模板。 该模板 [$scope.app] 还用于存储视图之间需要共享的信息;
  • 第 111 行:分析任务返回的错误代码;
  • 第 113 行:将结果 [result.data] 放入模型 [app] 中;
  • 第 116 行:控制器 [homeCtrl] 将控制权移交给控制器 [agendaCtrl]。 它通过第115行的代码告知对方已完成工作。该代码将由控制器[agendaCtrl]按以下方式处理:

Image

  • 第 11 行:获取对象 [$scope.app.view];
  • 第15行:处理由[homeCtrl]初始化的[$scope.app.view.done]字段;

3.8.10. 服务

  

[config, utils, dao] 服务即在介绍 Angular 时已描述过的服务:

  • 服务 [config] 已在第 3.7.4 节中介绍;
  • 服务 [utils] 已在第 3.7.5 节中介绍;
  • 服务 [dao] 已在第 3.7.6 节中介绍;

为便于参考,现重申这些服务的结构:

服务 [config]

  • 在 [1] 中:可以看到代码约有 250 行。该代码的主要内容是将 [2] 国际化消息的键值进行外部化处理。这样可以避免将这些键值硬编码在代码中;

服务 [utils]

 
  • 第 8 行:我们此前尚未遇到变量 [verbose]。它通过以下方式控制函数 [debug]:
 
  • 第22-25行:如果[verbose.on]的计算结果为false,则函数[utils.debug]不会执行任何操作。该变量与控制器变量[appCtrl]相关联:
 
  • 第 21 行:[app.debug] 取指针 [utils.verbose] 的值。因此,对 [app.debug] 进行的任何修改也会同步应用到 [utils.verbose] 上;
  • 第22行:[app.debug.on]的初始值取自配置文件。默认值为true.。该值可能会随时间变化。用户确实可以通过导航栏对其进行修改:
 
  • 第 45 行:一个复选框(type=checkbox)可用于更改 [app.debug.on] 的值(ng-model 属性);

服务 [dao]

 

3.8.11. 指令

  

[errors, footable, list, waiting] 指令已在介绍 Angular 时描述过:

  • [footable] 指令在第 3.7.8.6 节中引入;
  • 指令 [list] 于第 3.7.12 节中引入;
  • 指令 [errors] 和 [waiting] 引入于第 3.7.14 节;

我们之前未曾遇到过指令 [debug]。其内容如下:

 

第 11 行引用的文件 [debug.html] 如下:

 
  • 第 2 行:指令 [debug] 将其模板(格式为 JSON)显示在 Bootstrap 横幅中(第 1 行);

该指令仅在主模板 [app.html] 中使用:

 
  • 指令 [debug] 用于第 35 行。因此,当处于调试模式(ng-show 属性)时,它会显示模板 [$scope.app] 的 JSON 形式。这会产生如下效果:

解读这些信息需要对代码有深入的了解,但一旦掌握了相关知识,上述信息对调试就非常有用。这里我们对显示的 [$scope.app] 模型中的元素进行了高亮标注。需要提醒的是,[$scope.app] 是控制器共享的内存;

  • [waitingBeforeTask]:在发出任何 HTTP 请求前的模拟等待时间;
  • [debug]:调试模式——若显示此横幅,则该值必然为 true
  • [navbarrun]:布尔值,用于控制以下导航栏的显示:

Image

  • [navbarstart]:布尔值,用于控制以下导航栏的显示:

Image

  • [errors]:[errors] 指令的模板;
  • [view]:封装当前显示视图的信息;
  • [waiting]:指令 [waiting] 的模板;
  • [serverUrl, username, password]:Web 服务的连接信息;
  • [medecins]:适用于医生的 [list] 指令的模板;
  • [clients]:客户模板;
  • [menu]:控制显示的菜单选项。这些选项在 [navbar-run.html] 中定义:

Image

菜单选项位于第 16、23、29 和 36 行。

  • [formattedJour]:日历中选定的日期,格式为“yyyy-mm-dd”;
  • [agenda]:医生的日程表。其中包含空闲时段(rv==null)和已预约时段。对于已预约时段,会显示预约客户的姓名;
  • [selectedCreneau]:用于预约的时间段;

3.8.12. 控制器 [loginCtrl]

  

控制器 [loginCtrl] 关联视图 [views/login.html],该视图与主页面关联后生成以下页面:

Image

控制器 [loginCtrl] 如下所示:

Image

  • 第 13 行:[login] 将作为当前视图的模板;
  • 第 14 行:[app] 是控制器之间的共享内存;
  • 第 16 行:将当前视图的信息填入 [app.view] 中;

该初始化代码将出现在每个控制器中。对于视图 V1(其模板为 M1)的控制器 C1,初始化代码如下:

1
2
3
var app=$scope.app;
var M1=$scope.M1;
app.view={url: config.urlV1, model:M1, done:false};
  • 第 18 行:可能还记得,[appCtrl] 启动了一个由对象 [app.task.action] 引用的模拟等待。我们使用该任务中的 [promise] 来等待其结束;
  • 第 39 行:方法 [login.setLang] 负责处理语言切换;
  • 第 47 行:方法 [login.authenticate] 负责处理用户身份验证;

让我们看看身份验证方法的主要流程:

Image

  • 第50-51行:[app.waiting]是加载提示条的模板;
  • 第 53 行:[app.errors] 是错误横幅的模板;
  • 第55行:启动一次模拟等待。对象[action, isFinished]由[login.task]引用,因此(由于[app.view.model=login]),也由[app.view.model.task]引用。 需要指出的是,这是任务能够被取消的条件;
  • 第57行:模拟等待结束后,加载医生;
  • 第62行:获取医生请求后,分析该请求。若已获取医生,则请求客户;
  • 第83行:分析获取到的响应并显示最终视图。具体实现如下:

Image

  • 第87行:在以下情况下,布尔值[task.isFinished]被设置为true
    • 用户取消了等待;
    • 医生请求以错误结束;
  • 第 91-98 行:已获取客户的情况;
  • 第93行:[app.clients]是指令[list]的模板,该指令将把客户显示在下拉列表中;
  • 第 97-98 行:准备切换视图(第 98 行),但在此之前需先标记控制器已完成工作(第 97 行)。需注意,[$scope.app.view.done] 用于导航控制;

这里需要注意的一点是,医生和客户数据已被缓存到浏览器中。今后将不再向 Web 服务请求这些数据。

3.8.13. 控制器 [homeCtrl]

  

控制器 [homeCtrl] 关联视图 [views/home.html],该视图与主页面结合后生成以下页面:

Image

控制器 [homeCtrl] 的结构如下:

Image

  • 第 12-20 行:这是导航控制。除 [loginCtrl] 外,所有控制器均包含此部分,因为页面 [/login.html] 可在无条件情况下访问;

Image

  • 第25-28行:此处的代码与[loginCtrl]控制器中的类似。因此,[home]是与该控制器关联的视图模板;
  • 第 33 行:这是一个我们尚未见过的属性。它是视图标题栏的模板:

Image

  • 第36行:[home.datepicker]是日历的模板;
  • 第38行:[app.menu]是导航栏菜单的模板。此处将包含选项[Agenda]。该选项用于查询医生的日程安排;

最后,该控制器有两个方法:

Image

日程表的显示(第51行)已在第3.7.8节中进行过说明。

3.8.14. 控制器 [agendaCtrl]

  

控制器 [agendaCtrl] 关联视图 [views/agenda.html],该视图与主页面关联后生成以下页面:

Image

控制器 [agendaCtrl] 的结构如下:

Image

  • 第 10-20 行负责导航控制;

Image

  • 第23-26行:[agenda]将作为与控制器[agendaCtrl]关联的视图模板;
  • 第36-44行:[app.titre]是以下标题栏的模板:

Image

  • 第 46 行:菜单将包含选项 [Home / Accueil]:

Image

控制器的方法如下:

Image

  • 第 95 行:方法 [agenda.supprimer] 已在第 3.7.9 节中讨论;

方法 [agenda.home] 是一个纯导航方法:

Image

方法 [agenda.reserver] 如下:

Image

  • 第 73 行:函数 [reserver] 的参数是时段编号(id);
  • 第77-86行:用于查找具有该标识符的时间段;
  • 第82行:找到的时间段被存入共享内存[app]。即将接管控制的[resaCtrl](第90行)将利用此信息显示其标题栏;
  • 第89-90行:跳转至[/resa.html];

3.8.15. 控制器 [resaCtrl]

  

控制器 [resaCtrl] 关联视图 [views/resa.html],该视图与主页面关联后生成以下页面:

Image

控制器 [resaCtrl] 的结构如下:

Image

  • 第 12-20 行:导航控制;

Image

  • 第24-27行:[resa]将作为当前视图的模板;
  • 第 38-45 行:[app.titre] 是下一标题栏的模板:

Image

  • 第 47 行:显示两个菜单选项:

Image

控制器的方法如下:

Image

方法 [resa.valider] 已在第 3.7.9 节中进行过探讨。

3.8.16. 语言管理

所有控制器均提供以下方法 [setLang]:

Image

该方法本可拆分到控制器 [appCtrl] 中。