【问题标题】:AngularJS: Inherited dependencies need to be duplicated?AngularJS:需要复制继承的依赖项?
【发布时间】:2018-01-15 18:56:18
【问题描述】:

结合使用 Angular 1.6 和 ES6 类我遇到了以下问题:

我写了一个有一些依赖的服务(惊喜!)

class myService {

    /*@ngInject*/
    constructor($q){
      this.$q = $q;
      this.creationDate = new Date();
    }

    doStuff(data){
      return this.$q.when(data);
    }
}

angular.module('app').service('myService', myService)

但是我得到了一个构建目标,其中的服务需要更高级一些,所以我对其进行了扩展并在这种情况下使用了扩展服务:

class myFancyService extends myService{

    /*@ngInject*/
    constructor($q, $http){
      super($q);
      this.$http = $http;
    }

    doFancyStuff(data){
      console.log(this.creationDate);
      return this.doStuff(data)
          .then((stuff) => this.$http.post('api.myapp', stuff));
    }
}

angular.module('app').service('myService', myFancyService)

到目前为止,这工作正常,但有一个主要缺点:

通过调用super(dependencies),我的基类的依赖项无法从@ngInject 自动注入。因此,我需要非常清楚,每当我更改 myService 的依赖项时,myFancyService(以及任何其他潜在的未来子类)的依赖项也需要更改。

我不能使用组合代替继承,因为myService 没有注册为角度服务,因此不能作为依赖注入。

问题:

有没有办法自动注入基类的依赖项?

如果没有,至少有办法让我的单元测试提醒我需要更新myFancyService 的依赖项吗?如果super($q) 的参数(或者可能只是参数的数量)等于 myService-constructor 的参数(数量),我还没有找到一种方法来测试 karma/jasmine。

【问题讨论】:

    标签: angularjs class inheritance dependency-injection ecmascript-6


    【解决方案1】:

    super 上面的代码中,需要明确指定参数。

    一种更防故障的方法是在当前类中执行所有依赖项分配:

    constructor($q, $http){
      super();
      this.$q = $q;
      this.$http = $http;
    }
    

    如果在父构造函数中使用这些服务,这可能会产生问题。测试父构造函数的参数并不容易,因为这涉及模块模拟。一个简单且相对可靠的测试方法是断言:

    expect(service.$q).toBe($q);
    expect(service.$http).toBe($http);
    

    这应该在任何 Angular 单元测试中完成,事实上,即使一个类没有被继承。

    考虑到@ngInject 所做的只是创建$inject 注解,更好的方法是引入处理DI 的基类:

    class Base {
      constructor(...deps) {
        this.constructor.$inject.forEach((dep, i) => {
          this[dep] = deps[i];
        }
      }
    }
    BaseService.$inject = [];
    
    class myService extends Base {
        /*@ngInject*/
        constructor($q){
          super(...arguments);
          ...
        }
        ...
    }
    

    在这一点上很明显@ngInject 不再有帮助,需要与arguments 混淆。没有@ngInject,就变成了:

    class myService extends Base {
        static get $inject() {
          return ['$q'];
        }
    
        constructor(...deps){
          super(...deps);
          ...
        }
        ...
    }
    

    如果依赖赋值是子构造函数中唯一要做的事情,则可以有效地省略构造函数:

    class myService extends Base {
        static get $inject() {
          return ['$q'];
        }
    
        ...
    }
    

    类字段和 Babel/TypeScript 更简洁(浏览器不支持原生):

    class myService extends Base {
        static $inject = ['$q'];
    
        ...
    }
    

    【讨论】:

    • 据我所知,这并不能回答我的问题:像expect(service.$q).toBe($q); 这样的子类测试不会提醒我更新子类依赖项,除非我已经记得更新测试。此外,您的 Baseclass 的示例实现不能在没有被继承的情况下用作服务,因为它没有自己的依赖项。 (它更像是一个抽象基类而不是一个工作服务,在某些情况下需要增强)。如果我误解了,请详细说明如何使用它来让孩子只管理自己的依赖关系!
    • 我不确定你的意思。上面代码的目的是避免像super($q) 这样的东西,因为它们需要小心维护。是的,Base 是抽象类,所有服务类都应该继承它,它允许管理子类中的所有部门。我不认为你的单元测试问题有一个好的解决方案,如果你忘记了要测试的东西,它就不会被测试。它看起来更像是 TypeScript 的用例,它能够检测到这种人为错误。
    【解决方案2】:

    要记住两件事:

    1. Inheritance 模式中,具有接口一致性 是必不可少的,子类可以重新实现方法或属性,但它们不能改变调用方法的方式(参数等...)
    2. 仍在向dependency injection 注册BaseService,但您可能不需要这样做,因为它对您来说看起来像是一个抽象类。

    这可以解决你的问题(运行脚本看看发生了什么) 您基本上需要在每个 derived class 中扩展 static $inject property 并在每个子构造函数中使用解构:

    • 好处:你不需要知道父类有什么依赖。
    • 约束:始终在子类中使用第一个参数(因为rest operator 必须是最后一个)

    function logger(LogService) {
      LogService.log('Hello World');
    }
    
    class BaseService {
      static get $inject() { 
        return ['$q']; 
      }
    
      constructor($q) {
        this.$q = $q;
      }
      
      log() {
        console.log('BaseService.$q: ', typeof this.$q, this.$q.name);
      }
    }
    
    class ExtendedService extends BaseService {
      static get $inject() { 
        return ['$http'].concat(BaseService.$inject); 
      }
    
      constructor($http, ...rest) {
        super(...rest);
        this.$http = $http;
      }
      
      log() {
        super.log();
        console.log('ExtendedService.$http: ', typeof this.$http, this.$http.name);
      }
    }
    
    
    class LogService extends ExtendedService {
      static get $inject() { 
        return ['$log', '$timeout'].concat(ExtendedService.$inject); 
      }
    
      constructor($log, $timeout, ...rest) {
        super(...rest);
        this.$log = $log;
        this.$timeout = $timeout;
      }
      
      log(what) {
        super.log();
        this.$timeout(() => {
          this.$log.log('LogService.log', what);
        }, 1000);
      }
    }
    
    angular
      .module('test', [])
      .service('BaseService', BaseService)
      .service('ExtendedService', ExtendedService)
      .service('LogService', LogService)
      .run(logger)
    ;
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>
    
    <section ng-app="test"></section>

    我还在 babel-plugin-angularjs-annotate 中打开了feature requesthttps://github.com/schmod/babel-plugin-angularjs-annotate/issues/28

    【讨论】:

    • 我使用了static getter,因为properties initializers还不是标准的(如果你使用babel或typescript,你也可以使用它们)。
    • 这太好了,非常感谢!我正在对ng-annotate$inject-property 做更多的阅读,看看我是否可以以某种方式继续自动注入服务而不是输入字符串数组(并使其与参数列表保持同步)。但除此之外,这正是我想要的!
    • 如果可以的话,请您调查后采纳。
    • 我将使用这个解决方案(并且可能会添加类似这样的内容:stackoverflow.com/a/37330930/3967289 以帮助我自动绑定依赖项)
    • 你可以把它链接到这个帖子,这对以后的用户会很有帮助。太好了。
    猜你喜欢
    • 1970-01-01
    • 2014-10-14
    • 2020-07-20
    • 2016-06-10
    • 1970-01-01
    • 2022-12-22
    • 2015-06-23
    • 2021-02-12
    • 2019-12-19
    相关资源
    最近更新 更多