3. Angular JS 客户端
3.1. Angular JS 框架参考资料
本文开头已提供了两份关于 Angular JS 框架的参考资料。现将它们再次列出如下:
- [ref1]:由 Adam Freeman 撰写、Apress 出版的《Pro AngularJS》一书。这是一本非常优秀的书籍。书中示例的源代码可通过以下网址免费获取:[http://www.apress.com/downloadable/download/sample/sample_id/1527/];
- [ref2]:AngularJS 官方文档 [https://docs.angularjs.org/guide];
AngularJS 值得拥有一本专属的专著。亚当·弗里曼的这本书长达 600 多页,且每一页都物有所值。我们将描述一个 Angular 应用程序,并在描述过程中探讨该框架的基础知识。不过,我们将仅限于解释理解所提出的解决方案所必需的内容。 Angular 是一个功能极其丰富的框架,实现相同结果的方法多种多样。这可能会带来挑战,因为当你刚刚入门时,无法判断自己采用的方案是优于还是劣于其他方案。本文介绍的解决方案也是如此。它本可以采用不同的写法,甚至可能采用更优的实践方式。
3.2. Angular 客户端架构
Angular 的客户端架构与经典 MVC Web 应用程序相似,但存在一些差异。例如,Spring MVC Web 应用程序具有以下架构:
![]() |
客户端请求的处理流程如下:
- 请求 - 请求的 URL 格式为 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] 是 Spring 框架中负责处理传入 URL 的类。它会将 URL “路由”到应处理该请求的操作。这些操作是称为 [控制器] 的特定类中的方法。 此处的 MVC 中的 C 代表 [Dispatcher Servlet、Controller、Action] 这一链条。如果未配置任何 Action 来处理传入的 URL,[Dispatcher Servlet] 将返回请求的 URL 未找到(404 NOT FOUND 错误);
- 处理
- 被选中的 Action 可以使用 [Dispatcher Servlet] 传递给它的参数。这些参数可能来自多个来源:
- URL 的路径 [/param1/param2/...],
- URL 参数 [p1=v1&p2=v2],
- 浏览器随请求提交的参数;
- 在处理用户请求时,操作可能需要调用 [业务] 层 [2b]。一旦客户端的请求被处理完毕,可能会触发各种响应。一个典型的例子是:
- 若请求无法正确处理,则返回错误页面
- 否则则显示确认页面
- 操作会指示显示特定的视图 [3]。该视图将展示被称为视图模型的数据。这就是 MVC 中的 M。操作将创建这个 M 模型 [2c],并指示显示 V 视图 [3];
- 响应——选定的视图 V 使用操作生成的模型 M 来初始化其必须发送给客户端的 HTML 响应中的动态部分,然后发送该响应。
我们的 Angular 客户端架构与此类似,只是术语略有不同。首先,Angular 应用程序通常是单页 Web 应用程序(SPA):

- 用户通过以下形式请求应用程序的初始 URL:http://machine:port/contexte。浏览器向 Web 服务器发出请求以获取所请求的文档。这是一个通过 CSS 进行样式设置并由 JavaScript 实现动态化的 HTML 页面;
- 随后用户与呈现给他们的视图进行交互。我们可以区分多种交互类型:
- 无需与外部源交互的操作,例如隐藏或显示视图元素。这些操作由嵌入的 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] 中,[DAO] 层将发起一次或多次 AJAX 调用以获取数据;
- 在 [8] 和 [9] 中,M1 模型将被修改,并显示视图 V1;
如果事件触发了视图变更,在前两种情况下,控制器 C1 不会显示视图 V1,而是请求一个新的 URL [10]。这是一个浏览器内部的 URL,不会立即向 HTML 页面服务器发起 HTTP 请求。 此 URL 变更由路由器处理,该路由器配置为使每个内部 URL 对应一个视图 V 及其控制器 C。随后路由器显示新视图 Vn。显示前,其控制器 Cn 接管控制,构建模型 Mn,然后显示视图 Vn [11]。如果视图 Vn 的 HTML 页面未缓存于浏览器中,则会向 HTML 页面服务器请求该页面。
该架构的[呈现]层与JSF(Java Server Faces)架构相似:
- 视图 V 对应于 JSF Facelet 视图;
- 控制器 C 对应于 JSF Bean,即一个同时包含视图 V 的模型 M 及其事件处理程序的 Java 类;
[服务]层与我们惯常所见的[服务]层有所不同。在服务器端Web开发中,我们通常采用以下分层架构:
![]() |
在上图中,[Web]层仅通过[业务]层与[DAO]层进行通信。虽然没有任何因素会阻止我们将[DAO]层的引用注入到[Web]层中以实现这种通信,但我们避免这样做。
在 Angular 中,我们并不自我设限。此时架构如下所示:
![]() |
- 在[1]中,[表示]层可与任何服务直接通信;
- 在[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]中,界面切换为英文,包括日历;
3.4. Angular 项目设置
我们将逐步构建我们的 Angular 客户端。我们将使用 WebStorm IDE。
现在创建一个空文件夹 [rdvmedecins-angular-v1],然后用 WebStorm 打开它:
![]() |
- 在 [1] 中,打开一个文件夹;
- 在 [2] 中,选择我们创建的文件夹;
- 在 [3] 中,我们将获得一个空的 WebStorm 项目;
![]() |
- 在 [4] 中,通过 [文件 / 设置] 选项配置项目;
- 在 [5] 和 [6] 中,我们配置 [拼写] 属性,该属性用于管理拼写检查。默认情况下,此功能处于启用状态。由于下载的软件是英文的,程序中的法语注释会被标记为潜在拼写错误。因此,我们禁用此拼写检查 [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] 中,按照之前的步骤,我们下载了以下库:
用于将字符串 "user:password" 编码为 Base64; | ||
用于日历的国际化 | ||
将应用程序的内部 URL 路由到正确的控制器和视图; | ||
支持视图的国际化。这是一个独立于 Angular 的项目。此处将使用两种语言:法语和英语; | ||
提供与 Bootstrap 兼容的视觉组件。此处将使用其日历组件; | ||
Bootstrap CSS 框架。将用于构建视图; | ||
提供了一种“表格”类型的可视化组件。它具有“响应式”特性,能够适应屏幕尺寸; | ||
提供了一个“下拉列表”组件; |
![]() |
- 在 [25] 中,下载的库被安装在 [bower_components] 文件夹中;
- 在[26]中,我们可以看到jQuery库已被下载。这是因为Bootstrap依赖它。项目中安装JavaScript依赖项的机制类似于Java世界中的Maven:如果某个下载的库本身带有依赖项,这些依赖项会自动被下载;
[bower.json] 文件已发生变化:
所有已下载的依赖项均已列在该文件中。
3.5. Angular 客户端的首页
我们创建 Angular 客户端主页的初始版本:
![]() |
- 在 [1] 和 [2] 中,我们创建了一个名为 [app-01] 的 HTML 文件 [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é">
<!-- on 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 core 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>
<!-- angular js -->
<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,则必须在加载 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] 由运行在本地 63342 端口的 WebStorm 内部服务器提供;
- 在 [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 中,从外部源获取信息的操作都是异步的。这意味着操作启动后,控制权会立即返回视图,允许用户继续与之交互。应用程序会通过一个事件收到操作完成的通知。该事件由一个 JavaScript 函数处理,该函数随后可以更新或更改当前视图。如果操作可能需要很长时间,提供给用户取消操作的选项会很有帮助。 我们将系统地提供此选项。为此,我们将使用一个 Bootstrap 横幅:

要实现这一效果,我们将 [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
应用程序的不同视图将拥有一个共同的标题:

为实现此效果,我们将 [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">
<!-- debug mode -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- identification form -->
<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
控制栏将允许您通过下拉列表切换语言:

为实现此功能,我们将 [app-01.html] 复制为 [app-05.html],并在控制栏中添加以下代码行:
<button class="btn btn-success">
Connexion
</button>
<!-- languages -->
<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>
<!-- menu options -->
<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>
<!-- right buttons -->
<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 core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- local script -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- 第 5 行:必须导入 [bootstrap-select] 样式表;
- 第 13 行:[bootstrap-select] 使用了 [data-style] 属性。该属性用于设置下拉列表的样式。在此,我们将其样式设置为蓝色按钮 [btn-primary];
- 第 13 行:第 23 行使用了 [class] 属性。其值可以是任意内容;
- 第 14–17 行:下拉列表的元素。这些是标准的 HTML 标签;
- 第 22 行:必须引入 [bootstrap-select] JS 文件;
- 第 24–26 行:页面加载完成后执行的 JavaScript 脚本;
- 第 25 行:一个 jQuery 语句。我们对所有具有 [selectpicker] 类的元素 ($('.selectpicker')) 应用 [selectpicker] 方法 (selectpicker())。仅有一个元素:第 13 行中的 <select> 标签。[selectpicker] 方法来自第 22 行引用的 JS 文件;
3.6.7. 示例 7
为了显示医生的日程安排,我们将使用 [footable] JS 库提供的响应式表格:
![]() |
- [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";
#creneaux th {
text-align: center;
}
#creneaux 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 行:通过 Bootstrap 对 HTML 表格进行格式化 [class='table'];
- 第 13 行:[data-toggle] 属性指定了包含 [+/-] 符号的列,该符号用于展开/折叠行;
- 第 19 行:[data-hide='phone'] 属性指定当屏幕尺寸为手机屏幕时应隐藏该列。也可使用值 'tablet';
3.6.8. 示例 8
为了辅助用户,我们将在视图的主要组件周围添加工具提示:
![]() |
为此,我们将 [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>
<!-- menu options -->
<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 core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- local script -->
<script>
// --------------------- module 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]:定义其位置(底部、顶部、左侧、右侧);
Angular JS 允许您在 HTML 现有标签或属性基础上添加新的标签或属性。这种对 HTML 的扩展是通过 Angular 指令实现的。在此,[tooltip] 和 [tooltip-placement] 属性是由 [angular-ui-bootstrap] 创建的。
3.6.9. 示例 9
为了帮助用户选择预约日期,我们将提供一个日历:

与工具提示类似,此日历由 [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>
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
</body>
</html>
与之前一样,该页面关联了一个 Angular 模块(第 2 行和第 28 行)。日历由第 16 行中的 <datepicker> 标签定义,该标签由 [angular-ui-bootstrap] 库提供:
- [show-weeks='true']:显示周数;
- [class='well']: 用带圆角的灰色框包围日历;
- [ng-model='day']: [ng-*] 属性是 Angular 属性。[ng-model] 属性指定将被放入视图模型中的数据。当用户点击某个日期时,该日期将被放入模型的 [day] 变量中。第 10 行使用了该变量。{{expression}} 语法用于求值由模型中的元素组成的表达式。 在此,{{day}} 将显示模型中 [day] 变量的值。Angular 的一个关键特性是,视图会根据 [day] 变量的变化自动更新。因此,当用户更改日期时,这些更改将立即显示在第 10 行。一般而言,该过程的工作原理如下:
- 视图 V 与模型 M 相关联;
- Angular 监听模型 M,并在模型 M 发生变化时自动更新视图 V;
语法 {{day|date}} 被称为过滤器。显示的并非 [day] 的原始值,而是经过名为 [date] 的过滤器处理后的 [day] 值。该过滤器在 Angular 中是预定义的,用于格式化日期,并接受指定所需格式的参数。 因此,表达式 {{day | date:'fullDate'}} 表示我们希望采用完整日期格式,即 [2014年6月20日 星期五],因为日历默认使用英语。我们稍后将讨论其国际化问题。
3.6.10. 结论
我们已经介绍了将要使用的 Bootstrap CSS 框架的元素。这些都是被动组件:它们的事件尚未被处理。因此,点击按钮或链接不会产生任何效果。这些事件将在 JavaScript 中进行处理。虽然可以在不使用框架的情况下使用该语言,但正如服务器端的情况一样,客户端也需要某些框架。AngularJS 框架就是如此,它为开发由浏览器运行的 JavaScript 应用程序带来了新的方法。 接下来我们将对其进行介绍。
3.7. AngularJS 简介
接下来我们将演示该应用程序中使用的 AngularJS 框架的部分特性。其中一些我们已经接触过:
- 如果某个模块被附加到 HTML 页面上,则该页面由 AngularJS 驱动:
<html ng-app="rdvmedecins">
- Angular 允许您使用指令创建新的 HTML 标签和属性:
- Angular 允许您创建过滤器:
- 视图 V 显示模型 M。Angular 会监听模型 M,并在模型 M 发生任何变化时自动更新视图 V。模型 M 中变量的值可通过以下方式在视图 V 中显示:
我们将首先深入探讨 Angular 中 Model–View–Controller 设计模式的实现。让我们从架构角度回顾它们之间的关系:
![]() |
- 视图 V1 显示由控制器 C1 构建的模型 M1。控制器 C1 不仅包含模型 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 core JavaScript ================================================== -->
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// contrôleur
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// date minimale
$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)]。最后一个参数是实现该控制器的函数。其参数是 AngularJS 将提供给该函数的对象。
让我们回到 Angular 应用程序的架构:
![]() |
在上文中,C1 控制器包含 V1 视图的所有事件处理程序及其 M1 模型。这些事件处理程序可能需要一个或多个服务 [6] 来完成其任务。我们将所有这些作为参数传递给控制器的构造函数:
Si 服务是单例。Angular 会为每个服务创建一个实例。它们通过 Si 名称进行标识。为什么它们在上表中出现了两次?在生产环境中,JS 脚本会被压缩。在压缩过程中,上表会变为:
参数失去了原有名称。然而,这些正是服务名称。因此保留这些名称至关重要。这就是为什么它们作为字符串参数传递在函数之前。字符串在压缩过程中不会被修改。当 Angular 使用新数组构建控制器时,它会将 a1 替换为 S1,a2 替换为 S2,以此类推。因此参数的顺序非常重要。它必须与函数定义前面的服务顺序一致。
让我们回到控制器 [rdvMedecinsCtrl] 的定义:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// minimum date
$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 页面可以依赖多个控制器,无论这些控制器是否相互嵌套:
上文:
- [div1] 的内容(第 1–10 行)显示由控制器 c1 管理的模板 M1。该区域中的标签可以引用控制器 c1 的事件处理程序;
- [div11] 的内容(第 3–4 行)同时显示由控制器 c11 管理的 M11 模型以及 M1 模型。 存在模型继承关系。该区域内的标签既可引用控制器 c11 的事件处理程序,也可引用控制器 c1 的事件处理程序。但它们既不能引用控制器 c12 的 M12 模型,也不能引用其事件处理程序。控制器 c12 在第 3–5 行之间未被定义;
- 第 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">
<!-- the calendar-->
<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>
<!-- languages -->
<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 module
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 行展示了法语日期使用的“dd/mm/yy”格式;
- 第 65–95 行:数字格式化信息。此处不涉及;
- 第 96 行:文件的区域设置标识符“fr-fr”(fr-fr:法国法语,fr-ca:加拿大法语,……)
在文件 [angular-locale_en-us.js] 中,内容完全相同,但这次针对的是美式英语(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 行:模型中的 [day] 变量初始化为今天的日期。这确保日历显示时日期默认设置为今天;
- 第 223–230 行:定义在语言切换时调用的事件处理程序。请注意语法:
用于定义一个名为 [function_name] 的事件处理程序,该处理程序接受参数 [param1, param2, ...];
让我们回顾一下下拉列表的 HTML 代码:
<!-- languages -->
<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,以便日历能够刷新并使用新的区域设置。 在此,我们修改日历模型中的 [day] 变量。我们使用一个指向与当前显示日期完全相同的新日期的新指针(new)来初始化它。[$scope.day.getTime()] 表示从 1970 年 1 月 1 日到日历显示的日期之间经过的毫秒数。利用这个数值,我们重建一个新的日期。 当然,我们会得到相同的日期,日历仍将停留在原先显示的日期上。但 [$scope.day] 的值(实际上是一个指针)已经改变,日历将随之刷新;
- 第 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] 中,我们可以看到日历界面显示为英文,但 [Calendar, Languages] 中的文本并非如此。默认情况下,Angular 并未提供用于消息国际化的工具。在此,我们将使用 [angular-translate] 库(https://github.com/angular-translate/angular-translate)。
我们将开发以下示例:
![]() |
- 在[1]中,法语版本;
- 在[2]中,英文版本;
让我们来看看国际化所需的配置。对 [rdvmedecins.js] 脚本进行了如下修改:
// --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuration i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// messages français
$translateProvider.translations("fr", {
'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
'msg_langues': 'Langues',
'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
'msg_calendrier': 'Calendrier',
'msg_jour': 'Jour sélectionné : ',
'msg_meteo': "Aujourd'hui, il va pleuvoir..."
});
// messages anglais
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Languages',
'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
'msg_calendrier': 'Calendar',
'msg_jour': 'Selected day: ',
'msg_meteo': 'Today, it will be raining...'
});
// langue par défaut
$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 行),
- 第二个参数是翻译列表,采用字典形式 {'key1':'msg1', 'key2':'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">
<!-- a first text with HTML elements in it -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- a second text with parameters -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- a third text translated by the controller -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- the calendar-->
<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>
<!-- languages -->
<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={{dictionary]}}](第 5 行),适用于带或不带 HTML 元素且包含参数的消息;
- 语法 [{{'msg_key'|translate}}](第 9、13、23 行)适用于既不包含参数也不包含 HTML 元素的消息;
让我们来看看此视图中的不同消息:
诊所<br/>联合医生 | 联合医生 | |
日历 | 日历 | |
语言 | 语言 | |
所选日期: | 所选日期: |
现在我们来看看第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] 对象定义如下:

- 第 245 行:[msg] 对象的定义;
- 第 245 行:[text] 字段的值为 [msg_agenda],该字段关联了两个值:
- {{title}} {{first_name}} {{last_name}} 的日记<br/>于 {{day}} 在法语词典中;
- {{title}} {{first_name}} {{last_name}} 的日记<br/> 发布于 {{day}}(英语词典);
因此,待显示的消息包含四个参数 [title, first_name, last_name, day];
- 第 245 行:[model] 字段是一个字典,用于为这四个参数赋值。其中 [day] 参数存在问题。我们需要显示该日期的完整名称,而这取决于语言是法语还是英语。 因此,我们使用已在视图中用过的 [date] 过滤器,形式为 {{ day | date:'fullDate'}}。任何过滤器都可以在 JavaScript 代码中以 $filter('filter')(value, options) 的形式使用,其中 $filter 是预定义的 Angular 对象,'filter' 是过滤器的名称;
- 第 33–34 行:将预定义的 $filter 对象作为参数传递给控制器,以便在第 245 行中使用它;
让我们回到显示视图中的另一行代码:
<!-- un troisième texte traduit par le contrôleur -->
<h3 class="alert alert-danger">{{msg2}}</h3>
之前的所有翻译都是在视图中使用 [pascalprecht.translate] 模块的属性完成的。我们也可以选择在服务器端进行翻译。这里就是这样做的。在控制器中(如上图所示的第 247 行),我们有以下代码:
$scope.msg2 = $filter('translate')('msg_meteo');
由于 'translate' 同样是一个过滤器,因此我们使用了与 'date' 过滤器相同的语法。在此,我们请求键名为 'msg_meteo' 的消息。
让我们来探讨一下语言切换的机制。我们看到,[rdvmedecins] 模块中的 [config] 函数已将法语设为默认语言(如下第 9 行):
// i18n configuration
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {...});
// english messages
$translateProvider.translations("en", {...});
// default language
$translateProvider.preferredLanguage("fr");
}]);
请注意,默认区域设置也是法语。在 [rdvmedecins] 控制器的初始化中,我们写道:
// we put the locale in French
angular.copy(locales['fr'], $locale);
- 第 2 行:[locales] 是我们创建的一个字典;
[pascalprecht.translate] 模块提供的消息国际化功能与我们实现的日期本地化之间没有任何关联。后者使用了一个 $locale 变量,而 [pascalprecht.translate] 模块并未使用该变量。这两个过程是相互独立的。
现在,让我们看看当用户更改语言时会发生什么:

- 第 251 行:当语言发生变化时,会调用 [setLang] 函数,并传入两个参数 ['fr'、'en'] 中的一个;
- 第 252–257 行:已在前文解释过——它们修改了日历的 [$locale] 变量。这不会影响翻译的语言;
- 第 259 行:我们更改翻译语言。我们使用 [pascalprecht.translate] 模块提供的 [$translate] 对象。为此,我们需要将其注入控制器:
// controller
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]中,日期仍显示为法语,而视图的其余部分则为英语;
- 在[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] 属性并非如此。因此,我们可以在控制器中强制进行此评估:
// ------------------- evts manager
// language change
$scope.setLang = function (lang) {
...
// update msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// and msg day
$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] 函数处理。该函数如下所示:

- 第 267 行:日历点击处理程序;
- 第 269 行:我们使用 [msg] 消息强制更新显示的日期;
3.7.4. 示例 4:配置服务
让我们重新审视一下 AngularJS 应用程序的架构:
![]() |
在此,我们将重点探讨“服务”这一概念。这是一个相当宽泛的概念。虽然上文提到的 [DAO] 层显然属于服务,但任何 Angular 对象都可以成为服务:
- 服务遵循特定的语法规范。它拥有一个名称,Angular 通过该名称来识别它;
- Angular 可以将服务注入到控制器和其他服务中;
我们在 [rdvmedecins] 模块中配置的部分服务需要进行配置。由于服务可以被注入到另一个服务中,因此我们倾向于在一个名为 [config] 的服务中进行配置,并将该服务注入到需要配置的服务和控制器中。下面我们将描述这一过程。
我们将 [app-13.html] 复制为 [app-14.html],并进行以下修改:
<div class="container">
<!-- waiting msg control -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- the waiting message -->
<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] 脚本的重新组织:

- 第 6 行:应用程序的 [rdvmedecins] 模块;
- 第 9-10 行:应用程序的配置函数;
- 第 38-39 行:[config] 服务;
- 第 283-284 行:[rdvMedecinsCtrl] 控制器;
此前,我们在长达 200 行的控制器中定义了字典 locales={'fr':..., 'en': ...}。该字典显然属于配置元素,因此我们将它移至第 38–39 行的 [config] 服务中。该服务的定义如下:

- 第 38-39 行:使用 [angular.module] 对象的 [factory] 函数创建一个服务。 该函数的语法与前面的函数相同:factory('service_name', ['O1', 'O2', ..., 'On', function (O1, O2, ..., On){...}]),其中 O1 到 On 是 Angular 所识别的对象名称(预定义或由开发者创建),Angular 会将它们作为参数注入到 factory 函数中。 由于此处的函数没有参数,我们使用了更简短但同样有效的语法:factory('service_name', function (){...}]);
- 第 40 行:[factory] 函数必须通过返回的对象来实现服务。该对象即为服务本身。这就是该函数被称为 factory(对象创建工厂)的原因;
通常,服务代码采用以下形式:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// service preparation
...
// render the object implementing the service
return {
// fields
...
// methods
...
}
});
- 第 6 行:我们返回一个 JavaScript 对象,该对象既可以包含字段,也可以包含方法。正是这些方法负责处理服务;
在此,[config] 服务仅定义了字段,未定义方法。我们将应用程序中所有可配置的内容都放在这里:
- 第 42–47 行:待翻译消息的键;
- 第 59–62 行:应用程序的 URL;
- 第 64–69 行:远程 Web 服务的 URL;
- 第 71 行:对未响应的 Web 服务进行 HTTP 调用可能需要很长时间。此处,我们将 Web 服务的最大响应等待时间设置为 1 秒。超过此时间后,HTTP 调用将失败并抛出 JavaScript 异常;
- 第 73 行:每次调用服务器之前,我们将模拟一个等待,其持续时间在此以毫秒为单位设置。等待时间为 0 表示不等待。应用程序将设计为允许用户取消已发起的操作。为了使其可取消,操作必须至少持续几秒钟。我们将利用这个人工等待来模拟长时间运行的操作;
- 第 75 行:在 [debug=true] 模式下,当前视图中会显示额外信息。默认情况下,此模式处于启用状态。在生产环境中,我们会将此字段设置为 false;
- 第 77–278 行:用于两个语言环境 'fr' 和 'en' 的字典。该字典此前位于 [rdvMedecinsCtrl] 控制器中;
借助此服务,[rdvMedecinsCtrl] 控制器将演变为如下形式:

- 第 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">
<!-- the waiting message -->
<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>
<!-- the form -->
<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] 内容如下:

- 第 6 行:管理应用程序的 Angular 模块;
- 第 10 行:用于消息国际化的 [config] 函数;
- 第 41 行:我们之前描述过的 [config] 服务;
- 第 286 行:我们将要构建的 [utils] 服务;
- 第 315 行:我们将要构建的 [rdvmedecinsCtrl] 控制器;
我们在 [config] 函数中添加了一个新的消息键(第 6 行、第 11 行):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': "Temps d'attente : "
});
// english messages
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Waiting time:"
});
// default language
$translateProvider.preferredLanguage("fr");
}]);
我们在 [config] 服务中为该消息键添加了一行(第 6 行):
angular.module("rdvmedecins")
.factory('config', function () {
return {
// messages to be internationalized
...
waitingTimeText: 'msg_waiting_time_text',
[utils] 服务包含两个方法(第 4 行、第 12 行):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// display the Json representation of an object
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// waiting
function waitForSomeTime(milliseconds) {
// asynchronous waiting milliseconds milliseconds
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// we return the task
return task;
};
// service authority
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- 第 2 行:该服务名为 [utils](第一个参数)。它依赖于三个服务:两个预定义的 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:

- 通过语句 [$q.defer()] 创建一个异步任务 [task];
- 该任务可通过以下两种方法之一完成:
- [task.resolve(value)]:成功完成任务,并将值 [value] 返回给等待任务完成的用户;
- [task.reject(value)]:以错误终止任务,并将值 [value] 返回给等待任务完成的用户;
任务 [task] 可以定期向等待其完成的用户提供信息:
- [task.notify(value)]:将值 [value] 发送给等待任务完成的用户。任务继续运行;
希望等待任务完成的用户可使用其 [promise] 字段:
[promise] 对象具有以下 API(http://www.frangular.com/2012/12/api-promise-angularjs.html):

为了同时处理任务的成功和失败,我们编写如下代码:
- 第 1 行:我们获取任务的 Promise;
- 第 2 行:我们定义了在成功或失败时要执行的函数。我们可以选择不包含失败函数。[successCallback] 函数仅在 [task] 成功完成时执行 [task.resolve()]。[errorCallback] 函数仅在 [task] 失败时执行 [task.reject()]。
- 第 3 行:我们定义在前两个函数中的任一个执行完毕后要执行的函数。这里,我们放置了 [successCallback] 和 [errorCallback] 两个函数共有的代码。
让我们回到 [waitForSomeTime] 函数的代码:
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- 第 4 行:创建一个任务;
- 第 5–7 行:[$timeout] 对象允许您定义一个函数(第一个参数),该函数将在以毫秒为单位的指定延迟(第二个参数)后执行。在此,[$timeout] 函数的第二个参数即为该方法的参数(第 1 行);
- 第 6 行:经过 [milliseconds] 毫秒的延迟后,任务成功完成;
- 第 9 行:返回任务 [task]。这里需要特别注意的是,第 9 行是在 [$timeout] 对象定义完成后立即执行的。我们不会等待 [milliseconds] 延迟时间过去。因此,第 2 至 10 行的代码会在两个不同的时间点执行:
- 第一次是在定义 [$timeout] 对象时;
- 第二次是在超时 [milliseconds] 时间过去之后;
这是一个异步函数:其结果是在执行之后才获得的。
使用 [config] 服务的控制器代码如下:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- model initialization
// waiting message
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// waiting task
var task;
// logs
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// execution action
$scope.execute = function () {
// log
utils.debug('début', new Date());
// the waiting msg is displayed
$scope.waiting.visible = true;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
}
}]);
- 第 3 行:控制器使用了 [config] 服务;
- 第 7 行:我们在 [$scope.waiting] 对象中添加了 [time] 字段。[$scope.waiting.time] 对象接收用户设置的等待时间值;
- 第 8 行:视图中显示的等待消息的键被放置在 [$scope.waitingTimeText] 模型中。通常,V 视图显示的所有内容都必须放置在 [$scope] 对象中;
- 第 10 行:一个局部变量。它不会暴露给 V 视图;
- 第 12-13 行:调用 [config] 服务的 [debug] 方法。控制台将显示以下结果:
第 2 行:我们获取 locales['fr'] 对象的 JSON 表示形式。
- 第 16 行:用户点击 [Execute] 按钮时执行的方法;
- 第 18 行:显示该方法的执行开始时间;
- 第 22 行:启动 [waitForSomeTime] 任务。我们不等待其完成,而是继续执行后续的第 24 行;
- 第 24–30 行:定义任务成功完成时(第 26 行)和发生错误时(第 29 行)要执行的函数;
- 第 26 行:显示方法执行的结束时间;
- 第 29 行:显示操作已被取消。这仅在用户点击 [Cancel] 按钮时发生。随后第 41 行的指令会以失败代码终止该异步任务;
- 第 31–34 行:定义在前两个函数中的任一个执行完毕后需执行的函数;
理解此代码的执行顺序至关重要。如果用户设置了 3 秒延迟且未取消等待:
- 当用户点击 [执行] 按钮时,[$scope.execute] 函数将运行。 第16–34行会立即执行,无需等待3秒。执行完毕后,视图V与模型M同步。此时显示等待提示(ng-show=$scope.waiting.visible=true,第20行),并隐藏表单(ng-hide=$scope.waiting.visible=true,第20行);
- 从此时起,用户可以再次与视图进行交互。特别是,他们可以点击 [Cancel] 按钮;
- 如果用户未进行操作,3 秒后,[$timeout] 函数(参见下文第 5–7 行)将执行:
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- 3 秒后,代码被执行。该代码以成功代码(resolve)完成了任务 [task]。这将触发所有等待此任务完成的代码的执行(如下文第 4 行):
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
- 因此,上面的第 6 行(成功完成)将被执行。随后将执行第 11–14 行。该代码执行完毕后,我们将返回 V 视图,该视图随后将与其 M 模型进行同步。 等待消息被隐藏(ng-show=$scope.waiting.visible=false,第13行),表单被显示(ng-hide=$scope.waiting.visible=false,第13行);
此时屏幕显示如下:
如上所示,等待的开始和结束之间存在3秒的延迟(06:01–05:58)。反之,如果用户在3秒结束前取消等待,则会显示以下内容:
最后,需要理解的是,在任何给定时刻,系统中仅存在一个执行线程,即所谓的 UI(用户界面)线程。异步任务的完成会通过一个事件来触发,就像按钮点击一样。该事件不会被立即处理,而是被放入等待执行的事件队列中。 等到轮到它时,才会被处理。此处理过程使用 UI 线程,因此在此期间,界面会冻结,无法响应用户输入。正因如此,事件处理速度至关重要。由于每个事件都由 UI 线程处理,因此无需解决同时运行的线程之间的同步问题。在任何给定时刻,只有 UI 线程在执行。
3.7.6. 示例 6:HTTP 服务
现在我们介绍与 Web 服务器通信的 [dao] 服务:
![]() |
3.7.6.1. V 视图
![]() |
我们将编写一个表单来请求医生列表:

我们将 [app-01.html] 复制为 [app-16.html],然后按以下方式进行修改:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<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>
<!-- the request -->
<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>
<!-- list of doctors -->
<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>
<!-- the error list -->
<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(第 16 行)、server.url(第 20 行)、server.login(第 24 行)、server.password(第 28 行)];
- 第 34–39 行:显示医生列表。该列表并非始终可见(ng-show="medecins.show")。
- 第 35 行:这是之前已出现的 <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> 语法的另一种写法;
- 第 36 行:一个无序列表;
- 第 37 行:医生列表位于 [medecins.data] 模型中。Angular 指令 [ng-repeat] 允许遍历列表。语法 ng-repeat="doctor in medecins.data" 指示 <li> 标签针对 [medecins.data] 列表中的每个元素进行重复。列表中的当前元素称为 [medecin];
- 第 37 行:对于每个 <li>,我们显示由变量 [medecin] 指定的当前医生的头衔、名字和姓氏;
- 第 42–47 行:显示错误列表。该列表并非始终可见(ng-show="errors.show")。其显示方式与医生列表的显示模式相同。通常,要显示对象列表,我们会使用 Angular 指令 [ng-repeat];
- 第 51 行:JavaScript 代码现已移至 [rdvmedecins-04] 文件中
3.7.6.2. C控制器和M模型
![]() |
JavaScript 代码更改如下:

- 第 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) {
// ------------------- model initialization
// model
$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;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.medecins.data = result.data;
// the UI is updated
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the list of doctors
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$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.doctors] 将收集显示医生列表所需的信息:
<!-- list of doctors -->
<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] 将收集显示错误列表所需的信息:
<!-- the error list -->
<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 行:用户点击 [医生列表] 按钮时执行的方法:
<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 服务的根 URL [$scope.server.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, ...]},其中 [med] 是一个表示医生的对象(头衔、名字、姓氏),
- {err: n, messages: [msg1, msg2, ...]},其中 [msg] 是错误消息,且 n 不等于 0;
- 第 31 行:我们返回任务的 Promise。这里有几点需要理解。我们有两个 Promise:
- promise.then():返回第一个 Promise [promise1];
- return task.promise:返回第二个 Promise [promise2];
- 最终,promise = promise.then(...; return task.promise) 构成两个 Promise 的链式结构 [promise2.promise1]。[promise1] 仅在 Promise [promise2] 解析后才会被求值,即当任务 [dao.getData] 完成后。 该 Promise [promise1] 不依赖于任何异步任务。因此它将立即解析;
- 第 34–50 行:根据前面的解释,这些行只有在任务 [dao.getData] 完成后才会被执行。第 34 行传递给函数的参数 [result] 由方法 [dao.getData] 构建,并通过操作 [task.resolve(result)] 传递给调用代码,其中 [result] 具有以下形式:
- {err: 0, data: [med1, med2, ...]} 其中 [med1] 是一个表示医生的对象(头衔、名字、姓氏),
- {err: n, messages: [msg1, msg2, ...]} 其中 [msg1] 是一条错误消息,且 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.then] 始终返回且始终已解决的那个 Promise,
- 事件处理完毕后,控制权返回视图 V,浏览器继续处理下一个事件。由于第 28 行的 [promise] 已被解析,第 34 行的 [promise] 也将被解析,从而触发一个新事件。该事件随后被处理;
- 由于第 34 行使用的 Promise 已履行,第 34–49 行将依次执行。同样,由于未为失败情况定义函数,因此没有代码被执行,
- 因此我们到达第 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] 方法提供。该方法如下:
// error analysis in server response JSON
function getErrors(data) {
// data {err:n, messages:[]}, err!=0
// errors
var errors = [];
// error code
var err = data.err;
switch (err) {
case 2 :
// not authorized
errors.push('not_authorized');
break;
case 3 :
// forbidden
errors.push('forbidden');
break;
case 4 :
// local error
errors.push('not_http_error');
break;
case 6 :
// document not found
errors.push('not_found');
break;
default :
// other cases
errors = data.messages;
break;
}
// if no msg, we put one
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// return the list of errors
return errors;
}
- 第 2-3 行:接收到的 [data] 参数是一个具有两个属性的对象:
- [err]:一个错误代码;
- [messages]:一条消息列表;
- 第 5 行:我们将构建一个错误消息数组。这些消息是国际化的。因此,我们放入数组中的不是消息本身,而是它们的国际化键,第 27 行除外。在此情况下,我们使用 [data] 参数的 [messages] 属性。这些是实际的消息,而非消息键。然而,视图 V 会将其视为消息键,从而导致无法找到。 在此情况下,[translate] 模块会显示其未找到的消息键——即实际消息内容。这正是预期结果;
- 第 32–34 行:处理第 27 行中 [data.messages] 为空的情况。这种情况在编写的 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) {
// logs
utils.debug("[dao] init");
// ----------------------------------méthodes privées
// obtain data from the web service
function getData(serverUrl, username, password, urlAction, info) {
// asynchronous operation
var task = $q.defer();
// url request HTTP
var url = serverUrl + urlAction;
// basic authentication
var basic = "Basic " + $base64.encode(username + ":" + password);
// the answer
var réponse;
// all http requests must be authenticated
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// query 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);
// the task itself is returned so that it can be cancelled
return task;
// success
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} or {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// answer
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// we return the answer
task.resolve(réponse);
}
// failure
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// status analysis
var status = response.status;
var error;
switch (status) {
case 401 :
// unauthorized
error = 2;
break;
case 403:
// forbidden
error = 3;
break;
case 404:
// not found
error = 6;
break;
case 0:
// local error
error = 4;
break;
default:
// something else
error = 5;
}
// we return the answer
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- service instance [dao]
return {
getData: getData
}
}]);
- 第 77-79 行:该服务仅有一个字段:[getData] 方法,用于从 Web 服务 / JSON 中检索信息;
- 第 2 行:出现了一个我们尚未接触过的 [$http] 依赖项。这是一个预定义的 Angular 服务,用于实现与远程实体的 HTTP 通信;
- 第 6 行:一条日志,用于查看代码在应用程序生命周期的哪个阶段被执行;
- 第 10 行:[getData] 方法接受五个参数:
- [serverUrl]:Web 服务的根 URL(http://localhost:8080);
- [urlAction]:所请求的具体服务 URL(/getAllMedecins);
- [username]:用户的登录名;
- [password]:用户的密码;
- [info]:当通过 POST 操作访问所请求的具体服务 URL 时,包含附加信息的对象。对于 URL (/getAllMedecins),此参数未被传递,因此其值为 [undefined];
- 第 12 行:创建了一个异步任务;
- 第 14 行:请求服务的完整 URL(http://localhost:8080/getAllMedecins);
- 第 16 行:通过发送以下 HTTP 头部进行身份验证:
其中 [code] 是 [用户名:密码] 的 Base64 编码字符串;
第 16 行构建了 HTTP 头中的 [Basic 代码] 部分;
- 第 18 行:Web 服务响应;
- 第 20 行:Angular 在 HTTP 请求中默认发送的 HTTP 头部定义在 [$http.defaults.headers.common] 对象中。[Authorization:Basic code] 头部未包含在内;
- 第 21 行:我们将它添加到将要发送的 HTTP 头部中。赋值表达式的左侧是要初始化的 [Authorization] 头部,右侧则是该头部的值——在本例中,即第 16 行定义的值。因此,如果我们写:
Angular 将发送该 HTTP 头部:
- 第 23 行:[$http] 服务的方法返回 Promise。它们将被存储在 [promise] 变量中;
- 第 27 行:由于此处的 [info] 参数值为 [undefined],因此执行第 27 行。使用 GET 请求向 URL(http://localhost:8080/getAllMedecins)发送请求。为避免等待时间过长,我们设置了接收服务器响应的最大超时时间。默认情况下,该超时时间为一秒;
- 第 29 行:我们定义了两个在 Promise 履行时执行的方法:
- [success]:定义在第 34 行,是任务成功完成后 Promise 解析时执行的方法;
- [failure]:定义在第 45 行,当任务失败导致 Promise 解析时执行的方法;
- 这两个方法(准确地说应为函数)定义在 [getData] 函数内部。这是 JavaScript 允许的。在 [getData] 中定义的变量可在两个内部函数 [success] 和 [failure] 中访问;
- 第 31 行:我们返回第 12 行创建的任务。在此,我们必须回顾调用代码:
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
上文第 3 行用于获取一个任务。
- 第 34 行:[success] 函数将在稍后执行,即 HTTP 请求成功完成后。这种“成功”的概念与 HTTP 响应的第一行相关联。其形式为:
状态码是一个三位数字,用于指示请求是否成功。通常来说,2xx 和 3xx 代码表示成功,其余代码则表示失败。文本部分是一条简短的说明信息。以下是两种可能的响应示例,分别对应成功和失败情况:
- 第 36 行:服务器响应显示在控制台上。在 [404 未找到] 错误中,我们会看到类似以下内容:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
在此响应中,我们仅使用 [data]、[status] 和 [statusText] 字段。
- 第 38 行:我们从响应中提取 [data] 字段。它将呈现以下其中一种形式:
- {status: 0, data: [med1, med2, ...]},其中 [med1] 是一个表示医生的对象(头衔、名字、姓氏),
- {status: n, data: [msg1, msg2, ...]},其中 [msg1] 是一条错误消息,且 n 不等于 0;
![]() |

- 第 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, ...]},其中 [msg1] 表示一条错误消息,且 n 不等于 0;
此代码必须与控制器调用代码中检索此响应的方式相关联:
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
...
}
[task.resolve(response)] 的响应存储在上面的 [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 行:我们通过返回类型为 {err, messages} 的响应来成功完成任务(task.resolve),其中 [messages] 数组仅包含 [response.statusText] 消息。如果 Angular 未执行请求的 HTTP 调用,则该数组将为空字符串;
现在我们已经对应用程序有了整体和详细的了解,可以开始测试了。
3.7.6.5. 应用程序测试 - 1
让我们从有效输入开始:

![]() |
- 在[1]中,我们输入0以避免任何延迟;
- 在 [2] 中,尽管输入正确,我们仍会收到一条错误信息。我们尚未讲解过各种错误信息。在 [2] 中显示的是一条与错误代码 0 相关的通用信息,这对应于一个 Angular 异常。Angular 遇到了一个问题,导致它无法发出 HTTP 请求。在这种情况下,你需要检查 JavaScript 控制台日志。有两种方法可以做到这一点:
- 在 Chrome 浏览器中按 [F12];
- 使用 WebStorm 控制台;
在 WebStorm 控制台中,我们会发现各种消息,包括以下这条:
- 第 1 行:Angular 报告了一个错误,我们稍后会再回来处理;
- 第 2 行:[dao.getData] 方法的日志。这里有一些值得注意的细节:
- [status] 为 0,表明未发出任何 HTTP 请求。因此,[statusText] 为空,
- [url] 等于 [http://localhost:8080/getAllMedecins],这是正确的;
- HTTP 身份验证头 [Authorization":"Basic YWRtaW46YWRtaW4=] 也正确;
那么,为什么它没有成功呢?日志中的关键信息是 [未发现 'Access-Control-Allow-Origin' 头部]。要理解这一点,需要进行一番详细说明。让我们先回顾一下客户端/服务器应用程序的一般架构:

- Angular 应用程序的 HTML/CSS/JS 页面来自服务器 [1];
- 在 [2] 中,[dao] 服务向另一台服务器(服务器 [2])发起请求。然而,由于这存在安全漏洞,运行 Angular 应用程序的浏览器会阻止该请求。该应用程序只能查询其来源服务器,即服务器 [1];
实际上,说浏览器阻止了 Angular 应用程序查询服务器 [2] 并不准确。它实际上是向服务器 [2] 发送请求,询问其是否允许非本域的客户端进行查询。这种共享技术被称为 CORS(跨源资源共享)。 服务器 [2] 通过发送特定的 HTTP 头部来授予权限。正是因为我们的服务器 [2] 没有发送这些头部,浏览器才拒绝执行应用程序请求的 HTTP 请求。
现在让我们深入探讨细节。让我们检查 HTTP 请求过程中发生的网络流量。为此,在 Chrome 浏览器中,我们按 [F12] 打开开发者工具,并选择 [网络] 选项卡来查看网络流量:
![]() |
- 在[1]中,我们选择[网络]选项卡;
- 在 [2] 中,我们请求医生列表;
在[网络]选项卡中,我们获取到以下信息:
![]() |
- 在 [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. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
因此,我们必须修改服务器,使其发送预期的 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 {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuration data
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;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// list of doctors
@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 会通过默认处理机制自行处理 [OPTIONS] HTTP 请求。因此,响应始终由 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 configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- 第 8 行:该类是一个 Spring 配置类。它声明了将被放入 Spring 上下文中的 Bean;
- 第 12 行:[dispatcherServlet] Bean 用于定义处理客户端请求的 Servlet。其类型为 [DispatcherServlet]。该 Servlet 通常默认自动创建。如果我们自行创建,则可以对其进行配置;
- 第 14 行:我们创建了一个 [DispatcherServlet] 类型的实例;
- 第 15 行:我们指示该 Servlet 将 [OPTIONS] HTTP 命令转发给应用程序;
- 第 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 客户端检索医生列表。我们在 [网络] 选项卡中查看网络流量:
![]() |
- 在 [1] 中,我们可以看到服务器响应中现在包含了 HTTP 头部 [Access-Control-Allow-Origin: *]。然而,它仍然无法正常工作。我们在 [2] 中检查控制台日志。在那里,我们发现了以下日志:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Request header field Authorization is not allowed by Access-Control-Allow-Headers
我们可以看到,浏览器正在期待一个新的 HTTP 头部 [Access-Control-Allow-Headers],该头部将告知浏览器我们有权发送身份验证头部:
这可能是个好兆头。Angular 可能已尝试发送 HTTP GET 请求。不过,由于该请求包含身份验证头,因此它正在检查服务器是否接受该请求。
我们修改 Web 服务器 / JSON 以发送此标头。[RdvMedecinsCorsController] 类的更改如下:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [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 列表;
现在让我们看看其他一些有趣的情况:
我们输入错误的凭据 [登录名, 密码]:
![]() |
我们使用 [user / user] 登录,该用户无权访问该应用程序(仅 [admin] 具有访问权限):
![]() |
这次,错误信息不再是 [身份验证错误],而是 [访问被拒绝]。
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>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- customer list -->
<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>
<!-- the error list -->
<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] 组件定义(第 19 行的 data-style 和 class 属性);
- 第 20 行:使用 [ng-repeat] 指令生成下拉列表中的各种选项。请注意,选项的标签类型为 [Mme Julienne Tatou],而选项的值类型为 [100],其中 100 是所显示客户的 ID;
- 第 34 行:JavaScript 代码已移至新文件 [rdvmedecins-05];
3.7.7.2. C 控制器与 M 模型
文件 [rdvmedecins-05] 中的 JavaScript 代码是从文件 [rdvmedecins-04] 复制而来的:

几乎没有任何变化,除了控制器,它现在已调整为提供客户列表:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- model initialization
// model
$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;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
// style the drop-down list
$('.selectpicker').selectpicker();
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$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] 的一部分)应用于具有 [selectpicker] 类的 DOM 元素 [$('.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;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- 第 29–32 行:[getAllClients] 方法将处理浏览器发送给它的 [OPTIONS] HTTP 请求;
3.7.7.4. 应用程序测试 – 1
现在我们可以开始测试了。启动 Web 服务器,然后在 Angular 表单中输入有效值。我们会得到以下响应:

当 Angular 无法执行所请求的 HTTP 请求时,会显示此错误信息。此时,我们必须在控制台日志中查找原因。在那里,我们发现了以下消息:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
这本以为已经解决的问题。现在让我们查看当时发生的网络流量:

我们可以看到,使用 [OPTIONS] HTTP 方法的 [getAllClients] 操作成功了,但使用 [GET] HTTP 方法的 [getAllClients] 操作被取消了。对 [OPTIONS] 请求的响应如下:

CORS HTTP 头部确实存在。现在让我们检查一下 GET 请求期间的 HTTP 交互:

该 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 请求;
结合第一条消息来看,这意味着 Angular 同样期望 GET 请求携带 CORS 头部。然而,目前我们的 Web 服务仅在 [OPTIONS] HTTP 请求中发送这些头部。现在出现此错误,而查询医生列表时却未出现,这非常奇怪。我无法解释。
因此,我们需要再次修改 Web 服务。
3.7.7.5. 修改 Web 服务 – 2
![]() |
[GET] 和 [POST] 方法由 [RdvMedecinsController] 类处理。我们需要对其进行修改,以便这些方法发送 CORS 头部。具体操作如下:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAllClients(response);
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
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"}
因此,该方法确实接收到了客户列表。在验证了代码之后,我们开始怀疑以下这条指令,因为我们对其并不完全理解:
// on style la liste déroulante
$('.selectpicker').selectpicker();
我们将第 2 行注释掉并重新尝试。随后我们得到以下响应:
![]() |
至此,我们已准确定位了问题所在。正是对下拉列表应用 [selectpicker] 方法导致了此问题。查看出现错误的页面源代码,我们看到如下内容:
![]() |
- 我们发现,在[1]处,下拉列表及其元素确实存在,但未被显示[style='display:none'];
- 在[2]处,[bootstrap select]按钮已显示。下拉列表中的项目本应出现在<ul role='menu'>列表中。但这些项目并不存在,因此我们得到一个空列表。看来当[selectpicker]方法应用于下拉列表时,其内容当时是空的;
在网上搜索解决方案时,我们发现了这个方法。我们将代码替换为:
// on style la liste déroulante
$('.selectpicker').selectpicker();
并添加以下内容:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
[bootstrap-select] 样式是通过 [$timeout] 函数应用的。我们之前已经接触过这个函数,它允许在经过一定延迟后执行某个函数。这里未指定延迟时间,即表示延迟为零。前面的代码将一个事件放入浏览器的事件队列中。当当前事件(点击 [客户列表] 按钮)处理完毕后,V 视图将被显示。 紧接着,浏览器将检查其事件列表。由于延迟为零,[$timeout] 事件将位于列表顶部并被处理。随后,[bootstrap-select] 样式将应用于已填充的下拉列表。让我们看看结果:
![]() |
如果我们再次查看显示页面的源代码,会看到以下内容:
![]() |
之前为空的 [bootstrap-select] 按钮,现在已包含客户列表。
3.7.7.7. 使用指令
在 V 视图的 C 控制器中,我们发现了以下代码:
// on style la liste déroulante
$('.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] 指令可在以下上下文中使用:
在上例中,[select-enable] 属性将 [selectEnable] 指令应用于 HTML 元素 <div>。通过向任何 HTML 元素添加 [do-something] 属性,即可对其应用 [doSomething] 指令。请注意指令名称与其关联属性在拼写上的区别:我们从 [camelCase] 切换为 [camel-case]。
[selectEnable] 指令还可以按以下方式使用:
在此,[doSomething] 指令以 HTML 标签 <do-something> 的形式应用。
让我们回到语法
以及该指令 [link] 函数的三个参数:[scope, element, attrs]:
- scope:是包含 <div> 的视图的模型;
- element:即 <div> 标签本身;
- attrs:是 <div> 的属性数组。这些属性可用于向指令传递信息。在上例中,我们通过 attrs['selectEnable'] 来获取 [data] 信息。请注意,[selectEnable] 的写法是为了指代 [select-enable] 属性;
让我们回到指令的代码:
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] 来修改控制器,并在 [app-17B.html] 文件以及 [selectEnable.js] 指令文件中引用该新 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] 中,我们从控制器中移除了以下几行代码:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
此操作现由指令处理。
3.7.7.8. 应用程序测试 – 3
在测试新应用程序 [app-17B.html] 时,得到以下结果:
![]() |
- 在 [1] 中,我们得到一个空列表。
控制台日志显示如下:
- 第 1 行:初始化 [dao] 服务;
- 第 2 行:在首次显示视图 V 时,执行 [selectEnable] 指令;
- 第 3 行:当用户点击 [Client List] 按钮时,此行代码会被执行。我们可以看到 [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 初次显示时,[clients.data] 并不存在。因此,<select> 标签不会被生成,[selectEnable] 指令也不会被评估。当用户点击 [客户列表] 按钮时,[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]中,我们请求佩利西耶女士[2]的2014年6月25日[3]日程安排;
得到以下结果[4]:
![]() |
我们将分别探讨这两种观点。
3.7.8.2. 表单
我们将文件 [app-17.html] 复制为 [app-18.html],然后按如下方式修改代码:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the request -->
<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>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<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.day] 模型(ng-model 属性);
- 第 28 行:请求日历的按钮;
- 第 32–34 行:错误列表保持不变;
- 第 37–39 行:日历,我们稍后将展示;
- 第 42 行:通过复制 [rdvmedecins-05.js] 文件,将 JS 代码转移到 [rdvmedecins-06.js] 文件中;
3.7.8.3. C控制器
应用程序的 JS 代码如下:

此次变更仅会影响 [utils] 服务和 [rdvMedecinsCtrl] 控制器。
[rdvMedecinsCtrl] 控制器代码如下:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: '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()};
// style the drop-down list
$timeout(function () {
$('.selectpicker').selectpicker();
});
// for the French local calendar
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- 第 7 行:我们在发出 HTTP 请求前设置了 3 秒的超时;
- 第 8 行:HTTP 连接所需的参数是硬编码的;
- 第 10–17 行:医生列表是硬编码的;
- 第 18 行:[agenda] 模型配置了视图中日历的显示方式;
- 第 19 行:[calendar] 模型配置了视图中的日历显示。我们将最小日期 [minDate] 设置为今天,并将当前日期也设置为今天;
- 第 21–23 行:使用之前介绍的方法对下拉列表进行样式设置;
- 第 25 行:我们将应用程序的区域设置为 'fr'。默认情况下,它是 'en';
调用日历时执行的方法如下:
// exécution action
$scope.execute = function () {
// les infos du formulaire
var idMedecin = $('.selectpicker').selectpicker('val');
// vérification
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// on met le jour au format yyyy-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// mise à jour de la vue
$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 行:我们将用户选择的日期格式化为 [yyyy-mm-dd],这是 Web 服务器所期望的日期格式;
- 第 13-15 行:当 [execute] 方法执行完毕后,将显示加载横幅,并隐藏其他所有内容;
代码后续如下:
// simulated waiting
var task = utils.waitForSomeTime($scope.waiting.time);
// we ask for the doctor's diary
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// we return the promise of task completion
return task.promise;
});
// we analyze the result of the call to service [dao]
promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
} else {
// mistakes were made in obtaining the agenda
$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 元素都有一个 [slot] 属性,代表时间段。该属性通过 [text] 属性进行增强,该属性将以 [10:20–10:40] 的格式显示时间段的文本表示;
- 第 26–28 行:我们将用于显示日历时段的 HTML 表格设置为响应式布局。我们在第 3.6.7 节中已讲解过这一概念;
![]() |
- 第 27 行:要使表格具备响应式布局,必须对其应用 [footable] 方法。这里我们遇到了与 [bootstrap-select] 组件相同的难题。如果仅按第 17 行所示编写代码,会发现表格无法响应。我们通过使用 [$timeout] 函数(第 26 行)以相同的方式解决了这个问题;
- 第 31–34 行:处理 HTTP 请求失败的情况。此时将显示错误信息;
3.7.8.4. 显示日历
现在我们回到 [app-18.html] 文件中的日历代码。代码如下:
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
<!-- case of a doctor without consultation slots -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- doctor's diary -->
<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] 属性,它是一个时间段。每个时间段有两个我们感兴趣的元素:
- [doctorSlot.slot.appointment],即该时段内安排的预约(如有;appointment ≠ null);
- [doctorSlot.slot.text],即该时间段的 [开始:结束] 文本;
- 第 4 行:若医生没有可用时段,则显示一条特殊提示。这种情况虽不常见,但因数据库存在缺失,该情形确实会发生。该提示是否在 HTML 中渲染由 [ng-if] 指令控制;

[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 行:我们写入预约所对应的客户姓名 [4]。如果 [rv==null],则该信息不存在,但 Angular 会正确处理此情况,不会抛出错误;
- 第 34–39 行:显示两个按钮中的一个:[预约] 或 [删除]。是否存在预约决定了哪个按钮被选中;
3.7.8.5. 修改 Web 服务器
与前面的示例一样,必须修改 Web 服务器,以便 URL [/getAgendaMedecinJour] 发送 CORS 头部:
![]() |
在 [RdvMedecinsCorsController] 类中,添加一个新方法:
// doctor's diary
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
此方法将发送 [OPTIONS] HTTP 请求的 CORS 头部。我们必须在 [RdvMedecinsController] 类中对 [GET] HTTP 请求执行相同的操作:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. 使用指令
与之前一样,我们将把 DOM 操作移入指令中。我们有两项 DOM 操作:
- 在视图初次显示时:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
- 当日历显示时:
// we create an evt to style the table after the view is displayed
$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> 标签应用 [selectEnable] 指令(通过 [select-enable] 属性);
<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>
<!-- directives -->
<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 的代码行已被移除:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
// we create an evt to style the table after the view is displayed
$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>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
..
<!-- doctor's diary -->
<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 行:点击 [book] 按钮(ng-click 属性)由视图 V 中模型 M 的 [reserve] 方法处理。该方法接收待预订时段的编号;
- 第 26 行:点击 [delete] 按钮由视图 V 中模型 M 的 [reserve] 方法处理。该方法接收待删除预约的编号;
- 第 39 行:管理应用程序的 JavaScript 代码位于文件 [rdvmedecins-07.js] 中;
- 第 40 行:第 20 行应用的 [footable] 指令对应的 JS 代码;
3.7.9.2. C控制器
[rdvmedecins-07.js] 的 JavaScript 代码首先通过复制 [rdvmedecins-06.js] 文件创建,随后进行修改。常规的大段代码保持不变,主要修改集中在控制器中:

我们将分几个步骤描述 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) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: '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"}
];
// for the date
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}};
// ---------------------------------------------------------------- agenda initial
// the global asynchronous task
var task;
// we ask for the agenda
getAgenda();
// ------------------------------------------------------------------ réservation
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ suppression RV
$scope.supprimer = function (idRv) {
...
};
// obtaining the agenda
function getAgenda() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- 第 6 行:等待消息的配置。默认情况下,系统将在发出 HTTP 请求前等待 3 秒;
- 第 7 行:HTTP 请求所需的信息;
- 第 8 行:错误消息配置;
- 第9–17行:硬编码的医生;
- 第 18 行:一位特定的医生。将为其预约时间段进行预订;
- 第19–24行:硬编码的客户;
- 第26行:需要处理法语日期格式;
- 第 27 行:预约将安排在今天的日期;
- 第28行:在线预约服务要求日期格式为“yyyy-mm-dd”;
- 第 29 行:今天的日期格式为 [2014 年 6 月 26 日,星期四];
- 第 30 行:日历配置。[model] 属性承载待显示的国际化消息参数:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- 第 35 行:全局变量 [task] 表示当前正在执行的异步任务;
- 第 37 行:请求初始日历;
页面初始加载期间仅执行上述操作。如果一切顺利,视图将显示佩利西耶女士当天的日历。

3.7.9.4. 获取日历
日历通过以下 [getAgenda] 方法进行获取:
// obtaining the agenda
function getAgenda() {
// the URL service path
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// waiting msg
$scope.waiting.visible = true;
// we analyze the result of the call to service [dao]
task.promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// mistakes were made in obtaining the agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
这段代码与上一应用程序中研究的代码相同。有两处改动:
- HTTP 调用前不再进行模拟等待;
- 第 4 行:我们使用了在控制器初始化时创建的医生对象,以及之前构建的格式化日期;
该代码已被封装为一个函数,因为 [reserve] 和 [delete] 函数也会用到它。
3.7.9.5. 预订时段
![]() | ![]() |
请注意,客户是随机选定的。
预订代码如下:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// we create a RV with a random customer in the slot identified by [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// simulated waiting
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaAdd;
// data to be sent to the service
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there were errors in validating the appointment
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- 第 1 行:请注意,[reserve] 函数的参数是槽位编号(id 属性);
- 第4行:从初始化代码中硬编码的客户列表中随机选取一名客户。我们保留其标识符 [id];
- 第 7–8 行:等待 3 秒;
- 第11–18行:这些代码仅在3秒后执行;
- 第 12 行:预约服务的 URL [/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] 中,发送给 Web 服务的 JSON 文本格式为 {day, clientId, slotId};
- 在 [3] 中,客户端通知 Web 服务其正在发送 JSON 数据;
让我们回到 [reserve] 函数的 JS 代码:
- 第 14 行:我们以 JS 对象的形式创建待提交的值。Angular 在提交时会将其序列化为 JSON;
- 第 16 行:发起 HTTP 请求。待提交的值是 [dao.getData] 函数的最后一个参数。当存在该参数时,[dao.getData] 函数将执行 POST 请求而非 GET 请求(参见第 3.7.6.4 节的代码);
- 第 18 行:返回来自 HTTP 调用的 Promise;
- 第 23–29 行:仅在 HTTP 调用返回响应后才会执行;
- 第 23 行:[result] 参数的形式为 [err,data] 或 [err,messages],其中 [err] 是一个错误代码;
- 第 23–26 行:若发生错误,则显示错误信息;
- 第 28 行:若预约成功,则再次显示新日历;
3.7.9.6. 服务器修改
![]() |
在 [RdvMedecinsCorsController] 类中,我们添加以下方法:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [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 [/addAppt](第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) {
// headers CORS
rdvMedecinsCorsController.ajouterRv(response);
...
对于 [POST] 方法(第 1 行)和 [/addAppointment] URL(第 1 行),会调用我们刚刚添加到 [RdvMedecinsCorsController] 中的方法(第 4 行),从而返回与 [OPTIONS] HTTP 方法相同的 HTTP 头部。
3.7.9.7. 测试
让我们进行一个初始测试,预订任意一个可用的时段:
![]() |
像往常一样,我们需要检查控制台日志:
[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. Request header field Content-Type is not allowed by Access-Control-Allow-Headers.
查看网络流量,我们可以看到以下内容:
![]() |
- 在 [1] 和 [2] 中:仅有一个 HTTP 请求,即 [OPTIONS] 请求;
- 在 [3] 中,Angular 客户端请求了两项权限:
- 发送 HTTP 头部 [accept, authorization, content-type] 的权限;
- 发送 POST 请求的权限;
- 在 [4] 中:服务器对 [authorization] 头部进行授权。请记住,在服务器端,是我们自己发送了这个授权;
新功能在于,对于 POST 操作,Angular 客户端会向服务器请求额外的授权。因此,我们必须修改服务器以授予这些授权:
![]() |
在 [RdvMedecinsCorsController] 类中,我们修改了用于生成 OPTIONS、GET 和 POST 请求所发送的 HTTP 头信息的私有方法:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- 第 7 行:我们为 HTTP 头部 [accept, content-type] 添加了授权;
- 第 9 行:我们为 POST 方法添加了授权;
重启服务器后,我们重新运行了测试:
![]() |
这次,预约成功了。
3.7.9.8. 删除预约
![]() | ![]() |
[delete] 函数的代码如下:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// simulated waiting
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaRemove;
// data to be sent to the service
var post = {idRv: idRv};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there have been errors deleting the rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// the UI is updated
$scope.waiting.visible = false;
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- 第 1 行:请记住,函数参数是要删除的预约 ID。这段代码与预订代码非常相似。我们仅对差异部分进行说明;
- 第 9 行:此处的服务 URL 为 [/deleteAppointment],与之前一样,通过 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] 中,发送给 Web 服务的 JSON 文本采用 {idRv} 格式;
- 在 [4] 中,客户端通知 Web 服务其将发送 JSON 数据;
让我们回到 [delete] 函数的 JS 代码:
- 第 11 行:我们创建了要提交的对象。Angular 会自动将其序列化为 JSON;
其余代码与预订功能类似。
3.7.9.9. 服务器端更改
在服务器端,我们进行以下修改:
![]() |
在 [RdvMedecinsCorsController] 类中,我们添加以下方法:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
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 [/deleteAppointment](第 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) {
// headers CORS
rdvMedecinsCorsController.supprimerRv(response);
...
对于 [POST] 方法(第 1 行)和 [/deleteAppointment] URL(第 1 行),会调用我们刚刚添加到 [RdvMedecinsCorsController] 中的方法(第 4 行),从而返回与 [OPTIONS] HTTP 方法相同的 HTTP 头部。
3.7.10. 示例 10:创建和取消预约 - 2
现在,我们将展示与之前相同的应用程序,但不再为随机客户预约,而是从下拉列表中选择客户。
3.7.10.1. 应用程序的 V 视图
我们将展示以下表单:
![]() |
用户将在 [1] 中进行选择。
该代码与之前的应用程序类似,因此我们仅介绍主要区别。
我们将文件 [app-19.html] 复制为 [app-20.html],然后编写客户下拉列表 [1] 的代码:
<!-- customer list -->
<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>
<!-- directives -->
<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 控制器的代码演变如下:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
...
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- initilisation vue
// the global asynchronous task
var task;
// we ask for the customers, then the agenda
getClients().then(function () {
getAgenda();
});
...
// execution action
function getClients() {
....
};
} ]);
- 第 8 行:对象 [$scope.clients] 配置了视图 V 中的客户下拉列表;
- 第14–16行:异步地,我们首先请求客户列表,然后在获取列表后,再请求PELISSIER女士今天的日程安排。此处的语法之所以有效,是因为[getClients]函数返回了一个Promise;
[getClients] 方法用于获取客户列表:
function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
// we return the promise
return promise;
};
这是我们之前已经遇到并讨论过的代码。需要注意的关键部分是第 31 行:
- 第 27 行:我们返回第 10 行中的 Promise,即代码中获取的最后一个 Promise。该 Promise 只有在 HTTP 请求返回响应后才会履行;
[reserve] 方法稍作调整:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// on crée un RV pour le client sélectionné
var idClient = $(".selectpicker").selectpicker('val');
...
});
- 第4行:我们不再为随机客户预约,而是为客户列表中选定的客户预约。
3.7.11. 示例 11:a [selectEnable2] 指令
本例重新探讨指令。
3.7.11.1. V视图
应用程序显示以下视图:
![]() |
3.7.11.2. 该视图的 HTML 代码
[app-21.html] 视图的 HTML 代码如下:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<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>
<!-- list of doctors -->
<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>
<!-- guidelines -->
<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 行:仅当 [doctors.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] 中,其结构如下:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- model initialization
// the waiting msg
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// login information
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// errors
$scope.errors = {show: false, model: {}};
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
// the global asynchronous task
var task;
// ---------------------------------------------------- initialisation vue
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// we ask for customers, then doctors
getClients().then(function () {
getMedecins();
});
// customer list
function getClients() {
...
}
// list of doctors
function getMedecins() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- 第26–28行:先查询客户,再查询医生;
3.7.11.5. 测试
测试此新版本。
3.7.12. 示例 12:[list] 指令
我们将沿用之前的示例,但希望通过使用指令来简化 HTML 代码。目前,我们的 HTML 代码如下:
<!-- customer list -->
<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>
<!-- list of doctors -->
<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 行完全相同。它们适用于医生而非客户。我们希望能够写出以下内容:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<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) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
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> 元素将各自拥有独立的模板。默认情况下(scope 未初始化),它们共享模板;
- 第 12 行:[link] 函数,我们已经使用过多次;
要理解上述代码,你需要记住该指令的使用方式:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<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] 文件中查找它:
<!-- a list of customers or doctors -->
<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 将由指令的 JavaScript 代码使用;
- 第 6 行:使用 [model.data] 仅在有数据需要显示时生成 <select> 标签;
- 第 7–9 行:使用 [model.data] 生成下拉列表项;
3.7.12.2. HTML 代码
应用程序 [app-22.html] 的 HTML 代码如下:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<list model="clients" ng-if="clients.show"></list>
<!-- list of doctors -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- guidelines -->
<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) {
// ------------------- model initialization
...
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// our customers
$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>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<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) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
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] 文件生成的:
// our customers
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// customer list
$scope.getClients = function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// put the acquired data into a new model to force the view to refresh
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
}
- 第 1 行:为了允许在 [clients.data] 中拼接数组,该对象初始化为一个空数组;
- 第 18 行:我们将新的客户列表与 [clients.data] 数组中已有的客户列表进行拼接;
此前,我们曾编写过:
现在我们写:
要理解这段代码,你需要回顾在 [list2] 指令中,V 视图是如何使用 M 模型的:
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
[list2] 指令使用的模型是 [clients]。只有当视图 M 中的 [clients] 发生变化时,它才会被重新评估。对于修改,首先想到的做法是编写:
以应对新客户列表必须追加到原有列表中的情况。这样做会修改 [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 行:实例化 [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) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
// à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// on met à jour le modèle de la directive
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>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<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>
<!-- le message d'attente -->
<waiting model="waiting"></waiting>
<!-- la liste d'erreurs -->
<errors model="errors"></errors>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directives -->
<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) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "waiting.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
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] 模型将定义如下:
// the waiting msg
$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) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "errors.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
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] 模型将定义如下:
// there were errors in obtaining the customer list
$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">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
...
<!-- the module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- controllers -->
<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 module
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routage
$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 [/page1] 时要显示的视图。这是应用程序内部的路由。 该 URL 实际上是 [/rdvmedecins-angular-v1/app-21.html#/page1]。我们可以看到,它仍然使用容器 URL [/rdvmedecins-angular-v1/app-21.html],但在 # 字符后附加了额外信息。正是这些附加信息由 Angular 路由进行处理;
- 第 8 行:指定要插入到容器 [ng-view] 指令中的 HTML 片段:
- 第 9 行:指定此片段对应的控制器名称;
- 第 11–15 行:定义用户请求 URL [/page2] 时要显示的视图;
- 第 16–20 行:定义用户请求 URL [/page3] 时要显示的视图;
- 第 21–24 行:定义当请求的 URL 不属于前三个 URL 之一时应执行的路由(否则,请参见第 21 行);
- 第 23 行:重定向至 URL [/page1],从而显示第 6–10 行定义的视图;
3.7.15.5. 视图容器控制器
我们看到视图容器声明了一个控制器:
<div class="container" ng-controller="mainCtrl">
[mainCtrl] 控制器在 [mainController.js] 文件中定义:
// controller
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// page templates
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
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">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
- [mainCtrl] 控制器负责构建 1-7 区域的模型;
- 第 6 行包含的视图也有一个控制器。例如,[page1] 视图拥有 [page1Ctrl] 控制器。该控制器为第 6 行显示的区域构建模型。因此,该区域中存在两个模型:
- 由 [mainCtrl] 控制器构建的模型;
- 由 [page1Ctrl] 控制器构建的模型;
模型的命名遵循特定规范。在第 6 行显示的视图中,控制器 [mainCtrl] 和 [pagexCtrl] 的模型均可见。若这两个模型中的两个变量名称相同,其中一个将覆盖另一个。为避免这种命名冲突,我们创建了四个名称各异的模型:
容器 | 主控台 | 主 | 11 |
页面1 | page1Ctrl | page1 | 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">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<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]控制器中定义如下:
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
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) {
// page 1 template
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. 导航控件
现在,我们希望按以下方式控制导航流程 [页面1 --> 页面2 --> 页面3 --> 页面1]。因此,如果用户当前位于页面1 [/page1],并在浏览器中输入 URL [/page3],则不应接受此导航操作,用户应保持在页面1。
为实现这一目标,我们将页面控制器修改如下:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// authorized navigation?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// we return to the last URL
$location.path(main.lastUrl);
return;
}
// we store the URL of the page
main.lastUrl = '/page1';
// page template
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页,并在[2]中输入第3页的URL;
- 在[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é">
<!-- on 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>
<!-- controller [appCtrl], model [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core 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>
<!-- angular js -->
<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>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<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>
<!-- guidelines -->
<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>
<!-- controllers -->
<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">
<!-- navigation bars -->
<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>
<!-- the jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- page title -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- page errors -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- the waiting message -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- the current view -->
<ng-view></ng-view>
<!-- debug -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
无论显示的是哪个视图,它都将始终包含以下元素:
- 第 3-4 行:命令栏。第 3 行和第 4 行中的两个命令栏互斥;
![]()
![]()
- 第6行:应用程序徽标/文本:

- 第8行:标题

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

- 第13行:加载提示:

- 第 17 行:调试信息:

上述所有元素均由 [ng-show / ng-hide] 指令控制,这意味着即使它们存在,也不一定可见。
3.8.4. 应用程序视图
在主页面代码中,我们有:
<div class="container">
...
<!-- the current view -->
<ng-view></ng-view>
...
</div>
第 4 行接收应用程序的各个视图。这些视图在 [main.js] 模块中定义:

关于配置不同路由的作用,已在第 3.7.15.4 节(第 242 页)中进行过说明。
[login.html]视图为空,这意味着它不会向母版页中已有的元素添加任何新内容。
[home.html] 视图向母版页添加了以下元素:

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

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

3.8.5. 应用程序功能
第 1.3.3 节(第 7 页)已介绍了 Angular 客户端视图。为了便于阅读本新章节,我们在此重复说明。第一个视图如下:
![]() |
- [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 行:母版页的整个主体由 [appCtrl] 控制器控制。由于其位置,这使其成为应用程序的通用且主要的控制器。如第 3.7.15 节所述,由该控制器创建的模型将被插入到母版页中的所有视图所继承。
其代码如下:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// debug
utils.debug("[app] init");
// ----------------------------------------initialisation page
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
...
// ---------------------------------- méthodes
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
// this code must remain here as it refers to the preceding [cancel] function
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
第 10–14 行定义了应用程序中使用的五个模型:
app.html | appCtrl | |
login.html | 登录控件 | |
home.html | homeCtrl | |
reservation.html | resaCtrl | |
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] 中:
在 [C2] 中:
在两种情况下,$scope 都继承自 [appCtrl] 控制器,因此在 [C1] 和 [C2] 中是相同的(它是一个指针)。[$scope.app] 对象作为控制器之间的共享内存,在注释中常被称为“会话”,这模仿了传统 Web 应用程序中使用的会话,后者指的是连续 HTTP 请求之间的共享内存。
让我们回到 [appCtrl] 控制器的代码:
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
// [app.debug] and [utils.verbose] must always be synchronized
app.debug = utils.verbose;
app.debug.on = config.debug;
// no page title for the moment
app.titre = {show: false};
// no navigation bars
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// no errors
app.errors = {show: false};
// local default
angular.copy(config.locales['fr'], $locale);
// the current view
app.view = {url: undefined, model: {}, done: false};
// the current task
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- 第 8 行:[$scope.app] 将作为主页面的模型。它还将充当各个控制器之间的共享内存。与其在各处都写 [$scope.app.field=value],不如将指针 [$scope.app] 赋值给变量 [app],这样我们只需写 [app.field=value]。只需记住 [app] 是暴露给主页面的模型;
- 第 11 行:[app.debug.on] 是一个布尔值,用于控制应用程序的调试模式。默认情况下,它被设置为 true。其值与导航栏中的 [debug] 复选框相关联;
- 第 15 行:[app.navbarrun.show] 控制以下导航栏的显示:
![]()
- 第 16 行:[app.navbarstart.show] 控制以下导航栏的显示:
![]()
- 第 18 行:[app.errors] 是错误横幅的模板;

- 第 22 行:[app.view] 将包含当前视图的相关信息——即主页面中 [ng-view] 标签当前显示的视图。我们将在此处包含以下信息:
- [url]:当前视图的 URL,例如 [/agenda];
- [model]:当前视图的模型,例如 [$scope.agenda];
- [done]:当值为 true 时,表示当前视图已完成其工作,且我们正在切换到另一个视图;
- 第 24 行:启动一个异步任务,即模拟等待。该异步任务通过两个指针 [app.view.model.task.action] 和 [app.task] 进行引用;
已有两个方法被提取到控制器 [appCtrl] 中:
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
- 第 2 行:[app.cancel] 函数用于取消当前正在显示加载提示的任务。所有视图都会显示此提示,因此任务将在这里被取消;
- 第 7 行:[app.logout] 函数将用户重定向回登录页面。除 [/login] 视图外,所有视图均提供此选项;
[app.deconnecter] 函数如下:
// disconnect
app.deconnecter = function () {
// we return to the login page
$location.path(config.urlLogin);
};
- 第 4 行:返回 URL 为 [/login] 的登录页面;
3.8.8. 异步任务管理
在我们的应用程序中,任何给定时刻只会运行一个异步任务。虽然也可以让多个任务同时运行。例如,当应用程序启动时,它会通过两个连续的 HTTP 请求分别向 Web 服务查询医生列表和客户列表。我们也可以通过两个并行 HTTP 请求来实现相同的功能。Angular 提供了相应的工具。但在此,我们并未采用这种方法。
通过在 [appCtrl] 控制器中执行以下代码,可以取消当前正在运行的任务:
// cancel current job
app.cancel = function () {
utils.debug("[app] cancel task");
// cancel the current view's asynchronous 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] 视图。其初始化代码如下:
// retrieve the parent model
var login = $scope.login;
var app = $scope.app;
// current view
app.view = {url: config.urlLogin, model: login, done: false};
第 5 行,我们有 [model=login]。这意味着当我们修改 [login] 对象时,实际上是在修改 [app.view.model] 对象,即 [$scope.app.view.model]。当我们在 [loginCtrl] 控制器中需要模拟等待时,我们会这样写:
// simulated waiting
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
通过向 [login] 对象添加 [task] 字段,该字段便被添加到了 [$scope.app.view.model] 对象中。如果用户取消等待,[appCtrl.cancel] 中的代码:
// current page template
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
将成功完成模拟的等待(第 4–6 行)。
3.8.9. 导航控制
该应用程序使用的导航规则如下:
任意 | 是 | |
/login | 是,如果控制器 [loginCtrl] 已指示其已完成工作 | |
/home | 是 | |
/日历 | 是 | |
/home | 如果控制器 [homeCtrl] 已指示其工作已完成,则为是 | |
/reset | 是 | |
/议程 | 是 | |
/日历 | 是,如果控制器 [homeCtrl] 已指示其已完成工作 | |
/reset | 是 |
这是通过以下代码实现的:
对于 [agendaCtrl]:

- 第 11–20 行:导航规则的实现;
- 第 26 行:新的当前视图;
对于 [resaCtrl]:

- 第 12–20 行:导航规则的实现:
- 第 27 行:新的当前视图;
对于 [loginCtrl]:

- 此处没有导航控件,因为规则规定 URL [/login] 可从任何位置访问。因此,如果用户在浏览器中输入此 URL,无论当前视图为何,它都将生效;
- 第 16 行:新的当前视图;
第 3.8.7 节中提供了 [homeCtrl] 控制器的代码。
最后,对于如下规则:
/home | 是,如果控制器 [homeCtrl] 已表示其已完成工作 |
以下是一个从 URL [/home] 导航到 URL [/agenda] 的代码示例:
![]() |
上文中,我们位于 [homeCtrl] 控制器的 [displayCalendar] 方法中。用户请求了医生的日程表。
- 第 107 行:HTTP 任务的 Promise;
- 第 109 行:变量 [app] 已通过 [$scope.app] 初始化。如前所述,该对象用作 [app.html] 视图的模板。此模板 [$scope.app] 还用于存储需要在视图之间共享的信息;
- 第 111 行:分析任务返回的错误代码;
- 第 113 行:将结果 [result.data] 放入 [app] 模型中;
- 第 116 行:控制器 [homeCtrl] 将控制权移交给控制器 [agendaCtrl]。这表明它已完成第 115 行代码的处理。该代码将由控制器 [agendaCtrl] 按以下方式使用:

- 第 11 行:获取对象 [$scope.app.view];
- 第 15 行:处理由 [homeCtrl] 初始化的 [$scope.app.view.done] 字段;
3.8.10. 服务
![]() |
服务 [config, utils, dao] 已在 Angular 概述中进行过说明:
为方便查阅,以下是这些服务的结构:
[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 概述中进行过说明:
我们尚未遇到 [debug] 指令。其定义如下:
![]() |
第 11 行引用的 [debug.html] 文件内容如下:
- 第 2 行:[debug] 指令会在 Bootstrap 横幅中以 JSON 格式显示其模板(第 1 行);
该指令仅在主页面 [app.html] 中使用:
![]() |
- 第 35 行使用了 [debug] 指令。因此,在调试模式下(ng-show 属性),它会显示 [$scope.app] 模型的 JSON 表示形式。这将产生如下输出:
![]() |
要解读这些内容需要对代码有深入的理解,但一旦掌握了,上述信息对调试就非常有用。在此,我们已突出显示了显示的 [$scope.app] 模型的各个元素。请记住,[$scope.app] 是控制器之间的共享内存;
- [waitingBeforeTask]:任何 HTTP 请求前的模拟等待时间;
- [debug]:调试模式——若显示此横幅,该值必然为 true;
- [navbarrun]:一个布尔值,用于控制以下导航栏的显示:
![]()
- [navbarstart]:一个布尔值,用于控制以下导航栏的显示:
![]()
- [errors]:[errors] 指令的模板;
- [view]:封装当前显示视图的相关信息;
- [waiting]:[waiting] 指令的模板;
- [serverUrl, username, password]:Web 服务的登录凭据;
- [doctors]:应用于医生的 [list] 指令的模型;
- [clients]:与上述针对医生的模型相同;
- [menu]:控制显示的菜单选项。这些选项在 [navbar-run.html] 中定义:

菜单选项位于第 16、23、29 和 36 行。
- [formattedDay]:日历中选定的日期,格式为 'yyyy-mm-dd';
- [agenda]:医生的日程表。其中包含空闲时段(rv==null)和已预约时段。对于已预约时段,包含进行预约的客户姓名;
- [selectedCreneau]:用于预约的时间段;
3.8.12. [loginCtrl] 控制器
![]() |
[loginCtrl] 控制器与 [views/login.html] 视图相关联,当与母版页结合使用时,会生成以下页面:

[loginCtrl] 控制器如下所示:

- 第 13 行:[login] 将作为当前视图的模型;
- 第 14 行:[app] 是控制器之间的共享内存;
- 第 16 行:[app.view] 填充了来自当前视图的信息;
这段初始化代码将出现在每个控制器中。对于视图 V1 和模型 M1 的控制器 C1,我们将有以下初始化代码:
- 第 18 行:您可能还记得,[appCtrl] 启动了一个由 [app.task.action] 对象引用的模拟等待。我们使用该任务的 [promise] 来等待其完成;
- 第 39 行:[login.setLang] 方法负责处理语言切换;
- 第 47 行:[login.authenticate] 方法负责用户身份验证;
让我们来看看认证方法的主要步骤:

- 第 50–51 行:[app.waiting] 是加载横幅的模型;
- 第 53 行:[app.errors] 是错误横幅的模型;
- 第 55 行:启动模拟等待。对象 [action, isFinished] 由 [login.task] 引用,因此,由于 [app.view.model=login],它也被 [app.view.model.task] 引用。请注意,这是任务被取消的条件;
- 第 57 行:模拟等待结束后,加载医生信息;
- 第 62 行:获取医生数据后,分析其请求。若已获取医生数据,则请求客户端;
- 第 83 行:分析响应并显示最终视图。具体实现如下:

- 第 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] 视图相关联,当与母版页结合时,会生成以下页面:

[homeCtrl] 控制器的结构如下:

- 第 12–20 行:这是导航控件。除 [loginCtrl] 之外,所有控制器都包含此控件,因为 [/login.html] 页面无需任何条件即可访问;

- 第 25–28 行:此处的代码与 [loginCtrl] 控制器中的类似。[home] 即为与该控制器关联的视图模板;
- 第 33 行:一个我们尚未见过的属性。这是视图顶部栏的模型:
![]()
- 第 36 行:[home.datepicker] 是日历的模型;
- 第 38 行:[app.menu] 是导航栏菜单的模型。这里将显示 [Schedule] 选项。正是通过它,您可以查询医生的日程安排;
最后,控制器包含两个方法:

关于显示日程(第 51 行)的内容已在第 3.7.8 节中介绍。
3.8.14. [agendaCtrl] 控制器
![]() |
[agendaCtrl] 控制器与 [views/agenda.html] 视图相关联,该视图与母版页结合后,将生成以下页面:

[agendaCtrl] 控制器的结构如下:

- 第 10–20 行负责导航控制;

- 第 23–26 行:[agenda] 将是与 [agendaCtrl] 控制器关联的视图模板;
- 第 36–44 行:[app.title] 是以下标题栏的模板:

- 第 46 行:菜单中将包含 [Home] 选项:
![]()
控制器的方法如下:

- 第 95 行:[agenda.delete] 方法已在第 3.7.9 节中讨论过;
[agenda.home] 方法是一个纯粹的导航方法:

[agenda.reserve] 方法如下:

- 第 73 行:[reserve] 函数的参数是时段编号 (id);
- 第 77–86 行:旨在查找具有此标识符的时间槽;
- 第 82 行:将找到的时间槽放入共享内存 [app] 中。即将接管控制的控制器 [resaCtrl](第 90 行)将利用此信息显示其标题栏;
- 第 89–90 行:导航至 [/resa.html];
3.8.15. 控制器 [resaCtrl]
![]() |
控制器 [resaCtrl] 与视图 [views/resa.html] 相关联,当与主页面结合时,会生成以下页面:

[resaCtrl] 控制器的结构如下:

- 第 12–20 行:导航控件;

- 第 24–27 行:[resa] 将作为当前视图的模板;
- 第 38–45 行:[app.titre] 是后续标题栏的模板:

- 第 47 行:显示两个菜单选项:
![]()
控制器的方法如下:

第 3.7.9 节已讨论了 [resa.valider] 方法。
3.8.16. 语言管理
所有控制器都提供了以下 [setLang] 方法:

该方法本可以被提取到 [appCtrl] 控制器中。




























































































































