从这个系列的第一章开始到第五章,基于rxjs的响应式编程的基础知识基本上就介绍完了,当然有很多知识点没有提到,比如 Scheduler, behaviorSubject,replaySubject等,不是他们不重要,而是碍于时间、精力等原因没办法一一详细介绍。从这章开始将把响应式放在angular的大环境中,看如何在实际项目中去使用,当然这些都是个人在使用中的一些经验,如有不妥,欢迎指正。
另外本章开始的示例代码可能只是一些片段,或思路,正式要跑起来需要各位自己将代码放入正确的环境中。
angular中响应式接口无处不在
既然 angular 中内置了rxjs,必须有好多地方都能找到响应式的影子,客官请看:
ActivatedRoute - 经常用它来获取路由上的信息,比如传递的参数等。
export interface ActivatedRoute {
url: Observable
params: Observable;
queryParams: Observable;
fragment: Observable;
data: Observable;
get paramMap: Observable;
get queryParamMap: Observable;
toString(): string;
}
AbstractControl - FormControl的基类,尤其响应式表单中,你一定见过它。
export abstract class AbstractControl {
get valueChanges: Observable;
get statusChanges: Observable;
}
Http - 这个更不用说,使用Http通信的项目离了它简直了没法干活。
export class Http {
get(url: string, options?: RequestOptionsArgs): Observable;
post(url: string, body: any, options?: RequestOptionsArgs): Observable;
head(url: string, options?: RequestOptionsArgs): Observable;
}
EventEmitter - 组件向外传递数据时,你一定用过吧?
export class EventEmitter extends Subject {
subscribe(generatorOrNext?: any, error?: any, complete?: any): any;
}
没有发现Observable?仔细看,它继承自Subject,那Subject呢?接着看:
export declare class Subject extends Observable implements ISubscription {
...省略
}
Subject 最终还得继承自 Observable。当然还有很多其它的,总而言之请记住响应式的世界里everything is Observable,不管是输入还是输出。
搭建响应式的组件
输入和输出是编程中两个无处不在的东西,只要涉及到交互的东西,都可以把它抽象成输入和输出。
最明显的,当我们使用 @Input 和 @Output 无疑是在和输入和输出打交道,除此之外呢。如果我们把定义component的 Class看作一部分,那么它给template 传递的数据也可以认为是一种输出,而它从各service获取的数据也可以当作一种数据输入。基于这种想法,我们可以认为一个组件就是连接数据和模板的桥梁,它最主要的功能就是获取服务中的数据作为输入输出给模板,当然也可以获取模板中产生的数据作为输入输出给服务。于是我们可以抽象出这样一个组件:
export abstract class BaseComponent {
abstract subscription$$: Subscription; // 用于在组件销毁时取消不得不手动订阅的一些流。
abstract launch(option?: any): void; // 给服务输出数据
abstract initialModel(option?: any): void; // 从服务中获取数据输入
}
- initialModel 所有组件中要用到的数据都在这个方法中获得,再分发给数据的使用者。
- launch 所有组件中需要向服务传递的数据都会在这个方法中向外传递。
- subscription$$ 在实际项目中,无法避免会手动订阅一些流,其中的某一些流可能需要我们手动释放,这个变量可以全权负责,而且它的初始化基本上会被固定在 launch 方法中。
假设我们需要实现一个带有图片验证码的登录功能,我们来实现它的数据交互。
@Component({
...
})
export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
subscription$$: Subscription;
randomCode: Observable; // 随机码,在页面上展示给用户
generateCode$: Subject = new Subject(); // 和服务交互,通知服务我们需要一个随机码。
// 这里我们没有定义这个接口,你可以想像它就是登录表单的值,例如: { username: string; password: string; randomCode: string}; 它的功能就是发出登录请求的数据。
login$: Subject = new Subject();
constructor(private auth: AuthService) { } // 这个服务随后实现
ngOnInit() {
this.initialModel();
this.launch();
this.goTo(); // 初始化时就调用跳转函数。
}
initialModel() {
this.randomCode = this.auth.getRandomCode();
}
launch() {
this.subscription = this.auth.login(this.login$)
.add(this.auth.generateRandomCode(this.generateCode$));
}
goTo() {
this.subscription.add(
this.auth.isLoginSuccess().subscribe(success => {
// 跳转逻辑等等。
})
)
}
ngOnDestroy {
this.subscription$$.unsubscribe();
}
}
通过阅读以上代码,我们的核心关注点只需要放在 initialModel 和 launch 两个方法上,一个告诉我们获取了哪些数据,一个告诉我们输出了哪此数据。另外你会发现,登录动作和获取验证码的动作在组件初始化时就已经告诉了服务,这种命令式的是完全不同的两种风格,在命令式的风格中我们都是在等到用户点击登录按钮时才去调用login函数,发起登录动作。下面来看服务代码:
@Injectable()
export class AuthService {
login$: BehaviorSubject = new BehaviorSubject();
constructor(private http: Http) { }
login(data: Observable): Subscription {
// url: 请求的url; Response: angular 定义的http响应接口。
return this.http.post(url)
.map((res: Response) => {
// 假设登录成功会后台会返回token,这里我们利用 BehaviorSubject 来保存这个 token;
const body = res.json();
return body.data.token;
})
.subscribe(this.login$);
}
generateRandomCode(signal: Observable): Observable {
return this.http.get(url)
.map((res: Response) => {
// 假设数据保存在random 字段下
const body = res.json();
return body.data.random;
});
}
isLoginSuccess(): Observable {
return this.login$.mapTo(true);
}
}
基于开始说到的思路,我们基本搭建好了一个完全基于响应式风格的登录组件的骨架,可以说基本的套路出来了,暂时先到这里。各位可以先想一下可以扩展哪些功能,比如实现30秒换一次验证码,用户点击时立即更换验证码等,下次继续。