Posted on Leave a comment

[功能介紹-11] NgModules

Angular modularity

Angular有提供許多的功能,如FormsModule、HttpModule、RouterModule,都是NgModules。一些第三方資源提供者也有提供許多NgModules可使用,如Material Design、Ionic、AngularFire2等…。

NgModules可將一群功能性一致的components、directives和pipes組合在一起,讓外部可以直接引用並使用這個模組。例如FormsModule裡面會提供許多和表單驗證、表單資料繫結等的功能在裡面。

在要開發一個APP時,我們可以透過@NgModule的metadata去設定這一個APP會使用到的功能模組,設定的樣子如下:
(以src/app/app.module.ts為例)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
  imports:      [ BrowserModule ],
  providers:    [ Logger ],
  declarations: [ AppComponent ],
  exports:      [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

NgModule的metadata有下面幾項:

  • imports:這個模組所需用到的Angular提供的或第三方提供的Angular資源庫(如FormsModule、HttpModule等)。
  • providers:一些供這個模組使用的service,在此宣告後所有下面的元件都可以直接使用這個服務。
  • declarations:這個Module內部Components/Directives/Pipes的列表,聲明這個Module的內部成員
  • exports:用來控制將哪些內部成員暴露給外部使用。
  • bootstrap:這個屬性只有根模組需要設定,在此設定在一開始要進入的模組成員是那一個。

  • 關於這整個Angular NgModule的介紹也可參考系列文的:[功能介紹-8] Angular架構,會有更概觀的介紹。

    Bootstrapping

    每一個專案都一定會有一個根模組,也就是root module,我們會在main.ts去做Bootstrap這個根模組的動作,讓整個APP可以運行起來。

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';
    
    if (environment.production) {
      enableProdMode();
    }
    
    platformBrowserDynamic().bootstrapModule(AppModule);//用這一串文字來運行根模組AppModule

    在bootstrap的動作裡,會建立好執行環境並把在src/app/app.module.ts裡設定的bootstrap陣列裡的元素取出來並透過在該成員裡設定的selector,讓我們可以在src/index.html來顯示這個元件的VIEW。

    <body>
      <app-root></app-root>
    </body>

    為APP增加一個自製的directive

    下面是一個highlight的範例的directive
    src/app/highlight.directive.ts

    import { Directive, ElementRef } from '@angular/core';
    
    // 將這個區塊的背景顏色改為金色
    @Directive({ selector: '[highlight]' })
    export class HighlightDirective {
      constructor(el: ElementRef) {
        el.nativeElement.style.backgroundColor = 'gold';
      }
    }

    如果想使用這個自定的directive,可以在src/app/app.module.ts的declarations區塊裡增加這一個Directive

    declarations: [
      AppComponent,
      HighlightDirective,//在這邊加上去,這樣整個APP都可以使用這個功能。如<h1 highlight>{{title}}</h1>
    ],

    使用CLI加速開發速度

    這邊是Angular CLI的官網:https://cli.angular.io/
    這邊有很詳細的Angular CLI的教學:https://dotblogs.com.tw/wellwind/2016/09/30/angular2-angular-cli
    直接使用ng –help也可以看到CLI的詳細說明

    如果我們使用CLI來創建這個component, directive或pipe,CLI會自動將這個directive宣告在app.module.ts的metadata裡
    ng generate directive highlight
    可以讓我們在開發上更為加速

    下圖是ng generate可以帶的參數

    Service providers

    一個service可以注入至組件(@NgModule.providers)或者是元件(@Component.providers)。
    如果NgModule與Component同時有分別設定相同的類別,會產生兩個不同的實體,在該元件內則會優先注入使用元件裡自己設定的服務。
    一般而言,如果這個服務被廣泛的使用在很多的元件裡,我們會將providers宣告在根模組上。但如果這個服務只有這個元件在使用,則會放在Component的providers去宣告。

    下面的CLI指令可以創立一個user服務並加到模組裡使用
    ng generate service user –module=app
    可以看到app.module.ts的providers會增加一個名為UserService的類別

    providers: [ UserService ],

    在使用上,如果已經有設定好providers後,只要在元件的constructor裡面宣告一個變數是providers裡面設定好的service,就可以在元件裡直接取用了

     constructor(userService: UserService) {
        this.user = userService.userName;
      }

    其依賴注入的概念大概是這樣

    所有在providers裡面被宣告的物件實體會被存在angular的服務列表裡,因此在下面不同的元件裡可以直接取用到相同的服務實體。

    NgModule imports

    如果要使用ngIf這個directive,我們需要在app.module.ts導入BrowserModule

    imports: [ BrowserModule ],

    我們import了BrowserModule這個模組,就可以使用BrowserModule裡面有在ngModule裡去用exports去設定要曝露出來的directive、component、pipe等。

    這樣就可以在所有的元件中使用像下面這樣的模板語法

    <p *ngIf="user">
      <i>Welcome, {{user}}</i>
    <p>

    Re-exported NgModules

    其實上面的例子裡,ngIf是在CommonModule之下的,但是我們import了BrowserModule,我們可以看看官網對於BrowserModule的介紹

    由於BrowserModule在exports的地方將自己所import的CommonModule的功能再export出去,所以當我們import了BrowserModule後,也可以使用CommonModule所有export的功能,這個就是Re-exported。

    使用ModuleWithProviders

    如果我們有一個子元件,而我們希望那個子元件能夠如同核心元件一般,比所有其他子元件都更優先被載入(就像*ngIf)。
    或者是希望能夠在被所有元件初始化之前,優先設定好裡面的值時,可以自行將元件包成ModuleWithProviders然後在appModule用import的方式來import我們自己寫的核心元件。

    在Angular最廣泛使用這個功能的是Routing,我們必需先傳入一個導航設定的資料來設定Routing。
    我們也可以在自己所自製的模組裡使用這個功能。

    假如我們有一個服務是這樣的

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class UserService {
      userName = 'Sherlock Holmes';
    }

    我們希望能夠讓模組在使用這個服務前,先設定一個新的userName再引入使用。
    首先,要在剛剛的服務裡加上這些程式碼

    constructor(@Optional() config: UserServiceConfig) {
      if (config) { this._userName = config.userName; }
    }

    然後在CoreModule裡加上forRoot方法,這個forRoot的方法接受一個service class,並回傳一個ModuleWithProviders物件

    static forRoot(config: UserServiceConfig): ModuleWithProviders {
      return {
        ngModule: CoreModule,
        providers: [
          {provide: UserServiceConfig, useValue: config }
        ]
      };
    }

    最後在imports列表中呼叫它:

    imports: [
      BrowserModule,
      CoreModule.forRoot({userName: 'Miss Marple'}),
      AppRoutingModule
    ],

    這時,我們會看到userName是“Miss Marple”,而不是“Sherlock Holmes”。

    注:只在應用的根模塊AppModule中調用forRoot。如果在其它模塊(特別是惰性加載模塊)中調用它則違反了設計意圖,並會導致運行時錯誤。

    若我們想要防止有人重覆import這個CoreModule,可以用下面的方法

    constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
      if (parentModule) {
        throw new Error(
          'CoreModule is already loaded. Import it in the AppModule only');
      }
    }
    Leave a Reply