Angular適用的Singleton decorator

  • 256
  • 0
  • 2019-05-11

GitHub專案

在class套上@Singleton直接singleton化,省得到處貼那一樣的模板。本篇將描述它的使用案例以及我研究的過程。

 

在Angular6之後,Service可以用@Injectable({ providedIn: 'root' })來使它不需要在任何module作provide也能使用,這同時會讓它帶有singleton的特性。

@Injectable({
  providedIn: 'root'
})
export class MessageService {
  getMessage(): Observable<string> {
    return of('Message from MessageService');
  }
}

 

現在假設一個情境,你希望特定module下的service可以有不同的特性 - 當收到新訊息時額外再跳出一個toast(這邊簡單以alert代替)。但又不想把判斷環境的邏輯用if寫在service中讓它越變越雜亂,那我們通常就會把這個service切成一個interface,並讓它由不同的service來實作。

export abstract class MessageService {
  abstract getMessage(): Observable<string>;
}

@Injectable()
export class AlphaMessageService implements MessageService {
  message$ = new Subject<string>();

  constructor(
    private someEventSourceService: MessageSourceService,
  ) {
    this.initObservable();
  }

  initObservable() {
    this.someEventSourceService.someEvent$
      .subscribe(msg => {
        const message = `Message from AlphaMessageService - ${msg}`;
        this.message$.next(message);
      });
  }

  getMessage(): Observable<string> {
    return this.message$.asObservable();
  }
}

@Injectable()
export class BetaMessageService implements MessageService {
// ...
  constructor(
    private messageSourceService: MessageSourceService,
  ) {
    this.initObservable();
    this.getMessage().subscribe(alert);
  }
// ...
}

各個module再依需要來provide service

@NgModule({
  providers: [
    { provide: TitleService, useClass: AlphaTitleService }
  ]
})
export class Tab1Module {}

@NgModule({ providers: [{ provide: TitleService, useClass: BetaTitleService }] })
export class Tab2Module {}

@NgModule({ providers: [{ provide: TitleService, useClass: BetaTitleService }] })
export class Tab3Module {}

這同時也適用於依環境決定使用哪個Service實作的用法,既不用改動原本使用該service的code,又能將不同環境的程式實作切的一乾二淨。

 

Singleton?

現在我們已經可以讓各module有不同的service實作了,但這同時也帶來了一個問題。

當每個地方provide一個service時,Angular會各自為它們產生一個獨立的instance出來,也就是說在以上程式碼中的BetaTitleService會有兩個instance,並各自訂閱一次MessageSourceService中的事件,這時若收到一個新訊息時就會一次跳出兩個alert出來。

即然如此,我們可以先用最通用的方式讓它有singleton的特性。

@Injectable()
export class GammaMessageService implements MessageService {
  static instance: GammaMessageService;
  message$ = new Subject<string>();

  constructor(
    private someEventSourceService: MessageSourceService,
  ) {
    if (GammaMessageService.instance) {
      return GammaMessageService.instance;
    }
    GammaMessageService.instance = this;

    this.initObservable();
    this.getMessage().subscribe(alert);
  }
// ...
}

這樣固然是有效的,只是會變成說每個要singleton的service都要多加那5行看似重複的code。既然我們能用decorator來讓service變的可注入(雖然靠的是Angular),是不是代表decorator也能讓service強制變singleton呢?

為了達成這個目地,我先後在npm上找到了dilame/es7-singleton-decoratorkeenondrums/singleton這兩個lib,前者是完全無法與Angular的DI系統相容,後者是在若constructor中有其它依賴時會發生error。

@Singleton
@Injectable()
export class DeltaMessageService implements MessageService {
  constructor(
    private someEventSourceService: MessageSourceService,
  ) { }
}

 

當我深入追查後,得知錯誤來自DI發現這個class type有其它的相依,但無法解析它是哪個class。

複製.js中的段落到Angular的github上搜尋,就能查出它編譯自哪份.ts源碼。

 

接著發現是上面一點的_reflector.parameters()沒有抓到它的params導致。

這邊在偵錯時可以下conditional至你有興趣的class時才中斷,否則它會中斷個數十次才真正找到你要的。

 

 再一層層的步進下去,得知DI會先讀取prototype的parameter field來確認它有哪些params

private _ownParameters(type: Type<any>, parentCtor: any): any[][]|null {
    // ...

    // Prefer the direct API.
    if ((<any>type).parameters && (<any>type).parameters !== parentCtor.parameters) {
      return (<any>type).parameters;
}

 

那我們該怎麼辨呢?看來這個lib一樣不適用在Angular上(有趣的是ng serve --prod卻正常),我們來研究一下它是怎麼寫的

export const singleton = <T extends new (...args: any[]) => any>(classTarget: T) =>
  new Proxy(classTarget, {
    construct(target: Singleton<T>, argumentsList, newTarget) {
      if (!target[SINGLETON_KEY]) {
        target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)
      }
      return target[SINGLETON_KEY]
    },
})

從這可以發現它是利用Proxy來代理原class的construct,再套了個實現singleton的樣板而已,簡簡單單。

研究了下Proxy後發現它也能在get任一field時作代理,那讓我們來把它加上去。

export function Singleton<T extends new (...args: any[]) => any>(classTarget: T) {
  const paramTypes = Reflect.getOwnMetadata('design:paramtypes', classTarget);
// ...
    get(target: T, p: string | number | symbol, receiver: any): any {
      if (p === 'parameters') {
        return paramTypes;
      }
      return classTarget[p];
    },
// ...
}

get()中判斷若現在要取得的是parameters就回傳我們幫它準備好的parameters。至於第一行那Reflect.getOwnMetadata('design:paramtypes', classTarget)我其實也只是直接從Angular抄過來。這樣就大功告成了,我們成功找到一個在Angular DI下也能作singleton的decorator寫法,好不容易呀...

 

以上專案我放在StackBlitz方便大家玩,可以把Tab2Module中provide的service註解改一下來測試效果。