AngularJS:服务vs提供者vs工厂

3418
在AngularJS中,ServiceProviderFactory有什么区别?

249
我发现对于初学者而言,所有Angular术语都有些令人望而生畏。我们开始使用这份速成表格,使我们的程序员在学习Angular时更易于理解:http://demisx.github.io/angularjs/2014/09/14/angular-what-goes-where.html。希望这能对你的团队有所帮助。 - demisx
7
在我看来,理解这两者之间的区别最好的方法是使用Angular的官方文档:https://docs.angularjs.org/guide/providers ,该文档非常详细地解释了这个问题,并使用一个独特的例子帮助你理解。 - Rafael Merlin
3
感谢您,@Blaise!根据我在帖子中的评论,我故意省略了它,因为从我的经验来看,99%的用例都可以通过 service.factory 成功处理。我不想进一步复杂化这个主题。 - demisx
3
我认为这个讨论也非常有用 https://dev59.com/22Mk5IYBdhLWcg3w0RGU - Anand Gupta
3
这里有一些关于如何使用servicesfactoriesproviders的好答案。 - Mistalis
1
我认为这个话题的混淆是因为AngularJS在这个地方过度设计并且文档不够好。正如@demisx所提到的,似乎没有令人信服的用例需要使用服务而不是工厂,它们都是在提供程序之上的语法糖(与其他提供程序不同,可以在单例实例化之前在'module.config'中进行配置)。 - Mihail Kostira
30个回答

2911

从 AngularJS 邮件列表中我得到了一个惊人的帖子,其中解释了服务、工厂和提供者以及它们的注入用法。总结如下:

服务(Services)

语法:module.service('serviceName', function);
结果:当将 serviceName 声明为可注入参数时,您将获得该函数的实例(换句话说,new FunctionYouPassedToService()

工厂(Factories)

语法:module.factory('factoryName', function);
结果:当将 factoryName 声明为可注入参数时,您将获得通过调用传递给 module.factory 的函数引用所返回的值

提供者(Providers)

语法:module.provider('providerName', function);
结果:当将 providerName 声明为可注入参数时,您将获得(new ProviderFunction()).$get()。在调用 $get 方法之前实例化构造函数 - ProviderFunction是传递给 module.provider 的函数引用。

提供者的优点是它们可以在模块配置阶段进行配置。

这里查看提供的代码。

以下是 Misko 的更好解释:

provide.value('a', 123);

function Controller(a) {
  expect(a).toEqual(123);
}

在这种情况下,注入器只是简单地返回值本身。但是如果你想计算该值呢?那么就要使用工厂

provide.factory('b', function(a) {
  return a*2;
});

function Controller(b) {
  expect(b).toEqual(246);
}

因此,factory是一个负责创建值的函数。请注意,工厂函数可以请求其他依赖项。

但是,如果您想更加面向对象,并拥有一个名为Greeter的类呢?

function Greeter(a) {
  this.greet = function() {
    return 'Hello ' + a;
  }
}

那么要实例化,你需要编写

provide.factory('greeter', function(a) {
  return new Greeter(a);
});

那么我们可以像这样在控制器中请求“greeter”:

function Controller(greeter) {
  expect(greeter instanceof Greeter).toBe(true);
  expect(greeter.greet()).toEqual('Hello 123');
}

但是这样太啰嗦了。一个更简洁的写法是provider.service('greeter', Greeter);

但是如果我们想在注入之前配置Greeter类呢?那么我们可以这样写:

provide.provider('greeter2', function() {
  var salutation = 'Hello';
  this.setSalutation = function(s) {
    salutation = s;
  }

  function Greeter(a) {
    this.greet = function() {
      return salutation + ' ' + a;
    }
  }

  this.$get = function(a) {
    return new Greeter(a);
  };
});

然后我们可以这样做:

angular.module('abc', []).config(function(greeter2Provider) {
  greeter2Provider.setSalutation('Halo');
});

function Controller(greeter2) {
  expect(greeter2.greet()).toEqual('Halo 123');
}

顺带一提,servicefactoryvalue都是从provider派生而来。

provider.service = function(name, Class) {
  provider.provide(name, function() {
    this.$get = function($injector) {
      return $injector.instantiate(Class);
    };
  });
}

provider.factory = function(name, factory) {
  provider.provide(name, function() {
    this.$get = function($injector) {
      return $injector.invoke(factory);
    };
  });
}

provider.value = function(name, value) {
  provider.factory(name, function() {
    return value;
  });
};

58
请参考 https://dev59.com/XWYr5IYBdhLWcg3wVYwg#13763886,该链接讨论了service和factory之间的区别。 - Mark Rajcok
3
在第611次编辑中,我添加了角度常数和值的用法,以展示与已显示内容的其他差异。http://jsbin.com/ohamub/611/edit - Nick
17
虽然通过创建函数实例来调用服务,但它实际上只在注入器中创建一次,就像单例模式一样。http://docs.angularjs.org/guide/dev_guide.services.creating_services - angelokh
33
如果使用一个清晰的实际例子,这个示例可能会更加令人惊叹。我在尝试理解toEqualgreeter.Greet的意义时感到困惑。为什么不使用更真实、更相关的内容? - Kyle Pennell
5
使用expect()函数来解释某个内容是一个不好的选择。下次请使用真实世界的代码进行解释。 - Craig
显示剩余6条评论

818

JS Fiddle演示

使用factory/service/provider的“Hello world”示例:

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

//service style, probably the simplest one
myApp.service('helloWorldFromService', function() {
    this.sayHello = function() {
        return "Hello, World!";
    };
});

//factory style, more involved but more sophisticated
myApp.factory('helloWorldFromFactory', function() {
    return {
        sayHello: function() {
            return "Hello, World!";
        }
    };
});
    
//provider style, full blown, configurable version     
myApp.provider('helloWorld', function() {

    this.name = 'Default';

    this.$get = function() {
        var name = this.name;
        return {
            sayHello: function() {
                return "Hello, " + name + "!";
            }
        }
    };

    this.setName = function(name) {
        this.name = name;
    };
});

//hey, we can configure a provider!            
myApp.config(function(helloWorldProvider){
    helloWorldProvider.setName('World');
});
        

function MyCtrl($scope, helloWorld, helloWorldFromFactory, helloWorldFromService) {
    
    $scope.hellos = [
        helloWorld.sayHello(),
        helloWorldFromFactory.sayHello(),
        helloWorldFromService.sayHello()];
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="myApp">
<div ng-controller="MyCtrl">
    {{hellos}}
</div>
</body>


2
“this” 在 $get 函数中会改变上下文吗?- 在该函数中,您不再引用已实例化的提供程序。 - Nate-Wilkins
12
@Nate:实际上,this不会改变上下文,因为调用的是new Provider().$get(),其中Provider是传递给app.provider的函数。也就是说,$get()作为构建的Provider对象的方法被调用,所以this将指向Provider,正如示例所示。 - Brandon
1
@Brandon 哦,那还挺不错的。乍一看有点困惑 - 感谢你的澄清! - Nate-Wilkins
1
请问您能否解释一下在提供程序中使用的 $get 吗? - Arham Ali Qureshi
3
当我在本地运行时,为什么会出现“Unknown provider: helloWorldProvider <- helloWorld”的错误?将其注释掉后,其他两个示例也会出现相同的错误。是否存在一些隐藏的提供者配置?(Angular 1.0.8)-- 发现:https://dev59.com/yWct5IYBdhLWcg3wHJ3F - Antoine
4
@Antoine遇到"Unknown provide: helloWorldProvider"错误的原因是你在.config代码中使用了'helloWorldProvider',但是在myApp.provider('helloWorld', function())中定义provider时使用了'helloWorld'。换句话说,在你的config代码中,AngularJS是如何知道你正在引用helloWorld provider的呢?谢谢。 - jmtoung

661

简而言之:

1) 当你使用一个工厂时,你创建一个对象,向其中添加属性,然后返回该对象。当你将这个工厂传递给你的控制器时,该对象上的属性现在可以通过你的工厂在该控制器中使用。

app.controller(‘myFactoryCtrl’, function($scope, myFactory){
  $scope.artist = myFactory.getArtist();
});

app.factory(‘myFactory’, function(){
  var _artist = ‘Shakira’;
  var service = {};

  service.getArtist = function(){
    return _artist;
  }

  return service;
});


2) 使用 Service 时,AngularJS 在幕后使用“new”关键字实例化它。因此,您将向“this”添加属性,服务将返回“this”。当您将服务传递到控制器中时,通过您的服务,“this”上的那些属性现在将在该控制器上可用。

app.controller(‘myServiceCtrl’, function($scope, myService){
  $scope.artist = myService.getArtist();
});

app.service(‘myService’, function(){
  var _artist = ‘Nelly’;
  this.getArtist = function(){
    return _artist;
  }
});



3) 提供者(Providers)是您可以传递到.config()函数中的唯一服务。当您希望在将服务对象提供给用户之前为整个模块提供配置时,请使用提供者。

app.controller(‘myProvider’, function($scope, myProvider){
  $scope.artist = myProvider.getArtist();
  $scope.data.thingFromConfig = myProvider.thingOnConfig;
});

app.provider(‘myProvider’, function(){
 //Only the next two lines are available in the app.config()
 this._artist = ‘’;
 this.thingFromConfig = ‘’;
  this.$get = function(){
    var that = this;
    return {
      getArtist: function(){
        return that._artist;
      },
      thingOnConfig: that.thingFromConfig
    }
  }
});

app.config(function(myProviderProvider){
  myProviderProvider.thingFromConfig = ‘This was set in config’;
});



非 TL;DR 版本:

1)工厂
工厂是创建和配置服务的最流行方式。除了 TL;DR 中提到的,没有太多需要说明的。你只需创建一个对象,添加属性,然后返回该对象即可。然后将工厂传递到控制器中时,该对象上的这些属性现在可以通过工厂在控制器中使用。下面是一个更详细的示例。

app.factory(‘myFactory’, function(){
  var service = {};
  return service;
});

现在,我们将任何属性附加到“service”上时,当我们将“myFactory”传递给控制器时,这些属性都将对我们可用。

现在,让我们向回调函数添加一些“私有”变量。这些变量不会直接从控制器中访问,但是我们最终将设置一些getter/setter方法在“service”上,以便在需要时更改这些“私有”变量。

app.factory(‘myFactory’, function($http, $q){
  var service = {};
  var baseUrl = ‘https://itunes.apple.com/search?term=’;
  var _artist = ‘’;
  var _finalUrl = ‘’;

  var makeUrl = function(){
   _artist = _artist.split(‘ ‘).join(‘+’);
    _finalUrl = baseUrl + _artist + ‘&callback=JSON_CALLBACK’;
    return _finalUrl
  }

  return service;
});

在这里,您会注意到我们没有将那些变量/函数附加到“service”上。我们只是创建它们以便稍后使用或修改它们。

  • baseUrl是iTunes API需要的基本URL
  • _artist是我们想要查找的艺术家
  • _finalUrl是最终构建的完整URL,我们将调用iTunes
  • makeUrl是一个函数,将创建并返回我们的iTunes友好的URL。

现在我们的帮助器/私有变量和函数已经就位,让我们向“service”对象添加一些属性。我们放在“service”上的任何东西都可以直接在我们传递“myFactory”的任何控制器中使用。

我们将创建setArtist和getArtist方法,它们仅返回或设置艺术家。我们还将创建一个方法,该方法将使用我们创建的URL调用iTunes API。此方法将返回一个promise,一旦数据从iTunes API返回,该promise将得到满足。如果您在使用AngularJS中的promise方面没有太多经验,我强烈建议深入研究它们。

下面的setArtist接受一个艺术家,并允许您设置艺术家。getArtist返回艺术家。callItunes首先调用makeUrl()以构建我们将与$http请求一起使用的URL。然后它设置了一个promise对象,使用我们的最终url进行了$ http请求,因为$http返回一个promise,所以我们能够在请求之后调用.success或.error。然后,我们使用iTunes数据解析我们的promise,或者使用消息“出现错误”拒绝它。

app.factory('myFactory', function($http, $q){
  var service = {};
  var baseUrl = 'https://itunes.apple.com/search?term=';
  var _artist = '';
  var _finalUrl = '';

  var makeUrl = function(){
    _artist = _artist.split(' ').join('+');
    _finalUrl = baseUrl + _artist + '&callback=JSON_CALLBACK'
    return _finalUrl;
  }

  service.setArtist = function(artist){
    _artist = artist;
  }

  service.getArtist = function(){
    return _artist;
  }

  service.callItunes = function(){
    makeUrl();
    var deferred = $q.defer();
    $http({
      method: 'JSONP',
      url: _finalUrl
    }).success(function(data){
      deferred.resolve(data);
    }).error(function(){
      deferred.reject('There was an error')
    })
    return deferred.promise;
  }

  return service;
});

现在我们的工厂已经完成。我们现在可以将'myFactory'注入到任何控制器中,然后就能调用我们附加到服务对象的方法(setArtist、getArtist和callItunes)。

app.controller('myFactoryCtrl', function($scope, myFactory){
  $scope.data = {};
  $scope.updateArtist = function(){
    myFactory.setArtist($scope.data.artist);
  };

  $scope.submitArtist = function(){
    myFactory.callItunes()
      .then(function(data){
        $scope.data.artistData = data;
      }, function(data){
        alert(data);
      })
  }
});

以上控制器中,我们注入了‘myFactory’服务。然后我们使用来自‘myFactory’的数据在$scope对象上设置属性。如果您以前没有处理过Promise,那么上面唯一棘手的代码就是。由于callItunes返回Promise,我们能够使用.then()方法,并且仅在我们的Promise用iTunes数据实现时设置$scope.data.artistData。您会注意到我们的控制器非常“瘦”(这是一个很好的编码实践)。所有逻辑和持久化数据都位于我们的服务中,而不是我们的控制器中。

2) 服务
在创建服务时需要知道的最重要的事情之一是它是使用“new”关键字实例化的。对于JavaScript专家,这应该给你提供了一个有关代码性质的重要提示。对于那些具有有限JavaScript背景或对“new”关键字实际执行的操作不太熟悉的人,让我们回顾一些JavaScript基础知识,这些知识最终将帮助我们理解服务的性质。

为了真正看到您使用“new”关键字调用函数时所发生的变化,请让我们创建一个函数并使用“new”关键字调用它,然后让我们展示解释器在看到“new”关键字时做了什么。最终结果将是相同的。

首先让我们创建我们的构造函数。

var Person = function(name, age){
  this.name = name;
  this.age = age;
}

这是一个典型的JavaScript构造函数。现在,每当我们使用“new”关键字调用Person函数时,“this”将绑定到新创建的对象。

现在让我们在Person原型上添加一个方法,以便它可用于我们Person“类”的每个实例。

Person.prototype.sayName = function(){
  alert(‘My name is ‘ + this.name);
}

现在,因为我们将sayName函数放在原型上,每个Person的实例都可以调用sayName函数以便提醒该实例的名称。

既然我们已经有了Person构造函数和它原型上的sayName函数,让我们创建一个Person实例并调用sayName函数。

var tyler = new Person(‘Tyler’, 23);
tyler.sayName(); //alerts ‘My name is Tyler’

所以整个创建Person构造函数、将一个函数添加到它的原型中、创建Person实例,然后在它的原型上调用该函数的代码看起来像这样。

var Person = function(name, age){
  this.name = name;
  this.age = age;
}
Person.prototype.sayName = function(){
  alert(‘My name is ‘ + this.name);
}
var tyler = new Person(‘Tyler’, 23);
tyler.sayName(); //alerts ‘My name is Tyler’

现在让我们看看在JavaScript中使用“new”关键字时实际发生了什么。你首先应该注意到的是,在我们的示例中使用“new”后,我们能够像调用对象一样调用“tyler”的方法(sayName)——因为它确实是一个对象。

因此,首先,我们知道我们的Person构造函数正在返回一个对象,无论我们是否在代码中看到它。其次,我们知道,因为我们的sayName函数位于原型上而不是直接位于Person实例上,所以Person函数返回的对象必须在查找失败时委派给其原型。更简单地说,当我们调用tyler.sayName()时,解释器会说:“好的,我将查找刚刚创建的‘tyler’对象上的sayName函数,然后调用它。等等,我在这里没有看到它——我只看到名字和年龄,让我检查一下原型。是的,看起来它在原型上,让我调用它。”

下面是您可以考虑JavaScript中“new”关键字实际上在做什么的代码。它基本上是上面段落的代码示例。我放置了“解释器视图”或解释器看到代码的方式的注释。

var Person = function(name, age){
  //The below line creates an object(obj) that will delegate to the person’s prototype on failed lookups.
  //var obj = Object.create(Person.prototype);

  //The line directly below this sets ‘this’ to the newly created object
  //this = obj;

  this.name = name;
  this.age = age;

  //return this;
}

现在了解了JavaScript中'new'关键字的实际作用后,创建AngularJS服务应该更容易理解了。

创建服务时需要注意的最重要的一点是知道服务是使用'new'关键字实例化的。结合上面的示例,你现在应该认识到,你将直接将属性和方法附加到'this'中,然后从服务本身返回它。让我们看看这个过程。

与我们最初在工厂示例中所做的不同,我们不需要创建一个对象,然后返回该对象,因为正如之前多次提到的那样,我们使用了'new'关键字,因此解释器将创建该对象,使其委托给其原型,然后在无需我们进行工作的情况下将其返回给我们。

首先,让我们创建我们的‘私有’和帮助函数。这应该看起来非常熟悉,因为我们在工厂中做了完全相同的事情。我不会在这里解释每行代码的含义,因为我在工厂示例中已经这样做了,如果您感到困惑,请重新阅读工厂示例。

app.service('myService', function($http, $q){
  var baseUrl = 'https://itunes.apple.com/search?term=';
  var _artist = '';
  var _finalUrl = '';

  var makeUrl = function(){
    _artist = _artist.split(' ').join('+');
    _finalUrl = baseUrl + _artist + '&callback=JSON_CALLBACK'
    return _finalUrl;
  }
});

现在,我们将把所有可用于控制器的方法附加到'this'。

app.service('myService', function($http, $q){
  var baseUrl = 'https://itunes.apple.com/search?term=';
  var _artist = '';
  var _finalUrl = '';

  var makeUrl = function(){
    _artist = _artist.split(' ').join('+');
    _finalUrl = baseUrl + _artist + '&callback=JSON_CALLBACK'
    return _finalUrl;
  }

  this.setArtist = function(artist){
    _artist = artist;
  }

  this.getArtist = function(){
    return _artist;
  }

  this.callItunes = function(){
    makeUrl();
    var deferred = $q.defer();
    $http({
      method: 'JSONP',
      url: _finalUrl
    }).success(function(data){
      deferred.resolve(data);
    }).error(function(){
      deferred.reject('There was an error')
    })
    return deferred.promise;
  }

});

现在,就像我们的工厂一样,setArtist、getArtist和callItunes将在我们将myService传递到的任何控制器中都可用。以下是myService控制器(与我们的工厂控制器几乎完全相同)。

app.controller('myServiceCtrl', function($scope, myService){
  $scope.data = {};
  $scope.updateArtist = function(){
    myService.setArtist($scope.data.artist);
  };

  $scope.submitArtist = function(){
    myService.callItunes()
      .then(function(data){
        $scope.data.artistData = data;
      }, function(data){
        alert(data);
      })
  }
});

就像我之前提到的一样,一旦你真正理解了“new”的含义,服务(Services)在AngularJS中几乎与工厂(Factories)相同。

3) 提供者(Provider)

关于提供者,最重要的事情是它们是唯一可以传递到应用程序配置(app.config)部分的服务。如果您需要修改服务对象的某个部分,以便在应用程序的其他地方可用之前进行更改,则这非常重要。虽然与服务和工厂非常相似,但有一些区别,我们将进行讨论。

首先,我们以与服务和工厂类似的方式设置提供者。下面的变量是我们的“私有”变量和辅助函数。

app.provider('myProvider', function(){
   var baseUrl = 'https://itunes.apple.com/search?term=';
  var _artist = '';
  var _finalUrl = '';

  //Going to set this property on the config function below.
  this.thingFromConfig = ‘’;

  var makeUrl = function(){
    _artist = _artist.split(' ').join('+');
    _finalUrl = baseUrl + _artist + '&callback=JSON_CALLBACK'
    return _finalUrl;
  }
}

*如果以上代码的任何部分令人困惑,请查看工厂部分,我会更详细地解释所有内容。

您可以将提供程序视为具有三个部分。第一部分是稍后将被修改/设置的“私有”变量/函数(如上所示)。第二部分是在您的app.config函数中可用并因此可在任何其他地方之前进行更改的变量/函数(也显示在上面)。重要的是注意这些变量需要附加到“this”关键字。在我们的示例中,只有“thingFromConfig”可在app.config中进行更改。第三部分(如下所示)是所有变量/函数,在将“myProvider”服务传递到特定控制器时,这些变量/函数将在该控制器中可用。

使用Provider创建服务时,在您的控制器中可用的属性/方法是从$get()函数返回的那些属性/方法。下面的代码在'this'上放置了$get(我们知道它最终将从该函数返回)。现在,$get函数返回我们希望在控制器中可用的所有方法/属性。这是一个代码示例。

this.$get = function($http, $q){
    return {
      callItunes: function(){
        makeUrl();
        var deferred = $q.defer();
        $http({
          method: 'JSONP',
          url: _finalUrl
        }).success(function(data){
          deferred.resolve(data);
        }).error(function(){
          deferred.reject('There was an error')
        })
        return deferred.promise;
      },
      setArtist: function(artist){
        _artist = artist;
      },
      getArtist: function(){
        return _artist;
      },
      thingOnConfig: this.thingFromConfig
    }
  }

现在完整的提供程序代码看起来像这样

app.provider('myProvider', function(){
  var baseUrl = 'https://itunes.apple.com/search?term=';
  var _artist = '';
  var _finalUrl = '';

  //Going to set this property on the config function below
  this.thingFromConfig = '';

  var makeUrl = function(){
    _artist = _artist.split(' ').join('+');
    _finalUrl = baseUrl + _artist + '&callback=JSON_CALLBACK'
    return _finalUrl;
  }

  this.$get = function($http, $q){
    return {
      callItunes: function(){
        makeUrl();
        var deferred = $q.defer();
        $http({
          method: 'JSONP',
          url: _finalUrl
        }).success(function(data){
          deferred.resolve(data);
        }).error(function(){
          deferred.reject('There was an error')
        })
        return deferred.promise;
      },
      setArtist: function(artist){
        _artist = artist;
      },
      getArtist: function(){
        return _artist;
      },
      thingOnConfig: this.thingFromConfig
    }
  }
});

现在,就像在我们的工厂和服务中一样,只要我们将myProvider传递给任何控制器,setArtist、getArtist和callItunes都将可用。这是myProvider控制器(几乎与我们的工厂/服务控制器完全相同)。

app.controller('myProviderCtrl', function($scope, myProvider){
  $scope.data = {};
  $scope.updateArtist = function(){
    myProvider.setArtist($scope.data.artist);
  };

  $scope.submitArtist = function(){
    myProvider.callItunes()
      .then(function(data){
        $scope.data.artistData = data;
      }, function(data){
        alert(data);
      })
  }

  $scope.data.thingFromConfig = myProvider.thingOnConfig;
});

正如之前提到的,使用Provider创建服务的整个目的是在将最终对象传递给应用程序的其余部分之前,能够通过app.config函数修改某些变量。让我们看一个例子。

app.config(function(myProviderProvider){
  //Providers are the only service you can pass into app.config
  myProviderProvider.thingFromConfig = 'This sentence was set in app.config. Providers are the only service that can be passed into config. Check out the code to see how it works';
});

现在你可以看到,'thingFromConfig'在我们的提供程序中是一个空字符串,但当它显示在DOM中时,它将是'This sentence was set...'。


11
这篇优秀的文章唯一缺失的部分是相对于工厂使用该服务的优势,这在Lior所接受的回答中得到了明确解释。 - infinity
2
虽然可能不是很重要,但这里有一位博主对Angular提出了质疑,并且不喜欢providerProvider。http://codeofrob.com/entries/you-have-ruined-javascript.html - barlop
1
很棒的文章,但我还是有点困惑:$scope是一个服务,对吗?您说控制器本身应该很薄,不应拥有太多变量或方法。但是当我在控制器中向$scope添加一个方法,例如在$scope.myFunction() = function(){...}中,那就不同了,对吗?我只是真的不明白为什么我不会将方法定义或变量附加到$scope(在我的控制器中),而是将方法作为自定义服务或工厂的一部分。请告诉我。谢谢 - stackPusher
这是什么?app.service('myFactory', function () { var service = {}; return service; });(这是我经常使用的语法) - Alex 75
从你的博客中得到启发,非常清晰易懂,谢谢。在你的 Provider 示例中有一个小错别字,在最后一段代码中:getArtist = function () { return _artist; },,这不应该是一个冒号吗?(在你的博客上,已经有人修复了这个问题)。 - Rutwick Gangurde

516

所有服务都是单例;它们每个应用程序只实例化一次。可以是任何类型,无论是原始类型、对象字面量、函数,甚至是自定义类型的实例。

valuefactoryserviceconstantprovider 方法都是提供者。它们教导注入器如何实例化服务。

最冗长,但也是最全面的是 Provider 配方。 其余四种配方——Value、Factory、Service 和 Constant —— 只是在提供者配方上加了些语法糖。

  • Value 配方 是最简单的情况,您自己实例化服务并将实例化值提供给注入器。
  • Factory 配方 向注入器提供一个工厂函数,需要实例化服务时注入器会调用该函数。调用时,该工厂函数创建并返回服务实例。服务的依赖项作为函数的参数注入。因此使用此配方添加了以下功能:
    • 能够使用其他服务(具有依赖项)
    • 服务初始化
    • 延迟/懒惰初始化
  • Service 配方 几乎与 Factory 配方相同,但这里注入器使用 new 操作符调用构造函数而不是工厂函数。
  • Provider 配方 通常过于复杂。它通过允许您配置工厂的创建来添加一层间接性。

    只有在想要在应用程序启动之前对整个应用程序进行配置的情况下才应使用 Provider 配方。这通常只对行为可能在应用程序之间略有差异的可重用服务感兴趣。

  • 常量配方(Constant recipe)就像是值配方(Value recipe),但是它允许你在配置(config)阶段定义可用的服务。与使用Value配方创建的服务相比,它们更早可用。不像Values,它们不能使用装饰器(decorator)进行修饰。
  • 请参见提供者文档(provider documentation)

    2
    您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Matt
    2
    @Matt,是的,如果您已经有自己想要公开为服务的函数,则服务是一种简洁的方式。从文档中可以看到:myApp.factory('unicornLauncher', ["apiToken", function(apiToken) { return new UnicornLauncher(apiToken); }]);与:myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]); - janek
    5
    作为一个新手,我已经谷歌了一段时间有关服务和工厂的区别。我同意这是迄今为止最好的答案!我理解服务是一个服务类(例如编码器/解码器类),可能具有一些私有属性。而工厂则提供一组无状态的辅助方法。 - stanleyxu2005
    3
    其他答案中的示例未能清楚地解释服务和提供程序之间的核心区别,这是在实例化这些代码时注入的内容。 - Ashish Singh

    223

    理解 AngularJS 中 Factory、Service 和 Provider

    这些都用于共享可重复使用的单例对象。它有助于在您的应用程序/各个组件/模块之间共享可重用的代码。

    来自文档Service/Factory

    • 惰性实例化——当应用程序组件依赖于它时,Angular 才会实例化服务/工厂。
    • 单例——每个依赖于服务的组件都获取到由服务工厂生成的单个实例的引用。

    工厂模式

    工厂是一个函数,在创建对象之前可以进行操纵/添加逻辑,然后返回新创建的对象。

    app.factory('MyFactory', function() {
        var serviceObj = {};
        //creating an object with methods/functions or variables
        serviceObj.myFunction = function() {
            //TO DO:
        };
        //return that object
        return serviceObj;
    });
    

    用法

    它可以像类一样只是一系列函数的集合。因此,在将其注入到控制器/工厂/指令函数中时,它可以在不同的控制器中实例化。它每个应用程序只会被实例化一次。

    服务

    简单地说,当您查看服务时,请考虑数组原型。服务是一个使用“new”关键字实例化新对象的函数。您可以使用this关键字向服务对象添加属性和函数。与工厂不同,它不返回任何东西(它返回一个包含方法/属性的对象)。

    app.service('MyService', function() {
        //directly binding events to this context
        this.myServiceFunction = function() {
            //TO DO:
        };
    });
    

    用法

    在整个应用程序中需要共享单个对象时使用它。例如,已认证的用户详细信息、可共享的方法/数据、实用函数等。

    提供者

    提供者用于创建可配置的服务对象。您可以从 config 函数中配置服务设置。它通过使用 $get() 函数返回一个值,$get 函数会在 Angular 的运行阶段执行。

    app.provider('configurableService', function() {
        var name = '';
        //this method can be be available at configuration time inside app.config.
        this.setName = function(newName) {
            name = newName;
        };
        this.$get = function() {
            var getName = function() {
                 return name;
            };
            return {
                getName: getName //exposed object to where it gets injected.
            };
        };
    });
    

    使用方法

    当您需要在将服务对象提供给用户之前为其提供模块化配置时,例如,假设您想根据环境设置API URL(如devstageprod)。

    注意

    只有在Angular的配置阶段,提供程序才可用,而服务和工厂则不可用。

    希望这样能够清楚地解释工厂、服务和提供程序给您的理解带来帮助。


    1
    如果我想要一个具有特定接口的服务,但有两个不同的实现,并且将每个实现注入到控制器中,但使用ui-router绑定到不同的状态,我该怎么办?例如,在一个状态下进行远程调用,而在另一个状态下则写入本地存储。提供程序文档说要使用 only when you want to expose an API for application-wide configuration that must be made before the application starts. This is usually interesting only for reusable services whose behavior might need to vary slightly between applications,所以听起来好像不可能,对吗? - qix

    191

    当我意识到它们都是通过运行某个东西一次,存储其取得的值,然后在通过依赖注入引用时,呈现出相同的存储值时,我的启示来了。

    假设我们有:

    app.factory('a', fn);
    app.service('b', fn);
    app.provider('c', fn);
    

    三者之间的区别在于:

    1. a的存储值来自运行fn
    2. b的存储值来自用new实例化fn
    3. c的存储值先是通过new实例化fn获取一个实例,然后运行该实例的$get方法。

    这意味着AngularJS内部有一个类似缓存对象的东西,每个注入的值只会在第一次注入时被分配一次,它们所在的位置是:

    cache.a = fn()
    cache.b = new fn()
    cache.c = (new fn()).$get()
    

    这就是为什么我们在服务中使用this,并在提供者中定义一个this.$get的原因。


    2
    我也最喜欢这个答案。它们的共同点是通过 DI 在需要时提供对对象的访问。 通常你可以使用 factory 来完成。 只有在像 CoffeeScript、TypeScript、ES6 等语言中才需要使用 service,以便使用它们的类语法。 只有当你的模块被多个应用程序使用并具有不同的设置时,才需要使用 provider 并使用 app.config()。 如果你的服务是一个纯单例或者只能创建某些实例取决于你的实现。 - Andreas Linnert

    137

    服务 vs 提供商 vs 工厂:

    我试图保持简单。这是关于基本的 JavaScript 概念。

    首先,让我们谈谈 AngularJS 中的服务

    什么是服务: 在 AngularJS 中,服务只是一个单例 JavaScript 对象,可以存储一些有用的方法或属性。此单例对象基于 ngApp(Angular 应用程序)创建,并在当前应用程序中的所有控制器之间共享。当 AngularJS 实例化服务对象时,它会将此服务对象注册到唯一的服务名称中。因此,每次需要服务实例时,Angular 将搜索该服务名称的注册表,并返回对服务对象的引用。这样,我们就可以在服务对象上调用方法、访问属性等。 你可能会问,你是否也可以将属性、方法放在控制器作用域对象中!那么为什么您需要服务对象?答案是:服务在多个控制器作用域之间共享。如果您将某些属性/方法放在控制器的作用域对象中,则仅可在当前作用域中使用。但是,当您在服务对象上定义方法、属性时,它将全局可用,并且可以通过注入该服务来访问任何控制器作用域中的内容。

    因此,如果有三个控制器作用域,例如 controllerA、controllerB 和 controllerC,则所有作用域都将共享同一个服务实例。

    <div ng-controller='controllerA'>
        <!-- controllerA scope -->
    </div>
    <div ng-controller='controllerB'>
        <!-- controllerB scope -->
    </div>
    <div ng-controller='controllerC'>
        <!-- controllerC scope -->
    </div>
    

    如何创建一个服务?

    AngularJS提供了不同的方法来注册服务。在这里,我们将集中讨论三种方法:factory(..), service(..), provider(..);

    使用此链接以获取代码参考

    工厂函数:

    我们可以定义一个工厂函数如下。

    factory('serviceName',function fnFactory(){ return serviceInstance;})
    

    AngularJS提供了'factory('serviceName', fnFactory)'方法,它接受两个参数:serviceName和一个JavaScript函数。Angular通过调用fnFactory()函数来创建服务实例,如下所示。

    var serviceInstace = fnFactory();
    

    传递的函数可以定义一个对象并返回该对象。AngularJS 简单地将这个对象引用存储到作为第一个参数传递的变量中。从 fnFactory 返回的任何内容都将绑定到 serviceInstance。我们不仅可以返回对象,还可以返回函数、值等等,无论返回什么,都将对服务实例可用。

    例如:

    var app= angular.module('myApp', []);
    //creating service using factory method
    app.factory('factoryPattern',function(){
      var data={
        'firstName':'Tom',
        'lastName':' Cruise',
        greet: function(){
          console.log('hello!' + this.firstName + this.lastName);
        }
      };
    
      //Now all the properties and methods of data object will be available in our service object
      return data;
    });
    

    服务功能:

    service('serviceName',function fnServiceConstructor(){})
    

    另一种方法是注册服务。唯一的区别在于AngularJS尝试实例化服务对象的方式。这次Angular使用“new”关键字并调用构造函数,类似于以下方式。

    var serviceInstance = new fnServiceConstructor();
    

    在构造函数中,我们可以使用“this”关键字向服务对象添加属性/方法。 例如:

    //Creating a service using the service method
    var app= angular.module('myApp', []);
    app.service('servicePattern',function(){
      this.firstName ='James';
      this.lastName =' Bond';
      this.greet = function(){
        console.log('My Name is '+ this.firstName + this.lastName);
      };
    });
    

    服务提供者功能:

    Provider(提供者)函数是创建服务的另一种方式。假设我们想要创建一个服务,它可以向用户显示问候语信息,但我们还想提供一种功能,使用户可以设置自己的问候语信息。从技术角度来看,我们想要创建可配置的服务。我们该如何实现这个功能?当然有办法,应用程序可以传递自定义的问候语信息,AngularJS 将其提供给工厂/构造函数来创建我们的服务实例。

    在这种情况下,provider() 函数就起到作用了。使用 provider() 函数,我们可以创建可配置的服务。

    我们可以按照以下提供者语法来创建可配置的服务:

    /*step1:define a service */
    app.provider('service',function serviceProviderConstructor(){});
    
    /*step2:configure the service */
    app.config(function configureService(serviceProvider){});
    

    提供者语法在内部是如何工作的?

    1.我们在提供者函数中定义构造函数,并使用它来创建提供者对象。

    var serviceProvider = new serviceProviderConstructor();
    

    2.我们在app.config()中传递的函数将被执行。 这称为配置阶段,在此阶段我们有机会自定义我们的服务。

    configureService(serviceProvider);
    

    3. 最后,通过调用serviceProvider的$get方法创建服务实例。

    serviceInstance = serviceProvider.$get()
    

    使用provide语法创建服务的示例代码:

    var app= angular.module('myApp', []);
    app.provider('providerPattern',function providerConstructor(){
      //this function works as constructor function for provider
      this.firstName = 'Arnold ';
      this.lastName = ' Schwarzenegger' ;
      this.greetMessage = ' Welcome, This is default Greeting Message' ;
      //adding some method which we can call in app.config() function
      this.setGreetMsg = function(msg){
        if(msg){
          this.greetMessage =  msg ;
        }
      };
    
      //We can also add a method which can change firstName and lastName
      this.$get = function(){
        var firstName = this.firstName;
        var lastName = this.lastName ;
        var greetMessage = this.greetMessage;
        var data={
           greet: function(){
             console.log('hello, ' + firstName + lastName+'! '+ greetMessage);
           }
        };
        return data ;
      };
    });
    
    app.config(
      function(providerPatternProvider){
        providerPatternProvider.setGreetMsg(' How do you do ?');
      }
    );
    

    工作演示

    概要:


    工厂使用一个工厂函数来返回服务实例。serviceInstance = fnFactory();

    服务使用构造函数,并使用Angular使用'new'关键字调用此构造函数以创建服务实例。serviceInstance = new fnServiceConstructor();

    提供者定义providerConstructor函数,此providerConstructor函数定义一个工厂函数$ get 。 Angular调用$ get()以创建服务对象。提供程序语法具有在实例化服务对象之前配置服务对象的附加优点。serviceInstance = $get();


    89

    63

    工厂

    你向AngularJS提供一个函数,当需要使用工厂时,AngularJS会缓存并注入其返回值。

    例如:

    app.factory('factory', function() {
        var name = '';
        // Return value **is** the object that will be injected
        return {
            name: name;
        }
    })
    

    使用方法:

    app.controller('ctrl', function($scope, factory) {
         $scope.name = factory.name;
    });
    

    服务

    你向AngularJS提供一个函数,AngularJS将调用new来实例化它。由AngularJS创建的实例将被缓存和注入到请求服务时使用。由于使用new实例化服务,因此关键字this是有效的,并且引用该实例。

    例如:

    app.service('service', function() {
         var name = '';
         this.setName = function(newName) {
             name = newName;
         }
         this.getName = function() {
             return name;
         }
    });
    

    使用方法:

    app.controller('ctrl', function($scope, service) {
       $scope.name = service.getName();
    });
    

    提供者

    您向AngularJS提供一个函数,AngularJS将调用其$get函数。从$get函数返回的值将被缓存并在请求服务时注入。

    提供者允许您在AngularJS调用$get方法以获取可注入对象之前进行配置。

    示例:

    app.provider('provider', function() {
         var name = '';
         this.setName = function(newName) {
              name = newName;
         }
         this.$get = function() {
             return {
                name: name
             }
         }
    })
    

    用法(作为控制器中的可注入项)

    app.controller('ctrl', function($scope, provider) {
        $scope.name = provider.name;
    });
    

    使用方法(在调用$get创建可注入对象之前配置提供程序)

    app.config(function(providerProvider) {
        providerProvider.setName('John');
    });
    

    56

    当我尝试处理提供程序时,发现了一些有趣的事情。

    对于提供程序而言,可注入对象的可见性与服务和工厂不同。如果声明了 AngularJS 的“常量”(例如:myApp.constant('a', 'Robert');),则可以将其注入到服务、工厂和提供程序中。

    但是,如果声明了 AngularJS 的“值”(例如:myApp.value('b', {name: 'Jones'});),则可以将其注入到服务和工厂中,但无法注入到创建提供程序的函数中。然而,您可以将其注入到为提供程序定义的 $get 函数中。这在 AngularJS 文档中有提及,但很容易被忽视。您可以在 %provide 页面的 value 和 constant 方法部分找到它。

    http://jsfiddle.net/R2Frv/1/

    <div ng-app="MyAppName">
        <div ng-controller="MyCtrl">
            <p>from Service: {{servGreet}}</p>
            <p>from Provider: {{provGreet}}</p>
        </div>
    </div>
    <script>
        var myApp = angular.module('MyAppName', []);
    
        myApp.constant('a', 'Robert');
        myApp.value('b', {name: 'Jones'});
    
        myApp.service('greetService', function(a,b) {
            this.greeter = 'Hi there, ' + a + ' ' + b.name;
        });
    
        myApp.provider('greetProvider', function(a) {
            this.firstName = a;
            this.$get = function(b) {
                this.lastName = b.name;
                this.fullName = this.firstName + ' ' + this.lastName;
                return this;
            };
        });
    
        function MyCtrl($scope, greetService, greetProvider) {
            $scope.servGreet = greetService.greeter;
            $scope.provGreet = greetProvider.fullName;
        }
    </script>
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接