前端通訊進階

在幾年前,天空一聲巨響,ajax 閃亮登場. 前端寶寶們如獲至寶~ 已經表單提交神馬的, 真的太 心累了. 有了ajax以後, 網頁的性能可大幅提高,告別刷新,告別如水的流量. 不過,長江後浪推前浪,一代更比一代強. 因爲ajax被同域限制着, 致使, 多服務器配置,雲服務資源的存儲 沒辦法充分利用. 因此,業界想到另一種方法--JSONP. JSONP實際上和ajax沒有半點關係,惟一相同的就是都是異步執行,並且JSONP完美解決了CD(cross domain)問題.
科技就是第一輩子產力, web發展so fast. 之前追求就是靜態網頁,顯示信息而已。 如今,正朝着web2.0,webapp前進。 之前的單向交流 已經不能知足 需求了。 怎麼辦呢?
改唄~
因此,緊接着SSE,websocket 誕生了. 至今爲止, 前端通訊方式算是告一段落。 這裏咱們將圍繞上述的幾種通訊方式進行,簡單的介紹.
如下是幾個技術的順序.javascript

  • ajaxhtml

  • JSOP前端

  • SSEjava

  • websocketnode

ok~ 進入主題吧~jquery

AJAX

相信這個應該不用過多的講解了吧.
差很少就4步:git

  • 建立xhr對象程序員

  • 監聽請求github

  • 設置回調web

  • 設置參數

  • 發送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");

經過使用'nn'進行兩次換行操做--即,產生空行便可.

使用自定義事件

服務器端不只能夠返回指定數據,還能夠返回指定事件.不過默認狀況下都是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 的運做流程圖=>
此處輸入圖片的描述
看不清的話,請新建一個標籤頁看,放大就能看見了.

發展圖譜

很少說了, 上圖~

fetch 補充

fetch 相關補充,能夠查閱覽 前端 fetch 通訊

轉載請註明做者和原文地址:https://segmentfault.com/a/11...

相關文章
相關標籤/搜索