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个回答

45

这对于新手来说非常令人困惑,我尝试用简单的语言澄清。

AngularJS Service:用于在控制器中使用服务引用共享实用函数。 Service在本质上是单例的,因此在浏览器中只会创建一个实例,并且在整个页面中使用相同的引用。

在服务中,我们将函数名称作为带有this对象的属性。

AngularJS Factory:工厂的目的与服务相同,但在这种情况下,我们创建一个新对象,并将函数作为该对象的属性添加,最后返回该对象。

AngularJS Provider:其目的也是相同的,但Provider返回其$ get函数的输出。

定义和使用服务、工厂和提供程序在http://www.dotnetfunda.com/articles/show/3156/difference-between-angularjs-service-factory-and-provider中有详细说明。


2
工厂和提供者也是单例对象吗?有哪些情况下建议使用工厂而不是服务? - Sunil Garg

35

对我来说,理解这两者的最好且最简单的方式是:

var service, factory;
service = factory = function(injection) {}

AngularJS 实例化特定组件的方式(简化):

// service
var angularService = new service(injection);

// factory
var angularFactory = factory(injection);

因此,对于服务而言,AngularJS组件是由服务声明函数所表示的类的对象实例。对于工厂而言,它是由工厂声明函数返回的结果。工厂可能与服务具有相同的行为:

var factoryAsService = function(injection) {
  return new function(injection) {
    // Service content
  }
}

最简单的思考方式如下:

  • 服务是一个单例对象实例。如果想要为代码提供单例对象,请使用服务。
  • 工厂是一个类。如果想要为代码提供自定义类(因为服务已经被实例化了,所以不能用服务),请使用工厂。

工厂“类”的示例在注释中提供,以及提供者的不同之处。


一个服务每次使用时都被实例化,它怎么可能是单例的呢?我无法理解这一点... - joe
服务只在依赖解析期间实例化一次,然后当您从注入器请求服务时,您总是获取相同的实例。您可以在此处轻松检查:http://jsfiddle.net/l0co/sovtu55t/1/,请在控制台中运行它。控制台显示服务仅实例化一次。 - Lukasz Frankowski
哦,我明白了。我本来期望能够直接使用 new MyService() 这样的语法 :) - joe

34

我的澄清:

基本上所有提到的类型(service、factory、provider等)都只是创建和配置全局变量(当然,这些全局变量对整个应用程序都是全局的),就像老式的全局变量一样。

尽管不推荐使用全局变量,但这些全局变量的真正用途是提供依赖注入,通过将变量传递给相关控制器。

创建“全局变量”的值存在许多复杂级别:

  1. 常量
    定义一个实际的常量,在整个应用程序期间不应修改,就像其他语言中的常量一样(JavaScript缺乏这种功能)。

  2. 这是一个可修改的值或对象,它作为某个全局变量,并且甚至可以在创建其他服务或工厂时进行注入(请参见后面的内容)。但是,它必须是“字面值”,这意味着必须写出实际值,并且不能使用任何计算或编程逻辑(换句话说,39myText{prop: "value"}是可以的,但2 + 2不可以)。
  3. 工厂
    更通用的值,可以立即计算。它通过向AngularJS传递具有计算值所需逻辑的函数来工作,AngularJS执行该函数,并将返回值保存在命名变量中。
    请注意,可以返回对象(在这种情况下,它将类似于service)或函数(它将作为回调函数保存在变量中)。
  4. 服务
    服务是factory的更简化版本,仅在值为对象时有效,并允许直接在函数中编写任何逻辑(就像它是构造函数一样),以及使用this关键字声明和访问对象属性。
  5. 提供者
    与是factory的简化版本的服务不同,提供者是初始化“全局”变量的更复杂但更灵活的方式,其中最大的灵活性是可以从app.config设置值。
    它的工作方式类似于使用serviceprovider的组合,通过向提供程序传递使用this关键字声明属性的函数,可以从app.config中使用它们。
    然后,它需要一个单独的$.get函数,在通过app.config文件设置上述属性后由AngularJS执行,该$.get函数的行为与上面的factory相同,其返回值用于初始化“全局”变量。

27

我的理解如下:

工厂模式: 您只需在工厂内创建一个对象并返回它。

服务模式:

您只需使用此关键字定义一个函数的标准函数。

提供者模式:

您定义了一个$get对象,可以用它来获取返回数据的对象。


你是不是把工厂和服务搞混了?服务是创建的,而工厂则返回。 - Flavien Volken
当您将服务名称声明为可注入参数时,您将获得该函数的实例。换句话说,是new FunctionYouPassedToService()。此对象实例成为AngularJS注册并在以后注入到其他服务/控制器中(如果需要)的服务对象。 //工厂 当您将factoryname声明为可注入参数时,您将获得通过调用传递给module.factory的函数引用返回的值。 - sajan
好的,所以在Angular中,工厂是一个单例模式,而“服务”实际上是一个工厂方法模式(按照常见的设计模式术语)。 - Flavien Volken

26

Angular文档总结:

  • 有五种配方类型,用于定义如何创建对象:ValueFactoryServiceProviderConstant
  • FactoryService是最常用的配方。它们之间唯一的区别在于Service配方更适合自定义类型的对象,而Factory可以生成JavaScript原始值和函数。
  • Provider配方是核心配方类型,其他所有配方都只是在它上面加了语法糖。
  • Provider是最复杂的配方类型。除非您正在构建需要全局配置的可重用代码片段,否则不需要使用它。

enter image description here


来自SO的最佳答案:

https://dev59.com/XWYr5IYBdhLWcg3wVYwg#26924234(<-- 精华) https://dev59.com/22Mk5IYBdhLWcg3w0RGU#27263882
https://dev59.com/bWQn5IYBdhLWcg3wzZtU#16566144


23

所有好的答案都已经被提到了。我想再加几点关于ServiceFactory的区别,以及它们之间的差异。同时,也有一些问题:

  1. 我应该使用服务还是工厂?有什么区别?
  2. 它们是否相同或具有相同的行为?

让我们从服务和工厂之间的区别开始:

  1. 两者都是单例模式:当Angular第一次发现这些依赖时,它会创建一个服务/工厂的单个实例。一旦实例被创建,同一个实例将永久使用。

  2. 可以用来建模带有行为的对象:它们都可以拥有方法、内部状态变量等等。虽然编写代码的方式会有所不同。

服务:

服务是一个构造函数,Angular将通过调用new yourServiceName()来实例化它。这意味着几件事情。

  1. 函数和实例变量将成为this的属性。
  2. 您不需要返回值。当Angular调用new yourServiceName()时,它将接收到带有您放在其中所有属性的this对象。

示例:

angular.service('MyService', function() {
  this.aServiceVariable = "Ved Prakash"
  this.aServiceMethod = function() {
    return //code
  };
});

当Angular将MyService服务注入到依赖它的控制器中时,该控制器将获得一个可调用函数的MyService,例如MyService.aServiceMethod ()

注意this关键字:

由于构造的服务是一个对象,因此在调用其内部方法时,这些方法可以引用this关键字:

angular.service('ScoreKeeper', function($http) {
  this.score = 0;

  this.getScore = function() {
    return this.score;
  };

  this.setScore = function(newScore) {
    this.score = newScore;
  };

  this.addOne = function() {
    this.score++;
  };
});

你可能会想在promise链中调用ScoreKeeper.setScore,比如如果你通过从服务器获取得分来初始化分数:$http.get('/score').then(ScoreKeeper.setScore). 但是这样做有问题,因为ScoreKeeper.setScore将被绑定到null,你会得到错误。更好的方法是 $http.get('/score').then(ScoreKeeper.setScore.bind(ScoreKeeper))

Service返回值:

由于JavaScript构造函数的工作方式,如果从constructor函数中返回一个复杂值(即对象),调用者将获得该对象而不是实例本身。

这意味着你基本上可以复制下面的工厂示例,将factory替换为service,它就能够工作:

angular.service('MyService', function($http) {
  var api = {};

  api.aServiceMethod= function() {
    return $http.get('/users');
  };
  return api;
});

因此,当Angular使用new MyService()构建您的服务时,它将获得该api对象,而不是MyService实例。

这适用于任何复杂值(对象、函数),但不适用于原始类型。

工厂:

工厂是一个普通的函数,它返回一个值。返回值是注入到依赖该工厂的组件或服务中的。在Angular中,一个典型的工厂模式是返回一个具有函数作为属性的对象,就像这样:

angular.factory('MyFactory', function($http) {
  var api = {};

  api.aFactoryMethod= function() {
    return $http.get('/users');
  };

  return api;
});

工厂依赖项的注入值是工厂的返回值,它不一定是对象,也可以是函数。

上述第1和第2个问题的答案:

在大多数情况下,只需使用工厂即可。它们的行为更容易理解。不需要选择是否返回值,此外,如果您做错了事情,也不会引入错误。

当我谈论将它们作为依赖项注入时,我仍将它们称为“服务”。

服务/工厂的行为非常相似,有些人会说两者都可以。这有点真实,但我发现遵循John Papa风格指南的建议并坚持使用工厂更容易。


17

需要进一步澄清的是,工厂可以创建函数/基元类型,而服务则不能。请查看基于Epokk的这个jsFiddlehttp://jsfiddle.net/skeller88/PxdSP/1351/.

工厂返回一个可以调用的函数:

myApp.factory('helloWorldFromFactory', function() {
  return function() {
    return "Hello, World!";
  };
});

该工厂还可以返回一个带有可调用方法的对象:

myApp.factory('helloWorldFromFactory', function() {
  return {
    sayHello: function() {
      return "Hello, World!";
    }
  };
});

该服务返回一个带有可调用方法的对象:

myApp.service('helloWorldFromService', function() {
  this.sayHello = function() {
     return "Hello, World!";
  };
});

有关详细信息,请参阅我在以下链接中撰写的文章:http://www.shanemkeller.com/tldr-services-vs-factories-in-angular/


16
已经有很好的答案了,但我想分享一下我的看法。
首先: Provider 是创建一个 service(单例对象)的方式/方法,它应该由 $injector(AngulaJS 中的 IoC 模式实现方式)注入。

Value、Factory、Service 和 Constant(4 种方式)则是在 Provider 方式/方法上的语法糖。
视频中已经涵盖了Service vs Factory 部分: https://www.youtube.com/watch?v=BLzNCkPn3ao Service 实际上都和 new 关键字有关,我们知道它可以做 4 件事情:
1. 创建一个全新的对象。 2. 将其链接到其 prototype 对象。 3. 将 context 连接到 this。 4. 返回 this
Factory 则完全围绕着工厂模式展开,包含返回像 Service 一样的对象的函数。

这个简短的视频还涵盖了 Providerhttps://www.youtube.com/watch?v=HvTZbQ_hUZY(你可以在那里看到他们是如何从工厂转向提供程序的)。 Provider 方式/方法主要用于应用配置,在应用完全启动/初始化之前。

14

阅读了所有这些帖子后,我更加困惑了。但是所有的信息都很有价值。最终我发现了以下表格,它可以通过简单的比较提供信息:

  • 注入器使用配方来创建两种类型的对象:服务和特殊目的对象。
  • 有五种配方类型定义如何创建对象:Value、Factory、Service、Provider 和 Constant。
  • 工厂和服务是最常用的配方。它们之间唯一的区别在于,服务配方适用于自定义类型的对象,而工厂可以生成 JavaScript 原语和函数。
  • Provider 配方是核心配方类型,其他所有配方只是对它的语法糖。
  • Provider 是最复杂的配方类型。除非你正在构建需要全局配置的可重用代码片段,否则不需要它。
  • 除了 Controller 之外,所有特殊目的对象均通过 Factory 配方定义。

enter image description here

对于初学者:这可能不是正确的用例,但在高级别上,这就是这三个用例的使用情况。

  1. 如果你想在 Angular 模块配置函数中使用,应该创建为 provider

angular.module('myApp').config(function($testProvider){
$testProvider.someFunction();
})

  1. 需要将Ajax调用或第三方集成变为服务
  2. 要进行数据处理,请将其创建为工厂

在基本情况下,工厂和服务的行为相同。


12

以下是我为AngularJS中的对象工厂编写的代码模板。我使用了汽车/汽车工厂作为示例来说明。这使得在控制器中实现代码变得简单。

     <script>
        angular.module('app', [])
            .factory('CarFactory', function() {

                /**
                 * BroilerPlate Object Instance Factory Definition / Example
                 */
                this.Car = function() {

                    // initialize instance properties
                    angular.extend(this, {
                        color           : null,
                        numberOfDoors   : null,
                        hasFancyRadio   : null,
                        hasLeatherSeats : null
                    });

                    // generic setter (with optional default value)
                    this.set = function(key, value, defaultValue, allowUndefined) {

                        // by default,
                        if (typeof allowUndefined === 'undefined') {
                            // we don't allow setter to accept "undefined" as a value
                            allowUndefined = false;
                        }
                        // if we do not allow undefined values, and..
                        if (!allowUndefined) {
                            // if an undefined value was passed in
                            if (value === undefined) {
                                // and a default value was specified
                                if (defaultValue !== undefined) {
                                    // use the specified default value
                                    value = defaultValue;
                                } else {
                                    // otherwise use the class.prototype.defaults value
                                    value = this.defaults[key];
                                } // end if/else
                            } // end if
                        } // end if

                        // update 
                        this[key] = value;

                        // return reference to this object (fluent)
                        return this;

                    }; // end this.set()

                }; // end this.Car class definition

                // instance properties default values
                this.Car.prototype.defaults = {
                    color: 'yellow',
                    numberOfDoors: 2,
                    hasLeatherSeats: null,
                    hasFancyRadio: false
                };

                // instance factory method / constructor
                this.Car.prototype.instance = function(params) {
                    return new 
                        this.constructor()
                                .set('color',           params.color)
                                .set('numberOfDoors',   params.numberOfDoors)
                                .set('hasFancyRadio',   params.hasFancyRadio)
                                .set('hasLeatherSeats', params.hasLeatherSeats)
                    ;
                };

                return new this.Car();

            }) // end Factory Definition
            .controller('testCtrl', function($scope, CarFactory) {

                window.testCtrl = $scope;

                // first car, is red, uses class default for:
                // numberOfDoors, and hasLeatherSeats
                $scope.car1     = CarFactory
                                    .instance({
                                        color: 'red'
                                    })
                                ;

                // second car, is blue, has 3 doors, 
                // uses class default for hasLeatherSeats
                $scope.car2     = CarFactory
                                    .instance({
                                        color: 'blue',
                                        numberOfDoors: 3
                                    })
                                ;
                // third car, has 4 doors, uses class default for 
                // color and hasLeatherSeats
                $scope.car3     = CarFactory
                                    .instance({
                                        numberOfDoors: 4
                                    })
                                ;
                // sets an undefined variable for 'hasFancyRadio',
                // explicitly defines "true" as default when value is undefined
                $scope.hasFancyRadio = undefined;
                $scope.car3.set('hasFancyRadio', $scope.hasFancyRadio, true);

                // fourth car, purple, 4 doors,
                // uses class default for hasLeatherSeats
                $scope.car4     = CarFactory
                                    .instance({
                                        color: 'purple',
                                        numberOfDoors: 4
                                    });
                // and then explicitly sets hasLeatherSeats to undefined
                $scope.hasLeatherSeats = undefined;
                $scope.car4.set('hasLeatherSeats', $scope.hasLeatherSeats, undefined, true);

                // in console, type window.testCtrl to see the resulting objects

            });
    </script>

这里有一个更简单的例子。我使用了一些第三方库,它们期望一个“位置”对象来公开纬度和经度,但是通过不同的对象属性。我不想篡改供应商代码,所以我调整了我正在传递的“位置”对象。

    angular.module('app')
.factory('PositionFactory', function() {

    /**
     * BroilerPlate Object Instance Factory Definition / Example
     */
    this.Position = function() {

        // initialize instance properties 
        // (multiple properties to satisfy multiple external interface contracts)
        angular.extend(this, {
            lat         : null,
            lon         : null,
            latitude    : null,
            longitude   : null,
            coords: {
                latitude: null,
                longitude: null
            }
        });

        this.setLatitude = function(latitude) {
            this.latitude           = latitude;
            this.lat                = latitude;
            this.coords.latitude    = latitude;
            return this;
        };
        this.setLongitude = function(longitude) {
            this.longitude          = longitude;
            this.lon                = longitude;
            this.coords.longitude   = longitude;
            return this;
        };

    }; // end class definition

    // instance factory method / constructor
    this.Position.prototype.instance = function(params) {
        return new 
            this.constructor()
                    .setLatitude(params.latitude)
                    .setLongitude(params.longitude)
        ;
    };

    return new this.Position();

}) // end Factory Definition

.controller('testCtrl', function($scope, PositionFactory) {
    $scope.position1 = PositionFactory.instance({latitude: 39, longitude: 42.3123});
    $scope.position2 = PositionFactory.instance({latitude: 39, longitude: 42.3333});
}) // end controller

;


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