《AngularJS深度剖析与最佳实践》一2.9 服务

2.9 服务

如果你是一个后端程序员,那么对服务(Service)的概念一定不会陌生。在Angular中,服务的概念是一样的,差别只在于技术细节。
服务是对公共代码的抽象,比如,如果在多个控制器中都出现了相似的代码,那么把它们提取出来,封装成一个服务,你将更加遵循DRY原则(即:不要重复你自己),在可维护性等方面获得提升。
如同我们在第1章的tree服务中所看到的,由于服务剥离了和具体表现相关的部分,而聚焦于业务逻辑或交互逻辑,它更加容易被测试和复用。
但是,在工程实践中,我们引入服务的首要目的是为了优化代码结构,而不是复用。复用只是一项结果,不是目标。所以,当你发现你的代码中混杂了表现层逻辑和业务层逻辑的时候,你就要认真考虑抽取服务了—哪怕它还看不到复用价值。
如果你遵循着测试驱动开发的方式,那么当你觉得测试很难写的时候,回头审视下,看是否这里可以抽取出一个服务,转而对服务进行测试。
服务的概念通常是和依赖注入紧密相关的,Angular中也一样。如果你困惑于在JavaScript中是如何实现依赖注入的,请参见第3章“背后的原理”中的3.3节“依赖注入”。
由于依赖注入的要求,服务都是单例的,这样我们才能把它们到处注入,而不用手动管理它们的生命周期,并容许Angular实现“延迟初始化”等优化措施。
在Angular中,服务分成很多种类型:
常量(Constant):用于声明不会被修改的值。
变量(Value):用于声明会被修改的值。
服务(Service):没错,它跟服务这个大概念同名,原作者在“开发者指南”中把这种行为比喻为“把自己的孩子取名叫‘孩子’—一个会气疯老师的名字”。事实上,同名的原因是—它跟后端领域的“服务”实现方式最为相似:声明一个类,等待Angular把它new出来,然后保存这个实例,供它到处注入。
工厂(Factory):它跟上面这个“服务”不同,它不会被new出来,Angular会调用这个函数,获得返回值,然后保存这个返回值,供它到处注入。它被取名为“工厂”是因为:它本身不会被用于注入,我们使用的是它的产品。但是与现实中的工厂不同,它只产出一份产品,我们只是到处使用这个产品而已。
供应商(Provider):“工厂”只负责生产产品,它的规格是不受我们控制的,而“供应商”更加灵活,我们可以对规格进行配置,以便获得定制化的产品。
事实上,除了Constant外,所有这些类型的服务,背后都是通过Provider实现的,我们可以把它们看做让Provider更容易写的语法糖。一个明显的佐证是:当你使用一个未定义的服务时,Angular给你的错误提示是它对应的Provider未找到,比如我们使用一个未定义的服务:test,那么Angular给出的提示是:Unknown provider: testProvider <- test。
Constant比较特殊,我们稍后讲解,我们先来看其他几个。
Provider的声明方式如下:

angular.module('com.ngnice.app').provider('greeting', function() {
    var _name = 'world';
    this.setName = function(name) {
        _name = name;
    };
    this.$get = function(/这里可以放依赖注入变量/) {
        return 'Hello, ' + _name;
    };
});

使用时:

angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) {
    // 这里greeting应该等于'Hello, world',怎么样,你猜对了吗?
    $scope.message = greeting;
});

对Provider进行配置时:

angular.module('com.ngnice.app').config(function(greetingProvider) {
    greetingProvider.setName('wolf');
});

容器的伪代码如下:

var instance = diContainer['greeting'];         // 先找是否已经有了一个实例
if (!angular.isUndefined(instance)) {
    return instance;                            // 如果已经有了一个实例,直接返回
}

var ProviderClass = angular.module('com.ngnice.app').lookup('greetingProvider'); // 在服务名后面自动加上Provider后缀是Angular遵循的一项约定
var provider = new ProviderClass();   // 把Provider实例化
provider.setName('wolf');
instance = provider.$get();           // 调用$get,并传入依赖注入参数
diContainer['greeting'] = instance;   // 把调用结果存下来
return instance;

事实上,如果不需要对name参数进行配置,声明代码可以简化为:
angular.module('com.ngnice.app').value('greeting', 'Hello, world');
这也就是需要这么多语法糖的原因。
下面给出其他语法糖的等价形式:

2.9.1 服务

angular.module('com.ngnice.app').service('greeting', function() {
    this.sayHello = function(name) {
        return 'Hello, ' + name;
    };
});

等价于:

angular.module('com.ngnice.app').provider('greeting', function() {
    this.$get = function() {
        var Greeting = function() {
            this.sayHello = function(name) {
                return 'Hello, ' + name;
            };
        };
        return new Greeting();
    };
};

使用时:

angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) {
    $scope.message = greeting.sayHello('world');
});

2.9.2 工厂

angular.module('com.ngnice.app').factory('greeting', function() {
    return 'Hello, world';
});

等价于:

angular.module('com.ngnice.app').provider('greeting', function() {
    this.$get = function() {
        var greeting = function() {
            return 'Hello, world';
        };
        return greeting();
    }
});

使用时:

angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) {
    $scope.message = greeting;
});

在Angular源码中,它们的实现是这样的:

function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }

function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
        return $injector.instantiate(constructor);
    }]);
}

function value(name, val) { return factory(name, valueFn(val)); }

Angular提供了这么多种形式的服务,那么我们在工程实践中该如何选择?我们可以遵循下列决策流程:
需要全局的可配置参数?用Provider。
是纯数据,没有行为?用Value。
只new一次,不用参数?用Service。
拿到类,我自己new出实例?用Factory。
拿到函数,我自己调用?用Factory。
但是,还有另一种更加敏捷的方式:
是纯数据时,先用Value;当发现需要添加行为时,改写为Service;或当发现需要通过计算给出结果时,改写为Factory;当发现需要进行全局配置时,改写为Provider。
最酷的是,这个过程对于使用者是透明的—它不需要因为实现代码的改动而更改原有代码。如上面Value和Factory的使用代码,仅仅从使用代码中我们区分不出它是Value还是Factory。
接下来,我们来看Constant。与其他Service不同,Constant不是Provider函数的语法糖。更重要的差别是,它的初始化时机非常早,可以在angular.module('com.ngnice.app').config函数中使用,而其他的服务是不能被注入到config函数中的。这也意味着,如果你需要在config中使用一个全局配置项,那么它就只能声明为常量,而不能声明为变量。
在官方的开发指南中,给出了一个完整的对比表,见表2-1。

下面给出解释:
可以依赖其他服务:由于Value和Constant的特殊声明形式,显然没有进行依赖注入的时机。
使用类型友好的注入:这条没有官方的解释,我的理解是—由于Factory可以根据程序逻辑返回不同的数据类型,所以我们无法推断其结果是什么类型,也就是对类型不够友好。Provider由于其灵活性比Factory更高,因此在类型友好性上和Factory是一样的。
在config阶段可用:只有Constant和Provider类型在config阶段可用,其他都是Provider实例化之后的结果,所以只有config阶段完成后才可用。
可用于创建函数/原生对象:由于Service是new出来的,所以其结果必然是类实例,也就无法直接返回一个可供调用的函数或数字等原生对象。
如果你确实需要对一个没有提供Provider的第三方服务进行配置,该怎么办呢?Angular提供了另一种机制:decorator。这个decorator和前面提到过的装饰器型指令没有关系,它是用来改变服务的行为的。
比如我们有一个第三方服务,名叫ui,它有一个prompt函数,我们不能改它源码,但需要让它每次弹出提问框时都在控制台输出一条记录,那么我们可以这样写:

angular.module('com.ngnice.app').config(function($provide) {
    // $delegate是ui的原始服务
    $provide.decorator('ui', function($delegate) {
        // 保存原始的prompt函数
        var originalPrompt = $delegate.prompt;
        // 用自己的prompt替换
        $delegate.prompt = function() {
            // 先执行原始的prompt函数
            originalPrompt.apply($delegate, arguments);
            // 再写一条控制台日志
            console.log('prompt');
        };
        // 返回原始服务的实例,但也可以返回一个全新的实例
        return $delegate;
    })
});

这种方式给你了超级灵活性,你可以改写包括Angular系统服务在内的任何服务—事实上,angular-mocks模块就是使用decorator来MOCK $httpBackend、$timeout等服务的。
不过,如果你大幅修改了原始服务的逻辑,那么,这可能会给自己和维护者挖坑。俗话说,“不作死就不会死”。如果让我来总结decorator的使用原则,那就是—慎用、慎用、慎用,如果确实想用,请务必遵循“Liskov代换”原则,并写好单元测试。特别是,如果你想修改系统服务的工作逻辑,建议先多看几遍文档,确保你正确理解了它的每一个细节!

时间: 2024-07-31 16:30:59

《AngularJS深度剖析与最佳实践》一2.9 服务的相关文章

《AngularJS深度剖析与最佳实践》一2.12 单元测试

2.12 单元测试 我们在第1章中已经写过两个单元测试(unit test)了,这里我们简单讲一下理论知识. 在Angular中,单元测试的概念和传统的后端编程是一样的.也就是对某些小型功能块儿进行测试,保障其工作逻辑正常.单元测试要尽可能局部化,不要牵扯进很多个模块,必要时可进行mock(模拟). 2.12.1 MOCK的使用方式 由于JavaScript语言的动态特性,Mock一个普通对象不需要进行特别处理.比如,如果一个测试函数需要访问scope中的一个变量:name,但不用访问$watc

《AngularJS深度剖析与最佳实践》一1.6 实现AOP功能

1.6 实现AOP功能 至此,实现路由页面时用到的技术我们已经基本示范过了,接下来我们将开始实现一些高级功能.这些功能具有全局性的影响-基本上每个路由都会涉及它,如果我们把它嵌入到每个路由的实现里,那么代码中将出现大量的重复,编写和维护将会变成噩梦.这类功能,我们称其为"AOP功能",也就是"面向切面功能"(形象点说:路由是竖着并列在一起的,AOP功能则像一个平台一样支撑着它们).最典型的就是"登录"和"错误处理".接下来,我

《AngularJS深度剖析与最佳实践》一1.4 实现第一个页面:注册

1.4 实现第一个页面:注册 接下来,我们开始实现第一个迭代的第一个功能:10.注册.我们把能够通过URL独立访问的一项功能简称为"一个路由",这里为注册功能分配一个叫作/reader/create的路由.之所以不使用/register的形式,是希望在各个URL之间保持统一,这也是我们在整个项目中将贯穿的一个约定. 1.4.1 约定优于配置 如同后端开发一样,我们将reader称为controller,create称作action,中间还可以有一个id,所以,典型的URL是这样的:/$

《AngularJS深度剖析与最佳实践》一1.3 创建项目

1.3 创建项目 接下来,我们要新建一个项目.传统的方式是使用Yeoman工具,它是基于Node的一个项目生成器引擎,但本书使用的是FrontJet方式,所以这里讲两个方式. 1.3.1 Yeoman 这节我们先简单讲讲Yeoman. 首先用cnpm install -g yo命令来安装它. Yeoman只是个项目生成引擎,我们还需要安装一个Angular的项目模板,可以使用cnpm install -g generator-gulp-angular@0.8.1命令.为了让后续步骤和本书的描述保

《AngularJS深度剖析与最佳实践》一2.6 指令

2.6 指令 指令(directive)是Angular中一个很重要的概念,相当于一个自定义的HTML元素,在Angular官方文档中称它为HTML语言的DSL(特定领域语言)扩展. 按照指令的使用场景和作用可以分为两种类型的指令:它们分别为组件型指令(Component)和装饰型器指令(Decorator),它们的分类命名,并不是笔者独创的新法,它是在Angular 2.x中提出的概念,笔者认为它们也同样可以使用于Angular 1.x. 组件型指令主要是为了将复杂而庞大的View分离,使得页

《AngularJS深度剖析与最佳实践》一2.1 什么是UI

2.1 什么是UI 提起UI,你一定知道它是指用户界面(User Interface),但是如果细细剖析,你会发现它没那么简单. 对于一个用户界面,它实际上包括三个主要部分: 内容:你想展现哪些信息?包括动态信息和静态信息.注意,这里的内容不包括它的格式,比如生日,跟它显示为红色还是绿色无关,跟它显示为年月日还是显示为生辰八字也无关. 外观:这些信息要展示为什么样子?这包括格式和样式.样式还包括静态样式和动画效果等. 交互:用户点击了加入购物车按钮时会发生什么?还要更新哪些显示? 在前端技术栈中

《AngularJS深度剖析与最佳实践》一2.5 视图

2.5 视图 我们有了模块和控制器之后,内容和交互逻辑就已经基本确定了,接下来我们就得把它们展示给用户了,这时就用到"视图"(view).CSS并不在Angular的范围内,在实践中,常常结合一套成熟的CSS架构来做,比如Bootstrap就可以和Angular结合得非常好.Angular中实现视图的主体是模板.最常见的模板形式当然是HTML,也有通过Jade等中间语言编译为HTML的.模板中包括静态信息和动态信息,静态信息是指直接写死(hard code)在模板中的,而动态信息则是对

《AngularJS深度剖析与最佳实践》一2.13 端到端测试

2.13 端到端测试 端到端测试(e2e test),也称为场景测试,它模拟的是用户真实的操作场景:用户打开http://xxx地址.在搜索框中输入了abc.然后点击其后的搜索按钮.这时候,他期望看到一个列表,列出所有在标题的任意位置包含了字符串abc的条目,并且每条结果中的abc这三个字母被高亮.所谓端到端,也就是一端是浏览器,另一端是服务器,这个测试贯通了前后端,具有近似于验收测试的价值.端到端测试不是什么新技术,它在前后端分离架构盛行之前就已经被广泛采用了,比如Selenium,而且Sel

《AngularJS深度剖析与最佳实践》一1.1 环境准备

1.1 环境准备 进行开发的第一步是准备开发工具.对于用惯了IDE的程序员来说,可能需要适应一下IDE配合命令行的模式,不过最终你会爱上命令行模式的快速和简洁.我们将要使用的环境如下. NodeNode全称是Node.js,它是一个让JavaScript访问各种本地API和网络API的运行环境,在本书中,将大量使用基于Node的模块和工具. Node的安装非常简单,如果你使用Linux/Mac操作系统,建议从https://github.com/creationix/nvm下载:如果你使用Win