前陣子,在Media看到一篇文章《Node.js can HTTP/2 push!》。看到push這個字眼時,我想到的是WebSocket消息推送。難不成HTTP/2還能像WebSocket那樣能夠服務端主動推送消息?好厲害,我就一會兒來了興趣。javascript
然而閱讀完文章以後,發現理想與現實略有差距。簡單的說,HTTP/2 所謂的server push實際上是當服務器接收一個請求時,能夠響應多個資源。舉個栗子:瀏覽器向服務器請求index.html,服務器不只把index.html返回了,還能夠把index.js,index.css等一塊兒推送給客戶端。最直觀的好處就是,瀏覽器不用解析頁面再發起請求來獲取數據,節約了頁面加載時間。css
雖然略有差距,但看起來仍是挺有意思的,值得去嘗試一下。畢竟紙上得來終覺淺,絕知此事要躬行!html
我以前並未使用過HTTP/2,在進行實踐以前,總要先了解一下。關於HTTP/2,網上也有不少資料,我這裏就簡單說一下它最大的優勢:快!。這裏的快是相比HTTP 1.x 而言的,那爲何它會更快呢?前端
這裏的頭部指的是http請求頭headers。你們可能會想請求頭能有多大呢,跟資源相比算不上啥。其實否則,隨着互聯網的發展,請求頭裏攜帶的數據愈來愈多了,隨隨便一個「user-agent」就一長串。另外cookie也會被存放愈來愈多的信息。更煩的是,一個頁面全部的請求,都會帶上這些重複的請求頭數據。java
因此HTTP/2採用HPACK算法,能極大壓縮頭部數據,減小整體資源請求大小。大體的原理就是維護兩本字典,一本靜態字典,維護比較常見的頭部名稱。一本動態字典,維護不一樣請求的公共的頭數據。node
咱們知道,在HTTP 1.x中,咱們是能夠並行請求的。可是,瀏覽器對於同一個域名的並行請求是有上限的(FireFox, Chrome上限6個 )。因此不少網站的靜態資源站可能會有多個。並且每次請求都要從新創建TCP鏈接,想必大部分web工程師都瞭解過TCP三次握手,這個握手的代價也是比較高的。git
雖然http1.x裏有keep-alive能夠避免TCP三次握手,可是keep-alive又是串行的。因此要麼並行多握手,要麼串行不握手,都不是最好的結果,咱們但願的是並行也不握手。程序員
幸運的是HTTP/2解決了這個問題。當客戶端與服務端創建鏈接後,就會在雙方創建一個雙向流通道。這個流通道,能夠同時包含多個消息(http請求),不一樣消息各自的數據幀在流裏能夠亂序並行的發送,不會互相影響與堵塞,從而實現了一個TCP連接,併發執行N個http請求。經過提升併發,減小TCP鏈接開銷,HTTP/2的速度獲得了很大提高,尤爲是在網絡延遲比較高的狀況下。github
這裏用展示兩張網絡請求時間瀑布流對比圖:web
HTTP 1.1
HTTP/2
上文中,咱們描述了HTTP/2的鏈接會創建一個雙向流通道。Server Push就是在某次流中,能夠返回客戶端並無主動要的數據。
上述的頭部壓縮、多路複用,並不須要開發人員作什麼操做,只要開啓HTTP/2,瀏覽器也支持就能夠了。可是Server Push就須要開發人員編寫代碼去操做了。那咱們就動手,在Node上玩玩看。
在Node 8.4.0版本時,就對HTTP/2實驗性的支持了。2018年4月24日晚,Node v10終於發佈了,然而對於HTTP/2,仍是實驗性的支持。。。不過社區已經對HTTP/2移除實驗性進行討論了,相信在不遠的未來應該能看到Node對HTTP/2更好的支持。所以在這以前,咱們能夠先去掌握這個知識,作一些實踐。
咱們先根據Node文檔,建立一個HTTP/2服務。這裏須要提的一點就是,目前流行的瀏覽器都不支持未加密的、不安全的HTTP/2。因此咱們必須生成下證書與祕鑰,而後經過http2.createSecureServer
建立安全的HTTP/2連接。
想本身實踐,生成本地證書的同窗能夠參考這裏:傳送門。
// server.js const http2 = require('http2') const fs = require('fs') const streamHandle = require('./streamHandle/sample') const options = { key: fs.readFileSync('./ryans-key.pem'), cert: fs.readFileSync('./ryans-cert.pem'), } const server = http2.createSecureServer(options) server.on('stream', streamHandle) server.listen(8125)
而後咱們再照着文檔,編寫對流的處理,並推送一個url路徑爲 '/' 的數據。
// streamHandle/sample.js module.exports = stream => { stream.respond({ ':status': 200 }) stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => { if (err) throw err pushStream.respond({ ':status': 200 }) pushStream.end('some pushed data') pushStream.on('close', () => console.log('close')) }) stream.end('some data') }
而後咱們打開chrome,訪問https://127.0.0.1:8125 發現頁面顯示的一直是 some data。some pushed data這個主動推送的數據不知在哪裏。打開網路請求面板,也沒有任何其餘請求。
百思不得其解阿,但我又不想止步於此,怎麼辦呢?
我決定先寫一個正常的HTTP/2業務請求,代碼以下:
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 請求api stream.respond({ 'content-type': 'application/json', ':status': 200 }) stream.end(JSON.stringify({ success: true })) } else if (path.indexOf('static') >= 0) { // 請求靜態資源 const fileType = path.split('.').pop() const contentType = fileType === 'js' ? 'application/javascript' : 'text/css' stream.respondWithFile(`./src${path}`, { 'content-Type': contentType }) } else { // 請求html stream.respondWithFile('./src/index.html') } }
代碼大意就是,判斷請求連接,當請求地址帶有api
字眼時就返回一個json,當請求地址帶有static
時,就返回對應路徑的靜態資源。其餘狀況就返回一個html
文件。
html文件內容爲:
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <title>HTTP/2 Server Push</title> <link rel="shortcut icon" type=image/ico href=/static/favorite.ico> <link href=/static/css/app.css rel=stylesheet> </head> <body> <h1>HTTP/2 Server Push</h1> <script type=text/javascript src=/static/js/test.js></script> </body> </html>
運行後咱們再打開chrome,訪問https://127.0.0.1:8125 ,咱們能看到頁面正常渲染了,查看網絡面板,發現協議也已是HTTP/2。
這樣咱們就開發了一個很是簡單的HTTP/2應用。下一步,咱們再加上server push的功能,當訪問index.html
的請求時,咱們主動將js
的資源返回,看看瀏覽器是怎麼樣的響應狀況。
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 請求api部分代碼-略 } else if (path.indexOf('static') >= 0) { // 請求靜態資源部分代碼-略 } else { // 請求html時 主動推送js文件 stream.pushStream( { ':path': '/static/js/test.js' }, (err, pushStream, headers) => { if (err) throw err pushStream.respondWithFile('./src/static/js/test2.js', { 'content-type:': 'application/javascript' }) pushStream.on('error', console.error) } ) stream.respondWithFile('./src/index.html') } }
代碼大意就是,當客戶端請求index.html
時,服務端除了返回index.html
文件,順便把test2.js
這個文件推給服務端,客戶端若是再次請求 https://127.0.0.1:8125/static/js/test.js
時,就會直接獲取到test2.js
。
這裏我用test2.js
的目的是爲了方便的知道,客戶端請求的究竟是服務端推送的test2.js
文件,仍是直接經過服務器再次請求獲取到的test.js
文件。
其中test.js
會在頁面打印:This is normal js.test2.js
會在頁面打印:This is server push js.
按照指望,應該是後者。而後咱們打開chrome,訪問 https://127.0.0.1:8125,展示以下結果:
!!!!掀桌!!!!
這個展現結果並非意料中的打印出This is server push js
,頁面請求的js文件仍是正常網絡請求的,並不是是我主動推送的test2.js
。我翻山越嶺搜遍祖國內外,終於在Node的一條issue下看到相似的問題:http2 pushStream not providing files for :path entries (CHROME 65, works in FF) 。
Works in FireFox ??????? Chrome的bug ??????
你照着文檔寫代碼,結果卻不像文檔所展現,各類排查沒有用,最終發現是一些非主觀的緣由,程序員最大的痛苦莫過於此....而後我夾雜着痛苦心塞和峯迴路轉的心情,打開了本身的Firefox,訪問頁面,展示以下結果:
這回終於對了!能夠看到,頁面中打印的是test2.js
文件的輸出結果。
最開始依葫蘆畫瓢沒用,其實也是由於Chrome的bug。無論怎麼樣,咱們仍是往前邁進了巨大的一步。
ps: 本人chrome版本66.0.3359.117,依舊有此bug
雖然咱們前進了一大步,但是面臨了一個很尷尬的問題:咱們的靜態資源更可能是託管在cdn上的。那咱們實際場景就會遇到以下狀況:
這麼一說,這就是個雞肋啊!到頭來竹籃打水一場空?
作人仍是不能輕易的放棄治療。再仔細想一想,仍是有一些應用場景的---初始化的API請求。
如今不少單頁應用,每每有不少的初始化請求,獲取用戶信息、獲取頁面數據等等。而這些都是須要html加載完,而後js加載完,而後再去執行的。並且不少時候,這些數據不加載完,頁面都只能空白顯示。但是單頁應用的js資源每每又很大。一個vendor包好幾兆也很常見。等瀏覽器加載並解析完這麼大的包,可能已經不少時間消耗了。這時候再去請求一些初始化API,若是這些API又比較費時的話,頁面就要多空白很長時間。
但若是能在請求html時,咱們就把初始化的api數據推送給客戶端,當js解析完再去請求時,就能立刻獲取到數據,這就能節省寶貴的白屏時間。說幹就幹,咱們再次動手實踐!
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 請求api stream.respond({ 'content-type': 'application/json', ':status': 200 }) stream.end(JSON.stringify({ apiType: 'normal' })) } else if (path.indexOf('static') >= 0) { // 請求靜態資源代碼-略 } else { // 請求html stream.pushStream({ ':path': '/api/getData' }, (err, pushStream, headers) => { if (err) throw err pushStream.respond({ ':status': 200 , 'content-type': 'application/json'}); pushStream.end(JSON.stringify({ apiType: 'server push' })) }); stream.respondWithFile('./src/index.html') } }
一樣的,我讓正常請求api與服務端推送的api數據作一些差別,以便於更直觀的判斷是否獲取了服務端推送的數據。而後在前端的js文件中寫以下請求,並打印出請求結果:
window.fetch('/api/getData').then(result => result.json()).then(rs => { console.log('fetch:', rs) })
使人遺憾的是,咱們的到的是以下的結果:
請求的結果表示這並非server push的數據。吃一塹長一智,這會不會又是瀏覽器的什麼bug?亦或者是否是fetch
不支持獲取server push的數據?我立刻用XMLHttpRequest又寫了一版:
window.fetch('/api/getData').then(result => result.json()).then(rs => { console.log('fetch:', rs) }) const request = new XMLHttpRequest(); request.open('GET', '/api/getData', true) request.onload = function(result) { console.log('ajax:', JSON.parse(this.responseText)) }; request.send();
結果以下:
!!!!掀桌!!!!
居然還真的是fetch
不支持http2 server push!
其實除了fetch
不支持外,還有一個比較致命的問題,就是這個server push,在當下的node服務器上,不能對服務端推送資源的url進行模糊匹配。也就是說,若是一個請求有url動態參數的話,實際上是匹配不到的。像我例子中的stream.pushStream({ ':path': '/api/getData' }, pushHandle)
,若是前端請求的接口是 /api/getData?param=1
,那就得不到server push的數據了。
另外,它僅支持GET請求與HEAD請求,POST、PUT這些也是不支持的。
針對fetch
這個問題,我又了搜了下祖國內外,也沒得出個因此然來。這也變相的說明,目前社區裏針對server push這個特性使用的還不多,遇到問題時,很難快速的去定位與解決問題。
因此,彷佛在推送api上,它的應用場景又侷限了,僅適用於推送固定URL的初始化GET請求。
綜上所述,我得出的結論就是:目前在Node上,使用server push,極大的狀況與機率是不合適的,是付出大於收益的。主要因爲以下緣由:
注:上述內容僅侷限在Node服務,其餘服務器本人未有研究,不必定有上述問題
雖然server push我目前以爲很差用,可是HTTP/2仍是個好東西的,除了我文章開頭講的那些好處外,HTTP/2還有不少新奇的有用的特性,諸如流優先級、流控制等一些特性,本文並未講到。你們能夠去了解了解,對咱們將來開發高性能的web應該確定有不少幫助!
本文所涉及源碼:https://github.com/wuomzfx/ht...
原文連接:https://yuque.com/wuomzfx/art...