AngularJS中的非单一服务


90

AngularJS在其文档中明确指出“服务是单例”:

AngularJS services are singletons

违反直觉,module.factory也返回Singleton实例。

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


1
假设您可以这样做,对吗?其他Angular开发人员将不会期望注入依赖项的工厂一直在返回新实例。
Mark Rajcok

1
我想这是文档问题。我感到遗憾的是,这种支持没有得到支持,因为现在人们期望所有服务都是Singleton,但是我认为没有理由将它们限制为Singleton。
分心

Answers:


44

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

完成同一件事的更好方法是将工厂用作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可以轻松运行。但是对于并非完全如此典型的情况,这种方法很有意义。
乔什·大卫·米勒

4
@Pedr对不起,我忘记了这一点。这是一个超简单的演示:plnkr.co/edit/Xh6pzd4HDlLRqITWuz8X
Josh David Miller

15
@JoshDavidMiller您能指定为什么/做什么“破坏依赖注入,并且[为什么/做什么]该库的行为笨拙”?
okigan,2014年

77

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

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

更新

考虑以下针对非单一服务的请求。Brian Ford在其中指出:

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

还有他从工厂返回实例的示例:

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

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


谢谢你的例子。因此,没有办法让DI容器满足实例的依赖关系。唯一的方法是让它满足提供者的依赖关系,然后可以将其用于生成实例?
分心

谢谢。我同意这比在服务中使用new更好,但是我认为它仍然不足。为什么依赖于该服务的类应该知道或关心它所提供的服务是否为Singleton?这两种解决方案都无法抽象这个事实,并且我认为应该将DI容器内部的某些内容推到依赖项中。当您创建服务时,我认为允许创建者决定是否将其作为单例或单独实例提供是有害的。
分心

+1非常有帮助。我正在使用这种方法ngInfiniteScroll和自定义搜索服务,因此可以将初始化延迟到某些点击事件之前。第一个答案的JSFiddle使用第二个解决方案进行了更新:jsfiddle.net/gavinfoley/G5ku5
GFoley83

4
为什么使用new运算符不好?我觉得,如果您的目标是非单例,那么使用new是声明性的,并且可以很容易地立即分辨出什么是单例,哪些不是。基于是否要更新对象。
j_walker_dev 2014年

似乎这应该是答案,因为它满足了问题的要求-尤其是“更新的”附录。
lukkea 2015年

20

另一种方法是使用复制服务对象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
});

这是一个小家伙


真整洁!您知道此技巧背后有任何危险吗?毕竟,这只是对对象的angular.extending,所以我想应该没问题。然而,制作一份服务副本听起来有些吓人。
vucalur

9

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

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用他的“更新”附录封装了所有内容。
lukkea 2015年

1
您是说非Singleton服务将是持久性的。并且应该保持状态,kinda似乎恰恰相反。
eran otzap 2015年

2

这是我的非单例服务示例,来自ORM正在进行的工作。在示例中,我展示了一个基础模型(ModelFactory),该模型希望服务('用户','文档')能够继承并潜在地扩展。

在我的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' 
            }
        }
     */
  }
]);

1

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

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);

}

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

  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);
    }

  }

您会看到它提供了注入一些工厂步骤中不可用的特定服务的机会。您总是可以在工厂实例上进行注入,以供所有Model实例使用。

请注意,我必须剥离一些代码,以便可能出现一些上下文错误...如果您需要一个有效的代码示例,请告诉我。

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


好的方法-我想将$ serviceFactory视为npm软件包。如果您愿意,我可以构建它并将您添加为贡献者?
IamStalker

1

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

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

我相信一种解决方案是在服务内提供一个对象,您可以创建和设置新实例。但是,需要明确的是,服务的单个实例很重要,因为可能会多次创建和销毁控制器,但是服务需要持久性。您所寻找的可能不是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)
            }
        }
    }
}]);

0

这是我非常满意的另一种解决方法,特别是与启用了高级优化的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);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.