iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

網站一條龍 - 從架站到前端系列 第 28

[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,謹以血淚提醒各位邦友一定要記得訂閱(按讚加分享)。


上一篇
[Day27] 基礎的 Directive
下一篇
[Day29] Template Driven Form
系列文
網站一條龍 - 從架站到前端33

尚未有邦友留言

立即登入留言