如果你也想和我们一起, 翻译更多优质的 RxJS 文章以奉献给大家, 请点击[这里] https://github.com/RxJS-CN/rxjs-articles-translation
温馨提示: 文章较长, 原文中写的是 40 分钟阅读, 建议大家午后有大把空闲时间再慢慢读来
开发 web 应用时, 性能始终都是重中之重. 要想提升 Angular 应用的速度, 我们可以做一些工作, 比如要摇树优化 (tree-shaking),AoT (ahead-of-time), 模块的懒加载以及缓存. 想要对 Angular 应用的性能提升的实战技巧有一个全面了解的话, 我们强烈推荐你参考由 Minko Gechev https://twitter.com/mgechev 撰写的 Angular 性能检测表 https://github.com/mgechev/angular-performance-checklist#lazy-loading-of-resources . 在本文中, 我们将专注于缓存.
实际上, 缓存是提升网站用户体验的最有效的方式之一, 尤其是当用户使用宽带受限的设备或网络环境较差.
缓存数据或资源的方式有很多种. 静态资源通常都是由标准的浏览器缓存或 Service Workers 来进行缓存. 虽然 Service Workers 也可以缓存 API 请求, 但是对于图像, html,JS 或 CSS 文件等资源的缓存, 它们通常更为有用. 我们通常使用自定义机制来缓存应用的数据.
无论我们使用的是什么机制, 缓存通常都是提升应用的响应能力, 减少网络花销, 并具有内容在网络中断时可用的优势. 换句话说, 当内容被缓存的更接近消费者时, 比如在客户端, 请求将不会导致额外的网络活动, 并且可以更快速地检索缓存数据, 从而节省了网络往返的整个过程.
在本文中, 我们将使用 RxJS 和 Angular 提供的工具来开发一个高级缓存机制.
目录
动机
需求
实现基础缓存
自动更新
发送更新通知
按需拉取新数据
展望
特别鸣谢
动机
不时地就会有人问, 如何在大量使用 Observables 的 Angular 应用中缓存数据? 大多数人对于如何使用 Promises 来缓存数据有不错的理解, 但当切换至响应式编程时, 便会因为它的复杂度 (庞大的 API), 思维转化 (从命令式到声明式) 和众多概念而感到不知所措. 因此, 很难将一个基于 Promises 的现有缓存机制转换成基于 Observables 的, 当你想要缓存机制变得更高级点时更是如此.
在 Angular 应用中通常使用 HttpClientModule 中的 HttpClient 来执行 HTTP 请求. HttpClient 的所有 API 都是基于 Observable 的, 也就是说像 get ,post ,put 或 delete 等方法返回的都是 Observable . 因为 Observables 天生是惰性的, 所以只有当我们调用 subscribe 时才会真正发起请求. 但是, 对同一个 Observable 调用多次 subscribe 会导致源 Observable 一遍又一遍地重新创建, 每个订阅 (subscription) 上执行一个请求. 我们称之为冷的 Observables .
- export interface Joke {
- id: number;
- joke: string;
- categories: Array<string>;
- }
- export interface JokeResponse {
- type: string;
- value: Array<Joke>;
- }
- import { Injectable } from '@angular/core';
- import { HttpClient } from '@angular/common/http';
- @Injectable()
- export class JokeService {
- constructor(private http: HttpClient) { }
- get jokes() {
- ...
- }
- }
- import { map } from 'rxjs/operators';
- @Injectable()
- export class JokeService {
- constructor(private http: HttpClient) { }
- get jokes() {
- ...
- }
- private requestJokes() {
- return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
- map(response => response.value)
- );
- }
- }
- import { Observable } from 'rxjs/Observable';
- import { shareReplay, map } from 'rxjs/operators';
- const API_ENDPOINT = 'https://api.icndb.com/jokes/random/5?limitTo=[nerdy]';
- const CACHE_SIZE = 1;
- @Injectable()
- export class JokeService {
- private cache$: Observable<Array<Joke>>;
- constructor(private http: HttpClient) { }
- get jokes() {
- if (!this.cache$) {
- this.cache$ = this.requestJokes().pipe(
- shareReplay(CACHE_SIZE)
- );
- }
- return this.cache$;
- }
- private requestJokes() {
- return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
- map(response => response.value)
- );
- }
- }
- @Component({
- ...
- })
- export class JokeListComponent implements OnInit {
- jokes$: Observable<Array<Joke>>;
- constructor(private jokeService: JokeService) { }
- ngOnInit() {
- this.jokes$ = this.jokeService.jokes;
- }
- ...
- }
- import { interval } from 'rxjs/observable/interval';
- interval(10000).subscribe(console.log);
- import { timer } from 'rxjs/observable/timer';
- import { switchMap, shareReplay } from 'rxjs/operators';
- const REFRESH_INTERVAL = 10000;
- @Injectable()
- export class JokeService {
- private cache$: Observable<Array<Joke>>;
- constructor(private http: HttpClient) { }
- get jokes() {
- if (!this.cache$) {
- // 设置每 X 毫秒发出值的定时器
- const timer$ = timer(0, REFRESH_INTERVAL);
- // 每个时间点都会发起 HTTP 请求来获取最新数据
- this.cache$ = timer$.pipe(
- switchMap(_ => this.requestJokes()),
- shareReplay(CACHE_SIZE)
- );
- }
- return this.cache$;
- }
- ...
- }
- import { take } from 'rxjs/operators';
- @Component({
- ...
- })
- export class JokeListComponent implements OnInit {
- ...
- ngOnInit() {
- const initialJokes$ = this.getDataOnce();
- ...
- }
- getDataOnce() {
- return this.jokeService.jokes.pipe(take(1));
- }
- ...
- }
- import { Subject } from 'rxjs/Subject';
- @Component({
- ...
- })
- export class JokeListComponent implements OnInit {
- update$ = new Subject<void>();
- ...
- }
- <div class="notification">
- <span>There's new data available. Click to reload the data.</span>
- <button mat-raised-button color="accent" (click)="update$.next()">
- <div class="flex-row">
- <mat-icon>cached</mat-icon>
- UPDATE
- </div>
- </button>
- </div>
- import { Subject } from 'rxjs/Subject';
- import { merge } from 'rxjs/observable/merge';
- import { mergeMap } from 'rxjs/operators';
- @Component({
- ...
- })
- export class JokeListComponent implements OnInit {
- update$ = new Subject<void>();
- forceReload$ = new Subject<void>();
- ...
- ngOnInit() {
- ...
- const updates$ = merge(this.update$, this.forceReload$).pipe(
- mergeMap(() => this.getDataOnce())
- );
- ...
- }
- ...
- }
- import { Observable } from 'rxjs/Observable';
- import { Subject } from 'rxjs/Subject';
- import { merge } from 'rxjs/observable/merge';
- import { take, switchMap, mergeMap, skip, mapTo } from 'rxjs/operators';
- @Component({
- ...
- })
- export class JokeListComponent implements OnInit {
- showNotification$: Observable<boolean>;
- update$ = new Subject<void>();
- forceReload$ = new Subject<void>();
- ...
- ngOnInit() {
- ...
- const reload$ = this.forceReload$.pipe(switchMap(() => this.getNotifications()));
- const initialNotifications$ = this.getNotifications();
- const show$ = merge(initialNotifications$, reload$).pipe(mapTo(true));
- const hide$ = this.update$.pipe(mapTo(false));
- this.showNotification$ = merge(show$, hide$);
- }
- getNotifications() {
- return this.jokeService.jokes.pipe(skip(1));
- }
- ...
- }
来源: https://juejin.im/post/5b763a20f265da283e7145a9