【问题标题】:Non-Singleton Services in AngularJSAngularJS 中的非单例服务
【发布时间】:2013-05-13 15:03:10
【问题描述】:

AngularJS 在其文档中明确声明服务是单例:

AngularJS services are singletons

与直觉相反,module.factory 也返回一个 Singleton 实例。

鉴于非单例服务的用例很多,实现工厂方法返回服务实例的最佳方法是什么,以便每次声明 ExampleService 依赖项时,它都满足由ExampleService的另一个实例?

【问题讨论】:

  • 假设你能做到这一点,你会吗?其他 Angular 开发人员不会期望依赖注入的工厂一直返回新实例。
  • 我想这是文档的问题。我认为这很遗憾,因为现在人们期望所有服务都是单例,但我认为没有理由将它们限制为单例。

标签: service angularjs singleton instance factory


【解决方案1】:

我不完全确定您要满足什么用例。但是可以让工厂返回对象的实例。您应该能够修改它以满足您的需要。

var ExampleApplication = angular.module('ExampleApplication', []);


ExampleApplication.factory('InstancedService', function(){

    function Instance(name, type){
        this.name = name;
        this.type = type;
    }

    return {
        Instance: Instance
    }

});


ExampleApplication.controller('InstanceController', function($scope, InstancedService){
       var instanceA = new InstancedService.Instance('A','string'),
           instanceB = new InstancedService.Instance('B','object');

           console.log(angular.equals(instanceA, instanceB));

});

JsFiddle

更新

考虑以下对non-singleton services 的请求。 Brian Ford 在其中指出:

所有服务都是单例的想法并没有阻止你 编写可以实例化新对象的单例工厂。

以及他从工厂返回实例的示例:

myApp.factory('myService', function () {
  var MyThing = function () {};
  MyThing.prototype.foo = function () {};
  return {
    getInstance: function () {
      return new MyThing();
    }
  };
});

我还认为他的示例更好,因为您不必在控制器中使用new 关键字。它被封装在服务的getInstance 方法中。

【讨论】:

  • 感谢您的示例。所以没有办法让 DI 容器满足与实例的依赖关系。唯一的方法是让它满足与提供程序的依赖关系,然后可以使用该提供程序来生成实例?
  • 谢谢。我同意它比必须在服务中使用 new 更好,但是我认为它仍然不足。为什么依赖于服务的类应该知道或关心它所提供的服务是或不是单例?这两种解决方案都未能抽象出这一事实,并且正在将我认为应该在 DI 容器内部的东西推入依赖项。当您创建服务时,我知道允许创建者决定是否希望将其作为单例或单独实例提供是有害的。
  • +1 非常有帮助。我将这种方法与ngInfiniteScroll 和自定义搜索服务一起使用,因此我可以将初始化延迟到一些点击事件。第一个答案的 JSFiddle 用第二个解决方案更新:jsfiddle.net/gavinfoley/G5ku5
  • 为什么使用 new 运算符不好?我觉得如果您的目标是非单例,那么使用new 是声明性的,并且很容易立即分辨出哪些服务是单例,哪些不是。基于是否正在更新对象。
  • 似乎这应该是答案,因为它提供了问题所要求的内容 - 尤其是“更新”附录。
【解决方案2】:

我认为我们不应该让工厂返回 newable 函数,因为这开始破坏依赖注入,并且库的行为会很笨拙,尤其是对于第三方而言。简而言之,我不确定非单例服务是否有任何合法用例。

完成同样事情的更好方法是使用工厂作为 API 来返回对象集合,并附加了 getter 和 setter 方法。下面是一些伪代码,展示了如何使用这种服务:

.controller( 'MainCtrl', function ( $scope, widgetService ) {
  $scope.onSearchFormSubmission = function () {
    widgetService.findById( $scope.searchById ).then(function ( widget ) {
      // this is a returned object, complete with all the getter/setters
      $scope.widget = widget;
    });
  };

  $scope.onWidgetSave = function () {
    // this method persists the widget object
    $scope.widget.$save();
  };
});

这只是通过 ID 查找小部件然后能够保存对记录所做的更改的伪代码。

以下是该服务的一些伪代码:

.factory( 'widgetService', function ( $http ) {

  function Widget( json ) {
    angular.extend( this, json );
  }

  Widget.prototype = {
    $save: function () {
      // TODO: strip irrelevant fields
      var scrubbedObject = //...
      return $http.put( '/widgets/'+this.id, scrubbedObject );
    }
  };

  function getWidgetById ( id ) {
    return $http( '/widgets/'+id ).then(function ( json ) {
      return new Widget( json );
    });
  }


  // the public widget API
  return {
    // ...
    findById: getWidgetById
    // ...
  };
});

虽然没有包含在本示例中,但这些灵活的服务也可以轻松管理状态。


我现在没有时间,但如果有帮助,我可以稍后组装一个简单的 Plunker 来演示。

【讨论】:

  • 这真的很有趣。一个例子真的很有帮助。非常感谢。
  • 这很有趣。看起来它的功能类似于角度$resource
  • @JonathanPalumbo 你是对的 - 非常类似于 ngResource。事实上,Pedr 和我在另一个问题上开始了这个讨论,我建议采用类似于 ngResource 的方法。对于这样简单的示例,手动执行它没有任何优势 - ngResource 或 Restangular 可以正常工作。但是对于不那么典型的情况,这种方法是有意义的。
  • @Pedr 对不起,我忘记了。这是一个超级简单的演示:plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
  • @JoshDavidMiller 能否具体说明为什么/什么会“破坏依赖注入以及 [为什么/什么] 库会表现得笨拙”?
【解决方案3】:

另一种方法是使用angular.extend() 复制服务对象。

app.factory('Person', function(){
  return {
    greet: function() { return "Hello, I'm " + this.name; },
    copy: function(name) { return angular.extend({name: name}, this); }
  };
});

然后,例如,在您的控制器中

app.controller('MainCtrl', function ($scope, Person) {
  michael = Person.copy('Michael');
  peter = Person.copy('Peter');

  michael.greet(); // Hello I'm Michael
  peter.greet(); // Hello I'm Peter
});

这是plunk

【讨论】:

  • 真的很整洁!你知道这个技巧背后有什么危险吗?毕竟,它只是 angular.extend'ing 一个对象,所以我想我们应该没问题。然而,制作数十个服务副本听起来有点吓人。
【解决方案4】:

我知道这篇文章已经得到了答复,但我仍然认为在某些合法的情况下您需要使用非单例服务。假设有一些可重用的业务逻辑可以在多个控制器之间共享。在这种情况下,放置逻辑的最佳位置是服务,但是如果我们需要在可重用逻辑中保留一些状态怎么办?然后我们需要非单例服务,以便可以在应用程序中的不同控制器之间共享。这就是我实现这些服务的方式:

angular.module('app', [])
    .factory('nonSingletonService', function(){

        var instance = function (name, type){
            this.name = name;
            this.type = type;
            return this;
        }

        return instance;
    })
    .controller('myController', ['$scope', 'nonSingletonService', function($scope, nonSingletonService){
       var instanceA = new nonSingletonService('A','string');
       var instanceB = new nonSingletonService('B','object');

       console.log(angular.equals(instanceA, instanceB));

    }]);

【讨论】:

  • 这与 Jonathan Palumbo 的回答非常相似,只是 Jonathan 用他的“更新”附录封装了所有内容。
  • 你是说非单例服务将是持久的。并且应该保持状态,有点像相反的方式。
【解决方案5】:

这是我的非单例服务示例,它来自我正在研究的 ORM。在示例中,我展示了一个基础模型 (ModelFactory),我希望 services('users','documents') 继承和潜在扩展。

在我的 ORM 中,ModelFactory 注入其他服务以提供额外的功能(查询、持久性、模式映射),这些功能使用模块系统进行沙盒化。

在示例中,用户和文档服务具有相同的功能,但具有各自独立的范围。

/*
    A class which which we want to have multiple instances of, 
    it has two attrs schema, and classname
 */
var ModelFactory;

ModelFactory = function($injector) {
  this.schema = {};
  this.className = "";
};

Model.prototype.klass = function() {
  return {
    className: this.className,
    schema: this.schema
  };
};

Model.prototype.register = function(className, schema) {
  this.className = className;
  this.schema = schema;
};

angular.module('model', []).factory('ModelFactory', [
  '$injector', function($injector) {
    return function() {
      return $injector.instantiate(ModelFactory);
    };
  }
]);


/*
    Creating multiple instances of ModelFactory
 */

angular.module('models', []).service('userService', [
  'ModelFactory', function(modelFactory) {
    var instance;
    instance = new modelFactory();
    instance.register("User", {
      name: 'String',
      username: 'String',
      password: 'String',
      email: 'String'
    });
    return instance;
  }
]).service('documentService', [
  'ModelFactory', function(modelFactory) {
    var instance;
    instance = new modelFactory();
    instance.register("Document", {
      name: 'String',
      format: 'String',
      fileSize: 'String'
    });
    return instance;
  }
]);


/*
    Example Usage
 */

angular.module('controllers', []).controller('exampleController', [
  '$scope', 'userService', 'documentService', function($scope, userService, documentService) {
    userService.klass();

    /*
        returns 
        {
            className: "User"
            schema: {
                name : 'String'
                username : 'String'
                password: 'String'
                email: 'String'     
            }
        }
     */
    return documentService.klass();

    /*
        returns 
        {
            className: "User"
            schema: {
                name : 'String'
                format : 'String'
                formatileSize: 'String' 
            }
        }
     */
  }
]);

【讨论】:

    【解决方案6】:

    angular 仅提供 singleton 服务/工厂选项。 解决它的一种方法是拥有一个工厂服务,它将在您的控制器或其他消费者实例中为您构建一个新实例。 唯一注入的是创建新实例的类。 这是注入其他依赖项或根据用户规范初始化新对象(添加服务或配置)的好地方

    namespace admin.factories {
      'use strict';
    
      export interface IModelFactory {
        build($log: ng.ILogService, connection: string, collection: string, service: admin.services.ICollectionService): IModel;
      }
    
      class ModelFactory implements IModelFactory {
     // any injection of services can happen here on the factory constructor...
     // I didnt implement a constructor but you can have it contain a $log for example and save the injection from the build funtion.
    
        build($log: ng.ILogService, connection: string, collection: string, service: admin.services.ICollectionService): IModel {
          return new Model($log, connection, collection, service);
        }
      }
    
      export interface IModel {
        // query(connection: string, collection: string): ng.IPromise<any>;
      }
    
      class Model implements IModel {
    
        constructor(
          private $log: ng.ILogService,
          private connection: string,
          private collection: string,
          service: admin.services.ICollectionService) {
        };
    
      }
    
      angular.module('admin')
        .service('admin.services.ModelFactory', ModelFactory);
    
    }
    

    然后在您的消费者实例中,您需要工厂服务并在需要时调用工厂上的构建方法以获取新实例

      class CollectionController  {
        public model: admin.factories.IModel;
    
        static $inject = ['$log', '$routeParams', 'admin.services.Collection', 'admin.services.ModelFactory'];
        constructor(
          private $log: ng.ILogService,
          $routeParams: ICollectionParams,
          private service: admin.services.ICollectionService,
          factory: admin.factories.IModelFactory) {
    
          this.connection = $routeParams.connection;
          this.collection = $routeParams.collection;
    
          this.model = factory.build(this.$log, this.connection, this.collection, this.service);
        }
    
      }
    

    您可以看到它提供了注入一些在工厂步骤中不可用的特定服务的操作。 您始终可以在工厂实例上进行注入,以供所有模型实例使用。

    注意我必须去掉一些代码,这样我可能会犯一些上下文错误...... 如果您需要有效的代码示例,请告诉我。

    我相信 NG2 可以选择在 DOM 中的正确位置注入新的服务实例,因此您无需构建自己的工厂实现。将不得不拭目以待:)

    【讨论】:

    • 不错的方法 - 我希望将 $serviceFactory 视为一个 npm 包。如果你喜欢我可以构建它并将你添加为贡献者?
    【解决方案7】:

    我相信有充分的理由在服务中创建对象的新实例。我们也应该保持开放的心态,而不是仅仅说我们不应该做这样的事情,但是单身人士这样做是有原因的。控制器通常在应用程序的生命周期内创建和销毁,但服务必须是持久的。

    我可以想到一个用例,您有某种工作流程,例如接受付款并且您设置了多个属性,但现在必须更改其付款类型,因为客户的信用卡失败并且他们需要提供不同的付款方式。当然,这确实与您创建应用程序的方式有很大关系。您可以重置支付对象的所有属性,或者您可以在服务中创建对象的新实例。但是,您不会想要新的服务实例,也不会想要刷新页面。

    我相信解决方案是在服务中提供一个对象,您可以创建一个新实例并对其进行设置。但是,需要明确的是,服务的单个实例很重要,因为控制器可能会被创建和销毁多次,但服务需要持久性。您正在寻找的可能不是 Angular 中的直接方法,而是您可以在服务中管理的对象模式。

    例如,我制作了一个重置按钮。 (这没有经过测试,它实际上只是一个在服务中创建新对象的用例的快速想法。

    app.controller("PaymentController", ['$scope','PaymentService',function($scope, PaymentService) {
        $scope.utility = {
            reset: PaymentService.payment.reset()
        };
    }]);
    app.factory("PaymentService", ['$http', function ($http) {
        var paymentURL = "https://www.paymentserviceprovider.com/servicename/token/"
        function PaymentObject(){
            // this.user = new User();
            /** Credit Card*/
            // this.paymentMethod = ""; 
            //...
        }
        var payment = {
            options: ["Cash", "Check", "Existing Credit Card", "New Credit Card"],
            paymentMethod: new PaymentObject(),
            getService: function(success, fail){
                var request = $http({
                        method: "get",
                        url: paymentURL
                    }
                );
                return ( request.then(success, fail) );
    
            }
            //...
        }
        return {
            payment: {
                reset: function(){
                    payment.paymentMethod = new PaymentObject();
                },
                request: function(success, fail){
                    return payment.getService(success, fail)
                }
            }
        }
    }]);
    

    【讨论】:

      【解决方案8】:

      这是我非常满意的另一种解决问题的方法,特别是与启用高级优化的 Closure Compiler 结合使用时:

      var MyFactory = function(arg1, arg2) {
          this.arg1 = arg1;
          this.arg2 = arg2;
      };
      
      MyFactory.prototype.foo = function() {
          console.log(this.arg1, this.arg2);
      
          // You have static access to other injected services/factories.
          console.log(MyFactory.OtherService1.foo());
          console.log(MyFactory.OtherService2.foo());
      };
      
      MyFactory.factory = function(OtherService1, OtherService2) {
          MyFactory.OtherService1_ = OtherService1;
          MyFactory.OtherService2_ = OtherService2;
          return MyFactory;
      };
      
      MyFactory.create = function(arg1, arg2) {
          return new MyFactory(arg1, arg2);
      };
      
      // Using MyFactory.
      MyCtrl = function(MyFactory) {
          var instance = MyFactory.create('bar1', 'bar2');
          instance.foo();
      
          // Outputs "bar1", "bar2" to console, plus whatever static services do.
      };
      
      angular.module('app', [])
          .factory('MyFactory', MyFactory)
          .controller('MyCtrl', MyCtrl);
      

      【讨论】:

        猜你喜欢
        • 2018-08-07
        • 2014-02-25
        • 1970-01-01
        • 2016-12-30
        • 2021-02-13
        • 1970-01-01
        • 2023-03-20
        • 1970-01-01
        • 2019-06-21
        相关资源
        最近更新 更多