在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-decorator、keenondrums/singleton這兩個lib,前者是完全無法與Angular的DI系統相容,後者是在若constructor中有其它依賴時會發生error。
@Singleton
@Injectable()
export class DeltaMessageService implements MessageService {
constructor(
private someEventSourceService: MessageSourceService,
) { }
}

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

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

再一層層的步進下去,得知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註解改一下來測試效果。