在 Angular 中使用 API – 模型适配器模式

2025-06-05

在 Angular 中使用 API – 模型适配器模式

最初于 2018 年 9 月 4 日在blog.florimondmanca.com上发布

在之前的一篇文章中,我写了关于REST API 设计的最佳实践。这些主要适用于服务器端。

自 2018 年 1 月以来,我也一直在使用Angular进行前端开发,既是业余爱好,也是专业工作。

今天,我想谈谈架构,并与大家分享一种设计模式,它帮助我构建和标准化REST APIAngular 前端应用程序集成的方式

TL;DR:

编写一个模型类并使用适配器将原始 JSON 数据转换为这种内部表示。

提示:您可以在 GitHub 上找到此帖子的最终代码:ng-courses

让我们开始吧!

问题

我们到底想要解决什么问题?

如今,前端应用程序通常需要与外部后端服务进行大量交互以交换、保存和检索数据。

我们今天感兴趣的一个典型示例是通过请求 REST API 来检索数据

这些 API 以给定的数据格式返回数据。这种格式可能会随着时间而变化,并且很可能不是在使用TypeScript构建的 Angular 应用中使用的最佳格式。

因此,我们要解决的问题是:

我们如何将 API 与 Angular 前端应用程序集成,同时限制更改的影响并充分利用 TypeScript 的强大功能?

课程订阅系统

举个例子,我们考虑一个课程订阅系统,学生可以申请课程并获取学习材料。

以下是我们正在尝试构建的功能的用户故事:

作为一名学生,我想查看课程列表,以便可以注册新课程。

为了实现这一点,我们提供了以下 API 端点:

GET: /courses/
Enter fullscreen mode Exit fullscreen mode

它的响应数据(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"
    }
]
Enter fullscreen mode Exit fullscreen mode

为什么不只是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);
    }
}
Enter fullscreen mode Exit fullscreen mode

然而,这种方法存在几个问题:

  • 容易出错:由于我们没有使用 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,
  ) { }
}
Enter fullscreen mode Exit fullscreen mode

注意,即使 API 端点将其作为 ISO 格式的日期提供给我们,它created仍然是一个JavaScript 对象。此时,您已经可以看到适配器潜伏其中。Datestring

现在我们有了模型,接下来我们开始编写一个用于实际发送 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 { }
Enter fullscreen mode Exit fullscreen mode

现在我们可以创建了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([]);
  }
}
Enter fullscreen mode Exit fullscreen mode

正如我们之前提到的,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);
  }
}
Enter fullscreen mode Exit fullscreen mode

此时,TypeScript 的编译器将会不幸地发出咆哮声(这是一个很好的观点!):

TypeScript 不高兴...为什么?

当然!我们还没有Course根据检索到的原始数据构建实例。相反,我们仍然返回一个Observable<Object>。我们可以使用RxJSmap运算符来解决这个问题。我们将数据数组映射到一个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),
      ))),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

呼!这会正常工作并按预期返回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;
}
Enter fullscreen mode Exit fullscreen mode

那么让我们来构建一下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),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

请注意,该适配器是可注入的。这意味着我们将能够像任何其他服务一样使用 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))),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

请注意,现在:

  • 代码更加DRY:我们在单独的元素(适配器)中抽象了逻辑,我们可以随意重用它。
  • 因此可读性CourseService更强
  • 由于模型及其适应逻辑保存在同一个文件中,因此认知负荷减少了。

我还向你保证过,模型适配器模式将有助于降低API 和应用之间的耦合度。让我们看看如果遇到以下情况会发生什么……

数据格式改变!

API 团队的小伙伴们对 API 的数据格式进行了修改。course现在,条目的格式如下:

{
    "id": 1,
    "code": "adv-maths",
    "label": "Advanced Mathematics",
    "created": "2018-08-14T12:09:45"
}
Enter fullscreen mode Exit fullscreen mode

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),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

一行代码就能改!应用的其余部分就能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
PREV
从 Angular 到 Vue:再次感觉自己像个初学者
NEXT
具有原生联合的微前端