RxJS能够让我们很轻松地创建和操控事件和streams,虽然会让开发变得复杂,但是会让异步代码变得易读。
创建大型的异步的应用程序并非易事,其回调函数引发的问题让诸多开发者头疼,我们称其为回调地狱。之前有promises, generators以及async/await来处理回调函数引发的问题。但现在我们有了另外一个解决方案,那就是RxJS。
RxJS在其github项目上的定义为“a set of libraries for composing asynchronous and event-based programs using observable sequences and fluent query operators”。说得通俗易懂点就是我们可以从事件以及其他数据源中创建streams,并且我们可以对streams进行合并,销毁,分离等操作,以获得我们想要的数据。
Observable或者stream(数据流)刚开始可能比较难理解。我会把它看成是一段时间内的事件或数据集合,而不是某个时间点上的单一事件或数据。
为了演示它是如何工作的,我们将要创建一个简单的天气应用。这个应用会根据你提交的邮编会返回邮编所在地区的气温。获得返回的气温后,我们会将气温和邮编一同显示在页面上。我们能够在页面上显示多组气温和邮编。最后,我们还会有个定时器能够定时刷新气温。
你可以在Github上查看最终源代码。
更新:这篇文章已经更新到RxJS version 5。
对原先的代码只有少量改动,有必要的情况下我会高亮这些改动。
起步
首页我们需要一个HTML页面来加载RxJS,还需要包含一些CSS,代码如下:
Weather Monitoring in RxJS
在浏览器中打开这个页面,并打开console面板,如果你能看到RxJS included? true,说明你已经可以开始响应式编程啦。在这个页面上我们有一个表单,表单下面包含邮编的输入框和一个按钮。首先我们的JavaScript将会获取这两个表单元素,然后为它们创建stream。我们还会获取id为app-container的DIV,我们将会在后面用到。
更新:我已经在页面上引用了RxJS 5.0,而不是之前的4.1
// 获取HTML元素
const appContainer = document.getElementById('app-container');
const zipcodeInput = document.getElementById('zipcode-input');
const addLocationBtn = document.getElementById('add-location');
上面是很基本的JavaScript获到DOM节点,没什么特别的
// 成生点击按钮的数据流
const btnClickStream =
Rx.Observable
.fromEvent(addLocationBtn, 'click')
// .map(() => true)
.mapTo(true)
.forEach(val => console.log('btnClickStream val', val));
在这里我们用到了RxJS!我们使用了Rx.Observable上的fromEvent方法,它为addLocationBtn的点击事件创建stream。意味着任何时候点击按钮,btnClickStream都会发送事件对象。因为我只需要知道点击事件的发生,所以我将点击事件产生的值用mapTo转换成布尔值true,我认为这样能够简化逻辑。当然这只是我喜欢的方式,如果你不喜欢,可以将它从代码中移除,没问题。最后,为了确保它正常工作,我们使用了forEach,它为数据流增加一个订阅者(subscriber),只是简单的输出值。
更新:RxJS 5.0新增了mapTo,它比map更可读。后者需要用一个函数来返回true。当然两者在这里的作用是一样的。
重新加载页面,点击几次按钮,你会发现console面板输出btnClickStream val true,说明代码正常工作了。现在我们去掉forEach,因为不再需要它了。接来下处理邮编,我们需要监听邮编输入框的变化,在这里要做一下过滤,只有输入的长度为5时才去处理,看下面代码:
// 成生邮编输入框的数据流
const zipInputStream =
Rx.Observable
.fromEvent(zipcodeInput, 'input')
.map(e => e.target.value)
.filter(zip => zip.length === 5)
.forEach(val => console.log('zipInputStream val', val));
这里我们为邮编输入框创建了stream,使用map从事件中获取输入的值,然后使用filter过滤掉所有长度不为5的输入值。最后我们通过forEach历遍所有的值,将它们输出在console中。
刷新页面,在输入框中输入一些值,在console中看结果。仍然,在后面的代码中我们并不需要forEach,所以去掉它。现在我们要做的就是当用户点击按钮时,才将输入的值传过来。为此我们需要创建一个新的stream。
// 当点击按钮时才获取zipcode
const zipcodeStream =
btnClickStream
.withLatestFrom(zipInputStream, (click, zip) => zip)
.distinct()
.forEach(val => console.log('zipcodeStream val', val));
这里,我们在btnClickStream上调用withLatestFrom操作符,然后传入zipInputStream。意思就是只要接收到btnClickStream发射过来的值,它就会从zipInputStream中获取最新的值,然后把这两个值传到处理函数上(withLatestFrom第二个参数),这个函数作用类似map。无论这个函数返回什么,都会被传到withLatestFrom返回的stream中。接下来我们使用distinct来拦截重复值,比如你之前传了12345,这一次再输12345,这个值就会被拦截。最后我们还是使用forEach来输出结果。
同样,我们刷新页面输入值,点击按钮,在console面板查看结果,然后去掉forEach。当调用weather API的时候我们需要让代码可以重用。
// 创建可重用的获取气温的stream
const getTemperature = zip => fetch(`http://api.openweathermap.org/data/2.5/weather?q=${zip},us&units=imperial&APPID=`).then(res => res.json());
const zipTemperatureStreamFactory = zip => Rx.Observable.fromPromise(getTemperature(zip)).map(({ main: { temp } }) => { temp, zip });
我们创建了两个函数。第一个getTemperature通过传入zip向weather API请求。因为fetch返回一个promise,所以我们使用了then。这个promise接收到回复,我们再将这个回复发json的形式返回,方便我们更好地处理数据。你需要更换上面代码中的
第二个函数同样使用zip code做参数。我们使用了fromPromise来创建了一个stream。这个操作符是将getTemperature函数返回的promise转换成stream。因为返回的是stream,所以我们可以使用Rx.Observable上的操作符对其进行操作。我们使用map将数据以object的形式返回。注意,在map参数上,我们使用了ES2015新的语法:解构(destructuring)。使用解构能够轻松地将气温数据提取出来。关于解构,你可以访问MDN文档了解更多。
现在我们已经可以从weather API中获取数据了,现在我们将在页面上增加些元素。
// 点击按钮获得邮编,然后请求气温打印在页面上
zipcodeStream
.flatMap(zipTemperatureStreamFactory)
.forEach(({ zip, temp }) => {
const locationEle = document.createElement('div');
locationEle.id = `zip-${zip}`;
locationEle.classList.add('location');
const zipEle = document.createElement('p');
zipEle.classList.add('zip');
zipEle.innerText = zip;
const tempEle = document.createElement('p');
tempEle.classList.add('temp');
tempEle.innerHTML = `${temp}°F`;
locationEle.appendChild(zipEle);
locationEle.appendChild(tempEle);
appContainer.appendChild(locationEle);
zipcodeInput.value = '';
});
我们在zip code的数据流上使用flatMap操作符。flatMap类似于map,不同的是它返回的是所有Stream中的每个Stream,然后取出每个stream中的值。意味着它会“打平”我们从weather API获得的stream,返回我们需要处理的数据,也就是包含邮编和气温的对象。
接下来就是我们使用forEach来处理我们获得的数据,将它们添加到页面中。最后我们清空输入框的值。
重新加载页面,然后在输入框中输入几个邮编,你会看到页面上有新增元素,包含了你输入的邮编以及对应的气温。
现在我们已经能够让气温显示在页面上,但是我们要让它保持更新。所以我们要创建了个stream,让它每个一段时间发射最新的数据。但是在此之前,我们要先拿到所以已经添加到页面上的邮编。怎样拿呢?可以使用ReplaySubject。ReplaySubjuct能够订阅一个stream,并且记住这个stream所有的值。我们就可以在任何时候重新拿到那些值。
// 创建一个stream,以便我们在想要的时候获得邮编
const replayZipsStream = new Rx.ReplaySubject();
zipcodeStream.subscribe(replayZipsStream);
这里我们创建了一个新的ReplaySubject对象,然后在zipcodeStream中订阅。意味着ReplaySubject会记住我们输入的所有的邮编。
// 创建个定时器,更新页面
Rx.Observable
.interval(20000)
// .flatMapLatest(() => replayZipsStream)
.switchMap(() => replayZipsStream)
.flatMap(zipTemperatureStreamFactory)
.forEach(({ zip, temp }) => {
console.log('Updating!', zip, temp);
const locationEle = document.getElementById(`zip-${zip}`);
const tempEle = locationEle.querySelector('.temp');
tempEle.innerHTML = `${temp}°F`;
});
首先我们要创建个stream,作用是在指定的间隔上发射值。发射什么值并不是我们要关心的,我们只是要在这个stream发射值的时候执行其他操作。然后我们使用了一个新的操作符switchMap。这里使用switchMap而不是flatMap的原因是我们只需要在replayZipsStream上有一个订阅者(subscriber)。如果我们使用flatMap,我们就会在相同的ReplaySubject上有多个订阅者,这会导致我们向weather API发送多个额外的请求。这时候我们就得到了一个包含邮编的stream,就像之前我们在页面中添加邮编时一样。所以我们可以用同样的方式处理stream返回的值。我们使用flatMap,把zipTempateratureStreamFactory传进去,后者会向weather API发送请求。最后,遍历所有返回回来的数据,将它们更新到页面上。
更新:在RxJS 5.0版本中,flatMapLatest已更改成switchMap
最后一次载新页面,添加几个邮编,你会看到它们会被添加到页面中。等待20秒,你会在console面板上看到消息,告诉我们所有东西都已经被更新了。你可能不会在页面上看到变化,因为在这20秒内,气温可能并不会发生变化。当然你可以在Rx.Observable.interval上自由改动间隔时间。
使用Auth0 Lock
假设你现在决定使用Auth0 Lock为你的天气应用添加身份验证,那么应该怎么做呢?其实挺简单的,因为Auth0 Lock的库把大部分的工作都做好了,我们所要做的就是一个按钮,点击后会显示一个modal。
首先,我们需要引入Auth0 Lock的库,初始化Lock,增加一个登陆按钮,点击后会弹出一个modal。
剩下唯一要做的事是将点击登陆按钮转换成stream,只要这个stream发射数据,我们就打开modal。
Rx.Observable
.fromEvent(document.getElementById('login'), 'click')
.forEach(() => lock.open());
至此,所以工作已经完了。
总结
Observables或者streams刚开始可能并不容易理解。我会把它想象成一段时间内事件的集合而不是单一事件。一旦把这个搞清楚,那么把DOM上面所有事件想象成streams都不是难事。使用RxJS可以很容易地创建streams,并且很容易操作。相比于其实框架或库,它能让你代码逻辑更清晰。
本文翻译自 https://auth0.com/blog/understanding-reactive-programming-and-rxjs/