用 Angular 开发 Web 应用(Part 2)-part文件

协作翻译

原文:Developing a Web Application Using Angular (Part 2)

链接:https://dzone.com/articles/developing-a-web-application-using-angular-part-2

译者:Tocy, 圣洁之子, wilde, aaronday, 我是菜鸟我骄傲, ismdeep, 亚林瓜子

在用 Angular 开发 Web 应用(Part 1)中,我们探讨了 Web 应用程序的基本用户界面(UI)设计,并为我们的项目奠定了基础,通过使用 Angular 命令行界面(CLI)为我们生成一个骨架项目。在本文中,我们将为我们的 Web 应用程序开发一个架构,并开始将我们的 Web 应用程序从抽象到具体的类的实现。

开发框架

就像我们为使用 Java 和 Spring 创建 REST Web 服务(第1部分)中后端 Web 服务开发的一般架构一样,许多 Web 应用程序都遵循相同的基本架构。

从最接近用户的组件开始,我们有一个与系统最终用户最直接的互连的 UI。

在 UI 和 Web 服务之间,我们有一个服务层可以抽象与 Web 服务的交互接口,来封装超文本传输协议(HTTP)操作(如 GET,POST 等)。最后,我们得到由 Web 服务生成并由 UI 层显示的资源。

虽然存在以严格分级方式显示所有分层架构的趋势(其中一层直接位于另一层之上),但从 UI 层、服务层和资源之间的交互角度来看我们的架构,却更为有利,如下图所示:

在这种架构中,最受依赖的层是资源层,它负责表示从 Web 服务检索资源;服务层负责对后端 Web 服务进行 HTTP 调用并获取资源;然后 UI 层将这些资源显示给用户,并允许用户改变资源的状态。

一旦更改完成,UI 层将指示服务层将这些更改提交到后端,这又导致服务层对后端执行 HTTP 调用。

这个过程可以在下面的序列图中看到,它描述了编辑 ID 为 1 的订单的步骤。请注意 OrderService 和 OrderResource 类分为用作订单的服务和资源。

如上图所示,用户的动作驱动 UI 层,接着,UI 层指示服务层与后端 Web 服务进行交互。然后,服务层创建新的资源,或使用 UI 层传递现有资源, 来表示收到或发送到 Web 服务的订单。

在整个过程中,服务层是负责创建资源的唯一层。此外,UI 层并不直接与 Web 服务交互,而是使用订单资源与服务层进行交互。

这种去耦合方式允许我们的层改变,但不会造成多米诺效应而改变其他层。例如,如果 Web 服务已改变(如,连接 Web 服务的 URL 改变),则只有服务层必须改动(更具体地说,只有与受影响的资源交互的服务必须改变)。尽管 Web 服务发生了变化,但 UI 层组件保持不变。

随着我们的架构的固化,我们现在可以继续实现我们的 Web 应用程序。我们将从实现资源层开始,因为它不依赖于其他层,并且被他们所依赖。

接下来,我们将实现服务层,因为它依赖于 UI 层;最后,我们将实现 UI 层,因为它不被任何其他层所依赖,但依赖于另外两层。

实现资源层

实现资源层的第一步是枚举我们将在系统中拥有的资源。由于我们与一个简单的 Web 服务进行接口,我们只有一个资源:订单。

基于我们的 Web 服务的设计,我们期望 Web 服务把资源发送给我们,类似以下内容(以 JSON 格式,即 JavaScript 对象表示法):

这意味着我们必须确保我们的订单资源可以从上述 JSON 中反序列化,并可以在我们的 UI 层中使用,为此,我们创建以下一组类:

从顶部开始,我们创建 Link 类,将超媒体抽象为应用程序状态引擎(Hypermedia as the Engine of Application State, HATEOAS)。

接下来,我们抽象出 OrderLinks 类,它包含了从 Web 服务预期的各种链接。最后,我们创建 Order 类,它抽象了我们从 Web 服务收到的 JSON 订单资源。

值得注意的是 Order 类包含了一个 deserialize 和一个 serialize 方法,这允许 Oder 对象能够从原始的 JSON 订单资源所创建,并且对于已有的 Oder 对象也可以被串行化为一个原始的 JSON 订单资源。

当我们手动执行串行化和反串行化时有两个主要原因,尽管 TypeScript 具有自动执行这些操作的能力:

1、我们可以变化 Oder 类内部字段的名字。Typerscript 所使用的默认的串行化技术使用字段名,这限制了我们将我们的字段命名为用在所希望的 JSON 中的同样的名字。

举例来说,我们将会被要求命名一个字段存储 HATEOAS 链接 _links ,因为这个名字已被后台所使用。这将会通过后台所使用的结构连接我们资源类的内部结构。

2、为了我们类的方法是可调用的,我们需要手动地将一个类型的对象示例化。举例来说,我们将使用默认的反串行化逻辑 const order = someRawJson 作为 Oder ,但是如果我们试图调用 order.toggleComplete() (或任何其他方法),我们将得到一个函数 toggleComplete 不能在给定类型中找到的错误状态。

这是由于订单类型并非 Oder :如果它是对象的话,我们只是简单地将某些原始的 JavaScript 对象看成无类型的,但是当我们视图调用那个对象的方法的时候,一个错误就会由于对象的实现类型不是 Order 而出现(因此也不会有所希望的方法),尽管所声明的类型是 Order 。

第二个问题如果手动地创建所希望的对象(使用 new 运算符)是非常难解决的。对于这个问题的更多信息请参看 this StackOverflow post。 因此,我们将手动地执行 Order 对象的串行化和反串行化 。

在具有更多资源的更大规模系统中,这种方法也许是不可行的。在那样的情况下,对 Typerscript 的串行化和反串行化技术将会继续被讨论。

下一个必须解决的问题是 costInCents 域的私有化。我们所连接的 Web 服务以美分为单位存储成本值,这样就不必操作小数点(这可能会导致便士丢失)。

虽然后端 Web 服务以这种方式维护其成本值,但是我们的 Order 资源可以用有多重好处的十进制形式处理成本。

其中最重要的是以可读的方式显示成本。例如,向用户显示成本以美分为单位,而不是以美元显示,例如$4.56。

其次,UI 可能会以小数的形式更改值。例如,允许我们的 Order 资源被编辑的输入字段可能包含一个小数,以便用户可以将该值输入为4.56而不是456。

因此,我们将使用 Typescript 类提供的封装来维护我们的以美分为单位的成本值,同时允许外部组件改变或阅读成本的以美元为单位的小数。

由于这种转换逻辑(以美分和十进位美元计算)可能难以获得正确的结果,因此用自动化单元测试来实现此功能是一个不错的想法。

虽然自动化单元测试通常使用 Java、Ruby、Python 或其他通用非前端编程语言的后端开发。但我们生成的 Angular 项目包含对 Jasmine 中的自动化单元测试的原生支持。 我们的类的单元测试规范如下所示:

在设置测试套件和单个测试用例之前,我们创建一个 OderderFactory ,将用来帮助我们创建想要的 Order 对象。 例如,我们正在测试资源的成本值时,我们编写一个工厂方法 createWithPrice() 来创建具有特定价格的对象。

接下来,我们使用 describe() 方法(由 Jasmine 提供)建立我们的测试套件。在本测试套件中,我们使用 it() 方法添加单独的测试用例。 在每个测试用例中,我们只需创建一个所需成本为零的原始资源,然后使用 expect 方法对方法的输出进行断言处理。

请注意,Angular 应用程序中的常见约定是将单元测试文件放在包含被测文件的同一目录中,并将 spec 添加到文件名中。例如,如果我们的 Order 资源包含在 src/app/order.resource.ts 中,我们要将 Jasmine 单元测试放在 src/app/order.resource.spec.ts 中。

尽管我们只使用了 Jasmine 的基本功能,但是还有许多其他强大的机制,使得 Jasmine 在其功能方面非常类似于标准的单元测试框架,例如 JUnit 。更多有关创建 Jasmine 测试的信息,请参阅“Jasmine简介”。要运行我们的单元测试,只需执行以下命令:

一旦测试被编译和执行,浏览器窗口就会打开。新窗口中的内容应类似于以下内容,表示所有测试已通过:

用 Angular 开发 Web 应用(Part 2)

在下一篇文章中,我们将继续实现我们的 Web 服务,从服务层开始,最终完成 UI 层,并使用我们 Web 应用程序与订单管理 Web 服务进行交互。

推荐阅读