javascript - Hur man har olika @Injectable för utveckling och produktion


Jag har ett rest-client-objekt som i produktionsmiljön kommer att peka på en RESTful-server och kommunicera med den. Men i utvecklingsläge kommer det helt enkelt att använda webbläsarens lagring för enkelhets skull.

Jag tycker att rätt sätt att göra detta är att ha två implementationer av ett gränssnitt, en för produktion och en för utveckling. På detta sätt behöver resten av applikationen inte veta om produktion/utveckling och kan testas på samma sätt. Allt jag behöver göra är att hitta ett sätt att injicera rätt instans för varje miljö. Och förresten, jag föredrar att inte inkludera utvecklingsversionen i produktionskoden alls!

Vet någon hur man gör detta?

[UPPDATERING]

Jag använder Angular CLI för att starta mitt projekt så jag använder Webpack (tror jag!). Jag föredrar att inte använda en extern mock-server eftersom jag vill hålla test-/utvecklingsmiljön isolerad från externa beroenden. På så sätt är allt jag behöver för att arbeta med mitt frontend-projekt bara det själv. Dessutom, även om min fråga är fokuserad på REST-klienten, kan denna fråga förstås generaliseras till vilken annan singleton-injektionsbar som helst.

Jag ställer den här frågan eftersom det i andra DI-ramverk som jag har arbetat med (nämligen Spring) är en del av ramverket att göra så. Så jag förväntade mig att se samma här också.

Vid det här laget vet jag att jag kan använda environment.production för att kontrollera om det är i produktionsläge eller inte. Men först och främst är detta en körningstidvariabel, men jag tror att jag letar efter en kompileringstidvariabel (jag är dock inte säker). Dessutom, även om vi har den variabeln, leder följande kod till ett kompilationsfel:

if (environment.production) {
  import { RestClientService } from './rest-client.service';
}
else {
  import { MockRestClientService as RestClientService } from './mock-rest-client.service';
}

ERROR in ... Duplicate identifier 'RestClientService'.
ERROR in ... An import declaration can only be used in a namespace or module.

Svar #1

Använd fabriksleverantör för detta:

export function restClientFactory(http: HttpClient): RestClientService {
  if (environment.production) {
    return new RestClientService(http);
  } else {
    return new MockRestClientService();
  }
}

@NgModule({
  providers: [
    { provide: RestClientService, useFactory: restClientFactory, deps: [ HttpClient ] },
  ],
})
class AppModule {}

Tillvägagångssättet med villkorliga import-satser fungerar inte, eftersom statiska import-satser endast är tillåtna på filens översta nivå. De kan inte vara inbäddade i en if/else-sats. Du kan använda dynamisk import() för att kringgå detta, men detta kommer också att leda till att båda implementationerna inkluderas i din bundle.

Tyvärr finns det ingen teknisk möjlighet att göra en kompileringstid-konfigurering av DI som i Spring.

Huvudproblemet med runtime-kontroll är att båda implementationerna kommer att ingå i din produktionspaket. Du kan förlita dig på tree-shaking, som är en del av produktionsbyggnaden, för att säkerställa att din mock-klient inte ingår i produktionspaketet.

Eftersom environment.production-variabeln aldrig ändras kan du förvänta dig att en gren av if/else aldrig nås och därför kan tas bort från paketet av tree-shaking-processen. Problemet är att verktyget som utför tree-shaking inte kan förstå det, eftersom variabeln inte deklareras som en const, utan som en egenskap hos ett objekt.

Om du lägger till export const production=true i environment.prod.ts och export const production=false i environment.ts och använder denna variabel i if/else-satsen, bör en av grenarna framgångsrikt tree shakes bort från produktionspaketet.

Observera att detta tillvägagångssätt är beroende av en implementeringsdetalj i Angular CLI. Ändå har jag framgångsrikt använt det i ett av mina projekt i mer än ett år nu.