原文:Advanced caching with RxJS
作者:Dominic Elm
在构建Web应用程序时,我们应该始终把性能放在首位。我们可以做很多事情来加速Angular应用程序,比如树抖动(tree shaking)、AoT(提前编译)、延迟加载模块或缓存。为了了解提高Angular应用程序性能的实践,我们强烈建议您查看Minko Gechev的《Angular 性能检查清单》。在这篇文章中,我们关注缓存。
事实上,缓存(caching)是改善网站体验的最有效方法之一,尤其是当用户使用带宽受限的设备或速度较慢的网络时。有几种方法可以缓存数据或资源。静态资源通常使用标准浏览器缓存或 Service Worker 进行缓存。虽然 Service Worker 也可以缓存API请求,但它们通常更适合于缓存图像、HTML、JS或CSS文件等资源。为了缓存应用程序数据,我们通常使用自定义机制。无论我们使用什么机制,缓存通常都会提高应用程序的响应能力,降低网络成本,并具有在网络中断期间内容可用的优势。换句话说,当内容被缓存到离消费者更近的地方时,比如在客户端,请求不会导致额外的网络活动,并且可以更快地检索缓存的数据,因为我们节省了整个网络往返的时间。在这篇文章中,我们将使用RxJS和Angular提供的工具开发一种高级缓存机制。
动机
我们时不时会遇到这样一个问题:如何在大量使用Observable的 Angular 应用程序中缓存数据。大多数人对如何用Promise缓存数据有很好的理解,但是当涉及到函数式反应式编程时,由于其复杂性(大型API)、思维方式的根本转变(从命令式到声明式)以及大量的概念,就感到不知所措了。因此,很难将现有的基于Promise的缓存机制转换为Observable,而想用高级一点的机制就更难了。
在Angular应用程序中,我们通常通过HttpClientModule附带的HttpClient执行HTTP请求。它的所有API都是基于Observable的,这意味着像get、post、put或delete这样的方法返回一个Observable。因为Observable本质上是懒惰的,只有当我们调用subscribe时才会发出请求。但是,对同一个Observable多次调用subscribe将导致源Observable被一次又一次地重新创建,因此,对每个订阅执行一个请求。我们称之为“冷Observable”。
这里简单解释一下什么叫“冷Observable”。根据RxJS的定义,冷Observable在订阅时开始运行,也就是说,当Subscribe被调用时,可观察序列才开始向观察者推送值。这与热Observable不同,比如鼠标移动事件或股票行情,它们甚至在订阅活动之前就已经产生了值。
这种行为使实现基于Observable的缓存机制变得很棘手。如果采用简单的方法,通常需要相当数量的样板文件(boilerplate),我们可能最终会绕过RxJS,这是可行的,但如果我们想利用Observable的能力,则不是推荐的方法。
需求
在深入研究代码之前,我们先定义我们的高级缓存机制的需求。我们想构建一个名为“笑话世界”的应用程序。这是一个简单的应用程序,随机显示给定类别的笑话(为了保持简单和集中,只有一个类别)。
这个应用程序有三个组件:AppComponent、DashboardComponent和JokeListComponent。AppComponent是我们的入口点,它呈现一个工具栏以及一个基于当前路由器状态填充的。DashboardComponent只是显示一个类别列表。从这里,我们可以导航到JokeListComponent,在屏幕上呈现笑话列表。笑话本身是从使用Angular的HttpClient服务的服务器获取的。为了保持组件的责任集中并分离关注点,我们希望创建一个负责请求数据的JokeService。然后,组件可以简单地注入服务并通过其public API访问数据。以上所有这些只是我们应用程序的体系结构,还没有涉及缓存。
接下来,从dashboard导航到列表视图时,我们会优先从缓存中请求数据,而不是每次都从服务器请求数据。这个缓存的底层数据每10秒更新一次。当然,我们可以使用更复杂的方法来更新缓存(例如web socket推送更新),每隔10秒轮询一次新数据并不是一个可靠的策略,这里只是为了尽量保持简单,以集中讨论缓存机制。
然后,我们会收到一些更新通知。对于我们的应用程序,我们希望UI(JokeListComponent)中的数据不是在缓存更新时自动更新,而是等待用户强制执行UI更新。为什么?想象一下,如果数据自动更新,一个用户可能正在读其中一个笑话,然后突然它就消失了,那将是糟糕的用户体验。因此,当有新数据可用时,我们的用户会收到通知,而不是自动更新。另外,为了让这个应用更有趣,我们希望用户能够强制缓存更新。这与单独更新UI不同,因为强制更新意味着从服务器新请求数据,更新缓存,然后相应地更新UI。
总结一下我们的需求:
应用程序包含两个组件,从组件A导航到B组件应该优先从缓存请求B的数据,而不是每次从服务器请求数据
缓存每10秒更新一次
UI中的数据不会自动更新,需要用户强制执行更新
用户可以强制进行更新,这将导致发起请求,并实际更新缓存和UI
app 效果图
实现基本的缓存
让我们从简单开始,逐步优化直到最终的完整解决方案。
第一步是创建一个Service。
接下来,我们将添加两个interface,一个用于描述笑话(joke)的数据结构,另一个用于强类型化HTTP请求的响应。这不仅是为了满足TypeScript,但最重要的是使用起来更加方便和明显。
export interface Joke {
id: number; joke: string; categories: Array;}export interface JokeResponse {
type: string; value: Array;}
现在让我们实现JokeService。我们不想暴露数据是从缓存提供还是从服务器新请求的实现细节,因此我们只需暴露一个属性jokes,它返回一个检测笑话列表的Observable。为了执行HTTP请求,我们需要确保在Service的构造函数中注入HttpClient服务。以下是JokeService的大致结构:
import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';@Injectable()export class JokeService {
constructor(private http: HttpClient) { } get jokes() {
... }}
接下来,我们实现一个私有方法requestJokes(),它使用HttpClient执行GET请求来检索笑话列表。
import { map } from 'rxjs/operators';@Injectable()export class JokeService {
constructor(private http: HttpClient) { } get jokes() {
... } private requestJokes() {
return this.http.get(API_ENDPOINT).pipe( map(response => response.value) ); }}
有了它,我们就拥有了实现jokes的getter方法所需的一切。一种简单的做法是简单地返回 this.requestJokes(),但这样没用。我们知道,HttpClient的所有public方法,例如get,都返回冷Observable。这意味着为每个订阅者重新发出整个数据流,从而导致HTTP请求的开销。而我们的应用程序想做的是利用缓存尽量减少网络请求数量并加提升载速度。所以我们想让数据流变成热Observable。不仅如此,每个新订阅服务器都应该接收最新的缓存值。有一个非常方便的运算符叫做shareReplay。此运算符返回共享订阅基础源