[Day28] 用 HttpClient 从 API 取得资料

一直到目前,我们的 component 仍然使用写死的物件当作资料来源,今天,我们就要来串起我们的前後端,用 HttpClient 取得资料然後再用 component 帮我们把资料显示在浏览器上。

开始之前

现在这个阶段如果我们马上用 Angular 发 http request 给我们的 .NET API,我们会因为 CORS 而被浏览器挡下这个 request,所以在我们开始使用 Angular 的 HttpClient 之前,我必须先来修改一下我们的 .NET API。

我们需要做的事情很简单,就是在 Startup.cs 里加入与 CORS 相关的 service 与 middleware,不过这里要注意一下,app.UseCors("xxx"); 需要加在 app.UseRouting();之後、app.UseEndpoints()之前。设定 CORS 的更多细节请参考这篇文章

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(option =>
    {
        option.AddPolicy("ironmanPolicy", policy =>
        {
            policy.WithOrigins("http://localhost:4200")
                .WithOrigins("https://mydomain.tw")
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
    });
}

@Input 与 Service

在 Angular 中,内层的 component 要拿到资料有两种做法,第一种是我们在 Day26 介绍的属性系结,让外层的 component 把资料传给内层的 component。另一种方法,是在内层的 component 中直接注入处理资料的 service,让 service 直接帮我们取得资料。今天,我们就来示范利用注入的方式,让 http request 帮我们的 component 取得资料。

在我们开始之前,我们先来新增另外一个资料来源

// ironman-list.component.ts
userListFromApi: IronmanUser[] = [];

HttpClient

像 http 这种这麽常用的东西,Angular 当然会提供内建的东西给我们用啦,而这个内建的东西就是 HttpClient 类别,要使用 HttpClient 类别,首先我们得要引用它,到 app.module.ts 中,新增引入 HttpClientModule 的程序码

// app.module.ts
import { HttpClientModule } from '@angular/common/http';
//...
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule // HttpClientModule 要放在 BrowserModule 後面
  ],
//...

接着,我们要让 Angular 帮我们注入 HttpClient 给我们的 component。Angular 的注入与 .NET 很相似,要在建构式中加入所依赖的类别当作参数,然後 Angular 看到建构式有参数,就会在服务容器中找到这个依赖,然後把实体注入给我们 component class。与 .NET 不同的是,在 Angular 中要在建构式使用依赖注入,这个参数必须要有 private 或 public 修饰词

// ironman-list.component.ts
constructor(private http: HttpClient) { }

注入 HttpClient 之後,我们就可以在 component class 中使用它了

// ironman-list.component.ts
  ngOnInit(): void {
    this.http.get<IronmanUser[]>(this.apiUrl + '/api/User')
      .subscribe(data => {
        this.userListFromApi = data;
      })
  }

我们来看一下这段程序码,this.http 就是刚刚透过依赖注入取得的 HttpClient 实体,我们呼叫这个实体的 get 泛型方法,从我们的 api 取得型别为「IronmanUser阵列」的资料。

HttpClient 使用观察者模式设计来处理非同步,使用 http.get()<> 方法会得到一个 Observable<资料型别> 的「可观察物件」,我们必须订阅这个可观察物件,并指定当任务完成时要做什麽後续处理。

观察者模式就像订报纸一样,我们打电话给报社说我们要订报纸,打完电话我们不会马上拿到报纸,但是我们知道明天报纸一印好报社就会把报纸送来给我们,我们可以先把柳橙汁放冰箱、培根先买好,隔天报纸一送到我们就能把柳橙汁跟培根拿到餐桌,配报纸享受悠闲的早晨。而在我们上面的例子中,我们透过 HttpClient 跟我们的 .NET API 订阅一份资料,等到这个资料送达,我们就把这份资料存到 this.userListFromApi 变数,然後 html 页面再帮我们用内嵌系结把资料显示在 table 里。
https://ithelp.ithome.com.tw/upload/images/20210928/20140664RsovSCTEDv.png

填坑

这边又有一个笔者挖的坑:之前在 DB 里verified 栏位在 API 使用 bool 型态来接,所以从 API 取回来的 verified 都会是 trule/false,会造成几个BUG:(1) 编辑的按钮跑不出来,因为之前用了三个等号(===),true 与 1 不相等。(2) 同样因为 verified 变成 true/false,ngSwitch 会无法显示红色 "BUG" 字样。(3) POST, PUT request 会变成 400 bad request,因为不符合 API 参数的预期资料型态。

如果要修正这些错误,必须把前後端含资料库的型态统一,可以考虑把 Angular app 里的 verified 属性改成 boolean 型态

包装 ironman-service

刚刚我们是直接在 component 里引入 HttpClient 来替我们打 API,但有些时候我们可能会希望把所有打 user API 的功能写在一起,然後让多个 component 共用这个 service,这时候我们就会需要注入我们自己写的 service,现在我们来稍微修改一下我们的程序,把打 "User" API 的功能写成一个 service。

首先,先用 ng 指令或点右键新增一个 ironman-service
ng g s ironman # g=generate s=service

然後,把 HttpClient 注入给这个 service,再把 打 API 取得使用者资讯的 function 写在这个 service 里。

@Injectable({
  providedIn: 'root'
})
export class IronmanService {

  apiUrl = 'https://mydomain.tw/api';
  httpOptions = {
    headers: new HttpHeaders({'Content-Type': 'application/json'})
  };

  constructor(private http: HttpClient) { }

  getUserList(): Observable<IronmanUser[]> {
    return this.http.get<IronmanUser[]>(`${this.apiUrl}/User`, this.httpOptions);
  }

  getUserDetail(id: number): Observable<IronmanUser> {
    return this.http.get<IronmanUser>(`${this.apiUrl}/User/${id}`, this.httpOptions);
  }

  addUser(userModel: IronmanUser): Observable<void> {
    return this.http.post<void>(`${this.apiUrl}/User`, userModel, this.httpOptions);
  }

  updateUser(userModel: IronmanUser): Observable<void> {
    return this.http.put<void>(
      `${this.apiUrl}/User/${userModel.userId}`, userModel, this.httpOptions);
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/User/${id}`, this.httpOptions);
  }
}

上面的程序码中,有三个稍微需要留意的地方

  • @Injectable() 装饰器告诉 Angular 这是一个可以被注入的 class,然後 'root' 代表我们直接让这个 class 可以从任何地方注入,也就是整个专案里只要有需要这个 class 的地方,透过依赖注入都能取得它。
  • service 里的 function 不是回传取回来的值,而是回传可观察物件 Observable<>,使用这个 service 的程序再透过订阅,自由的决定要如何运用这些资料。
  • 可观察物件还能透过 rxjs 做很多高阶串流的处理,想写好 Angular 的邦友请一定要读一下这个 rxjs 的系列文

service 准备就绪之後,我们再把它加到 app.module.ts 的 provider 阵列里

// ...
providers: [IronmanService],
// ...

最後,在 component 的建构式注入 IronmanService,然後呼叫 service 的 function 并订阅,就能从 API 取回资料

constructor(private ironmanService: IronmanService) { }

ngOnInit(): void {
this.ironmanService
  .getUserList()
  .subscribe(data => {
    this.userListFromApi = data;
  });
}

最後最後,有一个东西很重要,一定要强调三次
没有人订阅,request 就不会发出去
没有人订阅,request 就不会发出去
没有人订阅,request 就不会发出去

笔者不止一次花 30 分钟找没有订阅造成的 BUG,谨以血泪提醒各位邦友一定要记得订阅(按赞加分享)。


<<:  [Day13] 不打鱼晒网

>>:  Day16 - 语音辨识神级工具-Kaldi part1

[16] [烧瓶里的部落格] 06. 部落格的 Blueprint

部落格的 Blueprint 和会员验证时候的做法一样 部落格页面应该列出所有文章,允许已登入的会员...

[DAY23] Experiment, Run, MLflow

DAY23 Experiment, Run, MLflow 今天开始的几天内,会进入 Azure M...

TypeScript | Type 研究心得纪录 2

我习惯理解一个东西,可以套用日常的生活经验,找出类比、拟人化会帮助我更好理解,今天的议题是最近看到 ...

Learning How to Make a Movie

"The Great Movie Experience" as Myron En...

display : Inline、Block、Inline-Block

display:Inline、Block、Inline-Block 前言 display是用来设置每...