从这个系列的第一章开始到第五章,基于rxjs的响应式编程的基础知识基本上就介绍完了,当然有很多知识点没有提到,比如 Scheduler, behaviorSubject,replaySubject等,不是他们不重要,而是碍于时间、精力等原因没办法一一详细介绍。从这章开始将把响应式放在angular的大环境中,看如何在实际项目中去使用,当然这些都是个人在使用中的一些经验,如有不妥,欢迎指正。

另外本章开始的示例代码可能只是一些片段,或思路,正式要跑起来需要各位自己将代码放入正确的环境中。

angular中响应式接口无处不在

既然 angular 中内置了rxjs,必须有好多地方都能找到响应式的影子,客官请看:

ActivatedRoute - 经常用它来获取路由上的信息,比如传递的参数等。

export interface ActivatedRoute {
    url: Observable<UrlSegment[];>
    params: Observable<Params>;
    queryParams: Observable<Params>;
    fragment: Observable<string>;
    data: Observable<Data>;
    get paramMap: Observable<ParamMap>;
    get queryParamMap: Observable<ParamMap>;
    toString(): string;
}

AbstractControl - FormControl的基类,尤其响应式表单中,你一定见过它。

export abstract class AbstractControl {
    get valueChanges: Observable<any>;
    get statusChanges: Observable<any>;
}

Http - 这个更不用说,使用Http通信的项目离了它简直了没法干活。

export class Http {
    get(url: string, options?: RequestOptionsArgs): Observable<Response>;
    post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    head(url: string, options?: RequestOptionsArgs): Observable<Response>;
}

EventEmitter - 组件向外传递数据时,你一定用过吧?

export class EventEmitter<T> extends Subject {
    subscribe(generatorOrNext?: any, error?: any, complete?: any): any;
}

没有发现Observable?仔细看,它继承自Subject,那Subject呢?接着看:

export declare class Subject<T> extends Observable<T> 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<string>; // 随机码,在页面上展示给用户

    generateCode$: Subject<boolean> = new Subject(); // 和服务交互,通知服务我们需要一个随机码。

    // 这里我们没有定义这个接口,你可以想像它就是登录表单的值,例如: { username: string; password: string; randomCode: string}; 它的功能就是发出登录请求的数据。
    login$: Subject<LoginFormValue> = 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<LoginFormValue>): 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<boolean>): Observable<string> {
        return this.http.get(url)
            .map((res: Response) => {
                // 假设数据保存在random 字段下
                const body = res.json();

                return body.data.random;
            });
    }

    isLoginSuccess(): Observable<boolean> {
        return this.login$.mapTo(true);
    }
}

基于开始说到的思路,我们基本搭建好了一个完全基于响应式风格的登录组件的骨架,可以说基本的套路出来了,暂时先到这里。各位可以先想一下可以扩展哪些功能,比如实现30秒换一次验证码,用户点击时立即更换验证码等,下次继续。

图片描述


sxlwar
178 声望12 粉丝