angularjs - Klassnivå beroenden som kan injiceras i Angular 1.x med TypeScript?


Förlåt för den något invecklade titeln.

Jag har använt Angular 1.x i ett par år nu och har nyligen undersökt potentialen att använda TypeScript i en kodbas som jag refaktoriserar (också i Angular 1.x). För närvarande har jag svårt att tänka igenom hur man gör injicerbara beroenden av en klass (och klassfamiljer), snarare än beroenden av konstruktörsfunktioner.

Angular har en stark tendens att bygga applikationer med singelinstanstjänster, men jag har upptäckt att dessa singelinstanstjänster ofta kan bli förorenade med föråldrad/felaktig status under applikationens körningstid, eller skapa en onödig uppdelning mellan den funktionalitet som exponeras av tjänsterna och de data som de arbetar med.

Som en följd av detta är ett mönster som jag starkt har föredragit att använda Angulars .factory() för att generera konstruktionsfunktioner att användas på olika platser i mina applikationer. Inom ramen för ES5 och Angulars modulsystem kan du göra något som, t.ex.:

angular.module('SomeApp', [])
  .factory('BaseClass', baseClassFactory)
  .factory('SubClass', subClassFactory);


baseClassFactory.$inject = ['$http'];
function baseClassFactory ($http) {
  function BaseClass () {}

  BaseClass.prototype = {
    $http: $http
    /* some additional implementation */
  };

  return BaseClass;
}

subClassFactory.$inject = ['BaseClass'];
function subClassFactory (BaseClass) {
  function SubClass () {}

  SubClass.prototype = Object.create(BaseClass.prototype);
  angular.extend(SubClass.prototype, {
    someNewMethod: function () {
      this.$http.get('/some/url').then(function (data) {
        /* some operation on the data to update this instance */
      });
    }
  });

  return SubClass;
}

Den framträdande poängen är att jag nu kan skapa en familj av "klasser" som var och en har tillgång till injicerade beroenden genom initialiseringslogiken i mina fabriksfunktioner. Fabriker för underklasser injicerar uttryckligen resultaten av fabriker för föräldraklasser; vi tvingas utnyttja Angulars modul- och DI-system för att uppnå detta, men i avsaknad av något annat, fungerar det utmärkt.

I exemplet ovan kanske jag skapar en Model-klass vars instanser exponerar rådata tillsammans med operationer för att spara/radera/vad-som-nu med hjälp av $http. På andra ställen i min app behöver jag bara injicera en av mina klasser och jag kan new SubClass(someArgs).someNewMethod() utan problem.

I TypeScript (eller, antar jag, vanlig ES6 JS) är den här typen av mönster, som är beroende av runtime-beroendelösning av Angulars moduler och injicerbara komponenter, en duplicering av arbete från att använda TS/ES6-modulsystemet och arv.

För singleton-tjänster, eller någon annan klass där vi förväntar oss att beroenden ska injiceras till konstruktionsfunktioner, finns det inget problem. Faktum är att internet är fullt av exempel som:

class MyThing {

    static $inject = ['$http'];

    constructor(private $http: ng.IHttpService) {}

}

angular.module('asdf', []).service('myThing', MyThing);

Detta fungerar utmärkt, även för underklasser av MyThing.

Men vad jag inte har hittat ett bra sätt att göra är att uppnå effekten av mitt ursprungliga JS-exempel. I idealfallet skulle jag kunna deklarera vissa klasser som är avsedda att ha många instanser i appen, men som ändå har injicerbara beroenden. Något liknande följande:

class BaseClass {

    $http: ng.IHttpService;

    constructor(not, injectables) {
        /* i do not exist to be injected */
    }

}

class SubClass extends BaseClass {

    someMethod() {
        return this.$http.get('/some/url');
    }

}

det gör den nödvändiga $http tillgänglig på alla instanser av BaseClass, tillsammans med alla instanser av subklasser till BaseClass utan besväret med att göra:

angular.module('myModule', [])
    .factory('BaseClass', baseClassFactory)
    .factory('SubClass', subClassFactory)


baseClassFactory.$inject = ['$http'];
function baseClassFactory($http) {
    BaseClass.prototype.$http = $http;
    return BaseClass;
}

subClassFactory.$inject = ['$http'];
function subClassFactory($http) {
    SubClass.prototype.$http = $http;
    return SubClass;
}

Detta fungerar naturligtvis, men är mycket extra (felbenägen) strömlinjeformad kod. Det kräver också att författaren till alla underklasser är mycket medveten om varje beroende som specificerats av föräldern.

Jag hoppas att detta var begripligt. Har någon några förslag på hur man kan ta itu med detta problem?

Svar #1

Jag tror att det bör hanteras i Typescript på ett liknande sätt som det skulle göras i ES7:

class BaseClass {

    static $inject = ['$http'];

    constructor(...args) {
        (<any>this).constructor.$inject.forEach((service, index) => {
            this[service] = args[index];
        });
    }
}

På så sätt kan barn ärva beroenden eller ha egna genom att kalla super().