在 Angular 中使用 API – 模型适配器模式
最初于 2018 年 9 月 4 日在blog.florimondmanca.com上发布。
在之前的一篇文章中,我写了关于REST API 设计的最佳实践。这些主要适用于服务器端。
自 2018 年 1 月以来,我也一直在使用Angular进行前端开发,既是业余爱好,也是专业工作。
今天,我想谈谈架构,并与大家分享一种设计模式,它帮助我构建和标准化REST API与Angular 前端应用程序集成的方式。
TL;DR:
编写一个模型类并使用适配器将原始 JSON 数据转换为这种内部表示。
提示:您可以在 GitHub 上找到此帖子的最终代码:ng-courses。
让我们开始吧!
问题
我们到底想要解决什么问题?
如今,前端应用程序通常需要与外部后端服务进行大量交互以交换、保存和检索数据。
我们今天感兴趣的一个典型示例是通过请求 REST API 来检索数据。
这些 API 以给定的数据格式返回数据。这种格式可能会随着时间而变化,并且很可能不是在使用TypeScript构建的 Angular 应用中使用的最佳格式。
因此,我们要解决的问题是:
我们如何将 API 与 Angular 前端应用程序集成,同时限制更改的影响并充分利用 TypeScript 的强大功能?
课程订阅系统
举个例子,我们考虑一个课程订阅系统,学生可以申请课程并获取学习材料。
以下是我们正在尝试构建的功能的用户故事:
“作为一名学生,我想查看课程列表,以便可以注册新课程。 ”
为了实现这一点,我们提供了以下 API 端点:
GET: /courses/
它的响应数据(JSON格式)如下所示:
[
{
"id": 1,
"code": "adv-maths",
"name": "Advanced Mathematics",
"created": "2018-08-14T12:09:45"
},
{
"id": 2,
"code": "cs1",
"name": "Computer Science I",
"created": "2018-06-12T18:34:16"
}
]
为什么不只是JSON.parse()
?
一个快速而粗略的解决方案是进行 HTTP 调用,将其应用于JSON.parse()
响应主体,然后将其结果应用于Object
我们的 Angular 组件和 HTML 模板。事实证明,Angular 的HttpClient可以帮你做到这一点,所以我们甚至不需要做太多工作。
事实上,CourseService
执行这个 HTTP 请求非常简单:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CourseService {
constructor(private http: HttpClient) {}
list(): Observable<any> {
const url = 'http://api.myapp.com/courses/';
return this.http.get(url);
}
}
然而,这种方法存在几个问题:
- 容易出错:由于我们没有使用 TypeScript 的静态类型,因此存在大量无法及早发现的潜在错误。
- 高耦合:API 数据模式的任何变化都会影响整个代码库,即使它不会导致功能变化(例如重命名字段)。
因此,虽然服务本身易于维护和阅读,但对于我们其余的代码库来说肯定不会是一样的。
适应的需要
因此,如果仅仅使用 JSON 对象还不够,我们还有什么更好的选择?
好吧,我们来思考一下上面的两个问题。
首先,由于我们没有利用 TypeScript 的静态类型和面向对象编程 (OOP) 特性(例如类和接口)来建模数据,代码很容易出错。解决这个问题的一种方法是创建特定类(模型)的实例,以便 TypeScript 能够帮助我们处理它。
其次,我们在 API 数据格式方面遇到了高耦合问题。这是因为我们没有将这种数据格式从其他应用程序组件中抽象出来。为了解决这个问题,一种方法是创建一个内部数据格式,并在处理 API 响应时映射到该格式。
就是这样:我们刚刚概念化了模型适配器模式。它包含两个元素:
- 模型类使我们处于 TypeScript 的范围内,这确实有很多优点。
- 适配器充当单一接口,用于提取 API 的数据并构建模型的实例,我们可以在整个应用程序中放心地使用它。
我发现图表总是有助于理解概念,所以这里有一个给你:
模型适配器示例
现在我们已经了解了模型适配器模式,那么如何在课程系统中实现它呢?让我们从模型开始。
模型Course
这将是一个简单的 TypeScript 类,没有什么太花哨的东西:
// app/core/course.model.ts
export class Course {
constructor(
public id: number,
public code: string,
public name: string,
public created: Date,
) { }
}
注意,即使 API 端点将其作为 ISO 格式的日期提供给我们,它created
仍然是一个JavaScript 对象。此时,您已经可以看到适配器潜伏其中。Date
string
现在我们有了模型,接下来我们开始编写一个用于实际发送 HTTP 请求的服务。我们拭目以待吧。
掐灭CourseService
因为我们要进行 HTTP 调用,所以我们首先HttpClientModule
在我们的中导入AppModule
:
// app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
+ import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
+ HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
现在我们可以创建了CourseService
。根据我们的设计,我们将定义一个list()
方法来返回从端点获取的课程列表GET: /courses/
。
// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { Course } from './course.model';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CourseService {
list(): Observable<Course[]> {
// TODO
return of([]);
}
}
正如我们之前提到的,Angular 的一个优点HttpClientModule
是它内置了对 JSON 响应的支持。事实上,它HttpClient
默认会解析 JSON 主体并构建一个 JavaScript 对象。所以让我们在这里尝试使用它:
// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course } from './course.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CourseService {
private apiUrl = 'http://api.myapp.com/courses';
constructor(private http: HttpClient) { }
list(): Observable<Course[]> {
return this.http.get(this.apiUrl);
}
}
此时,TypeScript 的编译器将会不幸地发出咆哮声(这是一个很好的观点!):
当然!我们还没有Course
根据检索到的原始数据构建实例。相反,我们仍然返回一个Observable<Object>
。我们可以使用RxJS的map
运算符来解决这个问题。我们将数据数组映射到一个Course
对象数组:
// app/core/course.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course } from './course.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CourseService {
private apiUrl = 'http://api.myapp.com/courses';
constructor(private http: HttpClient) { }
list(): Observable<Course[]> {
return this.http.get(this.apiUrl).pipe(
map((data: any[]) => data.map((item: any) => new Course(
item.id,
item.code,
item.name,
new Date(item.created),
))),
);
}
}
呼!这会正常工作并按预期返回Observable<Course[]>
。
但是,此代码存在几个问题:
- 由于适配代码使服务变得混乱,因此很难阅读。
- 它不是 DRY(不要重复自己):我们需要在所有其他需要从 API 项目构建课程的方法中重新实现此逻辑。
- 它给开发人员带来了很高的认知负担:我们需要参考
course.model.ts
文件以确保我们提供了正确的参数new Course()
。
所以,一定有更好的方法……
输入:适配器
当然有!🎉
这时适配器就派上用场了。如果您熟悉 Gang of Four 的设计模式,您可能会认识到这里需要使用Bridge:
将抽象与其实现分离,以便两者可以独立变化。
这正是我们所需要的(但我们将其称为适配器)。
适配器本质上将 API 的对象表示转换为我们的内部对象表示。
事实上,我们甚至可以为适配器定义一个通用接口,如下所示:
// app/core/adapter.ts
export interface Adapter<T> {
adapt(item: any): T;
}
那么让我们来构建一下CourseAdapter
。它的adapt()
方法将接受一个课程项目(由 API 返回),并Course
以此构建一个模型实例。你应该把它放在哪里?我建议把它放在模型文件本身里面:
// app/core/course.model.ts
import { Injectable } from '@angular/core';
import { Adapter } from './adapter';
export class Course {
// ...
}
@Injectable({
providedIn: 'root'
})
export class CourseAdapter implements Adapter<Course> {
adapt(item: any): Course {
return new Course(
item.id,
item.code,
item.name,
new Date(item.created),
);
}
}
请注意,该适配器是可注入的。这意味着我们将能够像任何其他服务一样使用 Angular 的依赖注入系统:将其添加到构造函数中,然后立即使用它。
重构CourseService
现在我们已经将大部分逻辑抽象到了中CourseAdapter
,它CourseService
看起来是这样的:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Course, CourseAdapter } from './course.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CourseService {
private apiUrl = 'http://api.myapp.com/courses';
constructor(
private http: HttpClient,
private adapter: CourseAdapter,
) { }
list(): Observable<Course[]> {
return this.http.get(this.apiUrl).pipe(
// Adapt each item in the raw data array
map((data: any[]) => data.map(item => this.adapter.adapt(item))),
);
}
}
请注意,现在:
- 代码更加DRY:我们在单独的元素(适配器)中抽象了逻辑,我们可以随意重用它。
- 因此可读性
CourseService
更强。 - 由于模型及其适应逻辑保存在同一个文件中,因此认知负荷减少了。
我还向你保证过,模型适配器模式将有助于降低API 和应用之间的耦合度。让我们看看如果遇到以下情况会发生什么……
数据格式改变!
API 团队的小伙伴们对 API 的数据格式进行了修改。course
现在,条目的格式如下:
{
"id": 1,
"code": "adv-maths",
"label": "Advanced Mathematics",
"created": "2018-08-14T12:09:45"
}
name
他们把该领域的名字改成了label
!
以前,我们的前端应用程序在任何地方使用该name
字段,我们都必须将其更改为label
。
但现在我们安全了!因为我们有一个适配器,它的作用就是在 API 表示和我们的内部表示之间进行映射,所以我们可以简单地改变内部表示name
的获取方式:
@Injectable({
providedIn: 'root'
})
export class CourseAdapter implements Adapter<Course> {
adapt(item: any): Course {
return new Course(
item.id,
item.code,
- item.name,
+ item.label,
new Date(item.created),
);
}
}
一行代码就能改!应用的其余部分就能name
照常使用这个字段了。太棒了!✨
好的原则总是适用的
我在大多数涉及REST API 交互的 Angular 项目中都使用了模型适配器模式,并取得了巨大的成功。
它帮助我减少了耦合并充分利用了TypeScript 的强大功能。
总而言之,归根结底还是要坚持软件工程的经典原则。其中最重要的原则是单一职责原则——每个代码元素都应该只做一件事,并且做好它。
如果你有兴趣,你可以在 GitHub 上找到最终代码:ng-courses。
我希望这个简单的架构技巧能帮助您改进在 Angular 应用中集成 API 的方式。如果您自己已经通过其他方法取得了成功,我很乐意听听您的分享!
更新(2019 年 3 月 1 日):我为本文撰写了一篇后续文章。它解释了如何CourseService
在 Angular 组件中使用 created :
保持联系!
如果您喜欢这篇文章,您可以在 Twitter 上找到我,了解更新、公告和新闻。🐤
文章来源:https://dev.to/florimondmanca/consuming-apis-in-angular--the-model-adapter-pattern-3fk5