GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
字體大小: +
9 分鐘閱讀時間 (1783 個字)

Angular #26 - 深入 Service [3]

shutterstock_198004562
  • 相依注入(Dependency Injection)

    • 如果寫過 Java 用過 Spring 框架,應該對這個名詞不陌生才對。近幾年 .NET Core 也開始內建 DI,想必是發現有其必要性。Angular 中透過 DI 讓各個 Component 可以注入 service,而實際上究竟是怎麼運作的呢?
      • Injector
        • 當 Component 需要 Service 的時候,實際上將實體注入的,就是透過這個 Injector 這個實體
        • 如果有宣告在 @Component 的 metadata 中的話,每一個 Component 都可以擁有自己的 Injector,如果沒有宣告就會使用 Parent 的 Injector,以此類推一路往階層樹的移動,最後會走到 AppModule 所擁有的 Injector
      • Provider
        • 是一個維護著可被注入的 service 的陣列
        • 如果各位還記得,app.module.ts 在 NgModule 的 metadata 中就有一個 providers 陣列,所以理論上所有在這個 module 底下的 component 都可以用 AppModule 的 Injector
      • Hierarchical injection system
        • 綜合 Injectorprovider 在各層級的定義,就是 Angular DI 的運作方式,而這個運作機制主要是為了避免 service 在命名上的衝突,或是透過這樣的隔離控管哪些 Component 可以使用什麼 service
        • 承上,結論就是 Angular 中,並不一定會只有一個中央控管的 service registry,各層級都可以有自己的 Injector 與 Provider
    • 可以抽換被注入的 service 嗎?
      • 如果想在不更動 Component 的情況下抽換 service(e.g. 不同的實作或是測試時用的 mock),該怎麼做? Angular 提供以下幾種方式:
        • Alias: 這種做法可以用替另一個 service 取一個別名,設定方法如下:
            providers: [
              {
                provide: SomeService, 
                useExisting: SomeServiceV2
              }
            ]
          • 在這種情況下 SomeServiceV2 是不會被產生新的實例的
        • Class: 這種做法可以抽換掉且注入一個新的 instance
            providers: [
              {
                provide: SomeService, 
                useClass: SomeServiceV2
              }
            ]
          • 再次強調,這個設定方式會產一個新的 SomeServiceV2 實例(其他的 Component 也許已經透過它的 Injector 產了一個)
        • Factory: 會到這麼複雜也是很神奇了,總之就是在產 service 實例的當下需要動態依某個設定值決定要產什麼 instance
            providers: [
              {
                provide: SomeService, 
                useFactory: SomeServiceFactory,
                deps: [HttpClient]
              }
            ]
          • 如果要被建立的 service 本身又相依於其他的 service,必須在 deps 裡面列出
        • Value: 這種做法可以用一個靜態的(e.g. 物件),在測試的時候提供 mock 實作可能會用到
            providers: [
              {
                provide: SomeService, 
                useValue: SomeServiceV2
              }
            ]
    • 最後一個要注意的點
      • 當你在 providers 陣列中註冊 service 的時候,Angular 預設會用那個 service 的名稱當 token,方便之後取得實例用
      • 在 Component 中注入 service 且提供型別的時候,Angular 會依那個型別的名字去找實例
    • 為什麼要使用 Angular 的相依注入?
      • 可以專注在使用 service 而不用擔心如何建立
      • 承上,如果要使用的 service 還有相依於其他的 service,這些相依性也會自動被建立好,某種程度上也是讓你可以專注在使用
      • 比較好測試,因為可以抽換,如果自行建立就被綁死了
      • 可以控制 service 要注入在,不用擔心其他的 component/module 會用在什麼地方造成衝突
      • 可以保持 service 的乾淨度(e.g. 裡面不會有其他 component 的資料)
    • 相依注入的小撇步
      • 如果沒必要,就在最小的範圍注入 service
      • 用心命名,多打幾個字不會死(e.g. BPservice vs. BlogPostService)
      • 盡量單一職責(SRP),不要讓一個 service 相依於過多的其他 service,使得 service 本身異常肥大管東管西的,造成它難以測試
      • 讓 service 是有意義的,白話文就是,有沒有必要為了一個只使用一次的方法就開一個 service,這個平衡點跟上一條單一原則要思考一下如何拿捏分寸
      • 方法命名或是行為盡量一致。舉例來說,如果有好幾支 service 都是在處理 REST API 的呼叫,那可能各 service 的方法上都可以統一開出 load/save/update/delete,如此只要專注在注入什麼 service 而不用多花時間思考要使用什麼方法來達成需求
  • 沒有使用相依注入的 Service

    • 什麼情況下會不使用 DI?

      • 幾乎不存在這種情況,因為 DI 實在是太方便

      • 唯一的情境就是,要使用 Service 的時間點是在 Angular 的 LifeCycle 之外,舉例來說在系統完全啟動前先備好一些設定值

      • 來調整一下 src/app/services/config.service.ts

          export class ConfigService {
            private _api: string
        
            static set(property, value) {
              this['_' + property] = value;
            }
        
            static get(property) {
              return this['_' + property];
            }
          }
        • 這個是一般 Javascript module 的撰寫方式,如果有需要還可以再定義更多,不過目前的說明不需要
        • 特別留意方法都是 static
      • 接著直接在 main.ts 裡使用 ConfigService

          import { enableProdMode } from '@angular/core';
          import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
          import { AppModule } from './app/app.module';
          import { environment } from './environments/environment';
          import { ConfigService } from './app/services/config.service';
        
          ConfigService.set('api', 'https://angular-in-action-portfolio.firebaseio.com/stocks.json');
        
          if (environment.production) {
            enableProdMode();
          }
        
        platformBrowserDynamic().bootstrapModule(AppModule);
        • 如果還記得的話,main.ts 是整個 Angular 程式的進入點
        • 在上面的程式碼中,在 bootstrap 前就設定好了 api 的值,讓在 bootstrap 過程中 component 可以直接用 ConfigService
  • HttpClient

    • Angular 原生拿來處理 HTTP 請求的 service
      • 理論上 ng new 的時候就會一併裝這個 module 了,不過如果真的有什麼萬一的話,還是可以透過 npm install @angular/common 手動安裝
      • 強烈建議不要直接在 component 裡使用
      • 預設會搭配 Observables 處理 response
  • 在 StockService 裡使用 HttpClient

    • Fill in stock.service.ts as follows

        import { Injectable } from '@angular/core';
        import { HttpClient } from '@angular/common/http';
        import { ConfigService } from '../services/config.service';
        import { Stock } from './stocks.model';
      
        @Injectable()
        export class StocksService {
      
            constructor(private http: HttpClient) {}
      
            getStocks() {
              return this.http.get<Array<Stock>>(ConfigService.get('api'));
            }
      
        }
      • StockService 透過 ConfigService 取得 api 的 URL,並使用 HttpClient 取得股票相關的資料
      • 別忘了要 Import(雖然 VSCode 會提示)
      • constructor 中有注入 HttpClient, 但 ConfigService 沒有的理由是因為 ConfigService 是無法注入的(沒有 @Injectable)
      • getStocks 回傳的是 Observable,在沒有對這個 observable subscribe 之前,Http 請求是不會被發出的(Reactive Programming 的特性)
  • App Component 中使用 StockService

    • 調整 app.component.ts 如下:

        import { Component, OnInit, OnDestroy } from '@angular/core';
        import { AccountService } from './services/account.service';
        import { StocksService } from './services/stocks.service';
        import { AlertService } from './services/alert.service';
        import { Stock } from './services/stocks.model';
      
        @Component({
          selector: 'app-root',
          templateUrl: './app.component.html',
          styleUrls: ['./app.component.css'],
          providers: [StocksService]
        })
        export class AppComponent implements OnInit, OnDestroy {
          refresh: boolean = true;
          stocks: Stock[] = [];
          interval: any;
      
          constructor(
            private accountService: AccountService,
            private stocksService: StocksService,
            private alertService: AlertService ) { }
      
          ngOnInit(): void {
            this.accountService.init();
            this.load();
            this.interval = setInterval(() => {
              if (this.refresh) {
                this.load();
              }
            }, 15000);
          }
      
          toggleRefresh(): void {
            this.refresh = !this.refresh;
            let onOff = (this.refresh) ? 'on' : 'off';
            this.alertService.alert(`You have turned automatic refresh ${onOff}`,'info', 0);
          }
      
          ngOnDestroy() {
            clearInterval(this.interval);
          }
      
          reset(): void {
            this.accountService.reset();
            this.alertService.alert(`You have reset your portfolio!`);
          }
      
          private load() {
            this.stocksService.getStocks().subscribe(stocks => {
              this.stocks = stocks;
            },
            error => {
              console.error(`There was an error loading stocks: ${error}`);
            });
          }
        }
      • import StocksService,然後把它加到 @Component 的 providers 陣列
      • 承上,這樣設定只有 AppComponent 以及它的子 Component 可以使用這個 service
      • 當這個 Component 初始化完成的時候,會進到 OnInit 的生命週期,這時候會起一個 interval,每 15 秒更新一次資料
      • 此外,還會呼叫 load,在這就可以看到真正對 stockService 回傳的 Observable subscribe,並可以拿到 response(stocks),同時也可以在這加上 error 作例外處理
      • ngDestroy 是 implement OnDestroy 必須實作的方法,可以拿來在 Component 被移除的時候把 interval 清掉,避免 memory leak
    • 這個時候就可以把 app.component.html 所註解掉的部份全部解開,再 ng serve 看看華麗的結果了

  • 小結:

    • 本篇重點是
      • Angular CI 的原理講解
      • HttpClient 的基本使用
    • 下一篇將介紹 HttpInterceptor
      • 可以在一個統一的地方處理 request/response
      • 像是替所有的 request 都加上 JWT,或是針對特定的 response 作處理
×
Stay Informed

When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.

Angular #27 - 深入 Service [4]
Angular #25 - 深入 Service [2]

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
2026年4月03日, 星期五

Captcha 圖像