2.2 建立一个服务完成业务逻辑
如果我们把登录的业务逻辑在onClick方法中完成,这样当然也可以,但是这样做的耦合性太强了。设想一下,如果我们增加了微信登录、微博登录等,业务逻辑会越来越复杂,显然我们需要把这个业务逻辑分离出去。
那么我们接下来创建一个AuthService吧,首先我们要在在src中新建一个叫做core的文件夹(src\app\core),然后命令行中输入 ng g s core\auth (s这里是service的缩写,core)。auth.service.ts和auth.service.spec.ts这两个文件应该已经出现在你的目录里了。
下面我们为这个service添加一个方法,你可能注意到这里我们为这个方法指定了返回类型和参数类型。这就是TypeScript带来的好处,有了类型约束,你在别处调用这个方法时,如果给出的参数类型或返回类型不正确,IDE就可以直接告诉你错了。
import { Injectable } from '@angular/core';
@Injectable()
export class AuthService {
constructor() { }
loginWithCredentials(username: string, password: string): boolean {
if(username === 'wangpeng')
return true;
return false;
}
}
等一下,这个service虽然被创建了,但仍然无法在Component中使用。当然你可以在Component中import这个服务,然后实例化后使用,但是这样做并不好,仍然是一个紧耦合的模式,Angular 2提供了一种依赖性注入(Dependency Injection)的方法。
什么是依赖性注入
如果不使用DI(依赖性注入)的时候,我们自然的想法是这样的,在login.component.ts中import引入AuthService,在构造中初始化service,在onClick中调用service:
import { Component, OnInit } from '@angular/core';
//引入AuthService
import { AuthService } from '../core/auth.service';
@Component({
selector: 'app-login',
template: '
<div>
<input #usernameRef type="text">
<input #passwordRef type="password">
<button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
</div>
',
styles: []
})
export class LoginComponent implements OnInit {
//声明成员变量,其类型为AuthService
service: AuthService;
constructor() {
this.service = new AuthService();
}
ngOnInit() {
}
onClick(username, password) {
//调用service的方法
console.log('auth result is: ' + this.service.loginWithCredentials(username,
password));
}
}
这么做呢也可以跑起来,但存在以下几个问题:
由于实例化是在组件中进行的,意味着我们如果更改service的构造函数的话,组件也需要更改。
如果我们以后需要开发、测试和生产环境配置不同的AuthService,以这种方式实现会非常不方便。
下面我们看看如果使用DI是什么样子的,首先我们需要在组件的修饰器中配置AuthService,然后在组件的构造函数中使用参数进行依赖注入:
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../core/auth.service';
@Component({
selector: 'app-login',
template: '
<div>
<input #usernameRef type="text">
<input #passwordRef type="password">
<button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
</div>
',
styles: [],
//在providers中配置AuthService
providers:[AuthService]
})
export class LoginComponent implements OnInit {
//在构造函数中将AuthService示例注入到成员变量service中
//而且我们不需要显式声明成员变量service了
constructor(private service: AuthService) {
}
ngOnInit() {
}
onClick(username, password) {
console.log('auth result is: ' + this.service.loginWithCredentials(username,
password));
}
}
看到这里你会发现我们仍然需要import相关的服务,import是要将类型引入进来,而provider里面会配置这个类型的实例。当然即使这样还是不太爽,可不可以不引入AuthService呢?答案是可以的。
我们看一下app.module.ts,这个根模块文件中我们会发现也有个providers,根模块中的这个providers是配置在模块中全局可用的service或参数的:
providers: [
{provide: 'auth', useClass: AuthService}
]
providers是一个数组,这个数组呢其实是把你想要注入到其他组件中的服务配置在这里。大家注意到我们这里的写法和上面有点区别,没有直接写成:
providers:[AuthService]
而是给出了一个对象,里面有两个属性,provide和useClass,provide定义了这个服务的名称,有需要注入这个服务的就引用这个名称就好。useClass指明这个名称对应的服务是一个类,本例中就是AuthService了。这样定义好之后,我们就可以在任意组件中注入这个依赖了。
下面我们改动一下login.component.ts,去掉头部的import { AuthService } from '../core/auth.service';和组件修饰器中的providers,更改其构造函数为:
constructor(@Inject('auth') private service) {
}
我们去掉了service的类型声明,但加了一个修饰符@Inject('auth'),这个修饰符的意思是请到系统配置中找到名称为auth的那个依赖注入到我修饰的变量中。当然这样改完后你会发现Inject这个修饰符系统不识别,我们需要在@angular/core中引用这个修饰符,现在login.component.ts看起来应该是下面这个样子:
import { Component, OnInit, Inject } from '@angular/core';
@Component({
selector: 'app-login',
template: '
<div>
<input #usernameRef type="text">
<input #passwordRef type="password">
<button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
</div>
',
styles: []
})
export class LoginComponent implements OnInit {
constructor(@Inject('auth') private service) {
}
ngOnInit() {
}
onClick(username, password) {
console.log('auth result is: ' + this.service.loginWithCredentials(username,
password));
}
}
注意依赖性注入不是仅仅为Service服务的,任何的类都可以通过这种方式提供和注入,它提供了一种解耦的方式,通过Providers提供,通过constructor注入:
constructor(userService: UserService) {
userService.addUser({username: 'wang', password:'1234'});
}
注入器从哪得到的依赖?它可能在自己内部容器里已经有该依赖了。如果它没有,也能在提供商的帮助下新建一个。提供商就是一个用于交付服务的配方,它被关联到一个令牌。Angular会使用一些自带的提供商来初始化这些注入器。我们必须自行注册属于自己的提供商,通常用组件或者指令元数据中的providers数组进行注册。简单的类提供商是最典型的例子。只要在providers数值里面提到该类就可以了。
providers: [ AuthService, UserService ]
除了上面那种最简单的提供方式之外,我们还能以令牌方式提供。我们通常在构造函数里面,为参数指定类型,让Angular来处理依赖注入。该参数类型就是依赖注入器所需的令牌。Angular把该令牌传给注入器,然后把得到的结果赋给参数。下面是一个典型的例子:
providers: [
{ provide: 'auth', useClass: AuthService },
{ provide: 'user', useClass: UserService },
{ provide: BASE_URL, useValue: 'http://localhost:3000/todos' },
AuthGuardService
]
我们发现providers数组是由一系列的provide对象构成的,这个对象是{provide: ..., useClass: ...}或者{provide: ..., useValue: ...}形式的。我们把第一个属性叫令牌,第二个属性叫定义对象。这两种形式分别对应类供应商和值供应商。
值供应商通常用来进行运行期常量设置,比如网站的基础地址和功能标志等。那么最简单的那种情形是怎么回事呢?比如:providers: [ AuthGuardService ],其实这是一个语法糖,等价于{provide: AuthGuardService, useClass: AuthGuardService} 。
{ provide: BASE_URL, useValue: ‘http://localhost:3000/todos’ } 这个例子和其他的好像还是不太一样,BASE_URL不是个字符串对象也不是一个类对象。这是我们创建的一个令牌,这样创建的令牌拥有一个友好的名字,但不会与其他的同名令牌发生冲突:
import { OpaqueToken } from '@angular/core';
export const BASE_URL = new OpaqueToken('BASE_URL');
当然还有另外两种情形,一种叫别名提供商,我们为同一个对象起了不同的别名:
{ provide: MinimalLogger, useExisting: LoggerService },
另一种叫工厂提供商,提供商通过调用工厂函数来新建一个依赖对象,如下所示:
{ provide: HELLO, useFactory: helloFactory(2), deps: [Greeting, HelloService] }
使用这项技术,可以用包含了一些依赖服务和本地状态输入的工厂函数来建立一个依赖对象。helloFactory自身不是提供商工厂函数。真正的提供商工厂函数是helloFactory返回的函数:
export function helloFactory(take: number) {
return (greeting: Greeting, helloService: HelloService): string => {
/* ... */
};
};