服務端推,指的是由服務器主動的向客戶端發送消息(響應)。在應用層的HTTP協議實現中,「請求-響應」是一個round trip,它的起點來自客戶端,所以在應用層之上沒法實現簡易的服務端推功能。當前解決服務端推送的方案有這幾個:html
長輪訓雖然能夠避免短輪訓形成的服務端過載,但在服務端返回數據後仍須要客戶端主動發起下一個長輪訓請求,等待服務端響應,這樣仍須要底層的鏈接創建並且服務端處理邏輯須要相應處理,不符合邏輯上的流程簡單的服務端推送;前端
websocket鏈接相對而言功能最強大,可是它對服務器的版本有要求,在可使用websocket協議的服務器上儘可能採用此種方式;node
iframe永久幀則是在在頁面嵌入一個專用來接受數據的iframe頁面,該頁面由服務器輸出相關信息,如<script>parent.utils.exec("response")</script>,服務器不停的向iframe中寫入相似的script標籤和數據,實現另外一種形式的服務端推送。不過永久幀的技術會致使主頁面的加載條始終處於「loading」狀態,體驗不好。web
HTML5規範中提供了服務端事件EventSource,瀏覽器在實現了該規範的前提下建立一個EventSource鏈接後,即可收到服務端的發送的消息,這些消息須要遵循必定的格式,對於前端開發人員而言,只需在瀏覽器中偵聽對應的事件皆可。json
相比較上文中提到的3中實現方式,EventSource流的實現方式對客戶端開發人員而言很是簡單,兼容性上出了IE系的瀏覽器(IE、Edge)外其餘都良好;對於服務端,它能夠兼容老的瀏覽器,無需upgrade爲其餘協議,在簡單的服務端推送的場景下能夠知足需求。在瀏覽器與服務端須要強交互的場景下,websocket還是不二的選擇。跨域
瀏覽器端,須要建立一個EventSource對象,而且傳入一個服務端的接口URI做爲參數。瀏覽器
var evtSource = new EventSource('http://localhost:9111/es');
其中,'http://localhost:9111/es'爲服務端吐出數據的接口。目前,EventSource在大多數瀏覽器端不支持
跨域,所以它不是一種跨域的解決方案。服務器
默認EventSource對象經過偵聽「message」事件獲取服務端傳來的消息,「open」事件則在http鏈接創建後觸發,」error「事件會在通訊錯誤(鏈接中斷、服務端返回數據失敗)的狀況下觸發。同時,EventSource規範容許服務端指定自定義事件,客戶端偵聽該事件便可。websocket
evtSource.addEventListener('message',function(e){ console.log(e.data); }); evtSource.addEventListener('error',function(e){ console.log(e); })
事件流的對應MIME格式爲text/event-stream,並且其基於HTTP長鏈接。針對HTTP1.1規範默認採用長鏈接,針對HTTP1.0的服務器須要特殊設置。app
服務端返回數據須要特殊的格式,它分爲四種消息類型:
event, data, id, retry
其中,event指定自定義消息的名稱,如event: customMessagen;
data指定具體的消息體,能夠是對象或者字符串,如data: JSON.stringify(jsonObj)\n\n
,在消息體後面有兩個換行符n,表明當前消息體發送完畢,一個換行符標識當前消息並未結束,瀏覽器須要等待後面數據的到來後再觸發事件;
id爲當前消息的標識符,能夠不設置。一旦設置則在瀏覽器端的eventSource對象中就會有體現(假設服務端返回id: 369n),eventSource.lastEventId == 369
。該字段使用場景不大;
retry設置當前http鏈接失敗後,從新鏈接的間隔。EventSource規範規定,客戶端在http鏈接失敗後默認進行從新鏈接,重連間隔爲3s,經過設置retry字段可指定重連間隔;
每一個字段都有名稱,緊接着有個」:「。當出現一個沒有名稱的字段而只有」:「時,這就會被服務端理解爲」註釋「,並不會被髮送至瀏覽器端,如: commision。
因爲EventSource是基於HTTP鏈接之上的,所以在一段沒有數據的時期會出現超時問題。服務器默認HTTP超時時間爲2分鐘,在node端能夠經過response.connection.setTimeou(0)設置爲默認的2min超時, 所以須要服務端作心跳保活,不然客戶端在鏈接超時的狀況下出現net::ERR_INCOMPLETE_CHUNKED_ENCODING錯誤。經過閱讀相關規範,發現註釋行能夠用來防止鏈接超時,服務器能夠按期發送一條消息註釋行,以保持鏈接不斷。
下面提供koa的服務端代碼:
var fs = require('fs'); var path = require('path'); var PassThrough = require('stream').PassThrough; var Readable = require('stream').Readable; var koa = require('koa'); var Router = require('koa-router'); var app = new koa(); var router = new Router(); function RR(){ Readable.call(this,arguments); } RR.prototype = new Readable(); RR.prototype._read = function(data){ } router.get('/',function(ctx,next){ ctx.set('content-type','text/html'); ctx.body = fs.readFileSync(path.join(process.cwd(),'eventServer.html')); }); const sse = (stream,event, data) => { return stream.push(`event:${ event }\ndata: ${ JSON.stringify(data) }\n\n`) // return stream.write(`event:${ event }\ndata: ${ JSON.stringify(data) }\n\n`); } router.get('/es',function(ctx,next){ var stream = new RR()//PassThrough(); ctx.set({ 'Content-Type':'text/event-stream', 'Cache-Control':'no-cache', Connection: 'keep-alive' }); sse(stream,'test',{a: "yango",b: "tango"}); ctx.body = stream; setInterval(()=>{ sse(stream,'test',{a: "yango",b: Date.now()}); },3000); }); app.use(router.routes()); app.listen(9111,function(){ console.log('listening port 9111'); });
此處須要注意的是koa-router的返回值必須是一個Stream(Readable),這是因爲koa的特殊性形成的。若是context.body不是Stream是一個字符串或者Buffer實例,會直接在node原生中調用res.end(buffer),結束了HTTP響應:
koa lib/application.js // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res);
所以形成了服務端事件流沒法正確響應。而返回Stream類型的方式有幾種,如經過擴展stream模塊的Readable可讀流返回或者直接採用PassThrough流返回,亦可經過through2模塊或者Transform對象實現,歸根到底保證能夠從該stream對象中pipe出數據至http.ServerResponse對象中。
附頁面代碼
<!DOCTYPE html> <html> <head> </head> <body> <div> hello world </div> <p id="info"></p> <script> var infoShow = document.querySelector('#info'); var se = new EventSource('http://localhost:9111/es'); se.addEventListener('test',function(e){ infoShow.textContent += e.data+'\n'; }); se.addEventListener('error',function(e){ console.log(e); }) </script> </body> </html>