上回搭建了一个组件以及它所依赖的服务的基本结构,这节接着它继续。另外从本节开始,统一采用rxjs6的风格,6和5在写法上最大的不同就是弃用链式调用,而采用pipe的方法,当然也有一些其它的变更,请自行翻阅文档。

响应式组件

上节中错误的把generateRandomCode 添加到了 subscription中,实际中它返回的是一个Observable,先调整过来。

initialModel() {
        this.randomCode = this.auth.generateRandomCode(this.generateCode$);
}

删除 launch方法中的 .add(this.auth.generateRandomCode(this.generateCode$));

服务代码

验证码变更的逻辑有两处,第一是程序设定好的时间周期到达后,第二是用户点点击时。我们的组件中已经设置了一个subject来获取用户的输入,并且已经传给了服务,现在我们来完善服务中获取验证码的逻辑,也就是那个generateRandomCode方法。

generateRandomCode(signal: Observable<boolean>): Observable<string> {
    // 每30秒向后台获取一次验证码
    const period = timer(0, 1000 * 30) // 第一个参数延迟时间,第二个参数间隔周期。
            .pipe(
                mapTo(true)
            );

    // 将这两条流合并成一条请求流。
    const request = merge(signal, period);

    // 修改返回,当请求流中产生数据后,向服务器请求并将结果返回。
    return request.pipe(
        switchMapTo(this.http.get(url)),
        map((res: Response)) => {
            // 假设数据保存在random 字段下
            const body = res.json();

            return body.data.random;
        }
    )
}

随便码请求的过程就完成了,当然还可以添加其它逻辑,如限制用户10秒内最多可以获取一次,可以给传入的 signal 加 debounce time

const signal2 = signal.pipe(
    debounceTime(10 * 1000)
)

另外还有对于错误的处理等,可以参照之前 前端大耍 的那篇 《Angular Http 请求出错后重试》。

既然说到了http的失败重试,其实websocket也一样。

websocket的响应式

首先我们实现一个建议websocket连接的方法,代码如下:

// url 和 protocols 不需要解释; input: 请求数据的流,通过它来向服务端发送数据。
function connect(url, input, protocols) {

    const connectionStatus = new BehaviorSubject(0); // 用来查看当前连接的状态。

    const messages = new Observable(function (observer) {
        
        const socket = new WebSocket(url, protocols);

        const inputSubscription;

        const open = false;

        const forcedClose = false;

        const closed = () => {
            if (!open) return;
            connectionStatus.next(connectionStatus.getValue() - 1);
            open = false;
        };

        socket.onopen = () => {
            open = true;
            connectionStatus.next(connectionStatus.getValue() + 1);
            inputSubscription = input.subscribe( data => socket.send(data));
        };

        socket.onmessage = message => observer.next(message.data);

        socket.onerror = error => {
            closed();
            observer.error(error);
        };

        socket.onclose = event => {
            closed();
            if (forcedClose)
                observer.complete();
            else
                observer.error(new Error(event.reason));
        };

        return function () {
            forcedClose = true;
            
            if (inputSubscription)
                inputSubscription.unsubscribe();
            if (open) {
                closed();
                socket.close();
            }
        };
    });

    return { messages: messages, connectionStatus: connectionStatus };
}

利用上面实现的方法,可以很方便的拿到响应流,即 message。

@Injectable()
export class WebSocketService {
    inputStream: Subject<any> = new Subject(); // 和上面写好的方法进行通信

    message: Observable<any>; // 暴露给调用者来获取数据

    constructor() {
        this.connect()
    }

    // 暴露给其它服务或组件来发送数据,同时获得请求对应的响应。
    send(data) {
        this.inputStream.next(data);

        // 由于websocket 不同于http,请求和响应一一对应的很好,所以这里可能需要一定的条件来拿到请求对应的响应。
        return this.message.pipe(
            filter(message => message.flag === data.flag)
        );
    }

    private connect(): void {
        if(this.message) return;

        const { message, connectStatus } = connect(youUrl, this.inputSteam);

        this.message = message.pipe(
            map(response => /*处理数据逻辑*/),
            retryWhen(error => error.pipe(
                tap(err => console.log(err)),  // 打印一下错误,可以换成其它的逻辑。
                delay(500) // 500毫秒后发起重试
            )),
            share() // 将这个流变为hot,至于流的hot 和 cold可能需要单独的文章来解释。
        );

        connectStatus.subscribe(status => console.log('当前连接的状态:' + status))
    }
}

可以看出在响应式的代码中,很少需要维护一些中间数据状态,数据都是在流中获取,转换和传递,订阅者对数据最好可以实现开箱即用,无需另外的加工。

图片描述


sxlwar
178 声望12 粉丝