Angular SCAM

Angular-SCAM concept

陳建融 Randy Chen 2023/01/06 23:51:30
684

本文開始

在大型的 Angular Application ,都會使用大量的 Module 來將元件需要使用的功能給引用進來,但是,當我們在同一個 Module 裡面定義多個 Component 的時候,就需要從外部引入多個功能進來以讓這些 Component 都能吃到它們自身需要使用到的功能。

 

以上這種情況就會導致下面這個問題,假設這個模組有 A, B 兩個元件,當模組引入 A 元件需要使用的功能,但這些功能 B 元件都不需要使用,而模組也引入 B 元件需要使用的功能,但 A 元件都用不到,這樣子的情況會導致這個模組要做的事情太雜,且其功能也意義不明。

 

來舉個範例

@NgModule({

  providers: [

    ConfigService, 

    PromotionService, 

    AppService,

  ],

  declarations: [

    StringSlicePipe,

    CustomerFormatterPipe,

    ShoppingCartDealPipe,

    CurrencyFormatterPipe,

    CustomerCardComponent,

    ShoppingCartComponent,

    CustomerPortalComponent,

  ],

})

export class CustomerPortalModule {}

假設今天我們除了在 CustomerPortalModule 會需要使用到 ShoppingCartComponent 以外,在其他的模組中也會需要使用到 ShoppingCartComponent 很自然地我們會寫出以下程式碼

@NgModule({

  declarations: [

    ShoppingCartComponent,

    AnotherPageComponent,

  ],

})

export class AnotherPageModule {}

Ok,加完了,接下來我們先看看 ShoppingCartComponent 的建構式裡有注入哪些 service ,因為也需要引入進這個模組,如此一來 ShoppingCartComponent  才能在這個模組正常運作。

@Component({...})

export class ShoppingCartComponent {

...



  constructor(private ConfigService) {}



...

}

 

可以看到它有加入 ConfigService 這個服務,所以,我們要在 AnotherPageModule  引入它

@NgModule({

  providers: [

    ConfigService // 加入 ConfigService

  ],

  declarations: [

    ShoppingCartComponent,

    AnotherPageComponent,

  ],

})

export class AnotherPageModule {}

 

但是,這樣還是無法運作!! 因為 ConfigService 自己又引入了 PromotionService 這個服務,所以,為了 ConfigService 我們要再引入 PromotionService,所以,再改寫一下 AnotherPageModule 

@NgModule({

  providers: [

    ConfigService,

    PromotionService  // 加入 PromotionService

  ],

  declarations: [

    ShoppingCartComponent,

    AnotherPageComponent,

  ],

})

export class AnotherPageModule {}

 

接著,我們來看看 ShoppingCartComponent 的 template,發現它還有用一些 pipe 的功能,所以,要再引入這些 pipe 到模組中

@NgModule({

  providers: [

    ConfigService,

    PromotionService

  ],

  declarations: [

    CurrencyFormatterPipe, // 加入 pipe

    CustomerCardComponent, // 加入 pipe

    ShoppingCartComponent,

    AnotherPageComponent,

  ],

})

export class AnotherPageModule {}

 

經過以上一連串的引入,有發現了嗎?

當我們需要在其他地方使用某個元件的時候,就會要因為該元件有使用和注入的所有功能再次在這個模組再次引入這些功能。

 

巨大的 ShareModule 臭味

而以上這樣的問題,也就造就了為什麼很多專案裡面都會使用 ShareModule,在這個 Module 裡面一次引入所有元件需要使用到的功能,然後,在不同的模組間引入 ShareModule 以解決以上的問替。

 

It kinda works. But 有使用過 ShareModule 的方法的人應該都知道,這個 ShareModule 最終隨著專案擴充,會越長越大~

而這個巨大的 ShareModule 就會隱隱地飄出 bad code 的臭味(因為還是會落入 A 模組引入了 shareModule,但是 shareModule 裡有一半以上的功能 A 模組可能都不需要使用)~~

 

而我們就要透過 SCAM 這個概念來解決以上這些問題囉~

 

什麼是 SCAM 樣板?

它的全名叫做 Single Component Angular Module。

這個樣板的概念就是,直接讓元件客製化屬於它自己的 Module 。

Angular 的 Module 原本的用意,是用來封裝該模組中,所包含的元件彼此之間相關的程式碼(e.g. 元件所需的共用方法),但是,我們用 ShareModule 的概念把所有不相關的功能通通引入到同一個模組裡面,等於直接拋棄原本 Angular 模組的精神。

 

來個簡單的範例

@NgModule({

  declarations: [

    CustomerCardComponent,

    CustomerFormatterPipe

  ],

  exports: [CustomerCardComponent]

})

export class CustomerCardComponentModule {}

 

以上這個寫法是傳統的寫法,需要什麼就全部引入,因為 CustomerCardComponent 需要使用 CustomerFormatterPipe 所以,需要引用它。

 

那現在我們把它們拆開來,

Step 1. 為 CustomerFormatterPipe 定義屬於它自己的 Module

@NgModule({

  declarations: [

    CustomerFormatterPipe

  ],

  exports: [CustomerFormatterPipe]

})

export class CustomerFormatterPipeModule {}

 

Step 2. 在 CustomerCardComponentModule  引入 CustomerFormatterPipeModule

接下來,改寫一下原本在 CustomerCardComponentModule  直接引入 CustomerFormatterPipe 的寫法

@NgModule({

  imports: [

    CustomerFormatterPipeModule 

  ],

  declarations: [

    CustomerCardComponent

  ],

  exports: [CustomerCardComponent]

})

export class CustomerCardComponentModule {}

以上就是 SCAM 樣板的執行方式。

 

那我們把以上的執行方式,套用到最一開始的 ShoppingCartComponent 的範例

Step 1. 為元件定義屬於它自己個 Module

@NgModule({

  // I wont bother with these services, as we really should be making them providedIn: 'root'!

  providers: [

    ConfigService, 

    PromotionService,

  ],

  imports: [

    CustomerCardComponentModule,

    CurrencyFormatterPipeModule,

  ],

  declarations: [

    ShoppingCartComponent,

  ],

  export: [ShoppingCartComponent]

})

export class ShoppingCartComponentModule {}

 

Step 2. 引入 ShoppingCartComponentModule 模組到 CustomerPortalModule  裡

@NgModule({

  imports: [

    ShoppingCartComponentModule // 引入 ShoppingCartComponentModule 

  ],

  declarations: [

    StringSlicePipe,

    CustomerFormatterPipe,

    ShoppingCartDealPipe,

    // CurrencyFormatterPipe, 可以拿掉

    // CustomerCardComponent, 可以拿掉

    // ShoppingCartComponent, 可以拿掉

    CustomerPortalComponent,

  ],

  exports: [CustomerPortalComponent]

})

export class CustomerPortalModule {}

可以看到上面我們透過 SCAM 的模板設計方式先建造出屬於 ShoppingCartComponent 自己的模組後,再直接引入到 CustomerPortalModule ,如此一來是不是 Module 就可以少寫很多額外的程式碼,也不用引入東引入西的。

 

另外,我們在另外一個模組 AnotherPageModule 要使用 ShoppingCartComponent 也是直接引入 ShoppingCartComponentModule  就好,改寫如下

@NgModule({

  imports: [

    ShoppingCartComponentModule // 引入 ShoppingCartComponentModule 

  ],

  declarations: [

    AnotherPageComponent,

  ]

})

export class AnotherPageModule {}

是不是模組內部乾淨多了呢~~

 

PrimeNG UI framework 的 SCAM 樣板寫法

在 Angular 專案中,很常用一個叫 Primeng 的 UI Framework。

有別於這一篇教學文章直接寫出一個 module.ts 檔案,在 PrimeNG 裡 SCAM 樣板寫法筆者判斷應該都是直接將 module 定義的內容直接寫在各元件的定義檔裡。

這樣一來就不需要用像 shareModule 這個一大包的共用 Module 了。

 

Reference

1. Angular scam tutorial

2. Intro of Angular SCAM

3. PrimeNG - component sourceCode

陳建融 Randy Chen