當前位置:
首頁 > 科技 > 第三方Javascript開發系列之前後端介面協議

第三方Javascript開發系列之前後端介面協議

前言

今日早讀文章由騰訊@zmmbreez分享。

正文從這開始~

在Web網頁開發中有一個有意思的分支,既第三方Javascript腳本的開發。所謂第三方Javascript腳本,就是第三方服務商將自己的服務通過「HTML投放代碼」的形式提供給網站使用。由於Javascript的動態特性,一般的第三方服務都會直接或間接的提供Javascript文件給網站頁面載入。

之前我們已經討論過第三方Javascript的投放代碼設計相關要點了。這次我們進入下一步,來確定下前後端介面協議。下圖是一般的第三方Javascript腳本使用流程:

可以看到第三方Javascript是運行在開發者(即客戶)所提供的網頁中,而所在網頁的域名往往和我們的介面伺服器地址所在的域名不同。所以在設計前後端介面時需要使用支持跨域的前後端介面協議。

之所以出現跨域問題,是因為瀏覽器為了安全啟用了一種同源策略(same-origin policy)。

在討論同源策略的影響之前先要知道什麼是『源』。從同源策略的英文same-origin policy,我們就已經知道源的英文origin。大家可以在瀏覽器的console裡面運行一下console.log(location.origin)列印出當前網頁的源是什麼。就大概知道它其實是包含了網頁地址的一些部分。

可以看到『源』其實包含了三個部分:協議、域名和埠。只要這三者不同就會受到同源策略的影響。IE是一個例外:埠號並未加入到同源策略的組成部分之中。所以http://example.com:8080/和http://example.com屬於同源並且不受任何限制。

再說回同源策略的影響:它會阻止網頁上的Javascript向不同源的伺服器發起XMLHttpRequest請求,如下簡稱XHR。除此之外還會阻止兩個不同源的網頁互相訪問。這不是這篇文章要討論的點,這裡主要討論的是網頁Javascript與伺服器之間的跨域請求。

為什麼要阻止不同源的XHR請求呢?設想一下,你作為一個普通用戶在瀏覽器裡面刷著淘寶(https://taobao.com),這時候郵箱裡面來了一封郵件。標題非常的吸引人,於是你不小心在同一個瀏覽器中點開了(https://attack.com)。這時候網頁內包含的惡意代碼就可以向淘寶的伺服器去發起請求,獲取你的隱私(例如商品瀏覽記錄)。

因為你用了同一個瀏覽器,瀏覽器會記著你的登錄session(即cookie)。幸好瀏覽器有同源策略,taobao.com和attack.com不是同源所以不能發起請求。試想一下如果沒有同源策略,那網站將會是一個非常不安全的地方。

CORS

其實大部分的主流瀏覽器的XHR介面都已經支持了Cross-Origin Resource Sharing(CORS)。通過CORS瀏覽器便可以輕鬆實現跨域的請求了。例子如下:

functionxhr(url,params,callback,opts){

varXHR=XMLHttpRequest;

varxhr=newXHR();

xhr.open( POST ,url,true);

xhr.withCredentials=true;

if(typeofparams=== string ){

xhr.setRequestHeader( Content-Type , application/x-www-form-urlencoded );

}

if(callback){

xhr.onreadystatechange=function(){

if(xhr.readyState!==4){

return;

}

callback(xhr.responseText|| );

};

}

xhr.send(params);

}

xhr( http://test.com/xhr/users , id=0067ED );

你不需要擔心安全問題,因為瀏覽器會判斷請求返回的頭部中是否包含Access-Control-Allow-Origin: http://example.com這樣的頭部信息。如果返回的Access-Control-Allow-Origin值包含當前網頁的源,那麼XHR才會拿到正確的返回值。而這個頭部是否要返回完全是由伺服器端來控制的。

這裡面有兩個細節:

如果XML發起的請求不是一個簡單請求,那麼瀏覽器會便會先發起一次 CORS 預檢請求來檢查自己是否有許可權。所以為了避免多餘的請求,一定要確保自己的請求是簡單請求

如果想要在請求返回時由伺服器種上第三方cookie,那麼需要給XHR實例設置其withCredentials屬性為true。在IE8-9中,微軟給出過一個類似的標準實現:XDomainRequest介面(以下簡稱XDR)。這個介面雖然也能夠實現跨域請求,但是它有一些限制。限制之一就是IE會剝離所有的cookie之後再發給伺服器端,同時IE也會忽略所有的伺服器端返回中的Set-Cookie指令。簡而言之就是不支持cookie

從CORS的支持程度來說其實它已經幾乎被所有的主流瀏覽器支持了,除了IE

JSONP協議

提到跨域請求一般大家先想到的是JSONP,其本質是利用瀏覽器可以載入不同域名下的Javascript文件。伺服器端根據URL上的參數動態的生成不同的Javascript文件返回。這樣瀏覽器端便可以接收到服務端的返回結果了。

例如一個典型的JSONP請求介面地址如下:

http://test.com/jsonp?callback=callback&id=0067ED

伺服器端則會返回如下Javascript文件,其中包含了數據:

callback&&callback({

id: 0067ED ,

username: mmzhou

// ......

});

瀏覽器在接收到返回之後會自動運行這段JS,然後調用全局的callback方法。這個callback方法是由調用方的JS事先事先準備好的,這樣來接收到返回的參數。通過JSONP協議伺服器端可以傳給瀏覽器大量的數據。通常請求下一般都會使用JSONP來實現拉取數據、獲取配置等等才做。但有的時候我們僅僅需要向後端發送數據,並且不關心返回結果是什麼,甚至不在乎有沒有成功。

ping

這種只需要發送數據且不在乎返回結果的請求類型,我們在這篇文章中稱之為ping。也有很多人喜歡稱之為beacon。

小圖片

ping協議其實是一種很古老的跨域方法。它的本質是利用了瀏覽器可以獲取不同域名下圖片的原理,把請求參數放在了圖片地址的URL參數中發給後端。因為是圖片的請求,所以網頁上的JS是不能得到返回圖片是什麼樣的。它的大致JS代碼如下:

functionping(url,callback){

varimg=newImage();

window[key]=img;

vardone=function(){

img.onload=img.onerror=img.onabort=null;

window[key]=null;

img=null;

if(callback){

callback();

}

};

img.onload=img.onerror=img.onabort=done;

img.src=url;

}

ping( http://test.com/ping?id=0067ED&type=pageview );

瀏覽器通過Image介面實例化之後的賦值其src屬性為請求介面的URL地址,並把請求參數放在URL地址中。可以看到圖片實例不需要插入到頁面DOM樹中,避免了返回圖片展示出來被用戶看到。

後端在接到請求之後返回204請求(表示執行成功,但是沒有數據)或者是一張1x1像素的gif圖片。204與gif圖片的區別在於你是否想要知道請求響應是否成功。因為204返回會在部分瀏覽器下導致onerror的回調,這就會讓JS分不清是請求發送失敗還是成功。

頁面關閉時發送數據

ping請求是很輕的,因為它只需要創建一個Image實例即可。但是它也有一個缺陷。我們先假設第三方Javascript需要實現一個功能:在頁面關閉時發送請求給伺服器端記錄頁面已經被關閉。一般的做法是在onload或者beforeOnload事件中發送ping請求。絕大多數瀏覽器會延遲卸載以保證圖片的載入(數據發送成功),但並不是所有瀏覽器都是如此。而且瀏覽器會忽略在onload事件回調中產生的非同步XHR請求 。所以要確保請求發送成功是很難的。

如果瀏覽器支持CORS,那麼發送同步的XHR請求是可以 block 瀏覽器的UI主線程,同樣也可以block瀏覽器關閉直到伺服器端返回結果。但是如果伺服器端響應慢,耗時超過250ms以上(普通人能夠感知到了),這就會帶來很差的用戶體驗。同時在瀏覽器UI主線程中的同步XHR請求已經在標準中被列為deprecated,之後瀏覽器將可能會不再支持(儘管這是一個漫長的過程)。

既然 block 住瀏覽器的UI主線程是可以延遲瀏覽器關閉的,那麼可以想到另一個方法就是人為的設置一段時間的延遲。大概代碼如下:

ping( http://test.com/ping?id=0067ED&type=pageclose );

// dead loop for 200ms to make sure imgPing success.

varend;

vardelay=200;

varnow=+newDate();

for(end=now+delay;now

now=+newDate();

}

通過200ms的JS死循環來延遲瀏覽器關閉,為發送ping請求爭取到更多的時間。200ms是一個很短的時間,用戶一般不會察覺到。這其實在第三方Javascript開發中是一個常用的手段。

Navigator.sendBeacon

標準的制定者早已經看到了這個需求,所以已經提供了一個Navigator.sendBeacon介面來實現ping協議現在在做的事情。這個方法可以用來從瀏覽器向服務端非同步地發送小的HTTP數據。它不像之前提到的兩種方法,會延遲頁面的onload影響下一個頁面的載入。可惜的是至少目前為止(2017年5月8日)IE和Safari瀏覽器還沒有支持它。

window.addEventListener( unload ,function(){

navigator.sendBeacon( /ping ,data);

},false);

智能ping協議

無論是小圖片方式的ping請求還是JSONP請求本質上還是是一個GET請求,它通過URL地址上的參數來傳輸數據。因為URL地址的長度雖然HTTP協議中沒有明確說明,但是很多瀏覽器都有一個自己的上限。例如IE8的長度限制是2083。所以URL的長度最好不要太長。這就導致了一次JSONP的請求可以攜帶的請求參數量有限。這也是它們的最大缺點。

這點在XHR CORS和sendBeacon方法中是不存在的,因為它們可以發起POST請求,把請求參數放在HTTP body中。避免了數據傳輸量小的問題。

圖片、XHR CORS和sendBeacon介面各有優缺點,如下:

我們可以根據傳輸數據在這三者之中智能地選擇一種來傳輸。

普通情況下默認使用圖片方式,因為它最輕量(GET方法)沒有太多副作用。URL超過2083字元時優先使用navigator.sendBeacon方法,如果它不支持再使用XHR CORS方法。如果XHR CORS也不支持則再fallback到圖片方式發送數據。能發則發部分瀏覽器會截斷URL後發送。

如果是在onload事件回調中發送數據,則默認優先使用navigator.sendBeacon方法發送數據。不支持時再使用200ms延遲的小圖片方式發送數據。

submit協議

之前已經提到過無論是小圖片方式的ping協議還是JSONP協議,都存在一個發送數據量不能太大的問題。尤其是遇到圖片上傳之類的需求,使用它們是肯定做不到的。不過主流瀏覽器已經支持了FormData介面,有了它便可以通過XHR的方式發送文件了。例子如下:

// 用戶在表單中選好文件

varformElement=document.getElementById( form );

formElement.onsubmit=function(e){

// 提交時改為XHR提交

e.preventDefault();

varformData=newFormData(formElement);

xhr( http://test.com/xhr/file ,formData);

};

雖然主流瀏覽器都支持了FormData介面,但是IE

form標籤

HTML的form標籤的提交是不受到同源策略限制的。換言之我們可以通過form標籤來實現跨域請求。它的大致流程如下:

首先通過JS來創建一個同源的iframe標籤,並在其內部創建一個form標籤。然後把需要發送的數據項創建成一個個input標籤添加到form標籤內部。最後調用form標籤的submit方法觸發非同步提交(當前頁面不需要刷新)。

服務端接收到數據之後,把返回數據封裝成到一張HTML網頁中。這個網頁會在iframe標籤中執行展示,並將服務端返回的結果(例如JSON數據)通過跨域通訊的方式傳給第三方JS所在的運行環境(即window.parent)。

這之中有幾個細節:

同源的iframe標籤的src屬性必須是about: blank或者javascript: URL(即javascript偽協議),iframe標籤內的文檔可以繼承原始文檔的源。這樣就不會因為同源策略而導致外部的第三方JS不能在iframe標籤內添加form標籤。同時也可以不用走網路請求去載入一個空白頁面

之所以沒有使用form標籤的target屬性來指向提交目標的iframe標籤,是因為要減少對已有站點的影響

因為返回的HTML網頁的域名是我們的介面域名,與當前網頁不是同源,故會受到同源策略影響。需要用postMessage介面來實現跨域的通訊,這塊在稍後的文章中會提到

請確保介面返回的Content-Type頭部是text/html,不然部分瀏覽器可能會有不正確的行為

form表單是可以上傳文件的。在IE9+下因為一些特殊的安全策略,必須由用戶交互(例如點擊)才能觸發文件選擇框去選文件。不能通過input.click()這樣的JS方法來觸發。所以開發者需要創建一個用戶可見的form表單去讓用戶選文件然後非同步提交

在支持CORS和FormData介面的瀏覽器中,就可以優先XHR去提交數據。不支持的瀏覽器再fallback到form+iframe的方式非同步提交。至此我們大概可以得到一個完整submit協議,調用方式如下:

// 普通提交方式

submit( /api/submit ,data,function(data){

console.log(data);

});

// 文件上傳方式

varformElement=document.getElementById( form );

submit(formElement,function(data){

console.log(data);

});

總結

這篇文章中我們總結三種方式的前後端跨域介面協議:JSONP、ping以及submit協議。它們分別用於不同場景,主要的區別如下:

從表中可以看出來:JSONP協議主要適合於拉取數據、獲取配置等不需要大數據量提交的操作;ping協議則更適用於不要關心返回結果的少數據量提交,尤其適合在頁面關閉是發送數據;submit協議則適用於大數據量的提交,尤其適合文件上傳;

作為一個第三方Javascript腳本的開發者,我們需要在不同的場景下選擇最合適的介面協議。希望這篇文章對你有所幫助~

最後

為對前端開發感興趣的你,推薦一門由矽谷技術學習平台優達學城(Udacity)與GoogleGitHubAT&THack Reactor的網頁開發專家及招聘經理共同設計的認證項目,幫助學習者可以從零開始,系統掌握前端開發技能,達到行業領導者認可的矽谷水平。

此課程及實戰項目均與 Google、GitHub 等領先科技企業官方共同設計製作。此外還為學員提供一對一的在線學習輔導,專家代碼評審,學完課程即可獲得 Google&Github 頒發的學習認證,獲得全球頂尖企業的工作內推機會。

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

理解Node事件驅動架構
【第957期】JavaScript 模塊現狀
「零廣告,全乾貨」iWeb峰會上海站,最後500位參會名額限免來襲!
代碼審查應該關注什麼:數據結構
我對知乎前端相關問題的十問十答

TAG:前端早讀課 |

您可能感興趣

四十年的「停戰協議」被打破,Valentino和Mario Valentino 再度對簿公堂
App Engine彈性環境開始提供WebSockets協議
Git 協議版本 2 宣布推出:Git wire protocol 的一次重大更新
Newbee教練借賬號直播玩《Artifact》違反協議被開除
ngrinder 壓測grpc協議方案
Ouroboros協議
Origin Protocol起源協議上海站Meet Up落幕,未來在中國Origin會有更多動作
Inmarsat 和Addvalue簽協議 提供星座連續通信
因違反協議擅自直播V社《Artifact》Newbee隊員遭到開除
Apple Music與唱片公司Ministry of Sound達成獨家協議
Wireshark實戰分析之ARP協議
Zcash即將執行硬分叉 「Powers of Tau「協議已經完成
領英hubtoken—Human Trust Protocol-新一代的人類信任協議
Carry Protocol 凱利協議 新零售的區塊鏈解決方案
Justin Bieber拒絕婚前協議!和Hailey Baldwin不存在離婚!
英邁中國宣布與Palo Alto Networks達成分銷協議
Facebook公布Rift S和Quest攝像頭隱私協議
圖靈獎得主Sivio Micali的Algorand區塊鏈協議簡介
Tribune終止與Sinclair的收購協議 此前被特朗普支持
關於NVMe over Fabric協議和IO流程