rxjs观察者模式
This article follows on from my previous articles startWith and flatMap vs switchMap.
本文从我之前的文章 startWith 和 flatMap vs switchMap继承而来 。
I like writing about RxJS. Even though I’ve spent so much time with it lately with my active projects, I still find new and exciting features that improve my development workflow.
我喜欢写有关RxJS的文章。 尽管最近在活动项目上花了很多时间,但我仍然发现可以改善开发工作流程的令人兴奋的新功能。
Whilst building a new PWA that we’re working on at Intoware, I found a need to blend together RxJS Observables and JavaScript Promises. Here’s what I found out.
在构建我们在Intoware上正在研究的新PWA的同时,我发现需要将RxJS Observables和JavaScript Promises融合在一起。 这是我发现的。
TLDR; (TLDR;)
Use the from()
Observable to wrap your promises and then you can use them in an RxJS pipeline.
使用from()
Observable来包装诺言,然后可以在RxJS管道中使用它们。
Read on if you’d like to know the details.
如果您想了解详细信息,请继续阅读。
React性编程? (Reactive Programming?)
Before we dive in deep, some quick concepts. The core principles of Reactive Programming involve subscribing to a stream of data.
在深入探讨之前,请先快速了解一些概念。 响应式编程的核心原理涉及订阅数据流。
Before Reactive Programming, you would request the data you needed (perhaps a stock price) and then on a timer perhaps ask for it again.
在进行React式编程之前,您将需要所需的数据(也许是股票价格),然后在计时器上再次询问。
However, now, you can “subscribe” to a stream which will emit when new data is available. Much easier.
但是,现在,您可以“订阅”流,该流将在有新数据可用时发出。 容易得多。
Libraries like RxJS allow the easy creation and subscription of data streams. In addition, Operators will alter the stream in different ways to create a data pipeline.
像RxJS这样的库允许轻松创建和订阅数据流。 此外,操作员将以不同方式更改流以创建数据管道。
Examples of Operators could be:
运营商的示例可以是:
Filter — Only process the stream is certain conditions pass
筛选器 -仅在特定条件通过的情况下处理流
Throttle — Restrict in a time-based manner the data that is being emitted (usually to help with UI bottlenecks)
节流阀 —以基于时间的方式限制正在发射的数据(通常是为了帮助解决UI瓶颈)
Map — Transform the data from the stream (expose properties or perform calculations)
映射 —转换流中的数据(公开属性或执行计算)
Operators and Observables are tied together in some really interesting ways. A simple example could be:
运算符和可观察变量以某些非常有趣的方式绑定在一起。 一个简单的例子可能是:
Fetch me the latest stock ticker symbols, updating no more than once per minute
获取我最新的股票行情自动收录器符号,每分钟更新不超过一次
This would be achieved by:
这将通过以下方式实现:
Creating an
Interval
Observable that emits every 60 seconds创建每60秒发射一次的
Interval
可观察对象- Switching to a GET request to fetch the stock data 切换到GET请求以获取库存数据
- Add the “ShareReplay” Operator to serve the same data for multiple subscribers in the future 添加“ ShareReplay”运算符,以便将来为多个订户提供相同的数据
Note, if you don’t add ShareReplay then the entire pipeline would be called for each subscriber.
请注意,如果不添加ShareReplay,则将为每个订阅者调用整个管道。
fetchStock() {
return interval(1000).pipe(
switchMap(i => this.httpClient.get("http://www.example-stock-api.com/data.json")),
shareReplay()
);
}
fetchStock().Subscribe(i => Console.log("Stock Data: " + i));
In this example, the subscriber is simply waiting for the events and outputting to the console. Even though we have a single Observable with 2 Operators, the pipeline can be as simple or as complicated as you need it to be. There’s no limit as to the number of operators you can add.
在此示例中,订户只是在等待事件并输出到控制台。 即使我们只有一个带有2个运算符的Observable,流水线也可以根据您的需要而简单或复杂。 您可以添加的运算符数量没有限制。
答应吗 (Promises?)
Within the JavaScript world, Promises represent some code that will execute “later” but will notify you when it’s done.
在JavaScript世界中,Promises代表一些代码,这些代码将“稍后”执行,但在完成时会通知您。
Modern API’s such as IndexedDB and the CacheStorage API are promise-based and cannot be used synchronously. The clear advantage of this is that the main thread isn’t being blocked when waiting for some IO, as such, the browser is free to do other things while it fetches the data).
诸如IndexedDB和CacheStorage API之类的现代API都是基于承诺的,因此无法同步使用。 这样做的明显好处是, 在等待某些IO时主线程不会被阻塞,因此, 浏览器在获取数据时可以自由做其他事情)。
Plus, JavaScript now also supports async/await.
另外,JavaScript现在还支持异步/等待。
// Old-Style Promise -> Thencaches.open("MyStorageCache")
.then(cache => /* Do Something with the cache; */);// New-Style async/awaitconst cache = await caches.open("MyStorageCache");
/** Do something with the cache */
Using async/await, the code is much more readable as it’ll flow from top to bottom. Internally the JavaScript engine will wrap your async/await code into the old-style, but, as a developer, you don’t need to worry about that.
使用async / await ,由于代码从上到下流动,因此更具可读性。 在内部,JavaScript引擎会将您的异步/等待代码包装成旧样式,但是作为开发人员,您无需担心。
By using async/await you can escape Callback Hell. Having several nested then()
statements can be Confusing and Difficult to debug. Switching to async/await allows you to keep all the benefits of Promises, without the headaches.
通过使用异步/等待,您可以逃避回调地狱。 具有多个嵌套的then()
语句可能会令人困惑且难以调试。 切换到异步/等待使您能够保留Promises的所有优点,而不会感到头疼。
世界碰撞-放在一起 (Worlds Collide — Putting it Together)
Now that you’re up to speed with the concepts, you may be wondering what they have to do with each other? After all, Observables and Promises both solve completely different problems.
现在您已经掌握了这些概念,您可能想知道它们之间有什么关系? 毕竟,Observable和Promise都解决了完全不同的问题。
To help illustrate why you might want to do this, I’ve created a “Problem Definition”. This real-world example demonstrates why this technology might be useful, and perhaps you could see how it can apply to your own work.
为了帮助说明您为什么要执行此操作,我创建了一个“问题定义”。 这个真实的示例演示了为什么这项技术可能有用,并且也许您可以看到它如何应用于您的工作。
问题 (The Problem)
A website wishes to display live Stock Ticker data. When the site loads, the data must be immediately available from a cache. The data should update every 60 seconds, but, on a connection error, always return from the cache. If the user hits the refresh button, the data should be refreshed immediately.
一个网站希望显示实时股票行情数据。 网站加载后,必须立即从缓存中获取数据。 数据应该每60秒更新一次,但是,在出现连接错误时,始终会从缓存中返回数据。 如果用户单击刷新按钮,则应立即刷新数据。
The UI should be notified on a connection error to inform the user that the data isn’t Live.
应该在连接错误时通知UI,以通知用户数据不是实时的。
This doesn’t sound like a complicated problem, but, when you dig into all of the different states, especially with the error handling, without planning this code can get out of hand quickly.
这听起来不像是一个复杂的问题,但是,当您深入研究所有不同的状态时,尤其是在错误处理中,如果不计划此代码,可能会很快失控。
To tackle, let’s break it down.
要解决,让我们分解一下。
From the above statement, there are 4 potential data sources for the UI:
从上面的陈述中,UI有4个潜在的数据源:
Stock Ticker Data — Live or Cached
股票行情记录数据 -实时或缓存
Is Live Data — True if data is live, otherwise False
是实时数据 -如果数据实时,则为True,否则为False
Is Refreshing — True if data is currently refreshing, otherwise False
正在刷新 -如果当前正在刷新数据,则为True,否则为False
Error Message — If the data isn’t live, an associated error message
错误消息 -如果数据不存在,则相关的错误消息
And, in addition to the 4 data sources for the UI, there is an additional input command, “Refresh” which will interrupt the Timer.
并且,除了UI的4个数据源外,还有一个附加的输入命令“ Refresh ”将中断Timer。
As a bonus point, the refresh button should not be enabled when manual refreshing is occurring, to prevent double-clicks.
值得一提的是,在进行手动刷新时,不应启用刷新按钮,以防止双击。
So, without further ado, I present the design for our new, highly polished application.
因此,事不宜迟,我介绍了针对我们新的高度抛光应用程序的设计。
![Image for post](https://miro.medium.com/max/9999/1*V8PaZT80kZy2fxtWbndeFQ.png)
So … moving on.
所以……继续前进。
As the data is going to come from 2 places, we need the following API’s:
由于数据将来自2个地方,因此我们需要以下API:
Angular Http Client — Used for fetching the data (Observable-based)
Angular Http客户端 —用于获取数据(基于可观察性)
CacheStorage API — Used to store the result, and fetch in the event of an error (Promise-based)
CacheStorage API —用于存储结果,并在发生错误时进行提取(基于Promise)
The immediate challenge is that these API’s conform to different standards.
当前的挑战是这些API符合不同的标准。
To help illustrate the solution, I’m going to break this down into the following chunks:
为了帮助说明解决方案,我将其分为以下几个部分:
- Interval to return the Stock Data with Refresh 刷新返回库存数据的间隔
- Saving the Stock Data into the cache 将库存数据保存到缓存中
- Setting the Boolean States 设置布尔状态
- Error Handling 错误处理
I’m going to be building this using Angular 10 but the concepts will apply to whatever Framework you’re comfortable with.
我将使用Angular 10构建它,但是这些概念将适用于您喜欢的任何Framework。
From the screenshot above, the layout is defined as follows:
在上面的屏幕截图中,布局定义如下:
<div>
<h1>Developer Designed Stock Application</h1>
<button (click)="refreshRequest.emit()" [disabled]="isRefreshing$ | async">Refresh Stock Data</button>
<strong>Current Stock Price: {{ currentPrice$ | async }}</strong>
<span *ngIf="isLive$ | async">Data is being streamed live.</span>
<span *ngIf="!(isLive$ | async)">Data is cached.</span>
<i *ngIf="isRefreshing$ | async">Data is Refreshing...</i>
<strong *ngIf="error$ | async as error">Error: {{ error }}</strong>
</div>
As you can see, our HTML is monitoring 4 observables that represent the state of the application. The price will be updated automatically when the data stream changes and the other <span>
and <strong>
tags will display as necessary. In addition, the Refresh Button is disabled automatically when appropriate, and the request command is fired on the (click)
event.
如您所见,我们HTML正在监视代表应用程序状态的4个可观察对象。 当数据流更改时,价格将自动更新,并且其他<span>
和<strong>
标签将根据需要显示。 此外,在适当的时候会自动禁用“刷新按钮”,并在(click)
事件上触发请求命令。
第1步—图库照片 (Step 1 — Stock Data Observable)
The first task is to create the simple Stock Data Observable. Now, because a good, free, open Stock Ticker API isn’t available I’m going to cheat slightly and use an online random number generator. However, the effect is still the same.
第一项任务是创建简单的“可观察的库存数据”。 现在,由于没有一个好的,免费的,开放的Stock Ticker API,我将作弊并使用一个在线随机数生成器。 但是,效果仍然相同。
The ticker observable will be the primary pipeline for the entire application, where all other state variables will seed their values from.
可观察到的代码将是整个应用程序的主要管道,其他所有状态变量将从中获取其值。
The value for this observable will be updated from 3 sources:
该可观察值的值将从3个来源更新:
- When the application loads 应用程序加载时
- Every 60 seconds 每60秒
- When the refresh button is tapped 当点击刷新按钮时
To do this, we need to create a “seed”. Using the merge()
RxJS operator we are able to create an Observable that uses the values from all 3 sources and will fire when any of them emits a value. We aren’t interested in the value returned from merge()
and are merely using it to kick-start the process.
为此,我们需要创建一个“种子”。 使用merge()
RxJS运算符,我们可以创建一个Observable,它使用来自所有3个源的值,并在它们中的任何一个发出值时将触发。 我们对 merge()
返回的值不感兴趣, 而只是使用它来启动过程。
constructor(private httpClient: HttpClient) {
// The "seed" for our observable kicks off from:
// 1. Blank observable 'of' for an intitial kick
// 2. emit() from requestRequest
// 3. interval(60 seconds)
const seed$ = merge(of(null), this.refreshRequest, interval(10000));
this.currentPrice$ = seed$
.pipe(
// When the Observable fires, request the Stock Price
switchMapTo(this.httpClient.get(this._rngUrl, { responseType: 'text'})),
// Parse the value from the body into a number
map(i => parseInt(i))
);
}
The code demonstrates a really simple 2-stage pipeline that, when triggered, fetches the Stock Ticker price and parses it to a number.
该代码演示了一个非常简单的2级流水线,该流水线在触发时获取股票行情价格并将其解析为一个数字。
However, the magic is in the seed$
value. By using the merge
combinational operator, the pipeline will start for any of the 3 triggers.
但是,魔术在于seed$
价值。 通过使用merge
组合运算符,管道将为3个触发器中的任何一个启动。
![Image for post](https://miro.medium.com/freeze/max/9999/1*b_kY1gmpMaFCJCxF-O1UAQ.gif)
So, that’s Step 1 complete, on to Step 2.
因此,步骤1已完成,步骤2已完成。
第2步-缓存和承诺 (Step 2 — Cache and Promises)
Step 1 is a good start, but it’s missing a key requirement, saving and loading from the cache.
第1步是一个好的开始,但是缺少关键要求,即从缓存中保存和加载。
Currently, when the site starts there’s a small delay where the data is being fetched for the first time. And, in addition, if there are any errors, then no pricing data is displayed. This is where caching comes in.
当前,当站点启动时,会有一个很小的延迟,即首次获取数据。 而且,如果有任何错误,则不会显示任何定价数据。 这是缓存的来源。
It’s a very long topic, but, in short, modern browsers today come with a Cache Storage API. This is a comprehensive API used by developers to store any type of Blob data (ie: files). In this example, we’ll be using this to store any incoming Stock Data for use later.
这是一个很长的话题,但是,简而言之,当今的现代浏览器都带有Cache Storage API。 这是开发人员用来存储任何类型的Blob数据(即文件)的综合API。 在此示例中,我们将使用它来存储任何传入的库存数据以供以后使用。
/**
* Retrieves the value from "/stockdata.txt". Null if nothing stored.
*/
private async _retrieveCacheValue() {
// Open the cache
const cache = await caches.open("StockData");
// Check to see if the cache contains the required entry
const cacheMatch = await cache.match("/stockdata.txt");
// Return null if it doesn't
if (!cacheMatch) {
return null;
}
// Return the text representation of the Blob
return await cacheMatch.text();
}
/**
* Stores the value in the "/stockdata.txt" cache location
* @param newValue Value to store in cache
*/
private async _storeInCache(newValue: string) {
// Open the Cache
const cache = await caches.open("StockData");
// Create the Data Blob containing the values to cache
const dataBlob = new Blob([newValue], { type: "text/plain" });
// Store the cache data
await cache.put("/stockdata.txt", new Response(dataBlob));
}
These 2 defined functions are pretty simple. They are both async Promise-based methods which are required for the cache API.
这两个定义的函数非常简单。 它们都是高速缓存API所需的基于Promise的异步方法。
We now need to merge together both the cache manipulation and the HTTP data. So, finally, we get into the meat of the article, how to use Promises with Observables.
现在,我们需要将缓存操作和HTTP数据合并在一起。 因此,最后,我们深入探讨了如何将Promises与Observables结合使用 。
constructor(private httpClient: HttpClient) {
// Observable to fetch the first (only, not-null) cache value
const cacheValue$ = from(this._retrieveCacheValue()).pipe(
take(1), // Take the first value only
filter(i => !!i) // Only return non-null values
);
// The "seed" for our observable kicks off from:
// 1. Blank observable 'of' for an intitial kick
// 2. emit() from requestRequest
// 3. interval(60 seconds)
const seed$ = merge(of(null), this.refreshRequest, interval(10000));
// Observable to return the live price
const livePrice$ = seed$
.pipe(
// When the Observable fires, request the Stock Price
switchMap(() => this._fetchAndStore())
);
// Current Price Observable, now a merge of the cache and live values
this.currentPrice$ = merge(cacheValue$, livePrice$).pipe(
// Parse the value from the body into a number
map(i => parseInt(i)),
shareReplay()
);
}
/**
* Creates the HTTP Observable and store the value in the cache
*/
private _fetchAndStore() {
return this.httpClient.get(rngUrl, { responseType: 'text'})
.pipe(delayWhen(i => from(this._storeInCache(i))));
}
Our constructor has changed since Step 1, but still only contains 1 external Observable currentPrice$
. This Observable is now a merge of the cacheValue$
and the livePrice$
. Using the functionality of merge()
we’re able to retrieve the cache value first (as it will likely return immediately) and then later get the live price.
自第1步以来,我们的构造函数已更改,但仍仅包含1个外部Observable currentPrice$
。 现在,此Observable是cacheValue$
和livePrice$
的合并。 使用merge()
的功能, 我们能够首先检索缓存值(因为它可能会立即返回),然后再获取实时价格 。
Additionally, whenever the live price is updated, the cache is updated at the same time.
此外,每当实时价格更新时,高速缓存就会同时更新。
So now, at the end of Step 2, we’ve got an app that:
现在,在第2步结束时,我们有了一个应用程序:
- Returns the cached price when the page loads 返回页面加载时的缓存价格
- Requests the live price every 10 seconds (60s is a long time to wait) 每10秒请求实时价格(60s等待很长时间)
- Saves the price in the cache following a successful live GET 成功进行实时GET后将价格保存在缓存中
Within this Step, we’re using 2 new concepts from RxJS, from()
and delayWhen()
.
在此步骤中,我们使用RxJS的2个新概念, from()
和delayWhen()
。
概念-从(从本文的角度来看) (Concept — from (the point of the article))
Looking at the previous example with both the async _storeInCache()
and async _retrieveCacheValue()
methods, we need to find a way of turning these into Observables.
通过使用async _storeInCache()
和async _retrieveCacheValue()
方法async _storeInCache()
前面的示例,我们需要找到一种将它们转换为Observable的方法。
This is where from()
comes in.
这是from()
出现的地方。
There are several use-cases for from()
but the one we’re interested in allows us to wrap a Promise into an Observable. The then()
, catch()
and finally()
methods are all wrapped into the appropriate methods for Observables.
from()
有多个用例,但我们感兴趣的用例使我们可以将Promise包装到Observable中。 then()
, catch()
和finally()
方法都被包装到Observables的适当方法中。
Put simply, whatever value is returned from the promise will be emitted by the Observable.
简而言之, 从诺言返回的任何值都将由Observable发出 。
概念— delayWhen (Concept — delayWhen)
This probably deserves an article of its own, but, if you notice on stock.step2.ts:34 I’m using delayWhen()
. This will allow me to run an Observable and wait for it to finish.
这可能值得一读 ,但是,如果您在stock.step2.ts:34上注意到的话,我正在使用delayWhen()
。 这将允许我运行一个Observable并等待它完成。
Don’t emit the event from HTTP until the value has been successfully stored in the cache.
在成功将值存储在缓存中之前,不要从HTTP发出事件。
Note: I could have replaced this with tap(i => this._storeInCache())
and it would run asynchronously without blocking the pipeline. But … choices.
注意:我可以用 tap(i => this._storeInCache())
替换 它,它可以异步运行而不会阻塞管道。 但是……选择。
步骤3 –可观察的状态 (Step 3 — Observable States)
We’re now able to move onto our next requirement, implementing the isLive$
and isRefreshing$
observables. These are both boolean values that help the UI inform the user what is currently going on.
现在,我们可以继续执行下一个需求,实现isLive$
和isRefreshing$
可观察对象。 这两个都是布尔值,可以帮助UI通知用户当前正在发生什么。
The easiest way to achieve this is by using BehaviorSubject
. This is a really simple Observable that stores a single value and emits when it changes.
实现此目的的最简单方法是使用BehaviorSubject
。 这是一个非常简单的Observable,它存储一个值,并在更改时发出。
// Declare the isLive$ BehaviorSubject with an initial valuereadonly isLive$ = new BehaviorSubject<boolean>(false);// Set the value to trueisLive$.next(true);
You must make sure that your BehaviorSubject
contains an initial value, so in our case for both, we will just use false
.
您必须确保您的BehaviorSubject
包含一个初始值,因此在我们两种情况下,我们都将只使用false
。
To change the values on the BehavorSubject
, we can include a tap()
operator which can simply call the next()
method on the desired observable.
要更改BehavorSubject
上的值,我们可以包括tap()
运算符,该运算符可以简单地在所需的可观察对象上调用next()
方法。
By the end of this, when a value is emitted from either cache or live, set the livePrice$
observable.
livePrice$
,当从缓存或实时发出值时,将livePrice$
设置livePrice$
可观察的。
包装刷新 (Wrapping the Refresh)
The isRefreshing$
Observable is a little different. Yes, it is a BehaviorSubject
, but, we need to set a value before and after the HTTP Request.
isRefreshing$
Observable有点不同。 是的,它是一个BehaviorSubject
,但是,我们需要在HTTP Request之前和之后设置一个值。
As the pipeline doesn’t start to execute until the first observable has emitted (the HTTP Request) then we must wrap this to get access to the pre-emit state.
由于直到第一个可观察到的对象(HTTP请求)发出后管道才开始执行,因此我们必须包装它以访问预发射状态。
/**
* Creates the HTTP Observable and store the value in the cache
*/
private _fetchAndStore() {
// Create the fetch$ Observable
const fetch$ = this.httpClient.get(rngUrl, { responseType: 'text'})
.pipe(delayWhen(i => from(this._storeInCache(i))));
// Wrap the fetch$ Observable to set the isRefreshing$ value
return of(true).pipe(
tap(() => this.isRefreshing$.next(true)),
switchMapTo(fetch$),
tap(() => this.isRefreshing$.next(false))
);
}
The above code shows how we wrapped the fetch$
Observable in a simple pipeline that:
上面的代码显示了如何将fetch$
Observable包装在一个简单的管道中,该管道包括:
Sets the
isRefreshing$
value to true将
isRefreshing$
值设置为true- Performs the GET (run the network observable) 执行GET(可观察网络运行)
Sets the
isRefreshing$
value to false将
isRefreshing$
值设置为false
![Image for post](https://miro.medium.com/freeze/max/9999/1*GnDR-FXu0-eDw4B4C2zI5Q.gif)
As you can see, our UI is now displaying the refreshing state and the button is being disabled.
如您所见,我们的UI现在正在显示刷新状态,并且该按钮已被禁用。
And, the HTML is still unchanged from the beginning!
而且,HTML从一开始就保持不变!
Nil points for design
设计零分
步骤4 —错误处理 (Step 4 — Error Handling)
We’re now at the point where the cache is being stored and retrieved, the refresh events are working and the boolean states are being updated. But, there’s one problem.
现在我们到了存储和检索缓存的位置,刷新事件正在工作,布尔状态正在更新。 但是,有一个问题。
If the network fails, the entire pipeline will stop
如果网络出现故障,整个管道将停止
How come? Well, in the land of Observables, if an uncaught error reaches the end of the pipeline, the subscriber will stop listening for data.
怎么会? 好吧,在Observables领域, 如果未捕获的错误到达流水线的末端,则订阅者将停止监听数据 。
To get around this you need to use an Operator like catchError()
. So, in the event of an error:
为了解决这个问题,您需要使用像catchError()
这样的运算符。 因此,如果发生错误:
- Suppress the error and emit error state to the UI 抑制错误并将错误状态发送到UI
- Retrieve the latest value from the cache 从缓存中检索最新值
Due to a quirk with RxJS, we need to separate the seed$
from the livePrice$
. If we don’t do this then even if we suppress the error, the entire Observable pipeline will cease because the error will be traced back to the seed.
由于RxJS的古怪之处,我们需要将seed$
与livePrice$
分开。 如果我们不这样做,那么即使我们抑制该错误,整个Observable管道也会停止,因为该错误将追溯到种子。
constructor(private httpClient: HttpClient) {
// Observable to fetch the first (only, not-null) cache value
const cacheValue$ = from(this._retrieveCacheValue()).pipe(
take(1), // Take the first value only
filter(i => !!i), // Only return non-null values
tap(() => this.isLive$.next(false))
);
// The "seed" for our observable kicks off from:
// 1. Blank observable 'of' for an intitial kick
// 2. emit() from requestRequest
// 3. interval(60 seconds)
const seed$ = merge(of(null).pipe(delay(0)), this.refreshRequest, interval(10000));
// Observable to return the live price
const livePrice$ = of(null)
.pipe(
// When the Observable fires, request the Stock Price
switchMapTo(this._fetchAndStore()),
tap(() => this.isLive$.next(true)),
// Reset the error (if one was set previously)
tap(() => this.error$.next(null)),
// Catch any errors and return the cached value
catchError(() => this._handleError())
);
// Separate the live price from the seed
const wrappedLivePrice$ = seed$.pipe(switchMap(() => livePrice$));
// Current Price Observable, now a merge of the cache and live values
this.currentPrice$ = merge(cacheValue$, wrappedLivePrice$).pipe(
// Parse the value from the body into a number
map(i => parseInt(i)),
shareReplay()
);
}
private _handleError() {
// Create a new observable to return the cache value and clear the UI states
return from(this._retrieveCacheValue()).pipe(
tap(() => this.error$.next("Unable to refresh")),
tap(() => this.isRefreshing$.next(false)),
tap(() => this.isLive$.next(false)), // Inform that we're using the cache
);
}
As you can see in the above code example, the wrappedLivePrice$
starts with the seed$
and switches to the livePrice$
which internally handles the error.
如您在上面的代码示例中wrappedLivePrice$
, wrappedLivePrice$
以seed$
开头 并切换到内部处理错误的livePrice$
。
And, we make sure that we reset any UI values inside the error to allow any button states to refresh.
并且,我们确保重置错误内的所有UI值以允许刷新任何按钮状态。
![Image for post](https://miro.medium.com/freeze/max/9999/1*qZZgcsMX-MSGrjqDlZyaAg.gif)
结语 (Wrapping Up)
We got there in the end. But, through a small number of Observables with a few Promises, we’ve created a resilient, stock ticker application (with fake data, but who’s counting).
我们到了最后。 但是,通过少量的带有一些承诺的Observables,我们创建了一个有弹性的股票行情应用程序(使用伪造的数据,但是谁在计数)。
The entire app.component.ts code is available on GitHub at the bottom of the article. If you’d like to try it out, simply make a new Angular 10 project and replace the app.component.ts with the included file (make sure you also add HttpClientModule to your app.module.ts file also for the HTTP to work).
整个app.component.ts代码可在文章底部的GitHub上找到。 如果您想尝试一下,只需创建一个新的Angular 10项目并将app.component.ts替换为包含的文件(请确保还将HttpClientModule也添加到app.module.ts文件中,以使HTTP正常工作 )。
There are clearly more that we can do here, but what we’ve demonstrated is how to merge async/await Promises with RxJS Observables using the from()
observable.
显然,我们可以在这里做更多的事情,但是我们已经展示了如何使用from()
观察对象将Async / await Promises与RxJS Observables合并。
import { Component, EventEmitter } from '@angular/core';
import { of, Observable, interval, merge, from, BehaviorSubject } from 'rxjs';
import { switchMap, map, switchMapTo, delayWhen, filter, delay, tap, take, shareReplay, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
const rngUrl = "https://www.random.org/integers/?num=1&min=1&max=6000&col=1&base=10&format=plain&rnd=new";
@Component({
selector: 'app-root',
template: `
<div>
<h1>Developer Designed Stock Application</h1>
<button (click)="refreshRequest.emit()" [disabled]="isRefreshing$ | async">Refresh Stock Data</button>
<strong>Current Stock Price: {{ currentPrice$ | async }}</strong>
<span *ngIf="isLive$ | async">Data is being streamed live.</span>
<span *ngIf="!(isLive$ | async)">Data is cached.</span>
<i *ngIf="isRefreshing$ | async">Data is Refreshing...</i>
<strong *ngIf="error$ | async as error">Error: {{ error }}</strong>
</div>
`,
styles: [
'* { display: block; margin: 1rem 0; } h1 { margin: 0 } div { border: 1px solid grey; padding: 0.3em; display: inline-block; }'
]
})
export class AppComponent {
readonly currentPrice$ : Observable<number>;
readonly isLive$ = new BehaviorSubject<boolean>(false);
readonly isRefreshing$ = new BehaviorSubject<boolean>(false);
readonly error$ = new BehaviorSubject<string>(null);
refreshRequest = new EventEmitter();
constructor(private httpClient: HttpClient) {
// Observable to fetch the first (only, not-null) cache value
const cacheValue$ = from(this._retrieveCacheValue()).pipe(
take(1), // Take the first value only
filter(i => !!i), // Only return non-null values
tap(() => this.isLive$.next(false))
);
// The "seed" for our observable kicks off from:
// 1. Blank observable 'of' for an intitial kick
// 2. emit() from requestRequest
// 3. interval(60 seconds) - using 10 seconds because I'm impatient
const seed$ = merge(of(null).pipe(delay(0)), this.refreshRequest, interval(10000));
// Observable to return the live price
const livePrice$ = of(null)
.pipe(
// When the Observable fires, request the Stock Price
switchMapTo(this._fetchAndStore()),
tap(() => this.isLive$.next(true)),
// Reset the error (if one was set previously)
tap(() => this.error$.next(null)),
// Catch any errors and return the cached value
catchError(() => this._handleError())
);
// Separate the live price from the seed
const wrappedLivePrice$ = seed$.pipe(switchMapTo(livePrice$));
// Current Price Observable, now a merge of the cache and live values
this.currentPrice$ = merge(cacheValue$, wrappedLivePrice$).pipe(
// Parse the value from the body into a number
map(i => parseInt(i)),
shareReplay()
);
}
private _handleError() {
// Create a new observable to return the cache value and clear the UI states
return from(this._retrieveCacheValue()).pipe(
tap(() => this.error$.next("Unable to refresh")),
tap(() => this.isRefreshing$.next(false)),
tap(() => this.isLive$.next(false)), // Inform that we're using the cache
);
}
/**
* Creates the HTTP Observable and store the value in the cache
*/
private _fetchAndStore() {
// Create the fetch$ Observable
const fetch$ = this.httpClient.get(rngUrl, { responseType: 'text'})
.pipe(delayWhen(i => from(this._storeInCache(i))));
// Wrap the fetch$ Observable to set the isRefreshing$ value
return of(true).pipe(
tap(() => this.isRefreshing$.next(true)),
switchMap(() => fetch$),
tap(() => this.isRefreshing$.next(false))
);
}
/**
* Retrieves the value from "/stockdata.txt". Null if nothing stored.
*/
private async _retrieveCacheValue() {
const cache = await caches.open("StockData");
const cacheMatch = await cache.match("/stockdata.txt");
if (!cacheMatch) {
return null;
}
return await cacheMatch.text();
}
/**
* Stores the value in the /stockdata.txt cache location
* @param newValue Value to store in cache
*/
private async _storeInCache(newValue: string) {
// Open the Cache
const cache = await caches.open("StockData");
// Create the Data Blob containing the values to cache
const dataBlob = new Blob([newValue], { type: "text/plain" });
// Store the cache data
await cache.put("/stockdata.txt", new Response(dataBlob));
}
}
资源资源 (Resources)
As always, thank you to the excellent learnrxjs.com website. We used:
与往常一样,感谢您访问了出色的Learnrxjs.com网站。 我们用了:
Operator — from
运算符-来自
Operator — fromhttps://www.learnrxjs.io/learn-rxjs/operators/creation/from
运算符-来自 https://www.learnrxjs.io/learn-rxjs/operators/creation/from
BehaviorSubject
行为主体
BehaviorSubjecthttps://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject
BehaviorSubject https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject
Operator — merge
运算符-合并
Operator — mergehttps://www.learnrxjs.io/learn-rxjs/operators/combination/merge
运算符-合并 https://www.learnrxjs.io/learn-rxjs/operators/combination/merge
Operator — delayWhen
运算符-delayWhen
Operator — delayWhenhttps://www.learnrxjs.io/learn-rxjs/operators/utility/delaywhen
运算符-延迟 https://www.learnrxjs.io/learn-rxjs/operators/utility/delaywhen
Until next time.
直到下一次。
翻译自: https://medium.com/swlh/rxjs-tips-promises-into-observables-468e9534b5d9
rxjs观察者模式