乐趣区

响应式编程的思维艺术-5Angular中Rxjs的应用示例

本文是【Rxjs 响应式编程 - 第四章 构建完整的 Web 应用程序】这篇文章的学习笔记。

示例代码托管在:http://www.github.com/dashnowords/blogs

博客园地址:《大史住在大前端》原创博文目录

华为云社区地址:【你要的前端打怪升级指南】

[TOC]

一. 划重点

  • RxJS-DOM

    原文示例中使用这个库进行 DOM 操作,笔者看了一下 github 仓库,400 多星,而且相关的资料很少,所以建议理解思路即可,至于生产环境的使用还是三思吧。开发中 Rxjs 几乎默认是和 Angular 技术栈绑定在一起的,笔者最近正在使用 ionic3 进行开发,本篇将对基本使用方法进行演示。

  • 冷热 Observable

    • 冷 Observable 从被订阅时就发出整个值序列
    • 热 Observable 无论是否被订阅都会发出值,机制类似于 javascript 事件。
  • 涉及的运算符

    bufferWithTime(time:number)- 每隔指定时间将流中的数据以数组形式推送出去。

    pluck(prop:string)– 操作符,提取对象属性值,是一个柯里化后的函数,只接受一个参数。

二. Angular 应用中的 Http 请求

Angular应用中基本 HTTP 请求的方式:

import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {MessageService} from './message.service';// 某个自定义的服务
import {HttpClient, HttpParams, HttpResponse} from '@angular/common/http';

@Injectable({providedIn: 'root'})
export class HeroService {
  private localhost = 'http://localhost:3001';
  private all_hero_api = this.localhost + '/hero/all';// 查询所有英雄
  private query_hero_api = this.localhost + '/hero/query';// 查询指定英雄

  constructor(private http:HttpClient) { }
  
  /* 一般 get 请求 */
  getHeroes(): Observable<HttpResponse<Hero[]>>{return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'});
  }

  /* 带参数的 get 请求 */
  getHero(id: number): Observable<HttpResponse<Hero>>{let params = new HttpParams();
        params.set('id', id+'');
        return this.http.get<Hero>(this.query_hero_api,{params:params,observe:'response'});
  }
  
  /* 带请求体的 post 请求,any 可以自定义响应体格式 */
  createHero(newhero: object): Observable<HttpResponse<any>>{return this.http.post<HttpResponse<any>>(this.create_hero_api,{data:newhero},{observe:'response'});
  } 
}

express 中写一些用于测试的虚拟数据:

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/all', function(req, res, next) {
  let heroes = [{
    index:1,
    name:'Thor',
    hero:'God of Thunder'
  },{
    index:2,
    name:'Tony',
    hero:'Iron Man'
  },{
    index:3,
    name:'Natasha',
    hero:'Black Widow'
  }]
  res.send({
     data:heroes,
     result:true
  })
});

/* GET home page. */
router.get('/query', function(req, res, next) {console.log(req.query);
  let hero= {
    index:4,
    name:'Steve',
    hero:'Captain America'
  }
  res.send({
     data:hero,
     result:true
  })
});


/* GET home page. */
router.post('/create', function(req, res, next) {console.log(req.body);
  let newhero = {
     index:5,
     name:req.body.name,
     hero:'New Hero'
  }
  res.send({
     data:newhero,
     result:true
  })
});

module.exports = router;

在组件中调用上面定义的方法:

sendGet(){this.heroService.getHeroes().subscribe(resp=>{console.log('响应信息:',resp);
   console.log('响应体:',resp.body['data']);
 })
}

sendQuery(){this.heroService.getHero(1).subscribe(resp=>{console.log('响应信息:',resp);
  console.log('响应体:',resp.body['data']);
})
}

sendPost(){this.heroService.createHero({name:'Dash'}).subscribe(resp=>{console.log('响应信息:',resp);
  console.log('响应体:',resp.body['data']);
})
}

控制台打印的信息可以看到后台的虚拟数据已经被请求到了:

三. 使用 Rxjs 构建 Http 请求结果的处理管道

3.1 基本示例

尽管看起来 Http 请求的返回结果是一个可观测对象,但是它却没有 map 方法,当需要对 http 请求返回的可观测对象进行操作时,可以使用 pipe 操作符来实现:

import {Observable, of, from} from 'rxjs';
import {map , tap, filter, flatMap}from 'rxjs/operators';

/* 构建一个模拟的结果处理管道
*map 操作来获取数据
*tap 实现日志
*flatMap 实现结果自动遍历
*filter 实现结果过滤
*/
getHeroes$(): Observable<HttpResponse<Hero[]>>{return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'})
    .pipe(map(resp=>resp.body['data']),
          tap(this.log),
          flatMap((data)=>{return from(data)}),
          filter((data)=>data['index'] > 1)
    );
}

很熟悉吧?经过处理管道后,一次响应中的结果数据被转换为逐个发出的数据,并过滤掉了不符合条件的项:

3.2 常见的操作符

Angular中文网列举了最常用的一些操作符,RxJS 官方文档有非常详细的示例及说明,且均配有形象的大理石图,建议先整体浏览一下有个印象,有需要的读者可以每天熟悉几个,很快就能上手,运算符的使用稍显抽象,且不同运算符的组合使用在流程控制和数据处理方面的用法灵活多变,也是有很多 套路 的,开发经验需要慢慢积累。

四. 冷热 Observable 的两种典型场景

原文中提到的冷热 Observable 的差别可以参考这篇文章【RxJS:冷热模式的比较】,概念本身并不难理解。

4.1 shareReplay 与请求缓存

开发中常会遇到这样一种场景,某些集合型的常量,完全是可以复用的,通常开发者会将其进行缓存至某个全局单例中,接着在优化阶段,通过增加一个 if 判断在请求之前先检查缓存再决定是否需要请求,Rxjs提供了一种更优雅的实现。

先回顾一下上面的 http 请求代码:

getHeroes(): Observable<HttpResponse<Hero[]>>{return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'});
}

http请求默认返回一个 冷 Observable,每当返回的流被订阅时就会触发一个新的 http 请求,Rxjs中通过 shareReplay() 操作符将一个可观测对象转换为 热 Observable(注意:shareReplay()不是唯一一种可以加热 Observable 的方法),这样在第一次被订阅时,网络请求被发出并进行了缓存,之后再有其他订阅者加入时,就会得到之前缓存的数据,运算符的名称已经很清晰了,【share- 共享】,【replay- 重播】,是不是形象又好记。对上面的流进行一下转换:

  getHeroes$(): Observable<HttpResponse<Hero[]>>{return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'})
    .pipe(map(resp=>resp.body['data']),
      tap(this.log),
      flatMap((data)=>{return from(data)}),
      filter((data)=>data['index'] > 1),
      shareReplay() // 转换管道的最后将这个流转换为一个热 Observable)
  }

在调用的地方编写调用代码:

sendGet(){let obs = this.heroService.getHeroes$();
     // 第一次被订阅
     obs.subscribe(resp=>{console.log('响应信息:',resp);
     });
    // 第二次被订阅
     setTimeout(()=>{obs.subscribe((resp)=>{console.log('延迟后的响应信息',resp);
       })
     },2000)
}

通过结果可以看出,第二次订阅没有触发网络请求,但是也得到了数据:

网络请求只发送了一次(之前的会发送两次):

4.2 share 与异步管道

这种场景笔者并没有进行生产实践,一是因为这种模式需要将数据的变换处理全部通过 pipe() 管道来进行,笔者自己的函数式编程功底可能还不足以应付,二来总觉得很多示例的使用场景很牵强,所以仅作基本功能介绍,后续有实战心得后再修订补充。Angular中提供了一种叫做 异步管道 的模板语法,可以直接在 *ngFor 的微语法中使用可观测对象:

<ul>
  <li *ngFor="let contact of contacts | async">{{contact.name}}</li>
</ul>
<ul>
  <li *ngFor="let contact of contacts2 | async">{{contact.name}}</li>
</ul>

示例:

this.contacts = http.get('contacts.json')
                    .map(response => response.json().items)
                    .share();
setTimeout(() => this.contacts2 = this.contacts, 500);

五. 一点建议

一定要好好读官方文档。

退出移动版