瀏覽器出於安全的考慮,引入了同源策略。這種策略會對咱們頁面上執行的js訪問資源的時候進行限制,好比咱們不能直接經過js訪問不一樣源之下的頁面DOM結構,同時在對不一樣源發送請求時也沒法獲取到服務器響應內容(服務器會正常處理請求並返回響應內容,可是返回的內容被瀏覽器攔截掉了)。這裏還牽扯到「源」這個概念,若是咱們訪問的目標url和當前頁面所在的url二者的協議、域名、端口只要有一個不相同,那麼就認爲是屬於兩個不一樣的源。明白了源的定義以後,咱們再來看看在同源策略的做用下,咱們能夠在頁面上作的以及不能作的都有哪些操做。javascript
先說可以作的,好比經過js重定向咱們的頁面(修改location.href),表單提交,這些都是能夠的。還有就是經過嵌入一些HTML標籤來加載咱們須要的資源,好比script標籤引入一段腳本、img標籤插入一張圖片、link標籤加載樣式文件、iframe嵌入不一樣源的頁面等等也都是能夠的。這些也是咱們平常開發過程當中再正常不過的操做了。html
可是,同源策略對js訪問一些敏感資源則進行了限制。除了開頭提到的那兩點以外,還有就是js中沒法訪問不屬於同個源的cookie、LocalStorage中存儲的內容。具體來講,cookie和LocalStorage在控制哪些源能夠訪問的問題上仍是細微的差異,父域在設置cookie的時候能夠設定容許子域訪問這段cookie,同時Cookie只和域名以及路徑關聯,若是是同個域名不一樣端口的源依然是共享同個域名下的Cookie的,而LocalStorage則是以源爲單位進行管理,相互獨立,不一樣源之間沒法相互訪問LocalStorage中的內容。前端
客戶端與不一樣源的服務器的通訊問題是日常開發過程當中須要解決的很常見的問題,主要有如下幾種方式:java
這種方法應該是用得比較多的一種。CORS全稱是Cross-Origin Resource Sharing,翻譯過來就是跨域資源共享。基本思想就是引入一些自定義的HTTP Header來完成客戶端與服務端的通訊。json
對於一些簡單請求,瀏覽器在發送請求時會帶上Origin請求頭,指示當前的源,服務器端在處理請求時不會去檢查當前請求來源是否合法,依然會正常處理請求並響應,最終瀏覽器在拿到響應以後會檢查服務端響應的Access-Control-Allow-Origin列表中是否存在當前頁面所在的源,若是不存在會直接block掉當前請求。canvas
在瀏覽器看來,同時知足如下條件的請求都認爲是簡單請求:後端
請求方法爲GET或者POST; 只包含Accept、Accept-Language、Content-Language或者Content-Type(取值爲application/x-www-form-urlencoded
,multipart/form-data
, 或者text/plain
),其他狀況的Header則屬於非簡單Header;api
對於非簡單請求,瀏覽器會先向服務器發送一個Preflight請求,該請求使用Option方法,幷包含如下Header:跨域
其中後兩個Header只會出如今Preflight請求中。而後瀏覽器收到包含如下Header的服務器響應:瀏覽器
Preflight請求至此也算是告一段落,以後瀏覽器會檢查當前請求發出的源是否在服務端響應的Access-Control-Allow-Origin列出的源的列表中,若是是纔會發送真正的請求。在實驗過程當中,瀏覽器並不必定要在服務器支持Preflight請求查詢的請求方法和Header時才發送真正的請求,只要發出請求的源是合法的就會在Preflight請求以後把請求發出去。
JSONP 的全稱是 JSON with Padding,譯爲被填充的JSON。前端在指定要請求的URL時能夠經過和後端約定一個指定回調函數名稱的參數,確保後臺響應的腳本片斷中調用了前端指定的回調函數,以此能夠實現發送多個JSONP請求並且互不干擾。具體JSONP實現以下:
var delicious_callbacks = {};
function jsonp(url, callback) {
var uid = (new Date()).getTime();
delicious_callbacks[uid] = function (data) {
delete delicious_callbacks[uid];
callback(data);
};
url += "?jsonp=" + encodeURIComponent("delicious_callbacks[" + uid + "]");
var script = document.createElement('script')
script.src = url
document.body.appendChild(script)
};
jsonp("http://example.com/api", function(data) { // here we get the data });
複製代碼
JSONP這種方式自己也是存在必定缺陷的,很明顯它只能用於GET請求。另外,後端應用程序在處理過程可能會出現4xx、5xx錯誤或者遇到其餘意外狀況,致使沒法返回正確的js函數調用格式的字符串的狀況,因此還須要監聽script標籤的onerror事件來處理可能出現的意外狀況。
cookie做爲客戶端存儲的一種方案,在客戶端設置cookie也有如下幾種方法:
document.cookie="name=Jack;path=/"
document.cookie="age=25;path=/" // cookie中會同時保存name和age這兩個字段
複製代碼
cookie是有過時時間的,若是像上面的代碼同樣沒有顯式地設置cookie的過時時間,則在瀏覽器退出以後相應的cookie也會被清除。
通常狀況下,瀏覽器在訪問頁面時會自動將和當前域名以及路徑匹配的Cookie發送到服務器端。而在通常狀況下,咱們在頁面中發出的Ajax請求則不會自動將當前URL關聯的Cookie同請求一同發送到服務端。若是咱們須要在請求中將和當前訪問的URL的Cookie發送到服務端,能夠設置XMLHttpRequest對象的withCredentials屬性爲true。
而若是是要將當前頁面所在域名的Cookie發送到另外一個域名下的服務端,這時候須要對服務端進行配置,使其支持CORS,同時還須要注意此時服務端返回的Access-Control-Allow-Origin不能再設置爲‘*’,同時服務端須要返回Access-Control-Allow-Credentials: true,不然服務端的響應依然會被瀏覽器block掉。
在這樣配置以後,瀏覽器在發送這種攜帶憑據信息(也就是Cookie)的Ajax請求時就會把當前頁面所在的域名下path屬性和請求的URL相匹配(好比當前請求的URL爲/test/example,那麼設置在/,/test/,/test/example這些path之下的Cookie會被髮送)的Cookie一同發送到服務端。這樣就實現了Cookie的跨域共享。
除了和不一樣源的服務器進行通訊的需求之外,咱們還會遇到跨頁面通訊問題,須要訪問其餘頁面上的一些信息,或者將一些數據持久化,以供其餘頁面取用。具體方式以下:
經過這種方式跨域的兩個源須要知足必定的條件的,即兩個源的域名須要是父子域的關係或者是相同的域。由於頁面設置document.domain的值只能是當前域自己,或者是父域,而不能是其餘不相關的域名。只有兩個頁面的document.domain都設置成相同的值,嵌入iframe的頁面和iframe加載的頁面才能相互獲取到彼此的頁面信息(包括DOM結構、window對象等)。
在實踐中也發現須要注意的兩個問題:
瀏覽器單獨保存端口號。任何的賦值操做,包括
document.domain = document.domain
都會致使端口號被重寫爲null
。所以company.com:8080
不能僅經過設置document.domain = "company.com"
來與company.com
通訊。必須在他們雙方中都進行賦值,以確保端口號都爲null
。
瀏覽器具備這樣一個特性:同一個標籤頁或者同一個iframe框架加載過的頁面共享相同的window.name屬性值,意味着只要是在同一個標籤頁裏面打開過的頁面(不論是否同源),這些頁面上window.name屬性值都是相同的。利用這個特性,就能夠將這個屬性做爲在不一樣頁面之間傳遞數據的介質。
若是是經過iframe+window.name這種方式在徹底沒有父子域關係的兩個源之間傳遞數據(假設源A要獲取源B中的數據),源A頁面上的iframe在加載源B的目標頁面(源B頁面把數據設置在window.name屬性上)以後還須要再跳轉到源A的某個頁面上,以便於嵌入iframe的頁面經過(上面介紹的)和在iframe中的頁面將document.domain都設置爲源A的方式來獲取iframe中的數據。示例代碼以下:
// www.a.com/getData.html
<script type="text/javascript"> function getData() { var frame = document.getElementsByTagName("iframe")[0]; frame.onload = function () { var data = frame.contentWindow.name; // 此處獲取數據 alert(data); }; frame.contentWindow.location = "./aaa.html"; // 加載完www.b.com/data.html以後就加載www.a.com/下隨便一個頁面,獲取數據 } </script>
<iframe src="http://www.b.com/data.html" style="display: none;" onload="getData();"></iframe>
複製代碼
HTML5中引入了另一種跨頁面通訊的方式,稱爲跨文檔消息傳送。一樣能夠實現主頁面和嵌入的iframe子頁面(或者由當前頁面打開的頁面)之間完成數據的傳遞,另外這種方法也能夠用於當前JavaScript引擎線程和其餘worker線程之間完成數據交換。若是是與經過iframe加載的子頁面進行通訊,則須要先獲取到接收數據的目標頁面的window對象(具體經過前面提到的方法來獲取),經過該對象的postMessage方法能夠向目標頁面發送數據。
<!--send.html-->
<iframe src="./receiver.html" id="frame"></iframe>
<button id="send-btn">send message</button>
<script> var frame = document.getElementById('frame') document.getElementById('send-btn').addEventListener('click', function() { frame.contentWindow.postMessage({ name: 'Jack' }, 'http://localhost:8888') // 接收信息的頁面所在的源 }) </script>
<!--receiver.html-->
<script> window.addEventListener('message', function(e) { // 驗證消息發送方所在的源 if(e.origin === 'http://localhost:8888') { console.log(e.data) e.source.postMessage(...) // 回送消息 } }) </script>
複製代碼
若是是須要和頁面上的worker進行通訊,直接調用建立出來的Worker實例的postMessage方法,在Worker實例執行的腳本中則經過self或者this來訪問Worker實例,進而調用postMessage方法來完成通訊。
localStorage是HTML5引入的客戶端存儲方案,經過localStorage存儲的內容會一直保存在客戶端,除非調用removeItem方法顯式移除,不然內容將永久保留。MDN上對localStorage的介紹也提到了一種經過cookie在不支持localStorage的瀏覽器上實現localStorage的方法,經過將cookie的過時時間設置爲將來很長以後的一個時間點能夠模擬localStorage永久保留的特性,而在模擬localStorage移除存儲內容時則將對應的cookie。更進一步,若是不設置cookie的過時時間,還能夠用來模擬瀏覽器中的另外一種客戶端存儲方案--sessionStorage。和cookie不一樣的是,localStorage提供的存儲容量上限更大。
前面也提到了,localStorage存儲的內容是以源爲單位進行管理的,這意味着即便域名相同,端口不一樣的頁面也沒法經過localStorage進行通訊的。在瀏覽器的多個標籤頁中分別打開多個同源頁面,這些頁面中的window對象能夠經過監聽storage事件,當其餘標籤頁的頁面在設置localStorage中的內容時會觸發該事件來進行通知,經過這種方式也能夠實現跨頁面通訊。
CSS中引用的字體文件加載也存在跨域問題,須要設置CORS才能加載其餘域下的字體文件。默認狀況下定義新的字體不會當即去下載對應的字體文件,只有當頁面上的元素使用了這種字體纔會去下載對應的字體文件。
對於頁面上加載的跨域腳本執行出錯,頁面上綁定的錯誤處理函數window.onerror在默認狀況下是獲取不到具體的錯誤信息的,這時候須要在加載跨域腳本的標籤上使用crossorigin屬性,也就是在請求跨域腳本的時候執行CORS。crossorigin屬性能夠設置的值有:
設置爲其餘值都會被看做是anonymous關鍵字。設置了crossorigin屬性意味着還須要對服務器進行配置,使其支持CORS。若是服務端沒有正確配置CORS,跨域腳本是沒法正常下載的。
canvas中動態加載的圖片能夠直接畫到canvas中,可是在將canvas轉化成文件對象進行操做時也存在跨域問題,會遇到「Tainted canvases may not be exported」錯誤。這時候須要對動態加載的圖片對象設置crossOrigin屬性,同時也須要配置服務器使其支持CORS。
let img = new Image()
img.crossOrigin = 'anonymous'
img.src = "//localhost:8888/images/1751527990314_.pic.jpg"
img.onload = () => {
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
canvas.toBlob(blob => console.log(blob), 'image/jpeg', .75)
}
複製代碼
本文主要介紹了瀏覽器中的同源策略以及如何在同源策略的約束之下完成隸屬不一樣源的客戶端和服務端通訊,以及跨頁面通訊。這些跨域方法在實際使用中也須要從具體的場景出發,根據不一樣的通訊需求採用合適的方法。以上,若有疏漏之處,還望斧正。