前端通信進階

吳青強 8年前發布 | 23K 次閱讀 sse cors JavaScript開發 WebSocket JSONP Ajax

在幾年前,天空一聲巨響,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

  • 獲得數據執行回調

這里,我就直接上代碼了。

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 其實是和< script> 標簽 有很大的關系。 JSONP最大的優勢就是實現異步跨域的作用, 他到底是怎么做到的呢?
其實, JSONP就是利用script 的 src屬性,實現跨域的功能。

talk is cheap, show the code

<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內容。

processJSON({
    message:"I've already received"
});

然后,瀏覽器收到后,直接執行即可。 這里,我們來模擬一下服務器端蓋怎樣執行一個JSONP的函數。

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方法。

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

$.getJSON("http://girls.hustonline.net?callback=?", function(result){
  console.log(result);
});

這里,我們需要關注一下url里面中callback=?里的?的內涵。 jquery使用自動生成數的方式, 省去了我們給回調命名的困擾。 其實,最后?會被一串字符代替,比如: json23153123。 這個就代表你的回到函數名。
不過,還是推薦使用$。ajax,因為你一不小心就有可能忘掉最后的?
使用$。ajax發送jsonp

 $.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

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格式進行傳輸的。 但具體內容是怎樣的呢?

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。dataevent。lastEventId
上文說道, 每一段內容是通過換行實現的, 那服務器端應該怎么實現, 寫入的操作呢?
同樣, 這里以nodeJS 為例:

res.write("id: " + i + "\n");
res.write("data: " + i + "\n\n");

通過使用'\n\n'進行兩次換行操作--即,產生空行即可。

使用自定義事件

服務器端不僅可以返回指定數據,還可以返回指定事件。不過默認情況下都是message事件, 但我們也可以指定事件。 比如

event: myevent
data: third event
id: 101

這里出發的就是 myevent事件。 即, 這就是觸發自定義事件的方式。
在front-end 我們可以使用addEventListener 來進行監聽。

var source = new EventSource('/someEvents');
source.addEventListener('myevent', function(event){
    //doSth
}, false);

服務端使用SSE

由于使用的是HTTP協議,所以對于服務端基本上沒什么太大的改變。 唯一注意的就是, 發送數據使用res。write()即可,斷開的時候使用res。end();

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 + "\n\n");
    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

  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。 他是數據發送的最小單元。包含數據的長度和數據內容。
下面就是幾種常用的發送方式

 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的類型。

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

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。 例如:

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大神寫的一個函數。(我把英文改為中文了)

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。 來發送一個簡單請求

var url = 'http://example.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();

我們來看一下,只發送一次簡單請求時,請求頭和相應頭各是什么。(剔除無關的Headers)

//Request Headers
POST  HTTP/1.1
Origin: http://example.com
Host: api.bob.com
//Response Headers
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Vary
Content-Type: text/html; charset=utf-8

上面就是一個簡單的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的話,就會發送兩次請求。

var url = 'http://example.com/cors';
var xhr = createCORSRequest('POST', url);
xhr.setRequestHeader('Content-Type','application/json');
xhr.send();

第一次,我們通常叫做preflight req。 他其實并沒有發送任何 data過去。 只是將本次需要發送的請求頭發送過去, 用來驗證該次CORS請求是否有效。
上面的請求頭就有:

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 服務器可以處理,那么服務器就會正常返回, 如下的幾個頭。

//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。
第二次繼續驗證

=> 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+。
不是偉大的前端技術~ 而是, 可歌可敬的你們呀~ 心疼的我,能不能點個贊,讓我 心里平衡一下呢?

原文:https://segmentfault。com/a/1190000004682473

 本文由用戶 吳青強 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!