在幾年前,天空一聲巨響,ajax 閃亮登場. 前端寶寶們如獲至寶~ 已經表單提交神馬的, 真的太 心累了. 有了ajax之後, 網頁的效能可大幅提升,告別重新整理,告別如水的流量. 不過,長江後浪推前浪,一代更比一代強. 由於ajax被同域限制著, 導致, 多伺服器配置,雲服務資源的儲存 沒辦法充分利用. 所以,業界想到另外一種方法–JSONP. JSONP實際上和ajax沒有半點關係,唯一相同的就是都是非同步執行,而且JSONP完美解決了CD(cross domain)問題.
科技就是第一生產力, web發展so fast. 以前追求就是靜態網頁,顯示資訊而已。 現在,正朝著web2.0,webapp前進。 以前的單向交流 已經不能滿足 需求了。 怎麼辦呢?
改唄~
所以,緊接著SSE,websocket 誕生了. 至今為止, 前端通訊方式算是告一段落。 這裡我們將圍繞上述的幾種通訊方式進行,簡單的介紹.
以下是幾個技術的順序.
- ajax
- JSOP
- SSE
- websocket
ok~ 進入主題吧~
AJAX
相信這個應該不用過多的講解了吧.
差不多就4步:
- 建立xhr物件
- 監聽請求
- 設定回撥
- 設定引數
- 傳送xhr
- 獲得資料執行回撥
這裡,我就直接上程式碼了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var sendAjax = (function() { var getXHR = (function() { var xhr; if(window.XHRHttpRequest){ xhr = new XMLHttpRequest(); }else{ xhr = new ActiveObject("Microsoft.XMLHTTP"); } return xhr; })(); return function(url,opts){ //url為目標地址 var xhr = getXHR(), data; xhr.onreadystatechange = function(){ if(xhr.readyState===4||xhr.status===200){ data = JSON.parse(xhr.responseText); //將data解析為json物件 opts.callback(data); } } xhr.setRequestHeader('Content-Type','application/json'); xhr.open(opts.method,url); //寫入引數 xhr.send(JSON.stringify(opts.data)); //將引數json字元化 } })(); //呼叫執行 sendAjax('www.example.com',{ callback:function(data){ //... }, data:{ name:'JIMMY', age:18 } }) |
這樣差不多就完成了一個ajax的簡單模型。當然,我們也可以使用jquery提供的$.ajax函式, 只是他裡面做了更多的相容性和功能性.
JSONP
JSONP 就是 JSON with Padding… 我真的不知道這個名字的含義到時有什麼卵用…
一開始在使用JSONP時, 就是使用jquery的$.ajax函式就可以了. 但,這造成了一個很不好的impression. 總是讓我們以為,JSONP 和 ajax有什麼關聯似的. 而,事實上,他們兩個是完全不同的機制. xhr原理大家已經很清楚了,就是完完全全的非同步操作. 但JSONP的原理是什麼呢?
JSONP原理
JSONP 其實是和 標籤 有很大的關係. JSONP最大的優勢就是實現非同步跨域的作用, 他到底是怎麼做到的呢?
其實, JSONP就是利用script 的 src
屬性,實現跨域的功能.
talk is cheap, show the code
1 2 3 4 5 6 7 8 |
<script> function processJSON (json) { // Do something with the JSON response }; </script> <script src='http://www.girls.hustonline.net? callback=processJSON&name=jimmy&age=18'></script> |
上面的寫法有點不符合前端風味. 說明一下, 其實processJSON,其實就相當於一個回撥函式而已. 在script–src裡面的內容我們來瞧一瞧. 使用jsoncallback 來指定回撥函式名字, 並且傳入一些引數
- name = jimmy
- age = 18
這就是前端傳送JSONP的全部. 那應該怎麼執行呢?或者說,返回的內容是什麼呢?
很簡單, 根據jsoncallback裡面指定的函式名–processJSON. 在返回的js裡面使用processJSON(data); 來執行.
伺服器端返回的js內容.
1 2 3 |
processJSON({ message:"I've already received" }); |
然後,瀏覽器收到後,直接執行即可. 這裡,我們來模擬一下伺服器端蓋怎樣執行一個JSONP的函式.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const util = require('util'), http = require('http'), url = require('url'); let data = JSON.stringify({ message:"I've already received" }); http.createServer(function(req, res) { req = url.parse(req.url, true); if (!req.query.callback) res.end(); console.log(`name is ${req.query.name} and his age is ${req.query.age}`); res.writeHead(200, { 'Content-Type': 'application/javascript' }) res.end(req.query.callback + "('" + data + "')") }).listen(80) |
ok~ 上面基本上就可以完成一個簡單的JSONP函式執行。 當然,express 4.x 裡面也有相關的JSONP 操作。 有興趣的同學可以看一看.
then, 我們可以模擬一下實在的JSONP請求.上面是直接將script 寫死在html內部, 這樣造成的結果可能會阻塞頁面的載入. 所以,我們需要以另外一種方式進行,使用非同步新增script方法.
1 2 3 4 5 6 7 8 9 |
var sendJSONP = function(url,callbackName){ var script = docuemnt.createELement('script'); script.src = `${url}&callback=${callbackName}`; document.head.appendChild(script); } var sayName = function(name){ console.log(`your name is ${name}`); } sendJSONP('http://girls.hustonline.net?name=jimmy','sayName'); |
上面就是一個精簡版的JSONP了。 另外,也推薦使用jquery的getJSON和$.ajax進行請求.
先看一下getJSON
1 2 3 |
$.getJSON("http://girls.hustonline.net?callback=?", function(result){ console.log(result); }); |
這裡,我們需要關注一下url裡面中callback=?
裡的?
的內涵. jquery使用自動生成數的方式, 省去了我們給回撥命名的困擾。 其實,最後?
會被一串字元代替,比如: json23153123
. 這個就代表你的回到函式名.
不過,還是推薦使用$.ajax,因為你一不小心就有可能忘掉最後的?
.
使用$.ajax
傳送jsonp
1 2 3 4 5 6 7 |
$.ajax({ url: 'http://girls.hustonline.net?name=jimmy', dataType: 'jsonp', success: function(name){ console.log(name); } }); |
這樣,我們就可以利用jquery很簡單的傳送jsonp了.
SSE
ajax和JSONP 都是 client-fetch的操作. 但是有時候, 我們更需要伺服器主動給我們發資訊. 比如,現在的APP應用,完全可以實現伺服器傳送, 然後Client再處理. 而,SSE就是幫助我們向webapp靠近.
SSE 全稱就是 Server-Sent Events. 中譯 為 伺服器推送
.
他的技術並不是很難,和websocket不同,他依賴原生的HTTP,所以對於開發者來說更好理解。 比如,在nodeJS, 只要我不執行res.end(),並且一定時間持續傳送資訊的話,那麼該連線就會持續開啟(keep-alive). 其實通俗來說,就是一個長連線. 所以,以前我們通常使用ajax,iframe長輪詢來代替他.但是這樣有個缺點就是, 可操控性弱, 錯誤率高。 所以,正對於這點W3C, 覺得需要在客戶端另外指定一個機制–能夠保證伺服器推送, 實現連線的keep-alive,操作簡單… 在這樣背景下SSE誕生了.
但SSE和AJAX具體的區別在什麼地方呢?
- 資料型別不同: SSE 只能接受 type/event-stream 型別. AJAX 可以接受任意型別
- 結束機制不同: 雖然使用AJAX長輪詢也可以實現這樣的效果, 但是, 伺服器端(nodeJS)必須在一定時間內執行res.end()才行. 而SSE, 只需要執行res.write() 即可.
簡單demo
先看一個client端, 一個比較簡單的demo
1 2 3 4 5 6 7 8 9 10 11 |
var source = new EventSource('/dates'); //指定路由傳送 source.onmessage = function(e) { //監聽資訊的傳輸 var data = JSON.parse(e.data), origin = e.origin; }; source.onerror = function(e) { //當連線發生error時觸發 console.log(e); }; source.onopen = function(e) { //當連線正式建立時觸發 console.log(e); }; |
SSE主要就是建立一個EventSource物件. 裡面的引數就是傳送的路由, 不過目前還不支援CORS,所以也被限制在同源策略下.
在返回的source裡面包含了,需要處理的一切資訊.SSE也是通過事件驅動的,如上面demo所述. 這裡,SSE通常有一下幾類重要的事件.
eventName | effect |
---|---|
open | 當連線開啟時觸發 |
message | 當有資料傳送時觸發, 在event物件內包含了相關資料 |
error | 當發生錯誤時觸發 |
上面幾個方法比較重要的還是message方法. message主要用來進行資訊的接受, 回撥中的event 包含了返回的相關資料.
event包含的內容
property | effect |
---|---|
data | 伺服器端傳回的資料 |
origin | 伺服器端URL的域名部分,有protocol,hostname,port |
lastEventId | 用來指定當前資料的序號.主要用來斷線重連時資料的有效性 |
伺服器返回資料格式
上文說過,SSE 是以event-stream格式進行傳輸的. 但具體內容是怎樣的呢?
1 2 3 4 5 6 7 8 9 10 11 12 |
data: hi data: second event id: 100 event: myevent data: third event id: 101 : this is a comment data: fourth event data: fourth event continue |
上面就是一個簡單的demo. 每一段資料我們稱之為事件, 每一個事件經過空行分隔. :
前面是資料型別,後面是資料. 通常的型別有:
- 空型別: 表示註釋,在處理是會預設被刪除.比如
: this is a comment
. - event: 宣告該事件型別,比如message.
- data: 最重要的一個型別, 表示傳輸的資料。可以為string格式或者JSON格式. 比如:
data: {"username": "bobby"}
- id: 其實就是lastEventId. 用來表明該次事件在整個流中的序號
- retry: 用來表明瀏覽器斷開再次連線之前等待的事件(不常用)
其實上面最重要的兩個欄位就是data,id. 所以,我們一般獲取的話就可以使用 event.data
和event.lastEventId
.
上文說道, 每一段內容是通過換行實現的, 那伺服器端應該怎麼實現, 寫入的操作呢?
同樣, 這裡以nodeJS 為例:
1 2 |
res.write("id: " + i + "n"); res.write("data: " + i + "nn"); |
通過使用’nn’進行兩次換行操作–即,產生空行即可.
使用自定義事件
伺服器端不僅可以返回指定資料,還可以返回指定事件.不過預設情況下都是message
事件, 但我們也可以指定事件. 比如
1 2 3 |
event: myevent data: third event id: 101 |
這裡出發的就是 myevent事件。 即, 這就是觸發自定義事件的方式.
在front-end 我們可以使用addEventListener 來進行監聽.
1 2 3 4 |
var source = new EventSource('/someEvents'); source.addEventListener('myevent', function(event){ //doSth }, false); |
服務端使用SSE
由於使用的是HTTP協議,所以對於服務端基本上沒什麼太大的改變. 唯一注意的就是, 傳送資料使用res.write()即可,斷開的時候使用res.end();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*" //允許跨域 }); var num =0; var f = function(){ if(num===10){ res.end(); }else{ res.write("id: " + num + "n"); res.write("data: " + num + "nn"); num++; } setTimeout(f,1000); } f(); |
Ok~ 這裡有一個demo, 大家可以開啟控制檯看一下. 會發現,有一個連線一直處於Content-Download狀態. 該連線就是一個SSE。
相容性
目前SSE,在市面上大受歡迎, 不過總有一個SB, 離經叛道… 居然連edge都不支援. 偶爾去翻了一下,還在underConsideration. 結果底下的評論基本都是xxxx. 有空可以去看看, 逼逼MS程式設計師.
websocket
websocket 不同於其他的HTTP協議,他是獨立於HTTP存在的另外一種通訊協議。比如,像這樣的一個路徑ws://websocket.example.com/
,就是一個websocket 通訊. 通常的實時通訊並不會傳輸大量的內容, 所以,對於HTTP協議那種,進行連線時需要傳遞,cookie和request Headers來說, 這種方式的通訊協議,會造成一定的時延(latency). websocket通訊協議就是在這樣的背景下誕生了, 他與SSE,ajax polling不同的是–雙向通訊.
talk is cheap, show the code
我們來看一個簡單的websocket demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var socket = new WebSocket('ws://localhost:8080/'); socket.onopen = function () { console.log('Connected!'); }; socket.onmessage = function (event) { console.log('Received data: ' + event.data); socket.close(); }; socket.onclose = function () { console.log('Lost connection!'); }; socket.onerror = function () { console.log('Error!'); }; socket.send('hello, world!'); |
可以說上面就是一個健全的websocket 通訊了. 和SSE一樣,我們需要建立一個WebSocket物件, 裡面的引數指定連線的路由. 而且,他也是事件驅動的.
常見的事件監聽有.
event | effect |
---|---|
open | 當ws連線建立時觸發 |
message | 當有資訊到來時觸發 |
error | 當連線發生錯誤時觸發 |
close | 當連線斷開時觸發 |
websocket 傳送資料
另外,websocket 最大的特點就是可以雙向通訊。這裡可以使用.
ws.send()
方法傳送資料, 不過只能傳送String和二進位制. 這裡,我們通常call 資料叫做 Frames
. 他是資料傳送的最小單元.包含資料的長度和資料內容.
下面就是幾種常用的傳送方式
1 2 3 4 5 6 7 8 9 10 11 |
socket.send("Hello server!"); socket.send(JSON.stringify({'msg': 'payload'})); var buffer = new ArrayBuffer(128); socket.send(buffer); var intview = new Uint32Array(buffer); socket.send(intview); var blob = new Blob([buffer]); socket.send(blob); |
另外還可以使用binaryType指定傳輸的資料格式,不過一般都用不上,就不說了.
不過需要提醒的是, send方法,一般在open和message的回撥函式中呼叫.
websocket 接受資料
同理,和SSE差不多, 通過監聽message事件,來接受server傳送回來的資料. 接受其實就是通過event.data
來獲取. 不過, 需要和server端商量好data的型別.
1 2 3 4 5 6 7 |
ws.onmessage = function(msg) { if(msg.data instanceof Blob) { processBlob(msg.data); } else { processText(JSON.parse(msg.data)); //接受JSON資料 } } |
那server端應該怎樣處理websocket通訊呢?
websocket雖然是另外一種協議,不過底層還是封裝了TCP通訊, 所以使用nodeJS的net模組,基本就可以滿足,不過裡面需要設定很多的頭. 這裡推薦使用ws模組.
NodeJS 傳送websocket資料
簡單的websocket demo
1 2 3 4 5 6 7 8 9 10 11 |
var WebSocketServer = require('ws').Server , wss = new WebSocketServer({ port: 8080 }); //通過ws+ssl的方式通訊. 和HTTPS類似 wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { console.log('received: %s', message); }); ws.send('something'); }); |
可以參考treeHouse 編寫的WSdemo
為什麼websocket會有子協議
由於websocket 本身的協議對於資料格式來說,不是特別的清晰明瞭,ws可以傳輸text,blob,binary等等其他格式. 這樣對於安全性和開發效能來說,友好度很低。所以,為了解決這個問題, subprotocols 出現了. 在使用時,client和server都需要配置一樣的subprotocols. 例如:
1 2 |
var ws = new WebSocket('wss://example.com/socket', ['appProtocol', 'appProtocol-v2']); |
服務端需要將subprotocols傳送過去, 在handshakes的過程中,server 會識別subprotocols. 如果,server端也有相同的子協議存在, 那麼連線成功. 如果不存在則會觸發error, 連線就被斷開了.
websocket 協議內容
websocket 是有HyBi Working Group 提議並建立的。 主要的內容就是 一張表.
相比TCP來說, 真的是簡單~
其實一句話就可以說完.
Figure 17-1. WebSocket frame: 2–14 bytes + payload
具體內容是:
- 第一個位元(FIN) 表明, 該frame 是否資訊的最後一個. 因為資訊可以分多個frame包傳送. 但最終客戶端接收的是整個資料
- opcode(4bit)–操作碼, 表示傳送frame的型別 比如text(1)|| binary(2)
- Mask 位元位表示該資料是否是從 client => server.
- Extended length 用來表示payload 的長度
- Masking key 用來加密有效值
- Payload 就是傳輸的資料
websocket 能否跨域?
首先,答案是。 但,網上有兩部分內容:
WebSocket is subject to the same-origin policy
WebSocket is not subject to the same-origin policy
看到這裡我也是醉了. 事實上websocket 是可以跨域的。 但是為了安全起見, 我們通常利用CORS 進行 域名保護.
即,設定如下的相應頭:
Access-Control-Allow-Origin: http://example.com
這時, 只有http://example.com 能夠進行跨域請求. 其他的都會deny.
那什麼是CORS呢?
how does CORS work
CORS 是Cross-Origin Resource Sharing–跨域資源分享. CORS 是W3C 規範中 一項很重要的spec. 一開始,ajax 收到 the same origin policy 的限制 奈何不得。 結果出來了JSONP 等 阿貓阿狗. 這讓ajax很不安呀~ 但是,W3C 大手一揮, 親, 我給你開個buff. 結果CORS 就出來了。
CORS 就是用來幫助AJAX 進行跨域的。 而且支援性也超級好. IE8+啊,親~ 但是IE 是使用XDomainRequest 傳送的.(真醜的一逼)
所以,這裡安利一下Nicholas Zakas大神寫的一個函式.(我把英文改為中文了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // 檢查xhr是否含有withCredentials屬性 //withCredentials 只存在於XHR2物件中. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // 檢查是否是IE,並且使用IE的XDomainRequest xhr = new XDomainRequest(); xhr.open(method, url); } else { // 否則..基本上就不能跨域了 xhr = null; } return xhr; } |
然後, 就可以直接,xhr.send(body). 那CORS其實就完成了.
但,withCredentials
是什麼意思呢?
CORS中的withCredentials
該屬性就是用來表明,你的request的時候,是否帶上你的cookie. 預設情況下是不帶的. 如果你要傳送cookie給server的話, 就需要將withCredentials設定為true了.
xhr.withCredentials = true;
但是,server並不是隨便就能接受並返回新的cookie給你的。 在server端,還需要設定.
Access-Control-Allow-Credentials: true
這樣server才能返回新的cookie給你. 不過,這還有一個問題,就是cookie還是遵循same-origin policy的。 所以, 你無法使用document.cookie去訪問他. 他的CRUD(增刪查改)只能由 server控制.
CORS 的preflight 驗證
CORS的preflight request, 應該算是CORS中裡面 巨坑的一個。 因為在使用CORS 的時候, 有時候我命名只傳送一次請求,但是,結果出來了兩個。 有時候又只有一個, 這時候, 我就想問,還有誰能不懵逼.
這裡,我們就需要區分一下. preflight request的作用到底是什麼。
preflight request 是為了, 更好節省寬頻而設計的. 因為CORS 要求的網路質量更高, 而且 花費的時間也更多. 萬一, 你傳送一個PUT 請求(這個不常見吧). 但是服務端又不支援, 那麼你這次的 請求是失敗了, 浪費資源還不說,關鍵使用者不能忍呀~
所以, 這裡我們就需要區分,什麼是簡單請求, 什麼是比較複雜的請求
簡單請求
簡單請求的內容其實就兩塊, 一塊是method 一塊是Header
- Method
- GET
- POST
- Header
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID //這是SSE的請求頭
- Content-Type ,但只有一下頭才能算簡單
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
比如, 我使用上面定義好的函式createCORSRequest. 來傳送一個簡單請求
1 2 3 |
var url = 'http://example.com/cors'; var xhr = createCORSRequest('GET', url); xhr.send(); |
我們來看一下,只傳送一次簡單請求時,請求頭和相應頭各是什麼.(剔除無關的Headers)
1 2 3 4 |
//Request Headers POST HTTP/1.1 Origin: http://example.com Host: api.bob.com |
1 2 3 4 5 |
//Response Headers Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: Vary Content-Type: text/html; |
上面就是一個簡單的CORS 頭的互動。 另外,說明一個Access-Control-Allow-Origin
,該頭是必不可少的.
本來在XHR中, 一般可以通過xhr.getResponseHeader()來獲取相關的相應頭。 但是 在CORS中一般可以獲得如下幾個簡單的Header:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- ETag
- Last-Modified
- Pragma
如果你想暴露更多的頭給使用者的話就可以使用,Access-Control-Expose-Headers
來進行設定. 多個值用’,’分隔.
那傳送兩次請求是什麼情況呢?
我們如果請求的資料是application/json的話,就會傳送兩次請求.
1 2 3 4 |
var url = 'http://example.com/cors'; var xhr = createCORSRequest('POST', url); xhr.setRequestHeader('Content-Type','application/json'); xhr.send(); |
第一次,我們通常叫做preflight req. 他其實並沒有傳送任何 data過去. 只是將本次需要傳送的請求頭髮送過去, 用來驗證該次CORS請求是否有效.
上面的請求頭就有:
1 2 3 4 5 6 7 8 |
OPTIONS HTTP/1.1 Origin: http://example.com Content-Type: application/json Access-Control-Request-Method: POST Access-Control-Request-Headers: Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive |
Access-Control-Request-Method
就是用來表明,該次請求的方法.
請求內沒有任何附加的資料.
如果該次preflight req 伺服器可以處理,那麼伺服器就會正常返回, 如下的幾個頭.
1 2 3 4 5 6 7 |
//Response Header <= HTTP/1.1 204 No Content Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Max-Age: 86400 Access-Control-Allow-Headers: Custom-Header Access-Control-Allow-Origin: http://foo.com Content-Length: 0 |
說明一下里面的頭
- Access-Control-Allow-Methods: 指明伺服器支援的方法
- Access-Control-Max-Age: 表明該次preflight req 最長的生存週期
- Access-Control-Allow-Headers: 是否支援你自定義的頭. 比如: Custom-Header
這裡,主要要看一下Access-Control-Max-Age
. 這和preflight另外一個機制有很大的關係. 因為preflight 已經多發了一次請求, 如果每次傳送json格式的ajax的話, 那我不是每次都需要驗證一次嗎?
當然不是. preflight req 有自己的一套機制. 通過設定Max-Age 來表示該次prefilght req 的有效時間。 在該有效時間之內, 後面如果有其他複雜ajax 的跨域請求的話,就不需要進行兩次傳送驗證了.
而且,第二次的請求頭和相應頭 還可以減少不少重複的Header.
第二次繼續驗證
1 2 3 4 5 6 7 8 9 10 11 |
=> POST - HEADERS - Origin: http://example.com Access-Control-Request-Method: POST Content-Type: application/json; charset=UTF-8 <= HTTP/1.1 200 OK - RESPONSE HEADERS - Access-Control-Allow-Origin: http://example.com Content-Type: application/json Content-Length: 58 |
ok~
最後上一張 Monsur Hossain大神話的CORS server 的運作流程圖=>
看不清的話,請新建一個標籤頁看,放大就能看見了.
發展圖譜
不多說了, 上圖~
臨終總結
心累~ 手痠~ 腰疼~
到底是什麼讓我堅持到現在, 是什麼讓我手碼1W+.
不是偉大的前端技術~ 而是, 可歌可敬的你們呀~ 心疼的我,能不能點個贊,讓我 心裡平衡一下呢?