构建完整的Web应用程序
在本章中,我们将构建一个典型的Web应用程序,在前端和后端使用RxJS。我们将转换文档对象模型(DOM)并使用Node.js服务器中的WebSockets进行客户端 - 服务器通信。
对于用户界面位,我们将使用RxJS-DOM库,这是由RxJS制作的同一团队的库,它提供了方便的Operator来处理DOM和浏览器相关的东西,这将使我们的编程更简洁。对于服务器部分,我们将使用两个完善的节点库,并将一些API与Observables包装在一起,以便在我们的应用程序中使用它们。
在本章之后,您将能够使用RxJS以声明方式构建用户界面,使用我们目前为止看到的技术并将它们应用于DOM。 您还可以在任何Node.js项目中使用RxJS,并且能够在任何项目中使用反应式编程和RxJS。
建立一个实时地震Dashboard
我们将为地震仪表板应用程序构建服务器和客户端部件,实时记录地震的位置并可视化显示。我们将在Node.js中构建服务器,并且改进我们的应用程序,使其更具互动性和更充足的信息量。
一开始的代码如下:
examples_earthquake/code1_3.js
代码语言:javascript复制var quakes = Rx.Observable
.interval(5000)
.flatMap(function() {
return Rx.DOM.jsonpRequest({
url: QUAKE_URL,
jsonpCallback: 'eqfeed_callback'
}).retry(3);
})
.flatMap(function(result) {
return Rx.Observable.from(result.response.features);
})
.distinct(function(quake) { return quake.properties.code; });
quakes.subscribe(function(quake) {
var coords = quake.geometry.coordinates;
var size = quake.properties.mag * 10000;
L.circle([coords[1], coords[0]], size).addTo(map);
});
这段代码已经有一个潜在的错误:它可以在DOM准备好之前执行,每当我们尝试在代码中使用DOM元素时就会抛出错误。我们想要的是在触发DOMContentLoaded事件之后加载我们的代码,这表示浏览器已经准备好dom了。
RxJS-DOM提供Rx.DOM.ready
Observable,当触发DOMContentLoaded时,它会发出一次。 因此,让我们将代码包装在initialize函数中,并在订阅Rx.DOM.ready
时执行它:
examples_earthquake_ui/code1.js
代码语言:javascript复制function initialize() {
var quakes = Rx.Observable
.interval(5000)
.flatMap(function() {
return Rx.DOM.jsonpRequest({
url: QUAKE_URL,
jsonpCallback: 'eqfeed_callback'
});
})
.flatMap(function(result) {
return Rx.Observable.from(result.response.features);
})
.distinct(function(quake) { return quake.properties.code; });
quakes.subscribe(function(quake) {
var coords = quake.geometry.coordinates;
var size = quake.properties.mag * 10000;
L.circle([coords[1], coords[0]], size).addTo(map);
});
}
Rx.DOM.ready().subscribe(initialize);
接下来,我们将在HTML中添加一个空表,我们将在下一部分填充地震数据:
代码语言:javascript复制<table>
<thead>
<tr>
<th>Location</th>
<th>Magnitude</th>
<th>Time</th>
</tr>
</thead>
<tbody id="quakes_info">
</tbody>
</table>
有了这个,我们准备开始为我们的仪表板编写新代码。
添加地震列表
新仪表板的第一个功能是显示地震的实时列表,包括有关其位置,大小和日期的信息。此列表的数据与来自USGS网站的地图相同。我们首先创建一个函数,在给定props对象参数的情况下返回一个row元素:
examples_earthquake_ui/code2.js
代码语言:javascript复制function makeRow(props) {
var row = document.createElement('tr');
row.id = props.net props.code;
var date = new Date(props.time);
var time = date.toString();
[props.place, props.mag, time].forEach(function(text) {
var cell = document.createElement('td');
cell.textContent = text;
row.appendChild(cell);
});
return row;
}
props参数与我们从USGS站点检索的JSON中的properties属性相同。
为了生成行,我们将再次订阅地震Observable。此订阅会在表格中为每次收到的新地震创建一行。 我们在initialize函数的末尾添加代码:
examples_earthquake_ui/code2.js
代码语言:javascript复制var table = document.getElementById('quakes_info');
quakes
.pluck('properties')
.map(makeRow)
.subscribe(function(row) { table.appendChild(row); });
pluck
运算符从每个地震对象中提取属性值,因为它包含makeRow所需的所有信息。 然后我们将每个地震对象映射到makeRow,将其转换为填充的HTML tr元素。 最后,在订阅中,我们将每个发出的行追加到我们的table中。
每当我们收到地震数据时,这应该得到一个数据稠密的表格。
看起来不错,而且很容易!不过,我们可以做一些改进。首先,我们需要探索RxJS中的一个重要概念:冷热Observable。
冷热Observable
无论Observers是否订阅它们,“热”Observable都会发出值。另一方面,“冷”Observables从Observer开始订阅就发出整个值序列。
热Observable
订阅热Observable的Observer将接收从订阅它的确切时刻发出的值。在那一刻订阅的每个其他Observer将收到完全相同的值。 这类似于JavaScript事件的工作方式。
鼠标事件和股票交易代码是热的Observables的例子。在这两种情况下,Observable都会发出值,无论它是否有订阅者,并且在任何订阅者收听之前可能已经生成了值。这是一个例子:
hot_cold.js
代码语言:javascript复制var onMove = Rx.Observable.fromEvent(document, 'mousemove');
var subscriber1 = onMove.subscribe(function(e) {
console.log('Subscriber1:', e.clientX, e.clientY);
});
var subscriber2 = onMove.subscribe(function(e) {
console.log('Subscriber2:', e.clientX, e.clientY);
});
代码语言:javascript复制// Result:
// Subscriber1: 23 24
// Subscriber2: 23 24
// Subscriber1: 34 37
// Subscriber2: 34 37
// Subscriber1: 46 49
// Subscriber2: 46 49
// ...
在该示例中,两个订阅者在发出Observable时都会收到相同的值。 对于JavaScript程序员来说,这种行为感觉很自然,因为它类似于JavaScript事件的工作方式。
现在让我们看看冷Observables是如何工作的。
冷Observable
只有当Observers订阅它时,冷Observable才会发出值。
例如,Rx.Observable.range返回一个冷Observable。订阅它的每个新观察者都将收到整个范围:
hot_cold.js
代码语言:javascript复制function printValue(value) {
console.log(value);
}
var rangeToFive = Rx.Observable.range(1, 5);
var obs1 = rangeToFive.subscribe(printValue); // 1, 2, 3, 4, 5
var obs2 = Rx.Observable
.delay(2000)
.flatMap(function() {
return rangeToFive.subscribe(printValue); // 1, 2, 3, 4, 5
});
了解我们何时处理热或冷的Observable对于避免细微和隐藏的错误至关重要。例如,Rx.Observable.interval
返回一个Observable,它以固定的时间间隔生成一个递增的整数值。 想象一下,我们想用它来将相同的值推送给几个观察者。 我们可以像这样实现它:
hot_cold.js
代码语言:javascript复制var source = Rx.Observable.interval(2000);
var observer1 = source.subscribe(function (x) {
console.log('Observer 1, next value: ' x);
});
var observer2 = source.subscribe(function (x) {
console.log('Observer 2: next value: ' x);
});
输出
代码语言:javascript复制Observer 1, next value: 0
Observer 2: next value: 0
Observer 1, next value: 1
Observer 2: next value: 1
...
这似乎没什么问题。 但现在想象我们需要第二个用户在第一个用户加入后三秒钟加入:
hot_cold.js
代码语言:javascript复制var source = Rx.Observable.interval(1000);
var observer1 = source.subscribe(function (x) {
console.log('Observer 1: ' x);
});
setTimeout(function() {
var observer2 = source.subscribe(function (x) {
console.log('Observer 2: ' x);
});
}, 3000);
输出
代码语言:javascript复制Observer 1: 0
Observer 1: 1
Observer 1: 2
Observer 1: 3
Observer 2: 0
Observer 1: 4
Observer 2: 1
...
现在我们看到有些东西真的没了。三秒后订阅时,observer2接收源已经推送过的所有值,而不是从当前值开始并从那里继续,因为Rx.Observable.interval是一个冷Observable。 如果热和冷Observables之间的的区别不是很清楚的话,那么这样的场景可能会令人惊讶。
如果我们有几个Observers订阅冷的Observable,他们将收到相同序列值的副本。严格来说,尽管观察者共享相同的Observable,但它们并没有共享相同的值序列。如果我们希望Observers共享相同的序列,我们需要一个热的Observable。
从冷到热使用publish
我们可以使用publish将冷的Observable变成热的。调用publish会创建一个新的Observable,它充当原始Observable的代理。它通过订阅原始版本并将其收到的值推送给订阅者来实现。
已发布的Observable实际上是一个ConnectableObservable,它有一个名为connect的额外方法,我们调用它来开始接收值。 这允许我们在开始运行之前订阅它:
hot_cold.js
代码语言:javascript复制// Create an Observable that yields a value every second
var source = Rx.Observable.interval(1000);
var publisher = source.publish();
// Even if we are subscribing, no values are pushed yet.
var observer1 = publisher.subscribe(function (x) {
console.log('Observer 1: ' x);
});
// publisher connects and starts publishing values
publisher.connect();
setTimeout(function() {
// 5 seconds later, observer2 subscribes to it and starts receiving
// current values, not the whole sequence.
var observer2 = publisher.subscribe(function (x) {
console.log('Observer 2: ' x);
});
}, 5000);
共享冷Observable
让我们回到我们的地震示例。到目前为止,我们的代码看起来很合理;我们有一个带有两个订阅的Observable地震:一个在地图上绘制地震,另一个在表格中列出地震。
但我们可以使代码更有效率。 通过让两个地震用户,我们实际上要求两次数据。 您可以通过在quakes
的flatMap
操作符中放入一个console.log来检查。
发生这种情况是因为quakes是一个冷Observable,并且它会将所有值重新发送给每个新订阅者,因此新订阅意味着新的JSONP请求。这会通过网络请求两次相同的资源来影响我们的应用程序性能。
对于下一个示例,我们将使用`share·运算符,当Observers的数量从0变为1时,它自动创建对Observable的预订。 这使我们免于重新连接:
examples_earthquake_ui/code2.js
代码语言:javascript复制var quakes = Rx.Observable
.interval(5000)
.flatMap(function() {
return Rx.DOM.jsonpRequest({
url: QUAKE_URL,
jsonpCallback: 'eqfeed_callback'
});
})
.flatMap(function(result) {
return Rx.Observable.from(result.response.features);
})
.distinct(function(quake) { return quake.properties.code; })
.share()
现在地震的行为就像一个热的Observable,我们不必担心我们连接多少观察者,因为他们都会收到完全相同的数据。
缓冲值
我们之前的代码运行良好,但请注意,每次我们收到有关地震的信息时都会插入一个tr节点。 这是低效的,因为每次插入我们都会修改DOM并导致重新绘制页面,使浏览器不必要地计算新布局。 这可能会导致性能下降。
理想情况下,我们会批处理几个传入的地震对象,并每隔几秒插入一批地震对象。手动实现会很棘手,因为我们必须保留计数器和元素缓冲区,我们必须记住每次批量重置它们。 但是使用RxJS,我们可以使用一个基于缓冲区的RxJS运算符,比如bufferWithTime
。
使用bufferWithTime
,我们可以缓冲传入的值,并在每x个时间段将它们作为数组释放:
examples_earthquake_ui/code3.bufferWithTime.js
代码语言:javascript复制var table = document.getElementById('quakes_info');
quakes
.pluck('properties')
.map(makeRow)
❶ .bufferWithTime(500)
❷ .filter(function(rows) { return rows.length > 0; }
.map(function(rows) {
var fragment = document.createDocumentFragment();
rows.forEach(function(row) {
❸ fragment.appendChild(row);
});
return fragment;
})
.subscribe(function(fragment) {
❹ table.appendChild(fragment);
});
这是新代码中正在发生的事情:
- B缓存每个传入值并每500毫秒释放一批值。
- 无论如何,
bufferWithTime
每500ms执行一次,如果没有传入值,它将产生一个空数组。 我们会过滤掉这些空数组。 - 我们将每一行插入一个文档片段,这是一个没有父文档的文档。这意味着它不在DOM中,并且修改其内容非常快速和有效。
- 最后,我们将片段附加到DOM。附加片段的一个优点是它被视为单个操作,只会导致一次重绘。 它还将片段的子元素附加到我们附加片段本身的同一元素。
使用缓冲区和片段,我们设法保持行插入性能,同时保持应用程序的实时性(最大延迟为半秒)。 现在我们已准备好为我们的仪表板添加下一个功能:交互性!
添加交互
我们现在在地图上和列表中发生地震,但两个表示之间没有相互作用。例如,每当我们点击列表上的地图时,就可以在地图上居中地震,并在我们将鼠标移动到其行上时突出显示地图上带圆圈的地震。 我们开始吧。
在Leaflet中,您可以在地图上绘制并将绘图放在各自的图层中,以便您可以单独操作它们。 让我们创建一组名为quakeLayer的图层,我们将存储所有地震圈。每个圆圈都是该组内的一个图层。 我们还将创建一个对象codeLayers,我们将存储地震代码和内部图层ID之间的相关性,以便我们可以通过地震ID来查找圆圈:
examples_earthquake_ui/code3.js
代码语言:javascript复制var codeLayers = {};
var quakeLayer = L.layerGroup([]).addTo(map);
现在,在初始化内部的地震Observable订阅中,我们将每个圆圈添加到图层组并将其ID存储在codeLayers中。 如果这看起来有点错综复杂,那是因为这是Leaflet允许我们在地图中引用图层的唯一方式。
examples_earthquake_ui/code3.js
代码语言:javascript复制quakes.subscribe(function(quake) {
var coords = quake.geometry.coordinates;
var size = quake.properties.mag * 10000;
var circle = L.circle([coords[1], coords[0]], size).addTo(map);
quakeLayer.addLayer(circle);
codeLayers[quake.id] = quakeLayer.getLayerId(circle);
});
我们现在创建悬停效果。我们将编写一个新函数isHovering,它返回一个Observable,它发出一个布尔值,表示在任何给定时刻鼠标是否在特定地震圈上:
examples_earthquake_ui/code3.js
代码语言:javascript复制❶ var identity = Rx.helpers.identity;
function isHovering(element) {
❷ var over = Rx.DOM.mouseover(element).map(identity(true));
❸ var out = Rx.DOM.mouseout(element).map(identity(false));
❹ return over.merge(out);
}
-
Rx.helpers.identity
是定义函数。 给定参数x,它返回x。 这样我们就不必编写返回它们收到的值的函数。 - over是一个Observable,当用户将鼠标悬停在元素上时会发出true。
- out是一个Observable,当用户将鼠标移动到元素之外时,它会发出false。
- isHovering将over和out合并,返回一个Observable,当鼠标悬停在元素上时发出true,当它离开时返回false。
使用isHovering,我们可以修改创建rows的订阅,这样我们就可以在创建时订阅每行中的事件:
examples_earthquake_ui/code3.js
代码语言:javascript复制var table = document.getElementById('quakes_info');
quakes
.pluck('properties')
.map(makeRow)
.bufferWithTime(500)
.filter(function(rows) { return rows.length > 0; })
.map(function(rows) {
var fragment = document.createDocumentFragment();
rows.forEach(function(row) {
fragment.appendChild(row);
});
return fragment;
})
.subscribe(function(fragment) {
var row = fragment.firstChild; // Get row from inside the fragment
❶ var circle = quakeLayer.getLayer(codeLayers[row.id]);
❷ isHovering(row).subscribe(function(hovering) {
circle.setStyle({ color: hovering ? '#ff0000' : '#0000ff' });
});
❸ Rx.DOM.click(row).subscribe(function() {
map.panTo(circle.getLatLng());
});
table.appendChild(fragment);
})
- 我们使用从行元素获得的ID在地图上获取地震的圆元素。 有了它,codeLayers为我们提供了相应的内部ID,它使用quakeLayer.getLayer获取了circle元素。
- 我们用当前行调用isHovering,然后我们订阅生成的Observable。 如果悬停参数为真,我们会将圆圈画成红色; 不然,它会是蓝色的。
- 我们订阅了从当前行中的click事件创建的Observable。 单击列表中的行时,地图将以地图中相应圆圈为中心。
使其更高效
经验丰富的前端开发人员知道在页面上创建许多事件是导致性能不佳的一个因素。 在前面的示例中,我们为每一行创建了三个事件。 如果我们在列表中获得100次地震,我们将在页面周围浮动300个事件,只是为了做一些亮点突出工作! 这对于表现来说太糟糕了,我们可以做得更好。
因为DOM中的事件总是冒泡(从子元素到父元素),前端开发人员中一个众所周知的技术是避免将鼠标事件单独附加到多个元素,而是将它们附加到父元素。 一旦在父项上触发了事件,我们就可以使用事件的target属性来查找作为事件目标的子元素。
因为我们需要为事件click和mouseover提供类似的功能,所以我们将创建一个函数getRowFromEvent:
examples_earthquake_ui/code3.pairwise.js
代码语言:javascript复制function getRowFromEvent(event) {
return Rx.Observable
.fromEvent(table, event)
❶ .filter(function(event) {
var el = event.target;
return el.tagName === 'TD' && el.parentNode.id.length;
})
❷ .pluck('target', 'parentNode')
❸ .distinctUntilChanged();
}
getRowFromEvent为我们提供了事件发生的表行。 以下是详细信息:
- 我们确保在表格单元格中发生事件,并检查该单元格的父级是否是具有ID属性的行。 这些行是我们用地震ID标记的行。
-
pluck
运算符在element的target属性中提取嵌套属性parentNode。 - 这可以防止多次获得相同的元素。 例如,使用mouseover事件会发生很多事情。
examples_earthquake_ui/code3.pairwise.js
在上一节中,我们在每行上附加事件mouseover和mouseout,以便在每次鼠标输入或退出行时更改地震圈颜色。 现在,我们将仅使用桌面上的mouseover事件,并结合方便的pairwise
运算符:
examples_earthquake_ui/code3.pairwise.js
代码语言:javascript复制getRowFromEvent('mouseover')
.pairwise()
.subscribe(function(rows) {
var prevCircle = quakeLayer.getLayer(codeLayers[rows[0].id]);
var currCircle = quakeLayer.getLayer(codeLayers[rows[1].id]);
prevCircle.setStyle({ color: '#0000ff' });
currCircle.setStyle({ color: '#ff0000' });
});
pairwise
将每个发射值与先前在阵列中发射的值进行分组。 因为我们总是获得不同的行,所以成对将始终产生鼠标刚刚离开的行和鼠标现在悬停的行。 有了这些信息,就可以相应地为每个地震圈着色。
处理click事件更简单:
examples_earthquake_ui/code3.pairwise.js
代码语言:javascript复制getRowFromEvent('click')
.subscribe(function(row) {
var circle = quakeLayer.getLayer(codeLayers[row.id]);
map.panTo(circle.getLatLng());
});
我们可以回到订阅quakes
来生成行:
examples_earthquake_ui/code3.pairwise.js
代码语言:javascript复制quakes
.pluck('properties')
.map(makeRow)
.subscribe(function(row) { table.appendChild(row); });
我们的代码现在更加干净,并且它不依赖于别处的row。 如果没有row,getRowFromEvent将不会尝试产生任何item。
更重要的是,我们的代码现在非常高效。 无论我们检索的地震信息量如何,我们总是只有一个鼠标悬停事件和单击事件,而不是数百个事件。
从Twitter获取实时更新
我们为地震制作实时仪表板的计划的第二部分是从Twitter添加与地球上发生的不同地震有关的报告和信息。 为此,我们将创建一个小型Node.js程序,该程序将获取与地震相关的文章流。
设置我们的Node.js环境
让我们开始配置我们的Node.js应用程序吧。除了RxJS,我们将使用两个第三方模块:ws和twit。这种类似的模块都是让我们保持最少的代码。
首先,让我们为我们的应用程序创建一个文件夹,并安装我们将使用的模块。 (请注意,npm命令的输出可能会因软件包的当前版本而异。)
客户端 - 服务器通信
现在我们准备开始构建我们的应用程序了。让我们在tweet_stream文件夹中创建一个名为index.js的新文件来加载我们将使用的模块:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制var WebSocketServer = require('ws').Server; var Twit = require('twit');
var Rx = require('rx');
要使用Twitter API,您需要在Twitter网站中请求使用者密钥和访问令牌。 完成后,使用配置对象创建一个新的Twit对象,如下所示:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制var T = new Twit({
consumer_key: 'rFhfB5hFlth0BHC7iqQkEtTyw',
consumer_secret: 'zcrXEM1jiOdKyiFFlGYFAOo43Hsz383i0cdHYYWqBXTBoVAr1x',
access_token: '14343133-nlxZbtLuTEwgAlaLsmfrr3D4QAoiV2fa6xXUVEwW9',
access_token_secret: '57Dr99wECljyyQ9tViJWz0H3obNG3V4cr5Lix9sQBXju1'
});
现在我们可以创建一个函数onConnect,它将完成搜索推文和将来与客户端通信的所有工作,并且我们可以启动一个WebSocket服务器,一旦WebSocket连接并准备好就会调用onConnect:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制function onConnect(ws) {
console.log('Client connected on localhost:8080');
}
var Server = new WebSocketServer({ port: 8080 });
Rx.Observable.fromEvent(Server, 'connection').subscribe(onConnect);
我们现在可以启动我们的应用程序,它应该在端口8080上启动WebSocket连接:
代码语言:javascript复制~/tweet_stream$ node index.js
由于我们尚未将任何浏览器连接到此服务器,因此尚未打印有关客户端连接的消息。现在让我们切换到dashboard的代码并执行此操作。我们将在RxJS-DOM中使用fromWebSocket运算符:
examples_earthquake_ui/code4.js
代码语言:javascript复制function initialize() {
var socket = Rx.DOM.fromWebSocket('ws://127.0.0.1:8080'); ...
在前面的代码中,fromWebSocket
创建一个Subject,作为WebSocket服务器的消息的发送者和接收者。 通过调用socket.onNext,我们将能够向服务器发送消息,通过订阅套接字,我们将收到服务器发送给我们的任何消息。
我们现在可以发送包含我们收到的地震数据的服务器消息:
examples_earthquake_ui/code4.js
代码语言:javascript复制quakes.bufferWithCount(100)
.subscribe(function(quakes) {
console.log(quakes);
var quakesData = quakes.map(function(quake) {
return {
id: quake.properties.net quake.properties.code,
lat: quake.geometry.coordinates[1],
lng: quake.geometry.coordinates[0],
mag: quake.properties.mag
};
});
socket.onNext(JSON.stringify({quakes: quakesData }));
});
我们可以为来自服务器的消息设置订阅者:
examples_earthquake_ui/code4.js
代码语言:javascript复制socket.subscribe(function(message) {
console.log(JSON.parse(message.data));
});
现在,当我们重新加载浏览器时,客户端消息应出现在服务器终端中:
代码语言:javascript复制~/tweet_stream$ node index.js
Client connected on localhost:8080
太棒了! 一旦开始从远程JSONP资源接收地震,浏览器就应该向服务器发送命令。 但是现在,服务器完全忽略了这些消息。 是时候回到我们的推文流代码并用它们做点什么了。
首先,我们将连接到从浏览器客户端到达服务器的消息事件。 每当客户端发送消息时,WebSocket服务器都会发出包含消息内容的消息事件。 在我们的例子中,内容是一个JSON字符串。
我们可以在onConnect函数中编写以下代码:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制var onMessage = Rx.Observable.fromEvent(ws, 'message')
.subscribe(function(quake) {
quake = JSON.parse(quake);
console.log(quake);
});
如果我们重新启动服务器(终端中的Ctrl-C)并重新加载浏览器,我们应该会看到终端上的地震细节打印出来。这是完美的。 现在我们已经准备好开始寻找与我们的地震有关的推文了。
检索和发送推文
我们正在使用Node.js twit的流式Twitter客户端连接到Twitter和搜索推文。 从现在开始,服务器中的所有代码都将在onConnect函数内部发生,因为它假定已经建立了与WebSocket的连接。 让我们初始化推文流:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制var stream = T.stream('statuses/filter', {
track: 'earthquake',
locations: []
});
这告诉我们的Twit实例T开始流式传输Twitter状态,按关键字地震过滤。 当然,这是非常通用的,而不是与现在发生的地震直接相关。 但请注意空位置数组。 这是一个纬度和经度边界的数组,我们可以用它们按地理位置过滤推文,以及地震一词。 那更加具体! 好的,让我们订阅这个流并开始向浏览器发送推文:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制Rx.Observable.fromEvent(stream, 'tweet').subscribe(function(tweetObject) {
ws.send(JSON.stringify(tweetObject), function(err) {
if (err) {
console.log('There was an error sending the message');
}
});
});
如果我们重新启动服务器并重新加载浏览器,我们应该在浏览器中收到推文,开发面板中的控制台应该打印推文。
这些推文尚未按地震位置进行过滤。 为此,我们需要对收到的每一条地震信息做以下事情:
- 取每个地震的经度和纬度对的震中坐标,创建一个边界框,界定我们认为与地震相关的推文的地理区域。
- 累积所有边界坐标,以便发送给客户端的推文与地图上的地震保持相关。
- 每次收到新地震的消息时,都会使用新坐标更新twit流。
这是一种方法:
examples_earthquake_ui/tweet_stream/index.js
代码语言:javascript复制Rx.Observable
.fromEvent(ws, 'message')
.flatMap(function(quakesObj){
quakesObj = JSON.parse(quakesObj);
return Rx.Observable.from(quakesObj.quakes);
})
❶ .scan([], function(boundsArray, quake) {
❷ var bounds = [
quake.lng - 0.3, quake.lat - 0.15,
quake.lng 0.3, quake.lat 0.15
].map(function(coordinate) {
coordinate = coordinate.toString();
return coordinate.match(/-?d (.-?d{2})?/)[0];
});
boundsArray.concat(bounds);
❸ return boundsArray.slice(Math.max(boundsArray.length - 50, 0));
})
❹ .subscribe(function(boundsArray) {
stream.stop();
stream.params.locations = boundsArray.toString();
stream.start();
});
以下是前面代码中发生的事情的一步一步:
- 我们再次见到我们的老朋友
scan
。 任何时候我们需要累积结果并产生每个中间结果,scan
是我们的朋友。 在这种情况下,我们将继续在boundsArray数组中累积地震坐标。 - 从地震震中的单纬度/经度坐标对,我们创建一个阵列,其中包含由西北坐标和东南坐标确定的区域。 用于近似边界的数字创建了一个大城市大小的矩形。之后,我们使用正则表达式将每个坐标的小数精度限制为两位小数,以符合Twitter API要求。
- 我们将生成的边界连接到boundsArray,它包含以前每个地震的边界。 然后我们采用最后25对边界(数组中的50个项目),因为这是Twitter API的限制。
- 最后,我们订阅了Observable,在onNext函数中,我们重新启动当前的twit流来重新加载更新的位置,以便通过我们新的累积位置数组进行过滤,转换为字符串。
重新启动服务器并重新加载浏览器后,我们应该在浏览器应用程序中收到相关的推文。 但是现在,我们只能看到开发人员控制台中显示的原始对象。 在下一节中,我们将生成HTML以在仪表板中显示推文。
在Dashboard上显示推文
既然我们正在接收来自服务器的推文,那么剩下要做的就是在屏幕上很好地展示它们。 为此,我们将创建一个新的HTML元素,我们附加传入的推文:
examples_earthquake_ui/index_final.html
代码语言:javascript复制<div id="tweet_container"></div>
我们还将更新socket Observable订阅以处理传入的tweet对象并将它们附加到我们刚刚创建的tweet_container元素:
examples_earthquake_ui/code5.js
代码语言:javascript复制socket
.map(function(message) { return JSON.parse(message.data); })
.subscribe(function(data) {
var container = document.getElementById('tweet_container');
container.insertBefore(makeTweetElement(data), container.firstChild);
});
任何新的推文都会出现在列表的顶部,它们将由makeTweetElement创建,这是一个创建推文元素的简单函数,并使用我们作为参数传递的数据填充它:
examples_earthquake_ui/code5.js
代码语言:javascript复制function makeTweetElement(tweetObj) {
var tweetEl = document.createElement('div');
tweetEl.className = 'tweet';
var content = '<img src="$tweetImg" class="avatar" />'
'<div class="content">$text</div>'
'<div class="time">$time</div>';
var time = new Date(tweetObj.created_at);
var timeText = time.toLocaleDateString() ' ' time.toLocaleTimeString();
content = content.replace('$tweetImg', tweetObj.user.profile_image_url);
content = content.replace('$text', tweetObj.text);
content = content.replace('$time', timeText);
tweetEl.innerHTML = content;
return tweetEl;
}
有了这个,我们终于有了一个带有相关的地理定位推文的侧边栏,可以让我们更深入地了解受地震影响的区域。
改进的想法
此仪表板已经正常运行,但可以进行许多改进。 一些想法,使它更好:
- 添加更多地震数据库。 USGS是一个很棒的资源,但它主要提供在美国发生的地震。 合并来自世界各地的地震报告,而不仅仅是美国,并在地图中将它们全部展示在一起将会很有趣。 为此,您可以使用
merge
和mergeAll
的帮助,并使用distinct
与选择器函数来避免重复。 - 每当用户点击推文时,将地图置于相关地震中心。 这将涉及通过地震在服务器上对推文进行分组,并且您可能希望使用
groupBy
运算符将推文分组到特定地理区域。
总结
在本章中,我们使用RxJS创建了一个响应式用户界面,使我们能够实时查看地球上发生的地震的各种数据。我们在浏览器客户端和Node.js服务器中都使用了RxJS,显示了使用Observable管理应用程序的不同区域是多么容易。
更重要的是,我们已经看到我们可以在客户端和服务器上以相同的方式使用RxJS,在我们的应用程序中随处可见Observable序列抽象。 不仅如此。我们实际上可以在其他编程语言中使用RxJS概念和运算符,因为许多编程语言都支持RxJS。
接下来我们将介绍Scheduler,它是RxJS中更高级的对象类型,它允许我们更精确地控制时间和并发性,并为测试代码提供了很大的帮助。