All posts by dotte

理解HTTP幂等性

基于HTTP协议的Web API是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的SOA或RESTful的Web API。为什么Web API如此流行呢?我认为很大程度上应归功于简单有效的HTTP协议。HTTP协议是一种分布式的面向资源的网络应用层协议,无论是服务器端提供Web服务,还是客户端消费Web服务都非常简单。再加上浏览器、Javascript、AJAX、JSON以及HTML5等技术和工具的发展,互联网应用架构设计表现出了从传统的PHP、JSP、ASP.NET等服务器端动态网页向Web API + RIA(富互联网应用)过渡的趋势。Web API专注于提供业务服务,RIA专注于用户界面和交互设计,从此两个领域的分工更加明晰。在这种趋势下,Web API设计将成为服务器端程序员的必修课。然而,正如简单的Java语言并不意味着高质量的Java程序,简单的HTTP协议也不意味着高质量的Web API。要想设计出高质量的Web API,还需要深入理解分布式系统及HTTP协议的特性。

幂等性定义

本文所要探讨的正是HTTP协议涉及到的一种重要性质:幂等性(Idempotence)。在HTTP/1.1规范中幂等性的定义是:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能帮助检查语法错误一样,HTTP规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。

分布式事务 vs 幂等设计

为什么需要幂等性呢?我们先从一个例子说起,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时用类函数的方式记为:

bool withdraw(account_id, amount)

withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;如果扣除失败则返回false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户也被多扣了一次钱。如图1所示:

20110810171503575

图1

这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的集成;另一方面分布式事务虽然能保证事务的ACID性质,而但却无法提供性能和可用性的保证。

另一种更轻量级的解决方案是幂等设计。我们可以通过一些技巧把withdraw变成幂等的,比如:

int create_ticket() 
bool idempotent_withdraw(ticket_id, account_id, amount)

create_ticket的语义是获取一个服务器端生成的唯一的处理号ticket_id,它将用于标识后续的操作。idempotent_withdraw和withdraw的区别在于关联了一个ticket_id,一个ticket_id表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的处理结果。这样,idempotent_withdraw就符合幂等性了,客户端就可以放心地多次调用。

基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤:1.调用create_ticket()获取ticket_id;2.调用idempotent_withdraw(ticket_id, account_id, amount)。虽然create_ticket不是幂等的,但在这种设计下,它对系统状态的影响可以忽略,加上idempotent_withdraw是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。如图2所示:

201106042051069339

图2

和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择。

HTTP的幂等性

HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。本文所讨论的HTTP幂等性主要针对RESTful风格的,不过正如上一节所看到的那样,幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。

HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。比如:GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。

HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。

比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:
The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line …… If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header.

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.
POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。而PUT所对应的URI是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

在介绍了几种操作的语义和幂等性之后,我们来看看如何通过Web API的形式实现前面所提到的取款功能。很简单,用POST /tickets来实现create_ticket;用PUT /accounts/account_id/ticket_id&amount=xxx来实现idempotent_withdraw。值得注意的是严格来讲amount参数不应该作为URI的一部分,真正的URI应该是/accounts/account_id/ticket_id,而amount应该放在请求的body中。这种模式可以应用于很多场合,比如:论坛网站中防止意外的重复发帖。

总结

上面简单介绍了幂等性的概念,用幂等设计取代分布式事务的方法,以及HTTP主要方法的语义和幂等性特征。其实,如果要追根溯源,幂等性是数学中的一个概念,表达的是N次变换与1次变换的结果相同,有兴趣的读者可以从Wikipedia上进一步了解。

参考

RFC 2616, Hypertext Transfer Protocol — HTTP/1.1, Method Definitions
The Importance of Idempotence
Stackoverflow – PUT vs POST in REST

from:http://www.cnblogs.com/weidagang2046/archive/2011/06/04/2063696.html

保证分布式系统数据一致性的6种方案

编者按:本文由「高可用架构后花园」群讨论整理而成,后花园是一个面向架构师的增值服务,如需了解,请关注「高可用架构」后回复 VIP

有人的地方,就有江湖

有江湖的地方,就有纷争

问题的起源

在电商等业务中,系统一般由多个独立的服务组成,如何解决分布式调用时候数据的一致性?

具体业务场景如下,比如一个业务操作,如果同时调用服务 A、B、C,需要满足要么同时成功;要么同时失败。A、B、C 可能是多个不同部门开发、部署在不同服务器上的远程服务。

在分布式系统来说,如果不想牺牲一致性,CAP 理论告诉我们只能放弃可用性,这显然不能接受。为了便于讨论问题,先简单介绍下数据一致性的基础理论。

强一致
当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。
弱一致性
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
最终一致性
弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。
在工程实践上,为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。但在电商等场景中,对于数据一致性的解决方法和常见的互联网系统(如 MySQL 主从同步)又有一定区别,群友的讨论分成以下 6 种解决方案。

1. 规避分布式事务——业务整合

业务整合方案主要采用将接口整合到本地执行的方法。拿问题场景来说,则可以将服务 A、B、C 整合为一个服务 D 给业务,这个服务 D 再通过转换为本地事务的方式,比如服务 D 包含本地服务和服务 E,而服务 E 是本地服务 A ~ C 的整合。

优点:解决(规避)了分布式事务。

缺点:显而易见,把本来规划拆分好的业务,又耦合到了一起,业务职责不清晰,不利于维护。

由于这个方法存在明显缺点,通常不建议使用。

2. 经典方案 – eBay 模式

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

消息日志方案的核心是保证服务接口的幂等性。

考虑到网络通讯失败、数据丢包等原因,如果接口不能保证幂等性,数据的唯一性将很难保证。

eBay 方式的主要思路如下。

Base:一种 Acid 的替代方案

此方案是 eBay 的架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章,是一篇解释 BASE 原则,或者说最终一致性的经典文章。文中讨论了 BASE 与 ACID 原则在保证数据一致性的基本差异。

如果 ACID 为分区的数据库提供一致性的选择,那么如何实现可用性呢?答案是

BASE (basically available, soft state, eventually consistent)

BASE 的可用性是通过支持局部故障而不是系统全局故障来实现的。下面是一个简单的例子:如果将用户分区在 5 个数据库服务器上,BASE 设计鼓励类似的处理方式,一个用户数据库的故障只影响这台特定主机那 20% 的用户。这里不涉及任何魔法,不过它确实可以带来更高的可感知的系统可用性。

文章中描述了一个最常见的场景,如果产生了一笔交易,需要在交易表增加记录,同时还要修改用户表的金额。这两个表属于不同的远程服务,所以就涉及到分布式事务一致性的问题。

文中提出了一个经典的解决方法,将主要修改操作以及更新用户表的消息放在一个本地事务来完成。同时为了避免重复消费用户表消息带来的问题,达到多次重试的幂等性,增加一个更新记录表 updates_applied来记录已经处理过的消息。

5ad0007faf14d9018dc

系统的执行伪代码如下

5bd0002b66b92973061

(点击可全屏缩放图片)

基于以上方法,在第一阶段,通过本地的数据库的事务保障,增加了 transaction 表及消息队列 。

在第二阶段,分别读出消息队列(但不删除),通过判断更新记录表 updates_applied 来检测相关记录是否被执行,未被执行的记录会修改 user 表,然后增加一条操作记录到 updates_applied,事务执行成功之后再删除队列。

通过以上方法,达到了分布式系统的最终一致性。进一步了解 eBay 的方案可以参考文末链接。

3. 去哪儿网分布式事务方案

随着业务规模不断地扩大,电商网站一般都要面临拆分之路。就是将原来一个单体应用拆分成多个不同职责的子系统。比如以前可能将面向用户、客户和运营的功能都放在一个系统里,现在拆分为订单中心、代理商管理、运营系统、报价中心、库存管理等多个子系统。

拆分首先要面临的是什么呢?

最开始的单体应用所有功能都在一起,存储也在一起。比如运营要取消某个订单,那直接去更新订单表状态,然后更新库存表就 ok 了。因为是单体应用,库在一起,这些都可以在一个事务里,由关系数据库来保证一致性。

但拆分之后就不同了,不同的子系统都有自己的存储。比如订单中心就只管理自己的订单库,而库存管理也有自己的库。那么运营系统取消订单的时候就是通过接口调用等方式来调用订单中心和库存管理的服务了,而不是直接去操作库。这就涉及一个『分布式事务』的问题。

5bd0002b66dc70ed910

分布式事务有两种解决方式

1. 优先使用异步消息。

上文已经说过,使用异步消息 Consumer 端需要实现幂等。

幂等有两种方式,一种方式是业务逻辑保证幂等。比如接到支付成功的消息订单状态变成支付完成,如果当前状态是支付完成,则再收到一个支付成功的消息则说明消息重复了,直接作为消息成功处理。

另外一种方式如果业务逻辑无法保证幂等,则要增加一个去重表或者类似的实现。对于 producer 端在业务数据库的同实例上放一个消息库,发消息和业务操作在同一个本地事务里。发消息的时候消息并不立即发出,而是向消息库插入一条消息记录,然后在事务提交的时候再异步将消息发出,发送消息如果成功则将消息库里的消息删除,如果遇到消息队列服务异常或网络问题,消息没有成功发出那么消息就留在这里了,会有另外一个服务不断地将这些消息扫出重新发送。

2. 有的业务不适合异步消息的方式,事务的各个参与方都需要同步的得到结果。这种情况的实现方式其实和上面类似,每个参与方的本地业务库的同实例上面放一个事务记录库。

比如 A 同步调用 B,C。A 本地事务成功的时候更新本地事务记录状态,B 和 C 同样。如果有一次 A 调用 B 失败了,这个失败可能是 B 真的失败了,也可能是调用超时,实际 B 成功。则由一个中心服务对比三方的事务记录表,做一个最终决定。假设现在三方的事务记录是 A 成功,B 失败,C 成功。那么最终决定有两种方式,根据具体场景:

  1. 重试 B,直到 B 成功,事务记录表里记录了各项调用参数等信息;
  2. 执行 A 和 B 的补偿操作(一种可行的补偿方式是回滚)。

对 b 场景做一个特殊说明:比如 B 是扣库存服务,在第一次调用的时候因为某种原因失败了,但是重试的时候库存已经变为 0,无法重试成功,这个时候只有回滚 A 和 C 了。

那么可能有人觉得在业务库的同实例里放消息库或事务记录库,会对业务侵入,业务还要关心这个库,是否一个合理的设计?

实际上可以依靠运维的手段来简化开发的侵入,我们的方法是让 DBA 在公司所有 MySQL 实例上预初始化这个库,通过框架层(消息的客户端或事务 RPC 框架)透明的在背后操作这个库,业务开发人员只需要关心自己的业务逻辑,不需要直接访问这个库。

总结起来,其实两种方式的根本原理是类似的,也就是将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性

4. 蘑菇街交易创建过程中的分布式一致性方案

交易创建的一般性流程

我们把交易创建流程抽象出一系列可扩展的功能点,每个功能点都可以有多个实现(具体的实现之间有组合/互斥关系)。把各个功能点按照一定流程串起来,就完成了交易创建的过程。

5ad0007faf4df6af7f3

面临的问题

每个功能点的实现都可能会依赖外部服务。那么如何保证各个服务之间的数据是一致的呢?比如锁定优惠券服务调用超时了,不能确定到底有没有锁券成功,该如何处理?再比如锁券成功了,但是扣减库存失败了,该如何处理?

方案选型

服务依赖过多,会带来管理复杂性增加和稳定性风险增大的问题。试想如果我们强依赖 10 个服务,9 个都执行成功了,最后一个执行失败了,那么是不是前面 9 个都要回滚掉?这个成本还是非常高的。

所以在拆分大的流程为多个小的本地事务的前提下,对于非实时、非强一致性的关联业务写入,在本地事务执行成功后,我们选择发消息通知、关联事务异步化执行的方案。

消息通知往往不能保证 100% 成功;且消息通知后,接收方业务是否能执行成功还是未知数。前者问题可以通过重试解决;后者可以选用事务消息来保证。

但是事务消息框架本身会给业务代码带来侵入性和复杂性,所以我们选择基于 DB 事件变化通知到 MQ 的方式做系统间解耦,通过订阅方消费 MQ 消息时的 ACK 机制,保证消息一定消费成功,达到最终一致性。由于消息可能会被重发,消息订阅方业务逻辑处理要做好幂等保证。

所以目前只剩下需要实时同步做、有强一致性要求的业务场景了。在交易创建过程中,锁券和扣减库存是这样的两个典型场景。

要保证多个系统间数据一致,乍一看,必须要引入分布式事务框架才能解决。但引入非常重的类似二阶段提交分布式事务框架会带来复杂性的急剧上升;在电商领域,绝对的强一致是过于理想化的,我们可以选择准实时的最终一致性。

我们在交易创建流程中,首先创建一个不可见订单,然后在同步调用锁券和扣减库存时,针对调用异常(失败或者超时),发出废单消息到MQ。如果消息发送失败,本地会做时间阶梯式的异步重试;优惠券系统和库存系统收到消息后,会进行判断是否需要做业务回滚,这样就准实时地保证了多个本地事务的最终一致性。

5b30008086e9f8d9583

5. 支付宝及蚂蚁金融云的分布式服务 DTS 方案

业界常用的还有支付宝的一种 xts 方案,由支付宝在 2PC 的基础上改进而来。主要思路如下,大部分信息引用自官方网站。

分布式事务服务简介

分布式事务服务 (Distributed Transaction Service, DTS) 是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。DTS 从架构上分为 xts-client 和 xts-server 两部分,前者是一个嵌入客户端应用的 JAR 包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复。

核心特性

传统关系型数据库的事务模型必须遵守 ACID 原则。在单数据库模式下,ACID 模型能有效保障数据的完整性,但是在大规模分布式环境下,一个业务往往会跨越多个数据库,如何保证这多个数据库之间的数据一致性,需要其他行之有效的策略。在 JavaEE 规范中使用 2PC (2 Phase Commit, 两阶段提交) 来处理跨 DB 环境下的事务问题,但是 2PC 是反可伸缩模式,也就是说,在事务处理过程中,参与者需要一直持有资源直到整个分布式事务结束。这样,当业务规模达到千万级以上时,2PC 的局限性就越来越明显,系统可伸缩性会变得很差。基于此,我们采用 BASE 的思想实现了一套类似 2PC 的分布式事务方案,这就是 DTS。DTS在充分保障分布式环境下高可用性、高可靠性的同时兼顾数据一致性的要求,其最大的特点是保证数据最终一致 (Eventually consistent)。

简单的说,DTS 框架有如下特性:

  • 最终一致:事务处理过程中,会有短暂不一致的情况,但通过恢复系统,可以让事务的数据达到最终一致的目标。
  • 协议简单:DTS 定义了类似 2PC 的标准两阶段接口,业务系统只需要实现对应的接口就可以使用 DTS 的事务功能。
  • 与 RPC 服务协议无关:在 SOA 架构下,一个或多个 DB 操作往往被包装成一个一个的 Service,Service 与 Service 之间通过 RPC 协议通信。DTS 框架构建在 SOA 架构上,与底层协议无关。
  • 与底层事务实现无关: DTS 是一个抽象的基于 Service 层的概念,与底层事务实现无关,也就是说在 DTS 的范围内,无论是关系型数据库 MySQL,Oracle,还是 KV 存储 MemCache,或者列存数据库 HBase,只要将对其的操作包装成 DTS 的参与者,就可以接入到 DTS 事务范围内。

以下是分布式事务框架的流程图

5c000029fd4a5102725

实现

  1. 一个完整的业务活动由一个主业务服务与若干从业务服务组成。
  2. 主业务服务负责发起并完成整个业务活动。
  3. 从业务服务提供 TCC 型业务操作。
  4. 业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在活动提交时确认所有的两阶段事务的 confirm 操作,在业务活动取消时调用所有两阶段事务的 cancel 操作。”

与 2PC 协议比较

  1. 没有单独的 Prepare 阶段,降低协议成本
  2. 系统故障容忍度高,恢复简单

6. 农信网数据一致性方案

1. 电商业务

公司的支付部门,通过接入其它第三方支付系统来提供支付服务给业务部门,支付服务是一个基于 Dubbo 的 RPC 服务。

对于业务部门来说,电商部门的订单支付,需要调用

  1. 支付平台的支付接口来处理订单;
  2. 同时需要调用积分中心的接口,按照业务规则,给用户增加积分。

从业务规则上需要同时保证业务数据的实时性和一致性,也就是支付成功必须加积分。

我们采用的方式是同步调用,首先处理本地事务业务。考虑到积分业务比较单一且业务影响低于支付,由积分平台提供增加与回撤接口。

具体的流程是先调用积分平台增加用户积分,再调用支付平台进行支付处理,如果处理失败,catch 方法调用积分平台的回撤方法,将本次处理的积分订单回撤。

5bd0002b671c5f212f4

2. 用户信息变更

公司的用户信息,统一由用户中心维护,而用户信息的变更需要同步给各业务子系统,业务子系统再根据变更内容,处理各自业务。用户中心作为 MQ 的 producer,添加通知给 MQ。APP Server 订阅该消息,同步本地数据信息,再处理相关业务比如 APP 退出下线等。

我们采用异步消息通知机制,目前主要使用 ActiveMQ,基于 Virtual Topic 的订阅方式,保证单个业务集群订阅的单次消费。

5ad0007faf7a576a640

总结

分布式服务对衍生的配套系统要求比较多,特别是我们基于消息、日志的最终一致性方案,需要考虑消息的积压、消费情况、监控、报警等。

参考资料

  • Base: An Acid Alternative (eBay 方案)

In partitioned databases, trading some consistency for availability can lead to dramatic improvements in scalability.

英文版 : http://queue.acm.org/detail.cfm?id=1394128 

中文版: http://article.yeeyan.org/view/167444/125572

 

感谢李玉福、余昭辉、蘑菇街七公提供方案,其他多位群成员对本文内容亦有贡献。对于上述方案表述不完善之处,欢迎读者留言指出。

from:http://www.cnblogs.com/soundcode/p/5590710.html

使用 Angular 2 实现单页应用程序

由于具有实现更高性能的浏览器和智能电话应用程序的潜力,单页应用程序 (SPA) 技术在软件行业引起了广泛的兴趣。在过去 5 年多的时间里,开发人员对 Angular(一个开源 SPA 框架)的兴趣远超他们对其他 Web 框架(React、Backbone、Meteor 和 Ember)的兴趣,这从 StackOverflow 网站上针对每种 Web 框架的问题数量上可以判断出:

曲线图: 有关 Web 框架的问题数量

(图像来源:tag-trends

Web 和移动开发人员非常喜欢 Angular 2(2016 年 9 月发布)。Angular 2 不是 Angular 1 的一次升级,而是一个全新的、不同的、更高级的框架。精通 Angular 2 已成为构建高性能、可扩展、稳健、现代的跨平台应用程序的一种很吃香的技能。

本教程将介绍 Angular 2 中的重要构建块,演示如何在开发环境和沙箱服务器中使用 Angular 2 编写和运行 SPA。要充分掌握本教程,您需要具有一定的 Web 编程经验,包括 JavaScript、TypeScript 和 HTML 的各方面知识。无需提前拥有 Angular 经验。练习完示例项目后,您就会为使用 Angular 2 创建自己的 SPA 做好充足的准备。

可以从教程末尾的 “可下载资源” 部分下载针对沙箱应用程序的完整示例代码。

为什么使用 SPA 和 Angular 2?

在用户启动 SPA 时,该应用程序仅呈现来自服务器的一个 HTML 页面。除了这个 HTML 页面,服务器还会向客户端发送一个应用程序引擎。该引擎控制整个应用程序,包括HTML 页面的处理、输入、输出、绘制和加载。通常,90–95% 的应用程序代码是在浏览器中运行;当用户需要新数据时或必须执行身份验证等安全操作时,就会在服务器中运行剩余代码。由于几乎消除了对服务器的依赖,所以 SPA 能在 Angular 2 环境中自动扩展:无论有多少用户同时访问服务器,在 90–95% 的时间里,应用程序的性能绝不会受到影响。

另外,因为大部分负载都在客户端上运行,所以服务器在大部分时间里都处于空闲状态。对服务器资源的低需求显著减少了服务器上的压力,也潜在地降低了服务器成本。

Angular 2 的另一个优势是,它可以帮助 SPA 有效地使用微服务。

通常,90–95% 的 SPA 代码在浏览器中运行;当用户需要新数据或必须执行身份验证等安全操作时,就会在服务器中运行剩余代码。

Angular 2 概念

Angular 2 中的关键概念包括:

从现在开始,我将 Angular 2 简称为 Angular。

模块

Angular 应用程序是模块化的。每个 Angular 应用程序拥有至少一个模块,即根模块,传统上将其命名为 AppModule。根模块可以是小型应用程序中唯一的模块,但大多数应用程序都拥有更多的模块。作为开发人员,您需要决定如何使用模块概念。通常,会将主要功能或特性映射到一个模块。假设您的系统中有 5 个主要区域。除了根模块之外,每个区域都将拥有自己的模块,总计 6 个模块。

组件

组件 控制页面的一片区域,该区域被称为视图。您定义组件的应用程序逻辑 — 它如何支持类中的视图。类通过属性和方法的 API 与视图交互。一个组件包含一个类、一个模板和元数据。模板是一个 HTML 表单,用于告诉 Angular 如何呈现组件。一个组件只能属于一个模块。

服务

服务 提供应用程序所需的任何值、功能或特性。服务通常是一个具有明确定义的较小用途的类;它应高效地执行具体的操作。组件大量使用服务。服务大量使用微服务。

路由

路由 支持在用户执行应用程序任务时,从一个视图导航到下一个视图。路由等效于用于控制菜单和子菜单的机制。

了解 SPA 的优势并掌握 Angular 概念后,是时候在示例项目上开始实践了。

需要做的准备工作

要完成示例项目,您需要在开发 PC 上安装 Node.jsAngular CLI(一个用于 Angular 的命令行接口):

  • 安装 Node.js:
    1. 下载 适合您的系统的版本,选择默认选项来完成安装。
    2. 从操作系统命令行运行 node -v 来验证版本号 — 在我的情况下,版本为 v6.9.1

    Node Package Manager (NPM) 会随 Node 自动安装。键入 npm -v 来查看它的版本号。当您安装来自公共 NPM 存储库的包时,会在后台运行 NPM。一个常见的 NPM 命令是 npm install,它用于下载您的 Angular 项目的 package.json 文件中列出的包版本。

  • 安装 Angular CLI:
    1. 运行 npm install -g angular-cli@1.0.0-beta.21 来安装我用于示例应用程序的版本(在编写本文时,仍处于公测阶段)。(如果您想尝试一个不同的编译版,可访问 CLI 站点。)需要花约 10 分钟才能完成安装。
    2. 成功完成安装后,在操作系统命令行上键入 ng -v,以查看您的 CLI 版本号 — 在我的操作系统下:
      angular-cli: 1.0.0-beta.21
      node: 6.9.1
      os: win32 x64

      如果您需要获得有关 ng 语法的帮助,可在操作系统命令行上键入 ng help

package.json 文件

package.json 文件(Angular 应用程序中一个关键的元数据文件)包含应用程序和它的依赖包的细节。此文件是 Angular 应用程序中最重要的文件,尤其是在将代码从一台计算机迁移到另一台时,或者是在升级期间。package.json 文件包含需要安装的包版本。

您无需为适合本教程练习的 package.json 感到担心。但在开发生产级应用程序时,必须多多留意此文件中列出的版本号。

以下是来自一个 package.json 文件的一些有效语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
{ "dependencies" :
  { "foo" : "1.0.0 - 2.9999.9999"
  , "bar" : ">=1.0.2 <2.1.2"
  , "baz" : ">1.0.2 <=2.3.4"
  , "boo" : "2.0.1"
  , "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
  , "til" : "^1.2"
  , "elf" : "~1.2.3"
  , "two" : "2.x"
  , "thr" : "3.3.x"
  , "lat" : "latest"
  }
}

npm install 命令对冒号右侧的部分语句的解释如下(其中版本 表示所使用的版本号):

  • 版本“:必须与版本 准确匹配
  • “>版本“:必须大于版本
  • “~版本“:约等于版本
  • “^版本“:兼容版本
  • “1.2.x”:1.2.0、1.2.1 等,但不能是 1.3.0
  • “*” :匹配任何版本
  • “”(空字符串)”:与 * 相同
  • 版本 1版本 2“:与 “>=版本 1 <= 版本 2” 相同

要进一步了解 package.json,可在操作系统命令行上键入 npm help package.json,以便在浏览器中查看帮助内容。

示例项目概述

示例项目包含一个开箱即用的 Angular 应用程序,以及您将在开箱即用的应用程序上开发的一个自定义应用程序。当您完成上述操作后,您将拥有一个包含 3 个微型应用程序的 Angular 应用程序,每个微型应用程序中的特性使用了 3 个 Web 服务 API:

所有应用程序逻辑都将在您的浏览器中运行。仅在浏览器需要新数据时,才需要服务器。事实上,您可以关闭服务器进程,它仍会在您的应用程序中工作,因为它是一个 SPA。

下图显示了应用程序拓扑结构:

拓扑结构图

您将使用 Angular CLI 创建您的项目(该项目默认情况下会包含 AppModuleAppComponent)和 4 个自定义组件:

  • 菜单组件
  • 天气组件
  • 电影组件
  • 货币组件

您将创建用于菜单导航的路由,并将以下服务注入到天气、电影和货币组件中:

  • 来自使用微服务的 HTTP 的数据
  • 在使用这些服务时跨组件的资源共享

创建基础应用程序和模块

准备好开始了吗?首先在操作系统命令行上转到您想放置项目目录的位置。

创建一个 Angular 项目

运行下面的命令来生成一个新 Angular 项目(其中 dw_ng2_app 是项目名称):

ng new dw_ng2_app --skip-git

安装所有需要的包和 Angular 基础应用程序(这将花费大约 10 分钟时间)后,您将返回到操作系统命令提示符上。如果您随后列出 /dw_ng2_app 目录的内容,就可以看到项目结构:

|— e2e
|— node_modules
|— src
angular-cli.json
karma.conf.js
package.json
protractor.conf.js
README.md
tslint.json

../dw_ng2_app/src 目录的内容包括:

|— app
|— assets
|— environments
favicon.ico
index.html
main.ts
polyfills.ts
styles.css
test.ts
tsconfig.json
typings.d.ts

../dw_ng2_app/src/app 目录(根模块文件夹)包含以下文件:

app.component.css
app.component.html
app.component.spec.ts
app.component.ts
app.module.ts
index.ts

运行开箱即用的 Angular 应用程序

更改到项目目录,运行 ng serve 来启动开箱即用的 Angular 应用程序。

默认情况下,该进程在端口 4200 上启动。如果您的 port 系统环境变量的值不是 4200,该进程将在此端口上启动。您可以运行 ng serve --port 4200 命令来覆盖默认端口号,这是一项可选操作。

打开您的浏览器并输入 URL http://localhost:4200/。您的 Angular 应用程序会显示 app works!,这表明应用程序已启动、运行并准备就绪:

正在运行的基础应用程序的屏幕截图

如果在应用程序运行过程中更改代码,Angular 会非常智能地监视并自动重新启动应用程序。尝试编辑 app.component.ts 文件,更改 title 的值。您可以看到,您的浏览器页面反映了这一更改:

该屏幕截图显示了 GUI 中已修改的标题

如何将模块链接到组件

在清单 1 中,第 20 行显示了 AppModule 模块的声明。

清单 1. app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

此模块仅包含一个组件 —AppComponent— 如上第 10 行所示。第 18 行表明,在引导进程下启动的第一个组件是 AppComponent

组件内部结构

清单 2 给出了 app.component.ts 文件中的主要应用程序组件的内容。

清单 2. app.component.ts
1
2
3
4
5
6
7
8
9
10
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'My Angular 2 app works!';
}

这是 app.component.html 文件中的主要应用程序组件的内容:

1
2
3
<h1>
  {{title}}
</h1>

元数据位置

元数据告诉 Angular 如何处理类。事实上,在您通过向 AppComponent 附加元数据给类时,告诉 Angular 它还不是组件。

可以使用 @Component 修饰器附加元数据,这会将类标识为组件。@Component 修饰器接受一个必需的配置对象,该对象包含 Angular 创建和呈现该组件和它的视图所需的信息。

清单 2 中的代码使用可用的 @Component 配置选项中的 3 个选项:

  • selector:一个 CSS 选择器,告诉 Angular 在父 HTML 文件中找到 selector 标记的地方创建和插入此组件的一个实例
  • templateUrl:组件的 HTML 文件
  • styleUrls:组件的样式表,比如 .css 文件

组件内的模板位置

模板是一个 HTML 表单,用于告诉 Angular 如何呈现组件。在 清单 2 的第 5 行上,templateUrl 指向一个名为 app.component.html 的视图。

组件内的数据绑定

数据绑定是一个 HTML 表单,用于告诉 Angular 如何呈现组件。在 app.component.ts 文件中,title 的值在类内设置,在 app.component.html 文件中使用。数据绑定可以是单向或双向的。在本例中,如果您在双花括号 {{ }} 内提及该变量,则该映射是单向的。值从类传递到 HTML 文件。

创建自定义组件和路由

此刻,您的 Angular 应用程序已准备就绪并能工作正常。这个基础应用程序有一个模块、一个组件、一个类、一个模板、元数据和数据绑定 — 但它仍缺少 4 个其他的重要部分:

  • 多个组件
  • 路由
  • 服务
  • 微服务的使用

接下来,您将创建这些自定义组件。

创建自定义组件

按 Ctrl-C 停止 Angular 进程(确保您在 Angular 项目的目录中,在本例中为 dw_ng2_app)。在命令提示符下,运行以下命令:

  • ng g c Menu -is --spec false --flat:在 AppModule 根模块(同一个文件夹中)内创建 Menu 组件。
  • ng g c Weather -is --spec false:在 AppModule 根模块内名为 weather 的子文件夹中创建 Weather 组件。
  • ng g c Currency -is --spec false:在 AppModule 根模块内名为 currency 的子文件夹中创建 Currency 组件。
  • ng g c Movie -is --spec false:在 AppModule 根模块内名为 movie 的子文件夹中创建 Movie 组件。

现在,有了创建的新组件 — 包括类、元数据和模板 — 您可以看到 AppModule 如何链接到这些组件。在清单 3 中,第 28 行包含 AppModule 模块的声明。此模块包含 5 个组件,包括根组件和其他 4 个组件,如第 14–18 行所示。

清单 3. app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { MenuComponent } from './menu.component';
import { WeatherComponent } from './weather/weather.component';
import { CurrencyComponent } from './currency/currency.component';
import { MovieComponent } from './movie/movie.component';
@NgModule({
  declarations: [
    AppComponent,
    MenuComponent,
    WeatherComponent,
    CurrencyComponent,
    MovieComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

创建路由

要让 Angular 能在组件之间导航,需要创建路由。使用清单 4 的内容覆盖 menu.component.html 文件,以便 HTML 包含所有组件的正确菜单。

清单 4. menu.component.html
1
2
3
4
5
6
7
8
9
<div class="row">
   <div class="col-xs-12">
    <ul class="nav nav-pills">
     <li routerLinkActive="active"> <a [routerLink]="['/weather']" >Weather</a></li>
     <li routerLinkActive="active"> <a [routerLink]="['/movie']" >Movie Details</a></li>
     <li routerLinkActive="active"> <a [routerLink]="['/currency']" >Currency Rates</a></li>
    </ul>
  </div>
 </div>

清单 4 中的代码提供了 GUI 与 URL 路径之间的映射。例如,当用户单击 GUI 中的 Movie Details 按钮时,Angular 知道它需要像 URL 路径为 http://localhost:4200/movie 一样运行。

接下来,将 URL 路径映射到组件。在根模块的相同文件夹中,创建一个名为 app.routing.ts 的配置文件,使用清单 5 中的代码作为其内容。

清单 5. app.routing.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { Routes, RouterModule } from '@angular/router';
import { CurrencyComponent } from "./currency/currency.component";
import { WeatherComponent } from "./weather/weather.component";
import { MovieComponent } from "./movie/movie.component";
const MAINMENU_ROUTES: Routes = [
    //full : makes sure the path is absolute path
    { path: '', redirectTo: '/weather', pathMatch: 'full' },
    { path: 'weather', component: WeatherComponent },
    { path: 'movie', component: MovieComponent },
    { path: 'currency', component: CurrencyComponent }
];
export const CONST_ROUTING = RouterModule.forRoot(MAINMENU_ROUTES);

在本例中,如果您的 URL 相对路径是 movie,则会告诉 Angular 调用 MovieComponent 组件。换句话说,相对路径 movie 映射到 URL http://localhost:4200/movie

现在,您需要将此视图链接到它的父组件。使用以下代码覆盖 app.component.html 文件内容:

1
2
3
4
5
<div class="container">
 <app-menu></app-menu>
 <hr>
 <router-outlet></router-outlet>
</div>

<app-menu></app-menu> 选择器会包含菜单。<router-outlet></router-outlet> 选择器是当前组件的占位符。根据 URL 路径,该值可以是以下 3 个组件中的任意一个:天气、电影或货币。

您还必须向该模块告知此路由。在 app.module.ts 文件中添加两项,如清单 6 中的第 11 和 25 行所示。

清单 6. app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { MenuComponent } from './menu.component';
import { WeatherComponent } from './weather/weather.component';
import { CurrencyComponent } from './currency/currency.component';
import { MovieComponent } from './movie/movie.component';
import { CONST_ROUTING } from './app.routing';
@NgModule({
  declarations: [
    AppComponent,
    MenuComponent,
    WeatherComponent,
    CurrencyComponent,
    MovieComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    CONST_ROUTING
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

现在,如果您运行应用程序并单击 Weather 链接,应用程序将会显示 weather works!

单击 Weather 链接后的应用程序屏幕截图

如果单击 Movie Details 链接,应用程序会显示 movie works!

单击 Movie Details 链接后的应用程序屏幕截图

如果单击 Currency Rates 链接,应用程序将会显示 currency works!

单击 Currency Rates 链接后的应用程序屏幕截图

您已成功修改了您的 Angular 应用程序,以便包含多个自定义组件和路由。现在您已经准备好执行最后两个重要操作:

  • 创建和配置服务
  • 使用微服务

创建服务

按下 Ctrl-C 停止 Angular 进程。运行下面的命令:

ng g service Shared --spec false

此命令在根模块文件夹中的 shared.service.ts 文件中创建该服务:

屏幕输出的屏幕截图显示创建了该服务

将 shared.service.ts 的内容替换为清单 7 中的代码。

清单 7. shared.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Injectable } from '@angular/core';
import { Http, Headers, Response } from "@angular/http";
import 'rxjs/Rx';
import { Observable } from "rxjs";
@Injectable()
export class SharedService {
    weatherURL1 = "https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22";
    weatherURL2 = "%2C%20";
    weatherURL3 = "%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys";
    findMovieURL1 = "http://www.omdbapi.com/?t=";
    findMovieURL2 = "&y=&plot=short&r=json";
    currencyURL = "http://api.fixer.io/latest?symbols=";
    totReqsMade: number = 0;
    constructor(private _http: Http) { }
    findWeather(city, state) {
        this.totReqsMade = this.totReqsMade + 1;
        return this._http.get(this.weatherURL1 + city + this.weatherURL2+ state + this.weatherURL3)
            .map(response => {
                { return response.json() };
            })
            .catch(error => Observable.throw(error.json()));
    }
    findMovie(movie) {
        this.totReqsMade = this.totReqsMade + 1;
        return this._http.get(this.findMovieURL1 + movie + this.findMovieURL2)
            .map(response => {
                { return response.json() };
            })
            .catch(error => Observable.throw(error.json().error));
    }
    getCurrencyExchRate(currency) {
        this.totReqsMade = this.totReqsMade + 1;
        return this._http.get(this.currencyURL + currency)
            .map(response => {
                { return response.json() };
            })
            .catch(error => Observable.throw(error.json()));
    }
}

清单 7 中的 import ... 语句是任何服务正常运行所必不可少的。@Injectable() 语句特别重要;它表明此 service 可注入到其他组件中 — 该技术通常被称为依赖注入

totReqsMade 变量在这里声明,将用于在 3 个组件之间传递它的值。这将跟踪为获得微服务结果而发出的服务请求总数。

您有 3 个方法,它们的名称表明了自己的功能:findWeather()findMovie()getCurrencyExchRate()。在方法执行期间,您的 Angular 应用程序将让浏览器访问网络来使用微服务。现在您将把组件链接到所创建的服务。

将 movie.component.ts 文件内容替换为清单 8 中的代码。

清单 8. movie.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Component, OnInit } from '@angular/core';
import { SharedService } from "./../shared.service";
@Component({
  selector: 'app-movie',
  templateUrl: './movie.component.html',
  styles: []
})
export class MovieComponent implements OnInit {
    id_movie: string = "";
    mv_Title: string = "";
    mv_Rated: string = "";
    mv_Released: string = "";
    mv_Director: string = "";
    mv_Actors: string = "";
    mv_Plot: string = "";
    constructor(private _sharedService: SharedService) {
    }
    ngOnInit() {
    }
    callMovieService() {
        this._sharedService.findMovie(this.id_movie)
            .subscribe(
            lstresult => {
                this.mv_Title = lstresult["Title"];
                this.mv_Rated = lstresult["Rated"];
                this.mv_Released = lstresult["Released"];
                this.mv_Director = lstresult["Director"];
                this.mv_Actors = lstresult["Actors"];
                this.mv_Plot = lstresult["Plot"];
            },
            error => {
                console.log("Error. The findMovie result JSON value is as follows:");
                console.log(error);
            }
            );
    }
}

这个重要的代码端调用服务方法来获取新数据。在本例中,它调用 callMovieService(),然后调用 this._sharedService.findMovie() 方法。

类似地,将 currency.component.ts 文件内容替换为清单 9 中的代码。

清单 9. currency.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component, OnInit } from '@angular/core';
import { SharedService } from "./../shared.service";
@Component({
  selector: 'app-currency',
  templateUrl: './currency.component.html',
  styles: []
})
export class CurrencyComponent implements OnInit {
 
  id_currency: string = "";
  my_result: any;
  constructor(private _sharedService: SharedService) {
  }
  ngOnInit() {
  }
  callCurrencyService() { 
    this._sharedService.getCurrencyExchRate(this.id_currency.toUpperCase())
      .subscribe(
      lstresult => {
                this.my_result = JSON.stringify(lstresult);
      },
      error => {
        console.log("Error. The callCurrencyService result JSON value is as follows:");
        console.log(error);
      }
      );
  }
}

将 weather.component.ts 文件内容替换为清单 10 中的代码。

清单 10. weather.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { Component, OnInit } from '@angular/core';
import { SharedService } from "./../shared.service";
@Component({
  selector: 'app-weather',
  templateUrl: './weather.component.html',
  styles: []
})
export class WeatherComponent implements OnInit {
  id_city: string = "";
  id_state: string = "";
  op_city: string = "";
  op_region: string = "";
  op_country: string = "";
  op_date: string = "";
  op_text: string = "";
  op_temp: string = "";
  constructor(private _sharedService: SharedService) {
  }
  ngOnInit() {
  }
  callWeatherService() {
    this._sharedService.findWeather(this.id_city, this.id_state)
      .subscribe(
      lstresult => {
        this.op_city = lstresult["query"]["results"]["channel"]["location"]["city"];
        this.op_region = lstresult["query"]["results"]["channel"]["location"]["region"];
        this.op_country = lstresult["query"]["results"]["channel"]["location"]["country"];
        this.op_date = lstresult["query"]["results"]["channel"]["item"]["condition"]["date"];
        this.op_text = lstresult["query"]["results"]["channel"]["item"]["condition"]["text"];
        this.op_temp = lstresult["query"]["results"]["channel"]["item"]["condition"]["temp"];
      },
      error => {
        console.log("Error. The findWeather result JSON value is as follows:");
        console.log(error);
      }
      );
  }
}

现在,更新该模块以包含这些服务。编辑 app.module.ts 文件,以包含清单 11 的第 12 和 28 行的两条语句。

清单 11. app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { MenuComponent } from './menu.component';
import { WeatherComponent } from './weather/weather.component';
import { CurrencyComponent } from './currency/currency.component';
import { MovieComponent } from './movie/movie.component';
import { CONST_ROUTING } from './app.routing';
import { SharedService } from "./shared.service";
@NgModule({
  declarations: [
    AppComponent,
    MenuComponent,
    WeatherComponent,
    CurrencyComponent,
    MovieComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    CONST_ROUTING
  ],
  providers: [SharedService],
  bootstrap: [AppComponent]
})
export class AppModule { }

修改组件视图

现在还剩难题的最后一部分。您需要告诉 HTML 文件调用正确的服务方法。为此,请将 movie.component.html 文件的内容替换为清单 12 中的代码。

清单 12. movie.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<h2>Open Movie Database</h2>
<div class="col-md-8 col-md-offset-2">
 <div class="form-group">
  <input type="text" required [(ngModel)]="id_movie" (change)="callMovieService()" class="form-control" placeholder="Enter Movie name ...">
  <br><br>
  <h3>Movie Details</h3>
  <br>
  <p class="well lead">
      <i> Title :</i> {{ this.mv_Title }} <br>
      <i> Plot :</i> {{ this.mv_Plot }} <br>
      <i> Actors :</i> {{ this.mv_Actors }} <br>
      <i> Directed by :</i> {{ this.mv_Director }} <br>
      <i> Rated :</i> {{ this.mv_Rated }} <br>
      <i> Release Date :</i> {{ this.mv_Released }} <br>
  </p>
  <p class="text-info">Total # of all the service requests including Weather, Movie, and Currency is :
      <span class="badge">{{this._sharedService.totReqsMade}}</span>
  </p>
 </div>
</div>

movie.component.html 中编码了一些重要的信息:

  • {{ this._sharedService.totReqsMade }}:这是在服务级别上跟踪的值,它会在所有 3 个应用程序组件之间共享。
  • [(ngModel)]="id_movie":用户输入的 GUI 输入被传递到调用此 HTML 的类。在本例中,该类为 MovieComponent
  • (change)="callMovieService()“:当此字段值更改时,就会告诉系统调用 movie.component.ts 文件中包含的 callMovieService() 方法。
  • {{ this.mv_Title }}, {{ this.mv_Plot }}, {{ this.mv_Actors }}, {{ this.mv_Director }}, {{ this.mv_Rated }}, {{ this.mv_Released }}:显示从 callMovieService() -> this._sharedService.findMovie(this.id_movie) 执行的服务调用的结果。

将 weather.component.html 文件的内容替换为清单 13 中的代码。

清单 13. weather.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<h2>Yahoo! Weather </h2>
<div class="col-md-8 col-md-offset-2">
 <div class="form-group">
  <input type="text" [(ngModel)]="id_city" class="form-control" placeholder="Enter City name ..."><br>
  <input type="text" [(ngModel)]="id_state" class="form-control" placeholder="Enter State. Example CA for California ..."><br>
  <button type="button" class="btn btn-primary" (click)="callWeatherService()">Submit</button>
  <br><br><br>
  <br>
  <p class="well lead">
    <i>City, State, Country :</i> {{ this.op_city }} {{ this.op_region }} {{ this.op_country }} <br>
    <i>Current Condition :</i> {{ this.op_text }} <br>
    <i>Current Temperature :</i> {{ this.op_temp }} <br>
  </p>
  <p class="text-info">Total # of all the service requests including Weather, Movie, and Currency is :
    <span class="badge">{{this._sharedService.totReqsMade}}</span>
  </p>
 </div>
</div>

最后,将 currency.component.html 文件的内容替换为清单 14 中的内容。

清单 14. currency.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<h2>Currency Exchange Rates</h2>
<div class="col-md-8 col-md-offset-2">
 <div class="form-group">
  <input type="text" [(ngModel)]="id_currency" (change)="callCurrencyService()" class="form-control" placeholder="Enter Currency Symbol. Example: GBP(,AUD,INR)...">
  <br><br>
  <h3>Rate Details</h3>
  <br>
  <p class="well lead">Exchange rate relative to Euro in a JSON format: : {{ this.my_result }} </p>
  <p class="text-info">Total # of all the service requests including Weather, Movie, and Currency is :
    <span class="badge">{{this._sharedService.totReqsMade}}</span>
  </p>
 </div>
</div>

现在,如果各部分均按预期运行,应用程序可接受浏览器中的用户输入。

运行应用程序并改进 UI

现在运行应用程序,输入一些值,然后查看结果。例如,单击 Weather 链接,输入 San Francisco 来查看该城市的天气条件:

返回天气结果的应用程序屏幕截图

一切正常,但还可以让 UI 更具吸引力。改进 GUI 的一种方法是使用 Bootstrap。(Angular 2 资料 是理想的选择,但截至编写本文时,它尚未正式发布)。

转到 Bootstrap 入门页面,将该页的以下两行复制到剪贴板:

1
2
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

打开 index.html 文件,将刚复制的语句粘贴到第 8 行下。

清单 15. index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>DwNg2App</title>
  <base href="/">
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

现在该应用程序在浏览器中看起来更加美观,拥有更容易理解的样式和菜单按钮,而不是 Weather、Movie Details 和 Currency Rates 链接:

改进后的 UI 的屏幕截图

嵌套的 index.html

现在花一分钟时间体会一下您刚完成的应用程序为什么被称为 SPA。

当 Angular 应用程序启动时,服务器将 index.html 文件发送到浏览器,而且 index.html 是浏览器显示的唯一文件。Angular 对该页面执行的任何操作都会插入到此视图中:

处理 SPA 流的代码的屏幕截图

index.html 末尾的 <app-root> 选择器被 app.component.html 的内容所取代。app.component.html 包含两个选择器:<app-menu><router-outlet><app-menu> 选择器中填入 menu.component.html 的内容,<router-outlet> 依据菜单选项而动态填充 — 也就是说,填入 weather.component.html、currency.component.html 或 movie.component.html 的内容。

除了 Angular 保留选择器 <router-outlet></router-outlet> 外,所有选择器都是静态的。此选择器在运行时期间依据路由器值而填充。仅显示 index.html,您编码的其他所有 HTML 文件都嵌套在 index.html 文件内。

模拟服务器

您的 Angular 项目已成功在开发计算机中运行。如果您能访问远程沙箱服务器,那么可以将代码迁移到那里,看看应用程序在被用户运行时的行为。(否则,可以跳到教程的 结束语 部分。)

确保 Node.js 和 Angular CLI 已安装在远程沙箱中。压缩本地项目文件夹中的所有内容,node_modules 目录及其内容除外。将压缩后的项目文件复制到沙箱服务器并解压。转到包含 package.json 的服务器目录并运行 npm install 命令。package.json 文件使 npm install 命令能转到 NPM 公共存储库,安装所有需要的包版本。运行此命令也会自动在服务器上创建 node_modules 目录和它的内容。

运行 ng serve 命令,以便在沙箱服务器中启动该应用程序,就像在开发计算机中所做的一样。按下 Ctrl-C 停止该进程。同样地,如果您想了解 ng serve 的其他选项,可以运行 ng help 命令。

使用 ng serve --port sandbox-port# --host sandbox-hostname 命令运行该应用程序。

现在 Angular 应用程序可在 URL http://sandbox-hostname:sandbox-port# 上访问。在开发计算机浏览器中运行该 URL 上的应用程序时,在沙箱服务器上按下 Ctrl-C 停止服务器 Angular 进程。请注意,整个应用程序都在开发计算机的浏览器中运行,尽管服务器 Angular 进程已关闭。这会告诉您,SPA 技术正在运行。该应用程序加载到浏览器中后,除非应用程序需要新数据,否则控制权绝不会转交给服务器。

在 SPA 领域,浏览器就是新型的服务器。如果 10 个用户在运行该应用程序,就会有 10 个浏览器处理该负载。如果 1,000 个用户在运行该应用程序,则有 1,000 个浏览器处理该负载。整个 Angular 应用程序都在浏览器的控制之下,除了在服务器中运行的任何逻辑,比如身份验证和数据库操作。

更高的性能和服务器压力的减轻,最终会提高用户体验和用户满意度 — 这是任何企业获得成功的关键。

在 SPA 领域,浏览器就是新型的服务器。

结束语

您学习了如何使用 Angular 2 编写一个 SPA,并在开发计算机和沙箱服务器中运行它。对于生产需求,请与您的 IT 部门协商。大多数生产应用程序拥有的一项主要功能是身份验证和授权,应在服务器中运行该功能(主要出于安全原因)。您可能需要一个专用服务器来处理这些操作。对于该服务器,可以使用 Node.js,在 Angular 2 在前端运行时,它可以充当在后端运行的服务器。(Node 和 Angular 都起源于 Google,所以它们能有效地协同运行。)

您可以考虑的用来提高应用程序性能的其他技术包括:

  • 捆绑:该进程将您的许多程序组合到一个文件中。
  • 微型化:压缩捆绑的文件,以便尽可能地减小项目大小。
  • 提前 (AoT) 编译:服务器复杂在构建过程中提前编译,而不是浏览器在运行时期间执行即时 (JIT) 编译。
致谢

感谢 IBM 的优秀员工 Guy Huinen、Dean P Cummings、Don Turner 和 Mark Shade 的评审工作和支持。


下载资源

from:http://www.ibm.com/developerworks/cn/web/wa-implement-a-single-page-application-with-angular2/index.html

为 Java 程序员准备的 Go 入门 PPT

这是 Google 的 Go 团队技术主管经理 Sameer Ajmani 分享的 PPT,为 Java 程序员快速入门 Go 而准备的。

视频

这个 PPT 是 2015年4月23日在 NYJavaSIG 中使用的。

前往 YouTube 观看视频

主要内容

1. Go 是什么,谁在使用 Go?
2. 比较 Go 和 Java
3. 代码示例
4. 并发
5. 工具

Go 是什么?

“Go 是开源的编程语言,可以很简单的构建简单,可靠和高效的软件。”

golang.org

Go 的历史

从 2007 后半年开始设计

  • Robert Griesemer, Rob Pike 和 Ken Thompson.
  • Ian Lance Taylor 和 Russ Cox

从 2009 年开始开源,有一个非常活跃的社区。

Go 语言稳定版本 Go 1 是在 2012 年早期发布的。

为什么有 Go?

Go 是解决 Google 规模的一个解决方案。

系统规模

  • 规划的规模为 10⁶⁺ 台机器
  • 每天在几千台机器上作业
  • 在系统中与其他作业进行协作,交互
  • 同一时间进行大量工作

解决方案:对并发的支持非常强大

第二个问题:工程规模

在 2011 年

  • 跨 40+ 办公室的 5000+ 名开发者
  • 每分钟有 20+ 修改
  • 每个月修改 50% 的代码基础库
  • 每天执行 5千万的测试用例
  • 单个代码树

解决方案:为大型代码基础库而设计的语言

谁在 Google 使用 Go?

大量的项目,几千位 Go 程序员,百万行的 Go 代码。

公开的例子:

  • 移动设备的 Chrome SPDY 代理
  • Chrome, ChromeOS, Android SDK, Earth 等等的下载服务器
  • YouTube Vitess MySQL 均衡器

主要任务是网络服务器,但是这是通用的语言。

除了 Google 还有谁在使用 Go?

golang.org/wiki/GoUsers

Apcera, Bitbucket, bitly, Canonical, CloudFlare, Core OS, Digital
Ocean, Docker, Dropbox, Facebook, Getty Images, GitHub, Heroku, Iron.io,
Kubernetes, Medium, MongoDB services, Mozilla services, New York Times,
pool.ntp.org, Secret, SmugMug, SoundCloud, Stripe, Square, Thomson
Reuters, Tumblr, …

比较 Go 和 Java

Go 和 Java 有很多共同之处

  • C 系列 (强类型,括号)
  • 静态类型
  • 垃圾收集
  • 内存安全 (nil 引用,运行时边界检查)
  • 变量总是初始化 (zero/nil/false)
  • 方法
  • 接口
  • 类型断言 (实例)
  • 反射

Go 与 Java 的不同之处

  • 代码程序直接编译成机器码,没有 VM
  • 静态链接二进制
  • 内存布局控制
  • 函数值和词法闭包
  • 内置字符串 (UTF-8)
  • 内置泛型映射和数组/片段
  • 内置并发

Go 特意去掉了大量的特性

  • 没有类
  • 没有构造器
  • 没有继承
  • 没有 final
  • 没有异常
  • 没有注解
  • 没有自定义泛型

为什么 Go 要省去那些特性?

代码清晰明了是首要的

当查看代码时,可以很清晰的知道程序将会做什么

当编写代码的时候,也可以很清晰的让程序做你想做的

有时候这意味着编写出一个循环而不是调用一个模糊的函数。

(不要变的太枯燥)

详细的设计背景请看:

示例

Java程序猿对Go应该很眼熟

Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

hello.go

package main
import "fmt"
func main() {
    fmt.Println("Hello, 世界!")
}

Hello, web server(你好,web服务)

package main

import (
    "fmt"
    "log"
    "net/http"
)
func main() {
    http.HandleFunc("/hello", handleHello)
    fmt.Println("serving on http://localhost:7777/hello")
    log.Fatal(http.ListenAndServe("localhost:7777", nil))
}
func handleHello(w http.ResponseWriter, req *http.Request) {
    log.Println("serving", req.URL)
    fmt.Fprintln(w, "Hello, 世界!")
}

(访问权限)类型根据变量名来声明。
公共变量名首字大写,私有变量首字母小写。

示例:Google搜索前端

func main() {
    http.HandleFunc("/search", handleSearch)
    fmt.Println("serving on http://localhost:8080/search")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
// handleSearch handles URLs like "/search?q=golang" by running a
// Google search for "golang" and writing the results as HTML to w.
func handleSearch(w http.ResponseWriter, req *http.Request) {

请求验证

func handleSearch(w http.ResponseWriter, req *http.Request) {
    log.Println("serving", req.URL)
    // Check the search query.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, `missing "q" URL parameter`, http.StatusBadRequest)
        return
    }

FormValueis 是 *http.Request 的一个方法:

package http
type Request struct {...}
func (r *Request) FormValue(key string) string {...}

query := req.FormValue(“q”)初始化变量query,其变量类型是右边表达式的结果,这里是string类型.

取搜索结果

 // Run the Google search.
    start := time.Now()
    results, err := Search(query)
    elapsed := time.Since(start)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

Search方法有两个返回值,分别为结果results和错误error.

func Search(query string) ([]Result, error) {...}

当error的值为nil时,results有效。

type error interface {
    Error() string // a useful human-readable error message
}

Error类型可能包含额外的信息,可通过断言访问。

渲染搜索结果

// Render the results.
    type templateData struct {
        Results []Result
        Elapsed time.Duration
    }
    if err := resultsTemplate.Execute(w, templateData{
        Results: results,
        Elapsed: elapsed,
    }); err != nil {
        log.Print(err)
        return
    }

结果results使用Template.Execute生成HTML,并存入一个io.Writer:

type Writer interface {
        Write(p []byte) (n int, err error)
}

http.ResponseWriter实现了io.Writer接口。

Go变量操作HTML模板

// A Result contains the title and URL of a search result.
type Result struct {
    Title, URL string
}
var resultsTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
  <ol>
  {{range .Results}}
    <li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
  {{end}}
  </ol>
  <p>{{len .Results}} results in {{.Elapsed}}</p>
</body>
</html>
`))

请求Google搜索API

func Search(query string) ([]Result, error) {
    // Prepare the Google Search API request.
    u, err := url.Parse("https://ajax.googleapis.com/ajax/services/search/web?v=1.0")
    if err != nil {
        return nil, err
    }
    q := u.Query()
    q.Set("q", query)
    u.RawQuery = q.Encode()
    // Issue the HTTP request and handle the response.
    resp, err := http.Get(u.String())
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

defer声明使resp.Body.Close运行在Search方法返回时。

解析返回的JSON数据到Go struct类型

developers.google.com/web-search/docs/#fonje

  var jsonResponse struct {
        ResponseData struct {
            Results []struct {
                TitleNoFormatting, URL string
            }
        }
    }
    if err := json.NewDecoder(resp.Body).Decode(&jsonResponse); err != nil {
        return nil, err
    }
    // Extract the Results from jsonResponse and return them.
    var results []Result
    for _, r := range jsonResponse.ResponseData.Results {
        results = append(results, Result{Title: r.TitleNoFormatting, URL: r.URL})
    }
    return results, nil
}

这就是它的前端

所有引用的包都来自标准库:

import (
    "encoding/json"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "net/url"
    "time"
)

Go服务器规模:每一个请求都运行在自己的goroutine里。

让我们谈谈并发。

通信顺序进程(Hoare,1978)

并发程序作为独立进程,通过信息交流的顺序执行。

顺序执行很容易理解,异步则不是。

“不要为共亨内存通信,为通信共享内存。”

Go原理: goroutines, channels, 和 select声明.

Goroutines

Goroutines 就像轻量级线程。

它们通过小栈(tiny stacks)和按需调整运行。

Go 程序可以拥有成千上万个(goroutines)实例

使用go声明启动一个goroutines:

go f(args)

Go运行时把goroutines放进OS线程里。

不要使用线程堵塞goroutines。

Channels

Channels被定义是为了与goroutines之间通信。

c := make(chan string)

// goroutine 1
c <- "hello!"

// goroutine 2
s := <-c
fmt.Println(s) // "hello!"

Select

select声明一个语句块来判断执行。

select {
case n := <-in:
  fmt.Println("received", n)
case out <- v:
  fmt.Println("sent", v)
}

只有条件成立的case块会运行。

示例:Google搜索(后端)

问: Google搜索能做些什么?

答: 提出一个问题,它可以返回一个搜索结果的页面(和一些广告)。

问: 我们怎么得到这些搜索结果?

答: 发送一个问题到网页搜索、图片搜索、YouTube(视频)、地图、新闻,稍等然后检索出结果。

我们该怎么实现它?

Google搜索 : 一个假的框架

We can simulate a Search function with a random timeout up to 100ms.

我们要模拟一个搜索函数,让它随机超时0到100毫秒。

var (
    Web   = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
)
type Search func(query string) Result
func fakeSearch(kind string) Search {
    return func(query string) Result {
        time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
        return Result(fmt.Sprintf("%s result for %q\n", kind, query))
    }
}

Google搜索: 测试框架

func main() {
    start := time.Now()
    results := Google("golang")
    elapsed := time.Since(start)
    fmt.Println(results)
    fmt.Println(elapsed)
}

Google搜索 (串行)

Google函数获取一个查询,然后返回一个的结果集 (不一定是字符串).

Google按顺序调用Web(网页)、Image(图片)、Video(视频)并将返回加入到结果集中。

func Google(query string) (results []Result) {
    results = append(results, Web(query))
    results = append(results, Image(query))
    results = append(results, Video(query))
    return
}

Google搜索(并行)

同时执行 Web,、Image、 和Video搜索,并等待所有结果。

func方法是在query和c的地方关闭的。

func Google(query string) (results []Result) {
    c := make(chan Result)
    go func() { c <- Web(query) }()
    go func() { c <- Image(query) }()
    go func() { c <- Video(query) }()
    for i := 0; i < 3; i++ {
        result := <-c
        results = append(results, result)
    }
    return
}

Google搜索 (超时)

等待慢的服务器。

没有锁,没有条件变量,没有返回值。

    c := make(chan Result, 3)
    go func() { c <- Web(query) }()
    go func() { c <- Image(query) }()
    go func() { c <- Video(query) }()
    timeout := time.After(80 * time.Millisecond)
    for i := 0; i < 3; i++ {
        select {
        case result := <-c:
            results = append(results, result)
        case <-timeout:
            fmt.Println("timed out")
            return
        }
    }
    return

防止超时

问: 如何防止丢掉慢的服务的结果?

答: 复制这个服务,然后发送请求到多个复制的服务,并使用第一个响应的结果。

func First(query string, replicas ...Search) Result {
    c := make(chan Result, len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

使用First函数

func main() {
    start := time.Now()
    result := First("golang",
        fakeSearch("replica 1"),
        fakeSearch("replica 2"))
    elapsed := time.Since(start)
    fmt.Println(result)
    fmt.Println(elapsed)
}

Google搜索 (复制)

使用复制的服务以减少多余延迟。

    c := make(chan Result, 3)
    go func() { c <- First(query, Web1, Web2) }()
    go func() { c <- First(query, Image1, Image2) }()
    go func() { c <- First(query, Video1, Video2) }()
    timeout := time.After(80 * time.Millisecond)
    for i := 0; i < 3; i++ {
        select {
        case result := <-c:
            results = append(results, result)
        case <-timeout:
            fmt.Println("timed out")
            return
        }
    }
    return

其他

没有锁,没有条件变量,没有调用。

总结

经过一些简单转换,我们使用 Go 的并发原语来转换一个

  • 顺序性的
  • 故障敏感的

程序为一个

  • 并发
  • 可复用的
  • 健壮的

工具

Go 有很多强大的工具

  • gofmt 和 goimports
  • The go tool
  • godoc
  • IDE 和编辑器支持

这语言就是为工具链设计的。

gofmt 和 goimports

Gofmt 可以自动格式化代码,没有选项。

Goimports 基于你的工作空间更新导入声明

大部分人可以安全的使用这些工具。

play.golang.org/p/GPqra77cBK

The go tool

The go tool 可以在一个传统目录布局中用源代码构建 Go 程序。不需要 Makefiles 或者其他配置。

匹配这些工具及其依赖,然后进行构建,安装:

% go get golang.org/x/tools/cmd/present

运行:

% present

godoc

为世界上所有的开源 Go 代码生成文档:

godoc.org

IDE 和编辑器支持

Eclipse, IntelliJ, emacs, vim 等等:

  • gofmt
  • goimports
  • godoclookups
  • code completion
  • code navigation

但是没有 “Go IDE”.

Go 工具无处不在。

Go 的下一步计划

Go 路线在线查看

tour.golang.org

大量的学习资料

golang.org/wiki/Learn

完美的社区

golang.org/project

Thank you

Sameer Ajmani

Tech Lead Manager, Go team

Google

@Sajma

sameer@golang.org

from:http://www.oschina.net/translate/go-for-java-programmers-ppt

DevOps的前世今生

目前在国外,互联网巨头如Google、Facebook、Amazon、LinkedIn、Netflix、Airbnb,传统软件公司如Adobe、IBM、Microsoft、SAP等,亦或是网络业务非核心企业如苹果、沃尔玛、索尼影视娱乐、星巴克等都在采用DevOps或提供相关支持产品。那么DevOps究竟是怎样一回事?在Puppet、RightScale分别DevOps出版的调查报告基础上,整理本文,以期为读者理清思路。另外,中国正在开展了一份自己的调查问卷,由南京大学发起,欢迎大家投票参与

DevOps是什么?从哪里来?

DevOps的概念

DevOps一词的来自于Development和Operations的组合,突出重视软件开发人员和运维人员的沟通合作,通过自动化流程来使得软件构建、测试、发布更加快捷、频繁和可靠。

DevOps概念早先升温于2009年的欧洲,因传统模式的运维之痛而生。

DevOps是为了填补开发端和运维端之间的信息鸿沟,改善团队之间的协作关系。不过需要澄清的一点是,从开发到运维,中间还有测试环节。DevOps其实包含了三个部分:开发、测试和运维。

换句话说,DevOps希望做到的是软件产品交付过程中IT工具链的打通,使得各个团队减少时间损耗,更加高效地协同工作。专家们总结出了下面这个DevOps能力图,良好的闭环可以大大增加整体的产出。

历史变革

由上所述,相信大家对DevOps有了一定的了解。但是除了触及工具链之外,作为文化和技术的方法论,DevOps还需要公司在组织文化上的变革。回顾软件行业的研发模式,可以发现大致有三个阶段:瀑布式开发、敏捷开发、DevOps。

DevOps早在九年前就有人提出来,但是,为什么这两年才开始受到越来越多的企业重视和实践呢?因为DevOps的发展是独木不成林的,现在有越来越多的技术支撑。微服务架构理念、容器技术使得DevOps的实施变得更加容易,计算能力提升和云环境的发展使得快速开发的产品可以立刻获得更广泛的使用。

(注:上图摘自上月红帽副总裁Ashesh Badani的一次新闻分享会)

DevOps的几个关键问题

好处是什么?

DevOps的一个巨大好处就是可以高效交付,这也正好是它的初衷。Puppet和DevOps Research and Assessment (DORA) 主办了2016年DevOps调查报告,根据全球4600位各IT公司的技术工作者的提交数据统计,得出高效公司平均每年可以完成1460次部署。与低效组织相比,高效组织的部署频繁200倍,产品投入使用速度快2555倍,服务恢复速度快24倍。在工作内容的时间分配上,低效者要多花22%的时间用在为规划好或者重复工作上,而高效者却可以多花29%的时间用在新的工作上。所以这里的高效不仅仅指公司产出的效率提高,还指员工的工作质量得到提升。

DevOps另外一个好处就是会改善公司组织文化、提高员工的参与感。员工们变得更高效,也更有满足和成就感;调查显示高效员工的雇员净推荐值(eNPS:employee Net Promoter Score)更高,即对公司更加认同。

快速部署同时提高IT稳定性。这难道不矛盾吗?

快速的部署其实可以帮助更快地发现问题,产品被更快地交付到用户手中,团队可以更快地得到用户的反馈,从而进行更快地响应。而且,DevOps小步快跑的形式带来的变化是比较小的,出现问题的偏差每次都不会太大,修复起来也会相对容易一些。

因此,认为速度就意味着危险是一种偏见。此外,滞后软件服务的发布也并不一定会完全地避免问题,在竞争日益激烈的IT行业,这反而可能错失了软件的发布时机。

为什么DevOps会兴起?为什么会继续火下去?

条件成熟:技术配套发展

技术的发展使得DevOps有了更多的配合。早期时,大家虽然意识到了这个问题的,但是苦于当时没有完善丰富的技术工具,是一种“理想很丰满,但是现实很骨感”的情况。DevOps的实现可以基于新兴的容器技术;也可以在自动化运维工具Puppet、SaltStack、Ansible之后的延伸;还可以构建在传统的Cloud Foundry、OpenShift等PaaS厂商之上。

来自市场的外部需求:这世界变化太快

IT行业已经越来越与市场的经济发展紧密挂钩,专家们认为IT将会有支持中心变成利润驱动中心。事实上,这个变化已经开始了,这不仅体现在Google、苹果这些大企业中,而且也发生在传统行业中,比如出租车业务中的Uber、酒店连锁行业中的Airbnb、图书经销商Amazon等等。能否让公司的IT配套方案及时跟上市场需求的步伐,在今天显得至关重要。

DevOps 2016年度报告给出了一个运维成本的计算公式:
停机费用成本 = 部署频率 * 版本迭代失败概率 * 平均修复时间 * 断电的金钱损失

来自团队的内在动力:工程师也需要

对于工程师而言,他们也是DevOps的受益者。微软资深工程师Scott Hanselman说过“对于开发者而言,最有力的工具就是自动化工具”(The most powerful tool we have as developers is automation)。工具链的打通使得开发者们在交付软件时可以完成生产环境的构建、测试和运行;正如Amazon的VP兼CTO Werner Vogels那句让人印象深刻的话:“谁开发谁运行”。(You build it, you run it)

实现DevOps需要什么?

硬性要求:工具上的准备

上文提到了工具链的打通,那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下:

  • 代码管理(SCM):GitHub、GitLab、BitBucket、SubVersion、TFS
  • 构建工具:Ant、Gradle、maven
  • 自动部署:Capistrano、CodeDeploy
  • 持续集成(CI):Bamboo、Hudson、Jenkins
  • 配置管理:Ansible、Chef、Puppet、SaltStack、ScriptRock GuardRail
  • 容器:Docker、LXC、Rkt、第三方厂商如AWS
  • 编排:Kubernetes、Apache Mesos、DC/OS
  • 服务注册与发现:Zookeeper、etcd、Consul
  • 脚本语言:python、ruby、shell
  • 日志管理:ELK、Logentries
  • 系统监控:Datadog、Graphite、Icinga、Nagios
  • 性能监控:AppDynamics、New Relic、Splunk
  • 压力测试:JMeter、Blaze Meter、loader.io
  • 预警:PagerDuty、pingdom、厂商自带如AWS SNS
  • HTTP加速器:Varnish
  • 消息总线:ActiveMQ、SQS
  • 应用服务器:Tomcat、JBoss
  • Web服务器:Apache、Nginx、IIS
  • 数据库:MySQL、Oracle、PostgreSQL等关系型数据库;cassandra、mongoDB、redis等NoSQL数据库
  • 项目管理(PM):Jira、Asana、Taiga、Trello、Basecamp、Pivotal Tracker

在工具的选择上,需要结合公司业务需求和技术团队情况而定。(注:更多关于工具的详细介绍可以参见此文:51 Best DevOps Tools for #DevOps Engineers

软性需求:文化和人

DevOps成功与否,公司组织是否利于协作是关键。开发人员和运维人员可以良好沟通互相学习,从而拥有高生产力。并且协作也存在于业务人员与开发人员之间。出席了2016年伦敦企业级DevOps峰会的ITV公司在2012年就开始落地DevOps,其通用平台主管Clark在接受了InfoQ的采访,在谈及成功时表示,业务人员非常清楚他们希望在最小化可行产品中实现什么,工程师们就按需交付,不做多余工作。这样,工程师们使用通用的平台(即打通的工具链)得到更好的一致性和更高的质量。此外,DevOps对工程师个人的要求也提高了,很多专家也认为招募到优秀的人才也是一个挑战。

DevOps的采用现状

哪些公司在用?

DevOps正在增长,尤其是在大企业中:调查发现,DevOps的接受度有了显著提高。74%的受访者已经接受了DevOps,而去年这一比例为66%。目前,在81%的大企业开始接受DevOps,中小企业的接受度仅为70%。

那么具体而言都有些公司在采用DevOps呢?Adobe、Amazon、Apple、Airbnb、Ebay、Etsy、Facebook、LinkedIn、Netflix、NASA、Starbucks、Target(泛欧实时全额自动清算系统)、Walmart、Sony等等。

他们怎么实施的?

首先,大企业正在自下而上接受DevOps,其中业务单位或部门(31%)以及项目和团队(29%)已经实施DevOps。不过,只有21%的大企业在整个公司范围内采用了DevOps。
其次,在工具层面上,DevOps工具的用量大幅激增。Chef和Puppet依然是最常用的DevOps工具,使用率均为32%。Docker是年增长率最快的工具,用量增长一倍以上。Ansible的用量也有显著增加,使用率从10%翻倍至20%。

并且调查还发现不到半数(43%)的公司在使用诸如Chef、Puppet、Ansible或Salt等配置工具;然而使用配置工具的公司更有可能同时使用多个工具。25%的受访者使用两种或更多配置工具,只使用一种工具的比例为18%。其中Chef和Puppet是最常用的组合:使用Chef的组织中有67%同时也使用Puppet,类似的,使用Puppet的组织中也有67%同时使用了Chef。

中国也在准备一份DevOps的报告

文中的统计数据来自于国外的DevOps调研报告。其中由Puppet发起的DevOps年度国际调查报告已经连续出版五年,先后收集了2.5万技术人员的答卷;2016年收集的有效答卷为4600份,不过仅有10%来自于亚洲。我们并不认为这样的采样率和采样数量可以充分地反映中国的DevOps行业现状。

目前,依托DevOps中国社区成员的积极参与支持,由南京大学发起开展《DevOps中国2016年度调查》活动。期望通过本次问卷调查,收集DevOps的率先实践者们关于DevOps实践及经验的相关信息,从而了解DevOps在中国推广应用的状况,并汇总在DevOps实践中可能遇到的障碍、挑战和最佳实践,最终通过调查报告进一步促进DevOps在中国的认知和推广,同时为DevOps的每一位实践者提供有价值的参考信息和帮助。

调查问卷的设计主要参考了国际上2014-2016年度的《State of DevOps Report》及《State of Agile Report》的调查问卷设计,以实现信息数据在国内和国际之间的可比性,并根据业内专家意见经过多轮修改。目前问卷中英文版已上线,点此可进行中文版调查。本次问卷调查为学术公益性质,所形成的年度调查报告将免费公开。

期待大家能填写调查问卷,支持中国DevOps的发展!

参考文章:
http://www.infoq.com/cn/news/2016/08/essence-enterprise-accelerate-op
http://www.infoq.com/cn/news/2016/09/devops-enterprise-ITV
https://devup.co/a-look-at-devops-tools-landscape-7220099c6b81#.oehabi9xp
https://en.wikipedia.org/wiki/DevOps
https://puppet.com/resources/white-paper/2016-state-of-devops-report
http://www.rightscale.com/lp/devops-trends-report