在本 系列 中目前为止,您已分别自定义了 User Group List 和 Information (UGLI) MEAN 应用程序。现在是时候看看如何准备您的网站来与其他网站良好地互动了。在 上一期 中,您在应用程序中使用了本地数据。这一次,我们将集成 MEAN 应用程序与 Meetup.com API,以显示有关即将举办的会议的信息。JSON 数据来自 Meetup,但页面的外观和行为方式全由您自己决定。您还将学习如何组合一些微数据,使您的网页更易于搜索引擎搜索。参见 下载 获取完整的示例代码。

首先,将来自 Meetup.com 的即将举办的活动信息添加到 UGLI 主页。

HTML 和微数据

如果访问 Meetup.com 上的 HTML5 Denver Users Group,您会看到一个类似图 1 的网页。

图 1. 一场即将举办的 HTML5 Denver User Group 会议的 Meetup.com 信息

一场即将举办的 HTML5 Denver User Group 会议的 Meetup.com 信息的屏幕截图

点击查看大图

可以看到,使用即将举办的活动信息进一步自定义 UGLI 主页所需的所有原始材料,都在 Meetup.com 页面上(即将发表的演讲的标题、会议时间和地点,等等),但 Meetup.com 页面的设计与该数据紧密耦合。您想要提取并在 MEAN 应用程序中重用的信息,与 HTML 元素杂乱地混在一起,如清单 1 所示。(为了清晰性和简洁性,我对 HTML 进行了编辑。)

清单 1. Meetup.com HTML 源代码
<ul>
    <li itemscope="" itemtype="http://data-vocabulary.org/Event">

        <span itemprop="eventType" 
              style="display:none;">Meetup</span>
        <h3>
            <a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" itemprop="url">
                <span itemprop="summary">"Developing Offline Applications" and "HTML 5 Animations"</span>
            </a>
        </h3>

        <!-- snip -->
    </li>
</ul>

Microformats、microdata 和 RDFa!哦天哪!

可通过多种竞争方式向您的 HTML 标记添加元数据。

Microformats 是一项定义常见元数据类型(比如联系人信息 (hCard) 和日历事件 (hCalendar))的方法。它们是最早在缺乏任何官方途径时 Web 开发人员创建的实用解决方案。在许多情况下,它们是之前的非 HTML 规范(比如 vCard 和 iCalendar)的 HTML 端口。

RDFa 随后作为向基于 XML 的 XHTML 添加新命名空间属性的官方方式出现。(其中的 a 表示资源描述框架 (Resource Description Framework) 中的属性。)RDFa 是万维网联盟 (W3C) 定义的规范之一 — 这家标准机构还发布了 HTML 和 CSS 规范。

Microdata 也是一个 W3C 规范,它与取代 XHTML 的新 HTML5 规范相关联。

我们不会详细讨论各种格式的相关特征,本教程仅介绍 Meetup.com 用于标记其网页的微数据。几年来,我基于客户已有的资源在各种项目中使用过每种格式,每种格式的实现难度都差不多。出于遗留原因,所有 3 种格式都受所有主要搜索引擎支持。

如果还未使用过任何这些格式,也可以选择最新的格式,那就是微数据。一些主要搜索引擎在 schema.org 网站上协作,为它们在其搜索结果中明显支持的许多最常见信息类型提供了一个交流中心:组织、人员、地点、产品和活动等等。在这里可同时找到微数据和 RDFa 表示的示例。

活动的标题(“Developing Offline Applications” 和 “HTML 5 Animations”)深入嵌套在 HTML 中的多层中。就 HTML 文档(和您的 Web 浏览器)而言,这个任意的字符串只是嵌套在一个无序列表 (<ul>) 内的几个列表项 (<li>) 之一。这个列表项有一个三级标题 (<h3>),大概二级和一级标题都已在文档分层结构中定义。在标题内是一个超链接,其中进而包含一个任意的文本 <span>

信息要按 Meetup.com 想要的格式显示,所有这些 HTML 标记必不可少。您的任务是以一种完全不同的方式在您应用程序中显示信息:您需要找到一种方式来从显示中分离出信息。

需要使用两个不同的语义层,如清单 1 所示。最基本的语义层我刚才已讨论:一项是一个列表项,另一项是一个超链接。这就是文档 语义。另一层是事件 语义,由 eventType、url 和 summary 等关键字表示。这些关键字与文档的呈现方式毫无关系。它们向搜索引擎(和查看 HTML 源代码的初学者)暗示信息的 “更高级含义”。这些属性是 HTML Microdata 规范的一部分。(有关微数据及其元数据前身的更多信息,请参阅 Microformats、microdata 和 RDFa!哦天哪! 边栏。)

这个特定的列表项中仅包含一个 event 的信息。搜索引擎知道此事实,是因为设计该页面的人向该列表项添加了 itemtype="http://data-vocabulary.org/Event"。该页面包含许多任意超链接,其中带有 itemprop="url" 的超链接是 event 本身的链接。eventType 为 Meetup— 一个任意字符串,但 Meetup.com 会在其所有网页上一致地使用它。活动的 summary 由带 itemprop="summary" 属性的 <span> 来标识。

<span> 元素是浏览器在呈现页面时忽略的一些 HTML 元素之一。<b> 元素内的文本呈现为加粗字体;<h1> 文本的显示字号比 <h2> 文本更大;<a> 文本可单击,通常使用蓝色且带下划线。当然,所有这些默认样式规则都可使用 CSS 覆盖。但 <span> 标记的存在仅用于添加您自己的 CSS 样式 — 或者对于 清单 1 中的 Meetup.com HTML 代码段,用于将 "Developing Offline Applications" and "HTML 5 Animations" 字符串包装在 itemprop="summary" 语义标签中。

有关可添加到 MEAN 标记中来进一步描述事件的完整元数据项列表,可首先访问 Meetup.com 用于定义事件的 URL:http://data-vocabulary.org/Event

向主页添加一个占位符事件

大体了解 Meetup.com 如何显示事件信息后,可对 uGLI 应用程序执行相同操作。

在测试应用程序的 root 中键入 mongod 来启动 MongoDB,然后键入 grunt 启动 Web 应用程序。在 Web 浏览器中访问 http://localhost:3000 时,您应看到在上一期中自定义的主页(如图 2 所示)。

图 2. UGLI 主页

UGLI 主页

点击查看大图

我想向主页添加即将举办的 HTML5 Denver User Group 活动。本教程的剩余部分将迭代式地完成此工作。首先,添加一些静态占位符数据来描绘页面的基本外观的线框图。

在文本编辑器中打开 public/modules/core/views/home.client.view.html。在标语下,添加活动的新标记,如清单 2 所示。

清单 2. public/modules/core/views/home.client.view.html
<section>
  <div class="jumbotron text-center">
    <!-- snip -->
  </div>

  <div class="row">
    <div>Monday, September 22, 2014</div>
    <h3><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>
    <div class="col-md-4">
     <h4>When</h4>
     <p>6pm</p>
     <h4>Where</h4>
     <address>
       <span>Rally Software</span><br>
       1550 Wynkoop<br>     
       Denver, CO<br>
     </address>
    </div>

    <div class="col-md-8">
     <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
     <p><b>7 pm: Dinner and Networking</b></p> 
     <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
    </div>
  </div>
</section>

在浏览器中查看更新的主页时,它应类似于图 3。

图 3. UGLI 主页线框图

UGLI 主页线框图

点击查看大图

有了基本的 HTML 结构后,可以让它显示得比默认样式更美观。为此,需要添加一些语义。

Bootstrap 提供了您使用的默认结构化 HTML 元素(比如 <div> 和 <h3>)的内置样式。它还提供了一些不属于默认 HTML 元素的附加的结构化类,比如 row 和col。

初学的 Web 开发人员常常会考虑网页的结构,而不是显示的信息。结果是,他们使用像 big-red-italic 和 left-column-header 这样的名称来编写自定义 CSS 类。从语法上讲该方法并没有错,但我发现使用像 event-date 和 event-location 这样的语义名称时,网站的长期维护更容易。这样,当客户在一年后返回要求将所有分类汇总标为绿色而不是红色时,我可编写一个 CSS 类来识别显示了哪些内容 (subtotals),而不是 如何显示 (green-body-text)。我也不太可能不经意地更改页面上其他恰好也使用了 green-body-text CSS 规则的元素。

返回到您刚编写的 HTML,添加一些具有合适的语义的 CSS 类,比如 event、event-date 和 event-title:

<div class="row center-block event">
<div class="event-date">Monday, September 22, 2014</div>
<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>

center-block 结构类来自 上一期 中介绍的 Bootstrap 库。稍后将会看到,将 event 类的 width 减小到 75% 时,center-block 将确保左侧和右侧的空白区域均等。

这非常适合在同一个元素上混合使用结构和语义类。事实上,从长远上来讲,它使我更容易快速识别哪些类是特定于应用程序的 (event-*),哪些类是通用的(row、center-block)。

将这些类添加到 HTML 后,就可以定义一些自定义 CSS 规则了。在文本编辑器中打开 public/modules/core/css/core.css。因为每个模块都拥有自己的 CSS,所以可保持 “面向组件” 的理念。而且通过抓取整个子目录树,更容易在项目间共享模块。

添加 CSS 样式规则,如清单 3 所示。

清单 3. public/modules/core/css/core.css
.event {
    width: 75%;
}

.event-date {
    font-style: italic;
}

.event-title {
    margin-top: 0;
}

现在您的主页看起来没那么粗糙,更加美观了,如图 4 所示。

图 4. 带 CSS 样式的 UGLI 主页

带 CSS 样式的 UGLI 主页

点击查看大图

最后,再次返回添加微数据元数据。能否跳过一步,在 CSS 规则中使用微数据元素,而不定义自定义元素?当然可以。但我想将它们分开。毕竟,它们具有两种不同的用途。一个用于 CSS 样式,另一个用于搜索引擎优化 (SEO)。假设您 5 年前遇到一个用户案例,要求您将微数据迁移到另一个更新的规范。如果在满足案例要求的同时,带来了影响网站外观的副作用,就太遗憾了。这两个特性应是完全不同的。

再一次在文本编辑器中打开 public/modules/core/views/home.client.view.html。为该活动添加新的语义微数据标记,如清单 4 所示。(有关使用微数据标记活动的范例,请参阅 富代码段 - 活动。)

清单 4. 添加微数据
<div class="row center-block event">
  <span>Meetup</span> 
  Monday, September 22, 2014
  <h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/"><span>"Developing Offline Applications" and 
    "HTML 5 Animations"</span></a></h3>
  <div class="col-md-4">
    <h4>When</h4>
    <p>6pm</p>
    <h4>Where</h4>
    <address>
      <span>Rally Software</span><br>
      <span>
        <span>1550 Wynkoop</span><br>  
        <span>Denver</span>, <span>CO</span><br>
      </span>
    </address>
  </div>

  <div class="col-md-8">
    <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
    <p><b>7 pm: Dinner and Networking</b></p> 
    <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
  </div>
</div>

肯定需要大量工作,最终才能在浏览器中得到与之前完全相同的显示结果,是不是?但值得高兴的是,由于您费力添加的所有语义数据,您的网站上升到了搜索结果的最前面。

我们不会逐行地详细解释清单 4,我仅指出一些重要的地方。

清单 1 中的 Meetup.com 示例一样,我们向一个没有显示的元素添加了 eventType(因为它仅用于 SEO):

<span>Meetup</span>

接下来,向活动添加一个日期以供人和机器使用:

Monday, September 22, 2014

作为人类,您可立即分析字符串 Monday, September 22, 2014,认识到它是一个日期。您还能够将 9/22/2014 和 2014-09-22 识别为同一个日期。但计算机更加注重字面内容,像这样的细小的格式更改可能导致重大的故障。在这个示例中,您执行了多处更改来消除歧义:

  • 您将 HTML 元素从通用的 <div> 升级为更具体的 (一个新的 HTML5 元素)。
  • CSS event-date 类识别数据的内容,而不是外观。
  • itemprop="startDate" 微数据属性将此日期识别为 event 的 startDate。
  • datetime 属性(HTML5 元素的一部分)清晰地表明该时间为 ISO 8601 格式。这样,既可提供一种机器可使用的时间,又可提供一种用于显示和人类使用的格式良好的时间。

对 HTML 的最大更改包括活动的地址,如清单 5 所示。您在 Event 模式内嵌套了多种新模式 —Organization and Address。

清单 5. 添加组织和地址的微数据
<h4>Where</h4>
<address>
    <span>Rally Software</span><br>
    <span>
        <span>1550 Wynkoop</span><br>  
        <span>Denver</span>, <span>CO</span><br>
    </span>
</address>

让您的浏览器能感知微数据

现在您已向网站添加一些微数据,如何确定您已正确添加?最简单的方法是为浏览器使用一种微数据扩展。在多种非官方的微数据扩展中,我喜欢在 Chrome 中使用的是 Semantic inspector

安装之后,此浏览器扩展通常会保持隐藏,直到您访问一个使用微数据的网站。Semantic inspector 在当前网页中找到微数据时,它会在地址栏显示一个红色的 m 图标。您可能对这个小图标弹出的频率很吃惊;您会在许多流行、主流的网站上看到它,包括 Google、Time.com 和 Walmart.com 等。单击该图标可显示详细信息,如图 5 所示:

图 5. 使用 Semantic inspector 查看微数据

使用 Semantic inspector 查看微数据的屏幕截图

现在您已有标注了微数据属性的基本的 HTML 线框图,是时候从这些连线中填入全新的 JSON 了。为此,创建一个新的事件模块,其中包含控制器、视图、模型和服务。

创建一个 AngularJS 模块

上一期 中,您使用 Yeoman 搭建了一个完整的 CRUD 模块,包括 Express 路由和一个 Mongoose 模型。对于此用户案例,不需要服务器端基础架构,因为原始 JSON 数据来自外部 Web 应用程序。幸运的是,MeanJS Yeoman 生成器的创建者预见到了这一需求,单单为应用程序的客户端 AngularJS 部分提供了另一个生成器。

键入 yo meanjs:angular-module events 来创建一个名为 events 的新 AngularJS 模块。AngularJS 模块 是特定于您应用程序中一种特定数据类型的文件的逻辑分组。根据官方 AngularJS 文档的描述,“可将模块视为您应用程序的不同部分(控制器、服务、过滤器、指令等)的容器。”

收到提示时,选择列表中的所有元素,如清单 6 所示。

清单 6. 生成一个模块
[?] Which folders would you like your module to include? 
 ? config
 ? controllers
 ? css
 ? directives
 ? filters
 ? img
 ? services
 ? tests
 ? views

 create public/modules/events/events.client.module.js

根据刚才的描述,您的模块只是一个空目录集合。您不会在编写的每个 AngularJS 模块中使用每个目录,但知道有一个容易记住、容易理解的地方来在时机成熟时放入模块的各部分,也很不错。

下一步是向模块添加一个控制器。

创建一个 AngularJS 控制器

搭建一个模块后,搭建一个控制器也非常容易。键入 yo meanjs:angular-controller events 并选择 events 模块,如清单 7 所示。

清单 7. 生成一个控制器
[?] Which module does this controller belongs to? 
  articles 
  core 
? events 
  talks 
  users 

create public/modules/events/controllers/events.client.controller.js
create public/modules/events/tests/events.client.controller.test.js

可以看到,Yeoman 生成器将该控制器放在 controllers 目录中,将关联的测试放在您指定的模块的 tests 目录中。

此刻要问的一个合理的问题是 “为什么我创建了一个控制器,为什么我应该关注它?”回想一下,AngularJS 是一个客户端 模型-视图-控制器 (MVC) 框架。您最终将得到一个视图(在本教程前面创建的 HTML <div> 元素),其中填入了模型数据(一个填入了来自 Meetup.com 的活动数据的 JSON 结构)。该视图如何访问该模型?控制器的工作是将各部分集合起来,为视图提供它需要的模型数据。

以下这个简单示例演示了 MVC 的各部分如何融合在一起。在文本编辑器中打开 public/modules/events/controllers/events.client.controller.js,如清单 8 所示。

清单 8. 一个空的、无存根的 AngularJS 控制器
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    // Events controller logic
    // ...
  }
]);

稍后,我们会将此控制器绑定到一个特定的 DOM 元素。$scope 变量将负责将模型传递给视图的重要工作。

向 $scope 添加一个 title 变量(模型),如清单 9 所示。

清单 9. 将一个 $scope 变量添加到 AngularJS 控制器
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';

  }
]);

接下来,将 EventsController 添加到 public/modules/core/views/home.client.view.html 中的 Event DOM 元素(视图)中:

<div class="row center-block event">

您可能已猜到,此代码将控制器绑定到 DOM 元素。$scope 变量仅对此 <div> 和它的子元素有效。如果愿意,可将一个控制器绑定到许多不同的 DOM 元素。每个元素会获得一个新控制器的唯一实例和它自己的唯一 $scope。

接下来,向您的线框 HTML 添加一个 {{title}} 占位符以取代硬编码的文本:

<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/"><span>{{title}}</span></a></h3>

在 Web 浏览器中查看结果时,您应看到 {{title}} 占位符替换为了通过 EventsController 提供的文本,如图 6 所示。

图 6. 占位符替换为了实际文本

该屏幕截图显示占位符替换为了实际文本

点击查看大图

现在您已有一个简单、有效的示例,是时候详细分析分析它了。(换句话说,您的应用程序已能正常工作,是时候再次破坏它了。)如果愿意,可向 $scope 添加许多变量,这些变量可以是简单的单个值,或者完整的 JSON 对象。

很快,您将会看到如何向 Meetup.com 发出一个 HTTP 请求,以检索下一个即将举办的活动的 JSON。到那时,在 $scope 中添加一些简化的模拟数据,以模拟您将从实际的 Ajax 调用获取的数据,如清单 10 所示。

清单 10. 模拟的 JSON 响应
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
        $scope.title = 'High Performance WebSocket';
        $scope.event = {
          'name': '"Developing Offline Applications" and "HTML 5 Animations"',
          'time': 1411430400000,
          'event_url': 'http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/',
          'description': '<p><b>6 pm : "Developing Offline 
          Applications with HTML 5" by Venkat Subramaniam</b></p>',
          'venue': {
            'name': 'Rally Software',
            'address_1': '1550 Wynkoop',
            'city': 'Denver',
            'state': 'CO',
          }      
       }
    }
]);

可以看到,$scope.event 变量包含一个复杂、嵌套的 JSON 对象。编辑您的视图来利用这个新模型数据,如清单 11 所示。

清单 11. 向 HTML 添加更多占位符
<h3 class="event-title"><a href="{{event.event_url}}"><span>{{event.name}}</span></a></h3>
<div class="col-md-4">
  <h4>When</h4>
  <p>{{event.time}}</p>
  <h4>Where</h4>
  <address>
    <span>{{event.venue.name}}</span><br>
    <span>
      <span>{{event.venue.address_1}}</span><br>   
      <span>{{event.venue.city}}</span>, 
      <span>{{event.venue.state}}</span><br>
    </span>
  </address>
</div>

<div class="col-md-8">
  {{event.description}}
</div>

在 Web 浏览器中查看结果时,只要向模板视图添加了占位符,就应显示来自 $scope.event 的值,如图 7 所示。

图 7. 包含模拟的 JSON 数据的 UGLI 主页

包含模拟的 JSON 数据的 UGLI 主页

点击查看大图

创建 AngularJS 服务来获取实际、实时的数据之前,必须完成两个与视图相关的简单任务:添加一些 AngularJS 过滤器来格式化日期,并在 {{event.description}} 占位符中显示所呈现的 HTML — 而不是原始的、转义的 HTML 代码。 —

添加 AngularJS 过滤器

AngularJS 过滤器(与相机或 Instagram 滤镜很像)可改变数据的外观。向视图添加过滤器,因为它们会影响模型数据的外观,而不更改内容本身。

通过在数据元素后添加一个竖线 (|) 和一个过滤器名称,如 {{product_code | uppercase}} 所示,可向模板占位符应用一个过滤器。AngularJS 提供了许多内置的过滤器,包括 uppercase、lowercase、currency 和 number。您甚至可编写自己的自定义过滤器。

我一直使用的一个过滤器是 date 过滤器,它使您能够使用自定义模式来格式化日期值的外观。

例如,向之前创建的 time 元素应用一个 date 过滤器:

{{event.time | 
  date:'EEEE, MMMM, d, yyyy'}}

可以注意到,您为两个不同的过滤器使用了同一个 event.time 字段。EEEE 代码显示星期几的完整英文,比如 Monday。EEE 代码将星期几缩写为 Mon;EE 缩写为 Mo;E 缩写为 M。M 代码同样适用于月份名称。d 代码适用于一月中的某一天,y 代码适用于年。

event.time 字段会在主页上出现多次。更改 When 的外观以显示小时和 AM/PM 后缀:

<h4>When</h4>
<p>{{event.time | date:'h a'}}</p>

MEAN 应用程序中对 AngularJS 过滤器的大量使用,突出了 MVC 设计模式的一个重要原则:模型数据应与任何视图内容独立。

有了正确的 event.time 格式之后,仅剩下 event.description 外观需要修复了。为此,必须让 AngularJS 知道它可安全地显示此字段的未转义 HTML。

显示未转义的 HTML

目前为止使用的所有 JSON 数据都是纯数据,即没有嵌套的 HTML 元素。event.description 字段是一个例外。

任何时候您从外部来源收到 HTML(无论是否值得信任)时,都面临着一种潜在的安全风险。包含的 HTML 可带来不想要的 JavaScript 库,它们可能向其他网站公开您的数据。

为了防御此风险,AngularJS 会自动清理模板化数据,将它遇到的任何 HTML 元素进行转义,将 “真实的” 尖括号替换为转义的等效表示 &gt; 和 &lt;。此行为不是像前一节中看到的那样的显式过滤,但是一种类似的理念。

对于 event.description 字段,必须告诉 AngularJS,一起显示外部 HTML 和本地 HTML 是没有问题的。为此,调整您的模板,删除 {{event.description}} 占位符并将它替换为 ng-bind-html 属性:

<div class="col-md-8"></div>

在浏览器中查看主页时,可见的、转义的 <b> 和 <p> HTML 元素应消失,取而代之的是呈现的文本。

有了控制器、模型和视图,还剩最后一步:将控制器中的模拟 JSON 内容替换为从 Ajax 请求返回的实时数据。要执行这一步,需要向模块添加另一个元素:一个服务

创建一个服务

比较 $http 与 $resource

AngularJS $http 服务 用于发出原始 HTML 请求,比如 GET、POST、PUT 和 DELETE。在发出一次性请求时使用此服务。$resource 服务 是一种更高级的便捷服务,用于管理模型对象上的完整 CRUD 生命周期。可使用更低级的 $http 服务执行类似的操作,但如果在处理模型时使用 $resource 服务,最终编写的自定义代码就少得多。用于创建完整 CRUD 模块的 Yeoman 生成器使用了 $resource 服务。要查看它的实际应用,请浏览 public/modules/articles/services/articles.client.service.js。

使用了一个 AngularJS 服务 来发出 Ajax 请求,这是与外部 Meetup.com API 进行交互的完美解决方案。

您可能想要直接从控制器发出 Ajax 请求,但这么做有点目光短浅。如果其他控制器中需要该活动数据,该怎么办?您肯定不想在控制器之间复制和粘贴源代码,对吧?为了方便跨多个控制器共享相同数据,可创建一个服务。

键入 yo meanjs:angular-service events 来创建一个 events 服务,如清单 12 所示。在提示时选择 events 模块。

清单 12. 生成一个 AngularJS 服务
$ yo meanjs:angular-service events
[?] Which module does this service belongs to? 
  articles 
  core 
events 

  talks 
  users 

create public/modules/events/services/events.client.service.js

AngularJS 提供了一个名为 $http 的预构建服务来发出 HTTP/Ajax 请求。(所有 AngularJS 服务都有一个 $ 前缀。)要使用 $http,需将它注入到您的服务中。在 events 服务能正常运行后,将它注入到 EventsController 中。(AngularJS 到处都使用了依赖性注入。)

还记得您在 EventsController 中使用的 $scope 对象吗?$scope 是一个注入到控制器中的服务。如清单 13 所示,注入 $scope 服务的方式是,声明它,然后将它以参数形式传递给函数。

清单 13. 注入 $scope 服务
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';
  }
]);

两次键入服务名称似乎有点多余,但这么做方便了在准备将 MEAN 应用程序部署到生产环境中时发生的精简和串联过程。

您已看到如何注入一个服务,是时候应用这一知识了。打开 public/modules/events/services/events.client.service.js,如清单 14 所示。

清单 14. public/modules/events/services/events.client.service.js
'use strict';

angular.module('events').factory('Events', [
    function() {
        // Events service logic

        // ...

        // Public API
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

注入 $http 服务,如清单 15 所示。

清单 15. 注入 $http 服务
angular.module('events').factory('Events', ['$http',
    function($http) {
        // Events service logic
        // ...

        // Public API
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

接下来,将 someMethod 更改为 getNextEvent 并删除一些基本功能的存根,如清单 16 所示。

清单 16. 从 Ajax 调用返回 JSON
'use strict';

angular.module('events').factory('Events', ['$http',
  function($http) {
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&amp;order=
        time&amp;limited_events=False&amp;group_urlname=HTML5-Denver-Users-Group&amp;desc=
        false&amp;offset=0&amp;photo-host=public&amp;format=json&amp;page=1&amp;fields=
        &amp;sig_id=13848777&amp;sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&amp;callback=JSON_CALLBACK';

        var request = $http.jsonp(url);
        return request;
      }
    };
  }
]);

(详细的)URL 将返回 HTML5 Denver User Group 的下一场即将举办的活动。(Meetup.com 提供了一个很好的 沙箱 来使用其 API。)如果将该 URL 复制到您的浏览器中,您将获得完整的 JSON 响应。为清楚起见,我编辑了该响应,如清单 17 所示。

清单 17. 来自 Meetup.com 的 API 的 JSON 响应
{
  "results": [
    {
      "status": "upcoming",
      "visibility": "public",
      "venue": {
        "id": 21506832,
        "name": "Rally Software",
        "state": "CO",
        "address_1": "1550 Wynkoop",
        "city": "Denver"
      },
      "id": "160326502",
      "time": 1411430400000,
      "event_url": "http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/",
      "description": "<p><b>6 pm : "Developing Offline Applications with HTML 5" 
      by Venkat Subramaniam ",
      "name": ""Developing Offline Applications" and "HTML 5 Animations""
    }
  ],
  "meta": {
    "count": 1,
    "total_count": 3,
    "next": "http://api.meetup.com/2/events?status=upcoming&amp;sig_id=13848777&amp;
    order=time&amp;limited_events=False&amp;group_urlname=HTML5-Denver-Users-Group&amp;
    desc=false&amp;sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&amp;photo-host=public&amp;offset=1&amp;
    format=json&amp;page=1&amp;fields="
  }
}

结果数组中的 JSON 对象看起来应很熟悉。您删除了 EventsController 中的类似数据。但请注意,完整的 JSON 响应包含其他对在主页上呈现数据没有必要的信息(比如 meta)。幸运的是,在传递 JSON 响应之前可对它执行转换。将转换逻辑添加到 events 服务,如清单 18 所示。

清单 18. 转换 JSON 响应
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&amp;order=
        time&amp;limited_events=False&amp;group_urlname=HTML5-Denver-Users-Group&amp;desc=
        false&amp;offset=0&amp;photo-host=public&amp;format=json&amp;page=1&amp;fields=
        &amp;sig_id=13848777&amp;sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&amp;callback=JSON_CALLBACK';

        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});
        return request;
      }
    };
  }

]);

有了转换逻辑,JSON 将仅包含来自结果数组的第一个元素。所有其他额外的 JSON 信息都会丢弃。

为了在开发过程中提供帮助,可添加 success 和 error 处理函数,如清单 19 所示。此代码会将响应数据记录到控制台。您可自由地定义此代码,或者完全忽略它。

清单 19. 添加 success 和 error 处理函数
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&amp;order=
        time&amp;limited_events=False&amp;group_urlname=HTML5-Denver-Users-Group&amp;desc=
        false&amp;offset=0&amp;photo-host=public&amp;format=json&amp;page=1&amp;fields=
        &amp;sig_id=13848777&amp;sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&amp;callback=JSON_CALLBACK';


        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});

        request.success(function(data, status, headers, config) {
            console.log('SUCCESS');
            console.log(data);
        });
        request.error(function(data, status, headers, config) {
            console.log('ERROR');
            console.log(data);
        });

        return request;
      }
    };
  }
]);

现在 events 服务已完成,可以将它注入到 EventsController 中。修改 EventsController,如清单 20 所示。

清单 20. 将 events 服务注入到 EventsController 中
'use strict';

angular.module('events').controller('EventsController', ['$scope', 'Events',
  function($scope, Events) {
        $scope.event = undefined;

        Events.getNextEvent().success(function(data){
          $scope.event = data;          
        });
    }
]);

如果所有功能都按预期运行,您应在主页上看到一段完整的活动描述,如图 8 所示。如果在演讲描述中看到了比之前模拟的更多的细节,就会知道一切正常。

图 8. 完整、有效的示例的实际应用

完整、有效的示例的实际应用

点击查看大图

隐藏闪烁的无样式内容

在主页首次呈现到它向 Meetup.com 发出 Ajax 请求的时间间隔里,您可能注意到了讨厌的 FOUC(Flash of Unstyled Content,无样式内容闪烁)。如果没有看到,刷新浏览器两次,就应该会看到。

FOUC 不是特别重大的错误,但它们无疑会使您的应用程序看起来不太专业。所幸,AngularJS 开发人员为这个常见问题提供了一个简洁的解决方案。

使用 ng-show 对 home.client.view.html 执行最后一次更改,以隐藏视图,直到模型数据就位:

<div class="row center-block event">

将 ng-show 属性添加到 <div> 中,会导致整个 <div> 隐藏,直到填充了 $scope.event 变量。对 Meetup.com 的 Ajax 请求返回 JSON(模型)时,将显示 <div>(视图)。

UGLI 应用程序真正开始成形了。您从外部 API 拉入了 JSON 数据,并使用微数据格式化了得到的视图,以便搜索引擎和其他自动化流程可与查看网页的人访问到相同的信息。

在下一期,我将使用 OAuth 解决授权和验证问题。请届时继续阅读 “精通 MEAN”。