在本章中,使用Rxjs,我们将创建一个典型的web application。我们将转化Dom Object Model(DOM)并且使用node.js中的websocket做c/s的交互。
- 用户界面部分,我们将使用RxJs-Domlibrary,这同样是Rxjs团队做的库,提供了方便的操作符来处理DOM和浏览器相关的使我们的编码更容易。服务器端:我们将是使用两个建立很好的node库,并用Observable封装他们的一些API到我们的application中。
- 在这一章之后,你将能使用RxJs创建用户界面在一个公开的方式,使用这种技术到我们目前看到为止,并应用到DOM上。你将任何的nodejs项目中使用RxJs并使用响应式编程和Rxjs在任何项目中。
Building a Real-Time Earthquake Dashboard
- 我们将建立一个服务器和客户端部分为地震仪表程序,接着我们遗留在第二章“真实地震的可视化“,我们将建立服务器端在Node.js,并提高我们的程序更具信息和交互性。
- screenshot展示给我们当完成时,仪表的样子。
- 我们的代码接着“真实地震的可视化”开始,如下是我们遗留的:
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);
});
- 这些代码已经有一个潜在的bug:它可以在DOM准备好之前执行。当我们试着操作DOM元素的时候就会抛出异常。我们想要执行的是DOMContentLoaded事件被触发后加载我们的代码,这标志着浏览器对这个页面的所有元素都是知道的。
- Rxjs—Dom 提供了Rx.DOM.ready() Observable,它发射一次,当DOMContentLoaded触发的时候。因此我们用一个initialize函数封装下我们的代码,并执行它当我们订阅Rx.DOM.ready();
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);
- 接着,我们将增加一个空的table到我们的HTML中,这里我们将显示下一个选择的地震信息:
<table>
<thead>
<tr>
<th>Location</th>
<th>Magnitude</th>
<th>Time</th>
</tr>
</thead>
<tbody id="quakes_info">
</tbody>
</table>
- 这样我们就可以开始我们仪表的新代码了。
Adding a List of Earthquakes
- 新表盘的第一个特点是展示真实地震列表,包含他们位置、量级和时间这些信息。这个列表数据和地图是一样的,也来自USGS网站。我们首先创建一个函数,它返回一个给定props对象参数的row元素:
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属性是一样的。
- 为了生成rows,我们将会创建另外一个订阅到quakes Observable上。这个订阅为没一个新收到的地震在table中创建了个一个row(行)。
- 我们在initialize函数的最后添加如下代码:
var table = document.getElementById(‘quakes_info‘);
quakes
.pluck(‘properties‘)
.map(makeRow)
.subscribe(function(row) { table.appendChild(row); });
- pluck操作符提取了每个地震对象的properties值,由于它包含我们需要给makeRow的所有信息。之后,我们map每个地震对象去makeRow,把它落户到HTML的tr元素上。最后,在这个订阅中,我们把每一个发射的row追加到我们的table上。
- 无论何时我们收到这个地震数据,这都会给我们一个漂亮的表格。
看起来很好,这也很简单。我们任然可以做些改善,我们需要研究RxJS中一个比较重要的概念:热和冷Observable。
- Hot and Cold Observables
- “hot”Observable 发射值无论Observable有没有被订阅。换句话说,“cold”Observable 只有每个Observable启动了才会发射序列全部的值。
- Hot and Cold Observables
- 一个observer订阅到一个hot Observable上,当订阅那刻开始会受到Observable发射的所有值。其他的Observer也会那时刻受到同样的值。这和JavaScript的事件工作机制很相似。
- 鼠标事件和证券交易所的股票就是hot Observable的例子。在这些状态下,无论Observable是否有订阅,它都会发射值。在任何订阅将要监听之前它已经在产生值了。下面就是例子:
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);
});
// Result:
// Subscriber1: 23 24
// Subscriber2: 23 24
// Subscriber1: 34 37
// Subscriber2: 34 37
// Subscriber1: 46 49
// Subscriber2: 46 49
// …
- 在这个例子中,连个订阅者都将从Observable上收到同样的值。对JavaScript程序员来说,这种行为很亲切是由于她和JavaScript的事件工作很相似。
- 现在让我们看看cold Observable如何工作。
- Cold Observables
- 一个冷Observable仅当Observer订阅的时候才发射值。
- 例如,Rx.Observable.range返回一个cold Observable,每一个新订阅的observer都将受到一个全部的range:
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
});
- 明白啥时候通过hot或者是cold Observable来处理去避免那些微妙和猥琐的bug是很有必要的。例如,Rx.Observable.interval 返回一个在规律的intervals单位时间段自增的整数。设想,我们想使用它,并推送同样的值到其他的Observer。我们可以这样实现:
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);
});
》
Observer 1, next value: 0
Observer 2: next value: 0
Observer 1, next value: 1
Observer 2: next value: 1
…
- 看起来好像起作用了。但是想想,我们需啊哟第二个订阅者在一个订阅这三秒后加入:
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);
》
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是一个cold Observable。如果hot和cold Observable不是很清楚,这些情况就会很出人意料之外的。
- 如果我们有若干observer监听cold Observable,他们将会受到这个序列值同样的副本。所以严格地将,尽管这些observer是在共用同样的Observable,它们没有真正共享严格上的序列值。如果我们需要observer共享同样的序列,我们需要hot Observable。
- From Cold to Hot Using publish
- 我们使用publish把cold Observable变为hot。调用publish创建了一个新的Observable,它扮演了源Observable的代理。订阅源Observable自己,并推送它收到的值到它的订阅者。
- 一个published Observable是一个真正的ConnectableObservable,它有一个connect方法,我们调用它开始接受值。这允许我们在它开始运行前订阅它:
// 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);
》
index.html:29 Observer 1: 0
index.html:29 Observer 1: 1
index.html:29 Observer 1: 2
index.html:29 Observer 1: 3
index.html:29 Observer 1: 4
index.html:29 Observer 1: 5
index.html:37 Observer 2: 5
…
- Sharing a Cold Observable
- 让我们回到我们的地震例子。到目前为止我们的代码看起来合理;我们有一个Observable的quakes和两个订阅。一个是在地图上绘制地震,另外一个是在table上把它们列出来。
- 但是我们可以使我们的代码更佳高效。由于有两个quakes的订阅,实际上请求这些数据两次。你可以在控制台打印quakes的flatMap操作符内部来检查它们。
- 这些发生应为quakes是cold Observable,并且它重复发射它所有的值给每个新的订阅者,因此,一个新的订阅意味着新的jsonp请求。这就导致我们的程序请求同样的网络资源两次。
- 在下一个例子中,我们将使用share操作符,它将自动创建一个订阅到那个Observable当Observer的数目重从0到1。这样我们就不用调用connect:
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()
现在,quakes表现的像个hot Observable,我们米有必要担心有多少observer我们连接了,因为他们将会接收到严格的同样数据。
- Buffering Values
- 我们之前的代码工作的很好,但是注意到,我们每次接受到一个关于地震的信息就要插入一个tr节点。这不是很高效,因为每次插入我们调整DOM并引起页面的重绘,这使浏览器做不必要的工作来计算新的布局。这导致显而易见的性能下降。
- 我们不得不维护计数器和元素缓存,并且,我们必须没一次的重置他们。但是在RxJS中,我们仅仅只要使用基于buffer的操作符,像bufferWithTime。
- 使用bufferWithTime我们可以缓存进来的值并基于每个x时间段的像数组一样地释放它们:
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);
});
- 这是这些新的代码在执行:
- 1:缓存每个进来的值,并每500毫秒释放一批。
- 2:bufferWithTime每500毫秒执行,如果没有进来的值,它都会产生一个空数组。我们将会过滤它。
- 3:我们插入每个row到一个document fragment,它是一个没有parent的document,这意味着它没有在DOM里,并且调整它的内容是非常快和有效率的。
- 4:我们把fragment添加到DOM上。追加fragment的优势是它是个单个的操作。仅仅引起一次重绘。它也添加fragment的孩子到同样的元素(我们追加fragment本身的)。
- 使用buffers和fragments,我们成功地保证了row的插入操作当,同时保存了我们程序的实时性(最大半秒的延时)。现在我们将准备添加我们仪表的下一个特性:交互性。
- Adding Interaction
- 现在再地图和列表上有地震,但是没有交互在两者间。它将会更好,例如,当我们点击列表的地震是,它给我们在地图上圈出来,并高亮显示当我们的鼠标移到它的row上。让我们来实现它。
- 在Leaflet上,你可以在地图上画,并画它们自己的层以便单独的操作它们。让我们创建一组叫做quakeLayer的layer,这里我们将素有地震的圈。每个圈将是一个layer在组里面。我们也将创建一个codeLayers对象,这里我们存储地震代码和内部layer ID的相关性,所以,我们可以通过地震ID指向圆圈:
var codeLayers = {};
var quakeLayer = L.layerGroup([]).addTo(map);
- 在quakes Observable的initialize内部订阅里,我们添加每一个圈到layer group并在codeLayers里保存它的ID。如果这看起来难理解,这是因为这是Leaflet允许我们地图上有针对性画地唯一途径。
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,它返回一个发射任意给定时刻鼠标是否在特定的地震上的Boolean值的Observable:
? 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);
}
- 1:Rx.helpers.identity是一个identity函数。给一个x参数,它返回一个x。这里我们没有必要写一个函数返回它们接受到的值。
- 2:over是一个Observable发射的true,当用户的鼠标悬浮在元素上。
- 3:out是一个Observable发射false,当用户的鼠标离开元素。
- 4:isHovering混合了over和out,返回一个Observable(发射true当鼠标悬浮一个元素上,发射false当鼠标离开)。
- 使用isHovering我们可以修改创建rows的订阅,所以我们订阅到每个row创建的事件。
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);
})
- 1:使用从row element获取的ID我们在地图上定位地震的圆圈。codeLayers给我们内部相关性的ID,使用quakeLayer.getLayer我们获取圆圈元素。
- 2:我们用现在的row调用isHovering同时我们订阅到结果的Observable上。若干悬浮的参数是true,我们将会花红色圆圈,否则将会是蓝色的。
- 3:我们订阅到目前row click事件创建的Observable上。当列表的row被点击,地图上对应位置将会有圆圈。
- Making It Efficient
- 在一个页面上创建好多事件是一个糟糕的性能的处方对前端开发者来说。在我们之前的例子,对每个row我们创建了三个事件。如果列表中有一百个地震,那么我们就有300个事件在这个页面上流动仅仅是为了高亮的动作。这是个相当糟糕的性能,我们可以做的更好。
- 由于DOM里面的事件是冒泡的(从字节点到父节点),一个众所周知的技术对前端开发者来说避免鼠标事件涉及到太多单个元素的方法是,把鼠标事件绑定到这些元素的父元素上。一旦父元素的事件被捕获,我们可以使用事件的target属性来寻找事件目标的元素。
- 由于我们需要事件click和mouseover相似的功能,所以我们将创建一个getRowFromEvent函数:
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给我们事件发生的table的row,如下是细节:
- 1:我们确认我们获取表中没个单位正在发生的事件,并且,我们检查这些单位的parent是否是一个ID属相的row。这些row是我们添加地震ID的那些。
- 2:pluck操作符精确提取嵌套parentNode里面的元素的target属性。
- 3:这阻止多次获取同样的元素。例如当mouseover事件多次发生时。
- 在上面的选择中,我们绑定每个mouseover和mouseout事件去改变地震圈的颜色,每次当鼠标进入或者是退出那个row时。现在,我们在table上仅仅使用mouseover事件,使用pairwise操作符来组合。
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每个发射的值和之前发射的值组到一个数组中。由于我们一直在获取去不同的row,pairwise将会一直产生这样的row,鼠标刚离开和鼠标正在悬停的。于是使用这些信息,很容易地给每个地震圈上颜色。
- 处理click事件也是相当简单的:
getRowFromEvent(‘click‘)
.subscribe(function(row) {
var circle = quakeLayer.getLayer(codeLayers[row.id]);
map.panTo(circle.getLatLng());
});
- 我们返回到刚订阅到quakes来产生row:
quakes
.pluck(‘properties‘)
.map(makeRow)
.subscribe(function(row) { table.appendChild(row); });
- 我们的代码显示更清新和符合语言规范了,它不依赖与以后的rows,如果没有rows,getRowFromEvent不会产生任何结果。
- 更重要的是,我们的代码很高效,我们我们获取的地震的规模如何,我们一直都是一个的那个的mouseover鼠标事件和单个的click事件,而不是上百个。
时间: 2024-11-08 06:10:42