【一起学AngularJS】专题篇——Providers

Providers

每个我们搭建的网站应用都是由多个对象组成的,它们互相之间交互来完成所有的工作。要使得网站顺利运行,这些对象需要首先被初始化并且联系在一起。在AngularJS中,大部分这样的对象都是自动的被Injector Service(注入器服务)来初始化和编织在一起的。
Injector服务创建两种类型的对象:Services(服务)和特殊对象
Service的API是由开发者自己定义的。
特殊对象则对应了AngularJS框架自带的一些API。这些对象的类型有:Controller(控制器)Directive(指令)Filter(过滤器)Animation(动画)
注入器需要知道按照什么方式来创建这些对象。我们可以通过注册一个recipe(配方)定制创建对象的方式。一共有5种配方。
最冗长,但是最全面的一个是Provider配方。剩下的四种是:Value、Factory、Service和Constant,这些配方的功能和Provider一样,仅仅是一些语法糖。
下面,我们来看一些不同的场景,学习下如何通过不通的配方类型来创建和使用服务。


说些关于模块(Modules)的事

为了让Injector知道如何创建对象并且编织它们,我们需要注册对应的配方。每个配方含有一个对象的标识以及创建它的方式描述。
每个配方肯定是属于某个Angular模块的。一个Angular模块就像一个包一样,包含了一个或者多个配方。由于手动加载模块依赖的其他对象是很麻烦的,所以模块中也可以包含了需要依赖的其他模块的信息。
当一个Angular应用以某个给定的模块启动时,Angular将先创建一个Injector的实例,然后这个实例再把定义在ng模块的所有的配方、应用模块 、依赖等作为一个总体统一注册。


值配方Value Recipe

我们先来创建一个简单的clientId服务,它将提供一个字符串,是一个远程API的通信认证码。你可以下面这样定义:

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

这里我们创建了一个叫做myApp的Angular模块,并且定制了一张配方来创建clientId服务,这里的配方就是'a12345654321x'这个字符串,这个例子很简单不是么?
下面你就可以通过数据绑定来显示它了:

myApp.controller('DemoController', ['clientId', function DemoController(clientId) {
  this.clientId = clientId;
}]);

  
    Client ID: {{demo.clientId}}
  

在这个例子中,我们使用了值配方来定义了一个服务的返回值。当DemoController调用这个clientId服务的时候,将返回这个值。
相信你可以举一反三!

工厂配方Factory Recipe

值配方写起来非常简便,但同时也缺少了一些我们创建服务时需要的重要特性。下面我们来看看值配方的兄弟——工厂配方。工厂配方比值配方多了以下功能:

  • 使用其他服务(能依赖其他服务)
  • 服务初始化
  • 延迟加载
    这个工厂配方将通过一个可带参数(对其他服务的依赖)的函数来创建一个服务。函数的返回值就是按照此配方创建的服务实例。

注意:Angular中所有的服务都是单例的。这意味着注入器将且仅将使用一次创建某个对象的配方。然后注入器就会缓存所有的实例引用,以便将来的使用。

工厂配方是值配方的一个更高级的形式,所以上面的例子使用工厂配方同样可以创建。如下:

myApp.factory('clientId', function clientIdFactory() {
  return 'a12345654321x';
});

但是这里的token是简单的字符串,使用值配方似乎更加简单,并且代码也更加简洁。
我们来做点复杂的操作,下面我们来创建一个能计算token用于校验远程API的服务。这个token的叫做apiToken,它将由存储在浏览器本地内存的一个密钥和客户端编号clientId计算而来。

myApp.factory('apiToken', ['clientId', function apiTokenFactory(clientId) {
  var encrypt = function(data1, data2) {
    // NSA-proof encryption algorithm:
    return (data1 + ':' + data2).toUpperCase();
  };

  var secret = window.localStorage.getItem('myApp.secret');
  var apiToken = encrypt(clientId, secret);

  return apiToken;
}]);

在上述代码中,apiToken服务是通过工厂配方创建的,而这个工厂配方依赖了clientId服务。这个工厂配方中使用了NSA-proof加密算法来产生一个验证token。

最佳实践: 工厂函数我们一般命名成 Factory(比如: apiTokenFactory). 当然命名公约不是必须的,它在查找代码和查看堆栈时很有用。

就像值配方一样,工厂配方可以创建一个任何类型的服务,可以是基本类型,也可以是函数,或者自定义类型的实例。

服务配方 Service Recipe

JavaScript开发者经常使用自定义类型去编写面像对象的代码。下面是一个自定义实例——unicornLauncher服务,它将把我们的独角兽发射到空中【注解:我也没看懂为啥举这个例子,独角兽有其他含义?】

function UnicornLauncher(apiToken) {

  this.launchedCount = 0;
  this.launch = function() {
    // Make a request to the remote API and include the apiToken
    ...
    this.launchedCount++;
  }
}

现在我们已经准备好发射独角兽了,不过apiToken服务还没有依赖进来,我们通过工厂配方来将其引用进来:

myApp.factory('unicornLauncher', ["apiToken", function(apiToken) {
  return new UnicornLauncher(apiToken);
}]);

然而这个例子最佳的解决方案是使用服务配方。
服务配方也能像工厂配方和值配方一样创建一个服务,但它是通过使用new操作符来调用一个构造器来实现的。这个构造器可以接受0个或者多个参数,用于传入这个服务实例所依赖的服务。

注意:服务配方的设计方式遵循构造器注入

上文中我们已经为UnicornLauncher设计了一个构造器,所以我们只需要通过下面的代码就可以把上文中的工厂配方编程服务配方了:

myApp.service('unicornLauncher', ["apiToken", UnicornLauncher]);

是不是更加简单了!!!

注意: 我们把配方的一种叫做了“服务”。我们很后悔这么做,而且我们知道我们早晚会被惩罚的。这就像我们把我们的子孙叫做“孩子”一样,这样他的老师就会很郁闷了(注解:老师喊他的名字的时候,其他小孩就会纳闷)。

提供者配方Provider Recipe
之前我们提到过,提供者配方是核心配方,其他配方只是在其基础之上构建的语法糖而已。提供者配方非常冗长繁琐,但是其功能也最强大,只是对于大多数情况来说,使用它有点过了。
提供者配方语法上是一个实现了$get方法的自定义类型。这个方法其实是一个工厂方法,和我们之前讨论的工厂配方没什么不通。实际上,当你创建了一个工厂配方时,本质是创建了一个提供者配方,只是其中的$get方法指向了你的工厂方法而已,当然这一切对用户来说都是透明的。
使用提供者配方只有一个场景,那就是当你需要暴露一些可以在应用启动之前配置应用的API的时候。这在各个AngularJS应用重用服务的时候特别有用。
我们的unicornLauncher服务太了,所以很多应用(AngularJS应用)都在用它。默认的情况下,独角兽号没有护盾。但是在很多星球上,大气层非常的厚,以至于我们必须要给独角兽号装上锡纸护盾,这样它就可以翱翔在银河系中惹,而不是烧毁在大气层中!(PS:老外的教程就是这么体贴,连举例都像那么回事!一点不马虎)。要是能够在每个发射应用中配置发射时是否安装锡纸护盾就好了,应用就可以根据自己的大气层情况决定是否安装了。 答案当然是可以的。我们可以让它变成可配置的:

myApp.provider('unicornLauncher', function UnicornLauncherProvider() {
  var useTinfoilShielding = false;

  this.useTinfoilShielding = function(value) {
    useTinfoilShielding = !!value;
  };

  this.$get = ["apiToken", function unicornLauncherFactory(apiToken) {

    // let's assume that the UnicornLauncher constructor was also changed to
    // accept and use the useTinfoilShielding argument
    return new UnicornLauncher(apiToken, useTinfoilShielding);
  }];
});

为了开启锡纸护盾,我们需要创建一个配置模块,并且把UnicornLauncherProvider注入进去。

myApp.config(["unicornLauncherProvider", function(unicornLauncherProvider) {
  unicornLauncherProvider.useTinfoilShielding(true);
}]);

注意独角兽号提供者已经被注入了配置函数。这次注入是由提供者注入器来注入的,它不同于普通的实例注入器。提供者注入器只注入和绑定所有的提供者实例。
在应用启动阶段,在Angular创建所有服务之前,它配置并且初始化所有的提供者。我们把这个阶段叫做配置阶段,它是Angular应用生命周期的一部分。在这个阶段中,服务是无法被访问的,因为它们还没有被初始化。
一旦配置阶段结束后,与提供者的交互旧结束了,配置和初始化服务的阶段开始了,我们把后面的这个阶段叫做运行阶段

常量配方 Constant Recipe

我们刚刚知道了Angular把生命周期分为配置阶段和运行阶段,并且你应该也知道了你可以通过配置函数来配置你的应用。因为配置阶段中,所有服务实例都是无法被访问的,简单的值配方实例(Value Objects)也是如此。
但是确实有些时候我们需要在配置阶段访问一些变量,而不是采用硬编码。而且大多数时候我们需要在配置和运行阶段都能访问到这些变量。这就轮到常量配方( Constant Recipe)来大显身手了。
下面我们要在配置阶段在准备发射时为我们的独角兽号打上星球名称的标签。星球名称是应用指定的,并且在运行时期能被各种各样的控制器使用。为了达到这个效果,我们可以创建一个常量:

myApp.constant('planetName', 'Greasy Giant');

然后,我们就可以像下面这样来配置unicornLauncherProvider了:

myApp.config(['unicornLauncherProvider', 'planetName', function(unicornLauncherProvider, planetName) {
  unicornLauncherProvider.useTinfoilShielding(true);
  unicornLauncherProvider.stampText(planetName);
}]);

因为常量配方在运行阶段也能被访问,所以我们也可以在控制器和模版中使用它:

myApp.controller('DemoController', ["clientId", "planetName", function DemoController(clientId, planetName) {
  this.clientId = clientId;
  this.planetName = planetName;
}]);

  
   Client ID: {{demo.clientId}}
   
Planet Name: {{demo.planetName}}

特殊目的对象 Special Purpose Objects

之前我们谈到过有一些不同于服务的特殊目的对象。这些对象是Angular框架的插件式扩展,因此它们都实现了Angular指定的接口。这些接口包含:控制器、指令(Directive)、过滤器和动画。
注入器创建特殊对象时(以及控制器对象的异常)所使用的指令是通过工厂配方实现的,当然这个实现过程对用户来说是透明的。
下面我们举个简单的例子来说明如果通过指令API(它依赖于我们之前的那个常量配方plantName,在本例中它叫"Plant Name:Greasy Giant")创建一个简单的控件。
由于指令的注册是通过工厂配方来完成的,因此我们可以使用和工厂配方相同的语法:

myApp.directive('myPlanet', ['planetName', function myPlanetDirectiveFactory(planetName) {
  // directive definition object
  return {
    restrict: 'E',
    scope: {},
    link: function($scope, $element) { $element.text('Planet: ' + planetName); }
  }
}]);

然后我们就可以像下面一样使用这个控件了:


  
   
  

通过这种工厂配方的方式,我们可以定义AnuglarJS的过滤器和注解,但是定义控制器则有点特别。我们定义一个控制器为自定义类型,并且通过构造函数的参数申明它的依赖服务,然后控制器将作为模块的一部分被注册。我们来看看之前我们的一个例子DemoController

myApp.controller('DemoController', ['clientId', function DemoController(clientId) {
  this.clientId = clientId;
}]);

每次应用需要一个DemoController实例的时候(在我们的例子应用中,只有一次),DemoController都将通过构造函数来创建一个新的实例。因此控制器和服务不一样,因为它们不是单例的。每当控制器的构造函数被调用时,都会调用所依赖的服务(本例子中是clientId服务)。

结论

让我们总结几点重要的:

  • 注入器使用配方来创建两类对象:服务 和 特殊对象
  • 一共有5种配方:工厂配方、值配方、服务配方、提供者配方、常量配方。
  • 工厂配方和服务配方是最常用的配方。它们两者的区别在于:服务配方适用于创建自定义类型的实例,而工厂配方可以更好的创建JavaScript基本类型和函数的实例。
  • 提供者配方是核心配方,并且其他的配方都是基于提供者配方的语法糖。
  • 提供者是最复杂的配方类型,除非你需要为一些重用的代码做全局多态化配置,否则尽量少用。(注解:举个例子,你是老板,有3个司机(多应用)和一辆宝马(公用代码),你希望每次喊他们服务来服务的时候,都能开你的宝马,但是3个司机各有的坐垫癖好,有人喜欢皮的,有人喜欢布的,有人则不喜欢坐垫。这就是对同一辆宝马的多太)
  • 所有的特殊对象除了控制器,都是通过工厂配方定义的。
Features Recipe type Factory Service Value Constant Provider
can have dependencies yes yes no no yes
uses type friendly injection no yes yes* yes* no
object available in config phase no no no yes yes**
can create functions yes yes yes yes yes
can create primitives yes no yes yes yes

* 需要使用new操作符来完成初始化
** 在配置阶段是无法访问服务对象的,但是可以访问提供者对象。

你可能感兴趣的:(【一起学AngularJS】专题篇——Providers)