你不知道的 XMLHttpRequest

本文詳細(xì)介紹了 XMLHttpRequest 相關(guān)知識(shí)敏簿,涉及內(nèi)容:

  • AJAX、XMLHTTP、XMLHttpRequest詳解募狂、XMLHttpRequest Level 1、Level 2 詳解
  • XHR 上傳角雷、下載數(shù)據(jù)祸穷、XHR 流式傳輸、XHR 定時(shí)輪詢和長(zhǎng)輪詢區(qū)別與優(yōu)缺點(diǎn)勺三、XMLHttpRequest 庫(kù) (Mock.js雷滚、Zone.js、Oboe.js吗坚、fetch.js)
  • XMLHttpRequest 常用代碼片段:
    • ArrayBuffer 對(duì)象轉(zhuǎn)字符串
    • 字符串轉(zhuǎn) ArrayBuffer 對(duì)象
    • 創(chuàng)建 XHR 對(duì)象
    • sendAsBinary() polyfill
    • 獲取 XMLHttpRequest 響應(yīng)體
    • 獲取 responseURL
    • 驗(yàn)證請(qǐng)求是否成功
    • 解析查詢參數(shù)為Map對(duì)象
    • XHR 下載圖片
    • XHR 上傳圖片
    • XHR 上傳進(jìn)度條
  • 分析 AJAX 請(qǐng)求狀態(tài)為 0祈远、GET請(qǐng)求方式為什么不能通過(guò)send() 方法發(fā)送請(qǐng)求體、簡(jiǎn)單請(qǐng)求和預(yù)請(qǐng)求商源、XMLHttpRequest對(duì)象垃圾回收機(jī)制车份、Get與Post請(qǐng)求區(qū)別、如何避免重復(fù)發(fā)送請(qǐng)求炊汹、AJAX 站點(diǎn) SEO 優(yōu)化等問(wèn)題躬充。

AJAX

AJAX 定義

AJAX即“Asynchronous JavaScript and XML”(異步的JavaScriptXML技術(shù)),指的是一套綜合了多項(xiàng)技術(shù)的瀏覽器網(wǎng)頁(yè)開(kāi)發(fā)技術(shù)。Ajax的概念由杰西·詹姆士·賈瑞特所提出充甚。

傳統(tǒng)的Web應(yīng)用允許用戶端填寫(xiě)表單(form)以政,當(dāng)提交表單時(shí)就向網(wǎng)頁(yè)服務(wù)器發(fā)送一個(gè)請(qǐng)求。服務(wù)器接收并處理傳來(lái)的表單伴找,然后送回一個(gè)新的網(wǎng)頁(yè)盈蛮,但這個(gè)做法浪費(fèi)了許多帶寬,因?yàn)樵谇昂髢蓚€(gè)頁(yè)面中的大部分HTML碼往往是相同的技矮。由于每次應(yīng)用的溝通都需要向服務(wù)器發(fā)送請(qǐng)求抖誉,應(yīng)用的回應(yīng)時(shí)間依賴于服務(wù)器的回應(yīng)時(shí)間。這導(dǎo)致了用戶界面的回應(yīng)比本機(jī)應(yīng)用慢得多衰倦。

與此不同袒炉,AJAX應(yīng)用可以僅向服務(wù)器發(fā)送并取回必須的數(shù)據(jù),并在客戶端采用JavaScript處理來(lái)自服務(wù)器的回應(yīng)樊零。因?yàn)樵诜?wù)器和瀏覽器之間交換的數(shù)據(jù)大量減少(大約只有原來(lái)的5%)[來(lái)源請(qǐng)求],服務(wù)器回應(yīng)更快了我磁。同時(shí),很多的處理工作可以在發(fā)出請(qǐng)求的客戶端機(jī)器上完成驻襟,因此Web服務(wù)器的負(fù)荷也減少了夺艰。

類似于DHTMLLAMP,AJAX不是指一種單一的技術(shù)沉衣,而是有機(jī)地利用了一系列相關(guān)的技術(shù)郁副。雖然其名稱包含XML,但實(shí)際上數(shù)據(jù)格式可以由JSON代替豌习,進(jìn)一步減少數(shù)據(jù)量存谎,形成所謂的AJAJ。而客戶端與服務(wù)器也并不需要異步斑鸦。一些基于AJAX的“派生/合成”式(derivative/composite)的技術(shù)也正在出現(xiàn)愕贡,如AFLAX草雕。 —— 維基百科

AJAX 應(yīng)用

AJAX 兼容性

JavaScript 編程的最大問(wèn)題來(lái)自不同的瀏覽器對(duì)各種技術(shù)和標(biāo)準(zhǔn)的支持诫钓。

XmlHttpRequest 對(duì)象在不同瀏覽器中不同的創(chuàng)建方法旬昭,以下是跨瀏覽器的通用方法:

// Provide the XMLHttpRequest class for IE 5.x-6.x:
// Other browsers (including IE 7.x-8.x) ignore this
//   when XMLHttpRequest is predefined
var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    var aVersions = ["Msxml2.XMLHttp.5.0", "Msxml2.XMLHttp.4.0", 
        "Msxml2.XMLHttp.3.0", "Msxml2.XMLHttp", "Microsoft.XMLHttp"];
    for (var i = 0; i < aVersions.length; i++) {
        try {
            xmlHttp = new ActiveXObject(aVersions[i]);
            break;
        } catch (e) {}
    }
}

詳細(xì)信息請(qǐng)參考 - Can I use XMLHttpRequest

AJAX/HTTP 庫(kù)對(duì)比

Support Features
All Browsers Chrome & Firefox1 Node Concise Syntax Promises Native2 Single Purpose3 Formal Specification
XMLHttpRequest ? ? ? ? ?
Node HTTP ? ? ? ?
fetch() ? ? ? ? ? ?
Fetch polyfill ? ? ? ? ? ?
node-fetch ? ? ? ? ?
isomorphic-fetch ? ? ? ? ? ? ?
superagent ? ? ? ? ?
axios ? ? ? ? ? ?
request ? ? ?
jQuery ? ? ?
reqwest ? ? ? ? ? ?

1 Chrome & Firefox are listed separately because they support fetch(): caniuse.com/fetch
2 Native: Meaning you can just use it - no need to include a library.
3 Single Purpose: Meaning this library or technology is ONLY used for AJAX / HTTP communication, nothing else.

詳細(xì)信息請(qǐng)參考 - AJAX/HTTP Library Comparison

XMLHTTP

XMLHTTP 定義

XMLHTTP 是一組API函數(shù)集,可被JavaScript菌湃、JScript问拘、VBScript以及其它web瀏覽器內(nèi)嵌的腳本語(yǔ)言調(diào)用,通過(guò)HTTP在瀏覽器和web服務(wù)器之間收發(fā)XML或其它數(shù)據(jù)。XMLHTTP最大的好處在于可以動(dòng)態(tài)地更新網(wǎng)頁(yè)骤坐,它無(wú)需重新從服務(wù)器讀取整個(gè)網(wǎng)頁(yè)绪杏,也不需要安裝額外的插件。該技術(shù)被許多網(wǎng)站使用纽绍,以實(shí)現(xiàn)快速響應(yīng)的動(dòng)態(tài)網(wǎng)頁(yè)應(yīng)用蕾久。例如:GoogleGmail服務(wù)、Google Suggest動(dòng)態(tài)查找界面以及Google Map地理信息服務(wù)拌夏。

XMLHTTP是AJAX網(wǎng)頁(yè)開(kāi)發(fā)技術(shù)的重要組成部分僧著。除XML之外,XMLHTTP還能用于獲取其它格式的數(shù)據(jù)障簿,如JSON或者甚至純文本盹愚。—— 維基百科

XMLHTTP 背景知識(shí)

XMLHTTP最初是由微軟公司發(fā)明的站故,在Internet Explorer 5.0中用作ActiveX對(duì)象杯拐,可通過(guò)JavaScript、VBScript或其它瀏覽器支持的腳本語(yǔ)言訪問(wèn)世蔗。Mozilla的開(kāi)發(fā)人員后來(lái)在Mozilla 1.0中實(shí)現(xiàn)了一個(gè)兼容的版本端逼。之后蘋(píng)果電腦公司在Safari 1.2中開(kāi)始支持XMLHTTP,而Opera從8.0版開(kāi)始也宣布支持XMLHTTP污淋。

大多數(shù)使用了XMLHTTP的設(shè)計(jì)良好的網(wǎng)頁(yè)顶滩,會(huì)使用簡(jiǎn)單的JavaScript函數(shù),將不同瀏覽器之間調(diào)用XMLHTTP的差異性屏蔽寸爆,該函數(shù)會(huì)自動(dòng)檢測(cè)瀏覽器版本并隱藏不同環(huán)境的差異礁鲁。

DOM 3(文檔對(duì)象模型 Level 3)的讀取和保存規(guī)范(Load and Save Specification)中也有類似的功能,它已經(jīng)成為W3C推薦的方法赁豆。截止2011年仅醇,大多數(shù)瀏覽器已經(jīng)支持∧е郑—— 維基百科

XMLHTTP 實(shí)現(xiàn)

  • ActiveXObject
  • XMLHttpRequest

什么是 ActiveX 控件

Microsoft ActiveX 控件是由軟件提供商開(kāi)發(fā)的可重用的軟件組件析二。使用 ActiveX 控件,可以很快地在網(wǎng)址节预、臺(tái)式應(yīng)用程序叶摄、以及開(kāi)發(fā)工具中加入特殊的功能。例如安拟,StockTicker 控件可以用來(lái)在網(wǎng)頁(yè)上即時(shí)地加入活動(dòng)信息蛤吓,動(dòng)畫(huà)控件可用來(lái)向網(wǎng)頁(yè)中加入動(dòng)畫(huà)特性。

ActiveXObject 對(duì)象

JavaScript 中 ActiveXObject 對(duì)象是啟用并返回 Automation 對(duì)象的引用糠赦。

ActiveXObject 語(yǔ)法

newObj = new ActiveXObject(servername.typename[, location])

參數(shù):

  • newObj
    • 必選 - ActiveXObject 分配到的變量名稱
  • servername
    • 必選 - 提供對(duì)象的應(yīng)用程序名稱
  • typename
    • 必選 - 要?jiǎng)?chuàng)建的對(duì)象的類型或類
  • location
    • 可選 - 要再其中創(chuàng)建對(duì)象的網(wǎng)絡(luò)服務(wù)器的名稱

ActiveXObject 使用

// 在IE5.x和IE6下創(chuàng)建xmlHttp對(duì)象
// servername - MSXML2
// typename - XMLHTTP.3.0
var xmlHttp = new ActiveXObject('MSXML2.XMLHTTP.3.0');
xmlHttp.open("GET", "http://localhost/books.xml", false);  
xmlHttp.send();  

詳細(xì)信息可以參考 - msdn - JavaScript 對(duì)象 - ActiveXObject 對(duì)象

XMLHttpRequest

XMLHttpRequest 是一個(gè)API, 它為客戶端提供了在客戶端和服務(wù)器之間傳輸數(shù)據(jù)的功能会傲。它提供了一個(gè)通過(guò) URL 來(lái)獲取數(shù)據(jù)的簡(jiǎn)單方式锅棕,并且不會(huì)使整個(gè)頁(yè)面刷新。這使得網(wǎng)頁(yè)只更新一部分頁(yè)面而不會(huì)打擾到用戶淌山。XMLHttpRequest 在 AJAX 中被大量使用哲戚。

XMLHttpRequest 是一個(gè) JavaScript 對(duì)象,它最初由微軟設(shè)計(jì),隨后被 Mozilla艾岂、Apple 和 Google采納. 如今,該對(duì)象已經(jīng)被 W3C組織標(biāo)準(zhǔn)化. 通過(guò)它,你可以很容易的取回一個(gè)URL上的資源數(shù)據(jù). 盡管名字里有XML, 但 XMLHttpRequest 可以取回所有類型的數(shù)據(jù)資源顺少,并不局限于XML。 而且除了HTTP ,它還支持fileftp 協(xié)議王浴。

XMLHttpRequest 語(yǔ)法

var req = new XMLHttpRequest();

XMLHttpRequest 使用

var xhr = new XMLHttpRequest(); // 創(chuàng)建xhr對(duì)象
xhr.open( method, url );
xhr.onreadystatechange = function () { ... };
xhr.setRequestHeader( ..., ... );
xhr.send( optionalEncodedData );

XMLHttpRequest 詳解

構(gòu)造函數(shù)

用于初始化一個(gè) XMLHttpRequest 對(duì)象脆炎,必須在所有其它方法被調(diào)用前調(diào)用構(gòu)造函數(shù)。使用示例如下:

var req = new XMLHttpRequest();

屬性

  • onreadystatechange: Function - 當(dāng) readyState 屬性改變時(shí)會(huì)調(diào)用它氓辣。

  • readyState: unsigned short - 用于表示請(qǐng)求的五種狀態(tài):

狀態(tài) 描述
0 UNSENT (未打開(kāi)) 表示已創(chuàng)建 XHR 對(duì)象秒裕,open() 方法還未被調(diào)用
1 OPENED (未發(fā)送) open() 方法已被成功調(diào)用,send() 方法還未被調(diào)用
2 HEADERS_RECEIVED (已獲取響應(yīng)頭) send() 方法已經(jīng)被調(diào)用钞啸,響應(yīng)頭和響應(yīng)狀態(tài)已經(jīng)返回
3 LOADING (正在下載響應(yīng)體) 響應(yīng)體下載中几蜻,responseText中已經(jīng)獲取了部分?jǐn)?shù)據(jù)
4 DONE (請(qǐng)求完成) 整個(gè)請(qǐng)求過(guò)程已經(jīng)完畢
  • response: varies - 響應(yīng)體的類型由 responseType 來(lái)指定,可以是 ArrayBuffer体斩、Blob梭稚、Document、JSON絮吵,或者是字符串弧烤。如果請(qǐng)求未完成或失敗,則該值為 null蹬敲。

  • response: varies - 響應(yīng)體的類型由 responseType 來(lái)指定暇昂,可以是 ArrayBuffer、Blob伴嗡、Document急波、JSON,或者是字符串瘪校。如果請(qǐng)求未完成或失敗澄暮,則該值為 null。

  • responseText: DOMString - 此請(qǐng)求的響應(yīng)為文本渣淤,或者當(dāng)請(qǐng)求未成功或還是未發(fā)送時(shí)未 null (只讀)

  • responseType: XMLHttpRequestResponseType - 設(shè)置該值能夠改變響應(yīng)類型赏寇,就是告訴服務(wù)器你期望的響應(yīng)格式:

響應(yīng)數(shù)據(jù)類型
"" (空字符串) 字符串(默認(rèn)值)
"arraybuffer" ArrayBuffer
"blob" Blob
"document" Document
"json" JSON
"text" 字符串
  • xhr.spec 規(guī)范中定義的 XMLHttpRequestResponseType 類型如下:

    enum XMLHttpRequestResponseType {
      "",
      "arraybuffer",
      "blob",
      "document",
      "json",
      "text"
    };
    
  • responseXML: Document - 本次請(qǐng)求響應(yīng)式一個(gè) Document 對(duì)象吉嫩,如果是以下情況則值為 null:

    • 請(qǐng)求未成功
    • 請(qǐng)求未發(fā)送
    • 響應(yīng)無(wú)法被解析成 XML 或 HTML
  • status: unsigned short - 請(qǐng)求的響應(yīng)狀態(tài)碼价认,如 200 (表示一個(gè)成功的請(qǐng)求)。 (只讀)

  • statusText: DOMString - 請(qǐng)求的響應(yīng)狀態(tài)信息自娩,包含一個(gè)狀態(tài)碼和消息文本用踩,如 "200 OK"渠退。 (只讀)

  • timeout: unsigned long - 表示一個(gè)請(qǐng)求在被自動(dòng)終止前所消耗的毫秒數(shù)。默認(rèn)值為 0脐彩,意味著沒(méi)有超時(shí)時(shí)間碎乃。超時(shí)并不能應(yīng)用在同步請(qǐng)求中,否則會(huì)拋出一個(gè) InvalidAccessError 異常惠奸。當(dāng)發(fā)生超時(shí)時(shí)梅誓,timeout 事件將會(huì)被觸發(fā)。

  • upload: XMLHttpRequestUpload - 可以在 upload 上添加一個(gè)事件監(jiān)聽(tīng)來(lái)跟蹤上傳過(guò)程

  • withCredentials: boolean - 表明在進(jìn)行跨站 (cross-site) 的訪問(wèn)控制 (Access-Control) 請(qǐng)求時(shí)佛南,是否使用認(rèn)證信息 (例如cookie或授權(quán)的header)梗掰。默認(rèn)為 false。注意:這不會(huì)影響同站 same-site 請(qǐng)求

方法

  • abort() - 如果請(qǐng)求已經(jīng)被發(fā)送嗅回,則立刻中止請(qǐng)求及穗。

  • getAllResponseHeaders() - 返回所有響應(yīng)頭信息(響應(yīng)頭名和值),如果響應(yīng)頭還沒(méi)有接收绵载,則返回 null埂陆。注意:使用該方法獲取的 response headers 與在開(kāi)發(fā)者工具 Network 面板中看到的響應(yīng)頭不一致

  • getResponseHeader() - 返回指定響應(yīng)頭的值,如果響應(yīng)頭還沒(méi)有被接收娃豹,或該響應(yīng)頭不存在焚虱,則返回 null。注意:使用該方法獲取某些響應(yīng)頭時(shí)懂版,瀏覽器會(huì)拋出異常著摔,具體原因如下:

  • open() - 初始化一個(gè)請(qǐng)求:

    • 方法簽名:

      void open(
         DOMString method,
         DOMString url,
         optional boolean async,
         optional DOMString user,
         optional DOMString password
      );
      
    • 參數(shù):

      • method - 請(qǐng)求所使用的 HTTP 方法,如 GET、POST赖淤、PUT、DELETE
      • url - 請(qǐng)求的 URL 地址
      • async - 一個(gè)可選的布爾值參數(shù)涂身,默認(rèn)值為 true泊窘,表示執(zhí)行異步操作。如果值為 false优床,則 send() 方法不會(huì)返回任何東西劝赔,直到接收到了服務(wù)器的返回?cái)?shù)據(jù)
      • user - 用戶名,可選參數(shù)胆敞,用于授權(quán)着帽。默認(rèn)參數(shù)為空字符串
      • password - 密碼杂伟,可選參數(shù),用于授權(quán)仍翰。默認(rèn)參數(shù)為空字符串
    • 備注:

      • 如果 method 不是有效的 HTTP 方法或 url 地址不能被成功解析赫粥,將會(huì)拋出 SyntaxError 異常
      • 如果請(qǐng)求方法(不區(qū)分大小寫(xiě))為 CONNECTTRACETRACK 將會(huì)拋出 SecurityError 異常
  • overrideMimeType() - 重寫(xiě)由服務(wù)器返回的 MIME 類型予借。例如越平,可以用于強(qiáng)制把響應(yīng)流當(dāng)做 text/xml 來(lái)解析,即使服務(wù)器沒(méi)有指明數(shù)據(jù)是這個(gè)類型灵迫。注意:這個(gè)方法必須在 send() 之前被調(diào)用喧笔。

  • send() - 發(fā)送請(qǐng)求。如果該請(qǐng)求是異步模式(默認(rèn))龟再,該方法會(huì)立刻返回书闸。相反,如果請(qǐng)求是同步模式利凑,則直到請(qǐng)求的響應(yīng)完全接受以后浆劲,該方法才會(huì)返回。注意:所有相關(guān)的事件綁定必須在調(diào)用 send() 方法之前進(jìn)行哀澈。

    • 方法簽名:

      void send();
      void send(ArrayBuffer data);
      void send(Blob data);
      void send(Document data);
      void send(DOMString? data);
      void send(FormData data);
      
  • setRequestHeader() - 設(shè)置 HTTP 請(qǐng)求頭信息牌借。注意:在這之前,你必須確認(rèn)已經(jīng)調(diào)用了 open() 方法打開(kāi)了一個(gè) url

    • 方法簽名:

      void setRequestHeader(
         DOMString header,
         DOMString value
      );
      
    • 參數(shù):

      • header - 請(qǐng)求頭名稱
      • value - 請(qǐng)求頭的值
  • sendAsBinary() - 發(fā)送二進(jìn)制的 send() 方法的變種割按。

    • 方法簽名:

      void sendAsBinary(
         in DOMString body
      );
      
    • 參數(shù):

      • body - 消息體

瀏覽器兼容性

  • Desktop
Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Basic support (XHR1) 1 1.0 5 (via ActiveXObject)7 (XMLHttpRequest) (Yes) 1.2
send(ArrayBuffer) 9 9 ? 11.60 ?
send(Blob) 7 3.6 ? 12 ?
send(FormData) 6 4 ? 12 ?
response 10 6 10 11.60 ?
responseType = 'arraybuffer' 10 6 10 11.60 ?
responseType = 'blob' 19 6 10 12 ?
responseType = 'document' 18 11 未實(shí)現(xiàn) 未實(shí)現(xiàn) 未實(shí)現(xiàn)
responseType = 'json' 未實(shí)現(xiàn) 10 未實(shí)現(xiàn) 12 未實(shí)現(xiàn)
Progress Events 7 3.5 10 12 ?
withCredentials 3 3.5 10 12 4

事件

  • loadstart - 當(dāng)程序開(kāi)始加載時(shí)膨报,loadstart 事件將被觸發(fā)。
  • progress - 進(jìn)度事件會(huì)被觸發(fā)用來(lái)指示一個(gè)操作正在進(jìn)行中适荣。
  • abort - 當(dāng)一個(gè)資源的加載已中止時(shí)现柠,將觸發(fā) abort 事件。
  • error - 當(dāng)一個(gè)資源加載失敗時(shí)會(huì)觸發(fā)error事件弛矛。
  • load - 當(dāng)一個(gè)資源及其依賴資源已完成加載時(shí)够吩,將觸發(fā)load事件。
  • timeout - 當(dāng)進(jìn)度由于預(yù)定時(shí)間到期而終止時(shí)丈氓,會(huì)觸發(fā)timeout 事件周循。
  • loadend - 當(dāng)一個(gè)資源加載進(jìn)度停止時(shí) (例如,在已經(jīng)分派“錯(cuò)誤”万俗,“中止”或“加載”之后)湾笛,觸發(fā)loadend事件。
  • readystatechange - readystatechange 事件會(huì)在 document.readyState屬性發(fā)生變化時(shí)觸發(fā)闰歪。

XMLHttpRequest Level 1

XMLHttpRequest Level 1 使用

首先嚎研,創(chuàng)建一個(gè) XMLHttpRequest 對(duì)象:

var xhr = new XMLHttpRequest();

然后,向服務(wù)器發(fā)出一個(gè) HTTP 請(qǐng)求:

xhr.open('GET', 'example.php');
xhr.send();

接著课竣,就等待遠(yuǎn)程主機(jī)做出回應(yīng)嘉赎。這時(shí)需要監(jiān)控XMLHttpRequest對(duì)象的狀態(tài)變化置媳,指定回調(diào)函數(shù)于樟。

xhr.onreadystatechange = function(){
  if ( xhr.readyState == 4 && xhr.status == 200 ) {
     alert( xhr.responseText );
  } else {
     alert( xhr.statusText );
  }
};

上面的代碼包含了老版本 XMLHttpRequest 對(duì)象的主要屬性:

  • xhr.readyState: XMLHttpRequest對(duì)象的狀態(tài)公条,等于4表示數(shù)據(jù)已經(jīng)接收完畢。
  • xhr.status:服務(wù)器返回的狀態(tài)碼迂曲,等于200表示一切正常靶橱。
  • xhr.responseText:服務(wù)器返回的文本數(shù)據(jù)。
  • xhr.statusText:服務(wù)器返回的狀態(tài)文本路捧。

XMLHttpRequest Level 1 缺點(diǎn)

  • 只支持文本數(shù)據(jù)的傳送关霸,無(wú)法用來(lái)讀取和上傳二進(jìn)制文件。
  • 傳送和接收數(shù)據(jù)時(shí)杰扫,沒(méi)有進(jìn)度信息队寇,只能提示有沒(méi)有完成。
  • 受到"同域限制"(Same Origin Policy)章姓,只能向同一域名的服務(wù)器請(qǐng)求數(shù)據(jù)佳遣。

XMLHttpRequest Level 2

XMLHttpRequest Level 2 針對(duì) XMLHttpRequest Level 1 的缺點(diǎn),做了大幅改進(jìn)凡伊。具體如下:

  • 可以設(shè)置HTTP請(qǐng)求的超時(shí)時(shí)間零渐。
  • 可以使用FormData對(duì)象管理表單數(shù)據(jù)。
  • 可以上傳文件系忙。
  • 可以請(qǐng)求不同域名下的數(shù)據(jù)(跨域請(qǐng)求)诵盼。
  • 可以獲取服務(wù)器端的二進(jìn)制數(shù)據(jù)。
  • 可以獲得數(shù)據(jù)傳輸?shù)倪M(jìn)度信息银还。

設(shè)置超時(shí)時(shí)間

新版本 XMLHttpRequest 對(duì)象风宁,增加了 timeout 屬性,可以設(shè)置HTTP請(qǐng)求的時(shí)限蛹疯。

 xhr.timeout = 3000;

上面的語(yǔ)句杀糯,將最長(zhǎng)等待時(shí)間設(shè)為3000毫秒。過(guò)了這個(gè)時(shí)限苍苞,就自動(dòng)停止HTTP請(qǐng)求固翰。與之配套的還有一個(gè)timeout事件,用來(lái)指定回調(diào)函數(shù)羹呵。

xhr.ontimeout = function(event){
  console.log('請(qǐng)求超時(shí)');
}

FormData 對(duì)象

AJAX 操作往往用來(lái)傳遞表單數(shù)據(jù)骂际。為了方便表單處理,HTML 5新增了一個(gè) FormData 對(duì)象冈欢,可以用于模擬表單歉铝。

FormData 簡(jiǎn)介

構(gòu)造函數(shù) FormData()

用于創(chuàng)建一個(gè)新的 FormData 對(duì)象。

語(yǔ)法

var formData = new FormData(form)
  • 參數(shù)
    • form 可選 - 一個(gè) HTML 上的 <form> 表單元素凑耻。當(dāng)使用 form 參數(shù)太示,創(chuàng)建的 FormData 對(duì)象會(huì)自動(dòng)將 form 中的表單值也包含進(jìn)去柠贤,文件內(nèi)容會(huì)被編碼

FormData 使用

首先,新建一個(gè) FormData 對(duì)象:

var formData = new FormData();

然后类缤,為它添加表單項(xiàng):

formData.append('username', 'semlinker');
formData.append('id', 2005821040);

最后臼勉,直接傳送這個(gè)FormData對(duì)象。這與提交網(wǎng)頁(yè)表單的效果餐弱,完全一樣宴霸。

xhr.send(formData);

FormData 對(duì)象也可以用來(lái)獲取網(wǎng)頁(yè)表單的值。

var form = document.getElementById('myform'); // 獲取頁(yè)面上表單對(duì)象
var formData = new FormData(form);
formData.append('username', 'semlinker'); // 添加一個(gè)表單項(xiàng)
xhr.open('POST', form.action);
xhr.send(formData);

上傳文件

新版 XMLHttpRequest 對(duì)象膏蚓,不僅可以發(fā)送文本信息瓢谢,還可以上傳文件。

1.為了上傳文件, 我們得先選中一個(gè)文件. 一個(gè) type 為 file 的 input 輸入框

<input id="input" type="file">

2.然后用 FormData 對(duì)象包裹選中的文件

var input = document.getElementById("input"),
    formData = new FormData();
formData.append("file",input.files[0]); // file名稱與后臺(tái)接收的名稱一致

3.設(shè)置上傳地址和請(qǐng)求方法

var url = "http://localhost:3000/upload",
    method = "POST";

4.發(fā)送 FormData 對(duì)象

xhr.send(formData);

跨域資源共享 (CORS)

新版本的 XMLHttpRequest 對(duì)象驮瞧,可以向不同域名的服務(wù)器發(fā)出 HTTP 請(qǐng)求氓扛。這叫做 "跨域資源共享"(Cross-origin resource sharing,簡(jiǎn)稱 CORS)论笔。

使用"跨域資源共享"的前提采郎,是瀏覽器必須支持這個(gè)功能,而且服務(wù)器端必須同意這種"跨域"翅楼。如果能夠滿足上面的條件尉剩,則代碼的寫(xiě)法與不跨域的請(qǐng)求完全一樣。

xhr.open('GET', 'http://other.server/and/path/to/script');

接收二進(jìn)制數(shù)據(jù)

XMLHttpRequest Level 1 XMLHttpRequest 對(duì)象只能處理文本數(shù)據(jù)毅臊,新版則可以處理二進(jìn)制數(shù)據(jù)理茎。從服務(wù)器取回二進(jìn)制數(shù)據(jù),較新的方法是使用新增的 responseType 屬性管嬉。如果服務(wù)器返回文本數(shù)據(jù)皂林,這個(gè)屬性的值是 "TEXT",這是默認(rèn)值蚯撩。較新的瀏覽器還支持其他值础倍,也就是說(shuō),可以接收其他格式的數(shù)據(jù)胎挎。

你可以把 responseType 設(shè)為 blob沟启,表示服務(wù)器傳回的是二進(jìn)制對(duì)象。

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

接收數(shù)據(jù)的時(shí)候犹菇,用瀏覽器自帶的 Blob 對(duì)象即可德迹。

一個(gè) **Blob **對(duì)象表示一個(gè)不可變的, 原始數(shù)據(jù)的類似文件對(duì)象。Blob 表示的數(shù)據(jù)不一定是一個(gè) JavaScript 原生格式揭芍。 File 接口基于Blob胳搞,繼承 blob功能并將其擴(kuò)展為支持用戶系統(tǒng)上的文件。

var blob = new Blob([xhr.response], {type: 'image/png'});

更多示例請(qǐng)參考 發(fā)送和接收二進(jìn)制數(shù)據(jù)

進(jìn)度信息

新版本的 XMLHttpRequest 對(duì)象肌毅,傳送數(shù)據(jù)的時(shí)候筷转,有一個(gè) progress 事件,用來(lái)返回進(jìn)度信息悬而。

它分成上傳和下載兩種情況呜舒。下載的 progress 事件屬于 XMLHttpRequest 對(duì)象,上傳的 progress 事件屬于XMLHttpRequest.upload 對(duì)象摊滔。

我們先定義progress事件的回調(diào)函數(shù):

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;

然后阴绢,在回調(diào)函數(shù)里面店乐,使用這個(gè)事件的一些屬性艰躺。

function updateProgress(event) {
  if (event.lengthComputable) {
    var percentComplete = event.loaded / event.total;
  }
}

上面的代碼中,event.total 是需要傳輸?shù)目傋止?jié)眨八,event.loaded 是已經(jīng)傳輸?shù)淖止?jié)腺兴。如果event.lengthComputable 不為真,則 event.total 等于0廉侧。

各個(gè)瀏覽器 XMLHttpRequest Level 2 的兼容性 - Can I use/xhr2

XHR 下載數(shù)據(jù)

XHR 可以傳輸基于文本和二進(jìn)制數(shù)據(jù)页响。實(shí)際上,瀏覽器可以為各種本地?cái)?shù)據(jù)類型提供自動(dòng)編碼和解碼段誊,這樣可以讓?xiě)?yīng)用程序?qū)⑦@些類型直接傳遞給XHR闰蚕,以便正確編碼,反之亦然连舍,這些類型可以由瀏覽器自動(dòng)解碼:

  • ArrayBuffer - 固定長(zhǎng)度二進(jìn)制數(shù)據(jù)緩沖區(qū)
  • Blob - 二進(jìn)制不可變數(shù)據(jù)
  • Document - HTML或XML文檔
  • JSON - JavaScript Object Notation
  • Text - 普通文本

XHR 下載圖片示例:

var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://avatars2.githubusercontent.com/u/4220799?v=3');
    xhr.responseType = 'blob'; // 1

    xhr.onload = function() {
        if (this.status == 200) {
            var img = document.createElement('img');
            img.src = window.URL.createObjectURL(this.response); // 2
            img.onload = function() {
                window.URL.revokeObjectURL(this.src); //3
            };
            document.body.appendChild(img);
        }
    };
    xhr.send();

(1) 設(shè)置響應(yīng)的數(shù)據(jù)類型為 blob

(2) 基于Blob創(chuàng)建一個(gè)唯一的對(duì)象URL没陡,并作為圖片的源地址 (URL.createObjectURL())

(3) 圖片加載成功后釋放對(duì)象的URL(URL.revokeObjectURL())

XHR 上傳數(shù)據(jù)

通過(guò) XHR 上傳數(shù)據(jù)對(duì)于所有數(shù)據(jù)類型來(lái)說(shuō)都是簡(jiǎn)單而有效的。實(shí)際上索赏,唯一的區(qū)別是當(dāng)我們?cè)赬HR請(qǐng)求中調(diào)用 send() 時(shí)盼玄,我們需傳遞不同的數(shù)據(jù)對(duì)象。其余的由瀏覽器處理:

var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string"); // 1

var formData = new FormData(); // 2
formData.append('id', 123456);
formData.append('topic', 'performance');

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

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); // 4
xhr.send(uInt8Array.buffer); // 5

(1) 發(fā)送普通的文本到服務(wù)器

(2) 通過(guò) FormData API 創(chuàng)建動(dòng)態(tài)表單

(3) 發(fā)送 FormData 數(shù)據(jù)到服務(wù)器

(4) 創(chuàng)建 Unit8Array 數(shù)組 (Uint8Array 數(shù)組類型表示一個(gè)8位無(wú)符號(hào)整型數(shù)組潜腻,創(chuàng)建時(shí)內(nèi)容被初始化為0)

(5) 發(fā)送二進(jìn)制數(shù)據(jù)到服務(wù)器

XHR send() 方法簽名:

void send();
void send(ArrayBuffer data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);

除此之外埃儿,XHR 還支持大文件分塊傳輸:

var blob = ...; // 1

const BYTES_PER_CHUNK = 1024 * 1024; // 2
const SIZE = blob.size;

var start = 0;
var end = BYTES_PER_CHUNK;

while(start < SIZE) { // 3
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');
  xhr.onload = function() { ... };

  xhr.setRequestHeader('Content-Range', start+'-'+end+'/'+SIZE); // 4
  xhr.send(blob.slice(start, end)); // 5

  start = end;
  end = start + BYTES_PER_CHUNK;
}

(1) 一個(gè)任意的數(shù)據(jù)塊 (二進(jìn)制或文本)

(2) 將數(shù)據(jù)庫(kù)大小設(shè)置為 1MB

(3) 迭代提供的數(shù)據(jù),增量為1MB

(4) 設(shè)置上傳的數(shù)據(jù)范圍 (Content-Range請(qǐng)求頭)

(5) 通過(guò) XHR 上傳 1MB 數(shù)據(jù)塊

監(jiān)聽(tīng)上傳和下載進(jìn)度

XHR 對(duì)象提供了一系列 API融涣,用于監(jiān)聽(tīng)進(jìn)度事件童番,表示請(qǐng)求的當(dāng)前狀態(tài):

事件類型 描述 觸發(fā)次數(shù)
loadstart 開(kāi)始傳輸 1次
progress 傳輸中 0次或多次
error 傳輸中出現(xiàn)錯(cuò)誤 0次或1次
abort 傳輸被用戶取消 0次或1次
load 傳輸成功 0次或1次
loadend 傳輸完成 1次

每個(gè) XHR 傳輸都以 loadstart 事件開(kāi)始,并以 loadend 事件結(jié)束威鹿,并在這兩個(gè)事件期間觸發(fā)一個(gè)或多個(gè)附加事件來(lái)指示傳輸?shù)臓顟B(tài)剃斧。因此,為了監(jiān)控進(jìn)度专普,應(yīng)用程序可以在 XHR 對(duì)象上注冊(cè)一組 JavaScript 事件偵聽(tīng)器:

var xhr = new XMLHttpRequest();
xhr.open('GET','/resource');
xhr.timeout = 5000; // 1

xhr.addEventListener('load', function() { ... }); // 2
xhr.addEventListener('error', function() { ... }); // 3

var onProgressHandler = function(event) {
  if(event.lengthComputable) {
    var progress = (event.loaded / event.total) * 100; // 4
    ...
  }
}

xhr.upload.addEventListener('progress', onProgressHandler); // 5
xhr.addEventListener('progress', onProgressHandler); // 6
xhr.send();

(1) 設(shè)置請(qǐng)求超時(shí)時(shí)間為 5,000 ms (默認(rèn)無(wú)超時(shí)時(shí)間)

(2) 注冊(cè)成功回調(diào)

(3) 注冊(cè)異趁醭模回調(diào)

(4) 計(jì)算已完成的進(jìn)度

(5) 注冊(cè)上傳進(jìn)度事件回調(diào)

(6) 注冊(cè)下載進(jìn)度事件回調(diào)

使用XHR流式傳輸數(shù)據(jù)

在某些情況下,應(yīng)用程序可能需要或希望逐步處理數(shù)據(jù)流:將數(shù)據(jù)上傳到服務(wù)器,使其在客戶機(jī)上可用筋粗,或者在從服務(wù)器下載數(shù)據(jù)時(shí)策橘,進(jìn)行流式處理。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/stream');
xhr.seenBytes = 0;

xhr.onreadystatechange = function() {  // 1
  if(xhr.readyState > 2) {
    var newData = xhr.responseText.substr(xhr.seenBytes); // 2
    // process newData
    xhr.seenBytes = xhr.responseText.length; // 3
  }
};

xhr.send();

(1) 監(jiān)聽(tīng) onreadystatechange 事件

(2) 從部分響應(yīng)中提取新數(shù)據(jù)

(3) 更新處理的字節(jié)偏移

這個(gè)例子可以在大多數(shù)現(xiàn)代瀏覽器中使用娜亿。但是,性能并不好丽已,而且還有大量的注意事項(xiàng)和問(wèn)題:

  • 請(qǐng)注意,我們正在手動(dòng)跟蹤所看到字節(jié)的偏移量买决,然后手動(dòng)分割數(shù)據(jù):responseText 正在緩沖完整的響應(yīng)沛婴!對(duì)于小的傳輸,這可能不是一個(gè)問(wèn)題督赤,但對(duì)于更大的下載嘁灯,特別是在內(nèi)存受限的設(shè)備,如手機(jī)躲舌,這是一個(gè)問(wèn)題丑婿。釋放緩沖響應(yīng)的唯一方法是完成請(qǐng)求并打開(kāi)一個(gè)新的請(qǐng)求。
  • 部分響應(yīng)只能從 responseText 屬性中讀取没卸,這將限制為僅限文本傳輸羹奉。沒(méi)有辦法讀取二進(jìn)制傳輸?shù)牟糠猪憫?yīng)。
  • 一旦讀取了部分?jǐn)?shù)據(jù)约计,我們必須識(shí)別消息邊界:應(yīng)用程序邏輯必須定義自己的數(shù)據(jù)格式诀拭,然后緩沖并解析流以提取單個(gè)消息。
  • 瀏覽器在處理緩沖數(shù)據(jù)方面有所不同:一些瀏覽器可能會(huì)立即釋放數(shù)據(jù)煤蚌,而其他瀏覽器可能會(huì)緩沖小的響應(yīng)并等到積累到一定大小的數(shù)據(jù)塊才釋放它們耕挨。
  • 瀏覽器對(duì)不同 Content-Type 資源類型的處理方式不同,對(duì)于某些資源類型允許逐步讀取 - 例如铺然,text / html 類型俗孝,而其他 Content-Type 類型只能使用 application / x-javascript。

XHR 定時(shí)輪詢

從服務(wù)器檢索更新的最簡(jiǎn)單的策略之一是讓客戶端進(jìn)行定期檢查:客戶端可以以周期性間隔(輪詢服務(wù)器)啟動(dòng)后臺(tái)XHR請(qǐng)求魄健,以檢查更新赋铝。如果新數(shù)據(jù)在服務(wù)器上可用,則在響應(yīng)中返回沽瘦,否則響應(yīng)為空革骨。

定時(shí)輪詢的方式很簡(jiǎn)單,但如果定時(shí)間隔很短的話析恋,也是很低效良哲。因此設(shè)置合適的時(shí)間間隔顯得至關(guān)重要:輪詢間隔時(shí)間過(guò)長(zhǎng),會(huì)導(dǎo)致更新不及時(shí)助隧,然而間隔時(shí)間過(guò)短的話筑凫,則會(huì)導(dǎo)致客戶端與服務(wù)器不必要的流程和高開(kāi)銷。接下來(lái)我們來(lái)看一個(gè)簡(jiǎn)單的示例:

function checkUpdates(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() { ... }; // 1
  xhr.send();
}

setInterval(function() { checkUpdates('/updates') }, 60000); // 2

(1) 處理服務(wù)端接收的數(shù)據(jù)

(2) 設(shè)置定時(shí)輪詢時(shí)間為 60s

定時(shí)輪詢會(huì)產(chǎn)生以下的問(wèn)題:

  • 每個(gè) XHR 請(qǐng)求都是一個(gè)獨(dú)立的 HTTP 請(qǐng)求,平均來(lái)說(shuō)巍实,HTTP 的請(qǐng)求頭可能會(huì)引起大約 800 字節(jié)的開(kāi)銷 (不帶HTTP cookie)滓技。

每個(gè)瀏覽器發(fā)起 HTTP 請(qǐng)求時(shí)都將攜帶額外的 500 - 800 字節(jié)的元數(shù)據(jù) (請(qǐng)求頭),如 user-agent棚潦、accept令漂、Cache-Control 緩存控制頭等。更糟糕的是丸边,500 - 800 字節(jié)是理想的情況叠必,如果攜帶 Cookies 信息,那么這個(gè)數(shù)值將會(huì)更大妹窖∥吵總而言之,這些未壓縮的 HTTP 元數(shù)據(jù)會(huì)引起很大開(kāi)銷嘱吗。

  • 如果數(shù)據(jù)能夠在間隔期間順序到達(dá)玄组,那么定時(shí)輪詢可以正常工作滔驾。但我們并沒(méi)有任何機(jī)制保證數(shù)據(jù)的正常接收谒麦。另外周期性輪詢也將會(huì)引起服務(wù)器上可用的消息及其傳送到客戶端之間引入額外的延遲。簡(jiǎn)單的理解是如果有輪詢期間有新的可用消息哆致,客戶端是不會(huì)馬上收到此新消息绕德,而是要等到下一次輪詢的時(shí)候,才能獲取最新數(shù)據(jù)摊阀。
  • 除非仔細(xì)考慮耻蛇,不然輪詢通常會(huì)成為無(wú)線網(wǎng)絡(luò)上昂貴的性能反模式。頻繁地輪詢會(huì)大量的消耗移動(dòng)設(shè)備的電量胞此。

輪詢開(kāi)銷

平均每個(gè) HTTP 1.x 請(qǐng)求會(huì)增加?大約 800字節(jié)的請(qǐng)求和響應(yīng)開(kāi)銷 (詳細(xì)信息可以查看 - Measuring and Controlling Protocol Overhead) 臣咖。另外在客戶端登錄后,我們還將產(chǎn)生一個(gè)額外的身份驗(yàn)證 cookie 和 消息ID; 假設(shè)這又增加了50個(gè)字節(jié)漱牵。因此夺蛇,不返回新消息的請(qǐng)求將產(chǎn)生 850字節(jié)開(kāi)銷!現(xiàn)在假設(shè)我們有10,000個(gè)客戶端酣胀,所有的輪詢間隔時(shí)間都是60秒:
$$
(850 bytes * 8 bits * 10,000) / 60 seconds ≈ 1.13 Mbps
$$
每個(gè)客戶端在每個(gè)請(qǐng)求上發(fā)送 850 字節(jié)的數(shù)據(jù)刁赦,這轉(zhuǎn)換為每秒 167 個(gè)請(qǐng)求,服務(wù)器上的吞吐量大約為 1.13 Mbps闻镶!這不是一個(gè)固定的值甚脉,此外該計(jì)算值還是在假設(shè)服務(wù)器沒(méi)有向任何客戶端傳遞任何新的消息的理想情況下計(jì)算而得的。

XHR 長(zhǎng)輪詢

周期性輪詢的挑戰(zhàn)在于有可能進(jìn)行許多不必要的和空的檢查铆农∥保考慮到這一點(diǎn),如果我們對(duì)輪詢工作流程進(jìn)行了輕微的修改,而不是在沒(méi)有更新可用的情況下返回一個(gè)空的響應(yīng)猴凹,我們可以保持連接空閑酝豪,直到更新可用嗎?

(圖片來(lái)源 - https://hpbn.co/xmlhttprequest/

通過(guò)保持長(zhǎng)連接精堕,直到更新可用孵淘,數(shù)據(jù)可以立即發(fā)送到客戶端,一旦它在服務(wù)器上可用歹篓。因此瘫证,長(zhǎng)時(shí)間輪詢?yōu)橄⒀舆t提供了最佳的情況,并且還消除了空檢查庄撮,這減少了 XHR 請(qǐng)求的數(shù)量和輪詢的總體開(kāi)銷背捌。一旦更新被傳遞,長(zhǎng)的輪詢請(qǐng)求完成洞斯,并且客戶端可以發(fā)出另一個(gè)長(zhǎng)輪詢請(qǐng)求并等待下一個(gè)可用的消息:

function checkUpdates(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() { // 1
    ...
    checkUpdates('/updates'); // 2
  };
  xhr.send();
}

checkUpdates('/updates'); // 3

(1) 處理接收到的數(shù)據(jù)并啟動(dòng)下一輪檢測(cè)更新

(2) 啟動(dòng)下一輪檢測(cè)更新

(3) 發(fā)起首次更新請(qǐng)求

那么長(zhǎng)時(shí)間輪詢總是比定期輪詢更好的選擇毡庆?除非消息到達(dá)率已知且不變,否則長(zhǎng)輪詢將始終提供更短的消息延遲烙如。

另一方面么抗,開(kāi)銷討論需要更細(xì)微的觀點(diǎn)。首先亚铁,請(qǐng)注意蝇刀,每個(gè)傳遞的消息仍然引起相同的 HTTP 開(kāi)銷;每個(gè)新消息都是獨(dú)立的 HTTP 請(qǐng)求。但是徘溢,如果消息到達(dá)率高吞琐,那么長(zhǎng)時(shí)間輪詢會(huì)比定期輪詢發(fā)出更多的XHR請(qǐng)求!

長(zhǎng)輪詢通過(guò)最小化消息延遲來(lái)動(dòng)態(tài)地適應(yīng)消息到達(dá)速率然爆,這是您可能想要的或可能不需要的行為站粟。如果對(duì)消息延遲要求不高的話,則定時(shí)輪詢可能是更有效的傳輸方式 - 例如曾雕,如果消息更新速率較高奴烙,則定時(shí)輪詢提供簡(jiǎn)單的 "消息聚合" 機(jī)制 (即合并一定時(shí)間內(nèi)的消息),這可以減少請(qǐng)求數(shù)量并提高移動(dòng)設(shè)備的電池壽命翻默。

XMLHttpRequest 庫(kù)

Mock.js

Mock.js 是一款模擬數(shù)據(jù)生成器缸沃,旨在幫助前端攻城師獨(dú)立于后端進(jìn)行開(kāi)發(fā),幫助編寫(xiě)單元測(cè)試修械。提供了以下模擬功能:

  • 根據(jù)數(shù)據(jù)模板生成模擬數(shù)據(jù)
  • 模擬 Ajax 請(qǐng)求趾牧,生成并返回模擬數(shù)據(jù)
  • 基于 HTML 模板生成模擬數(shù)據(jù)

詳細(xì)信息,請(qǐng)查看 - Mock.js 文檔

Zone.js

Zone 是下一個(gè) ECMAScript 規(guī)范的建議之一肯污。Angular 團(tuán)隊(duì)實(shí)現(xiàn)了 JavaScript 版本的 zone.js 翘单,它是用于攔截和跟蹤異步工作的機(jī)制吨枉。

Zone 是一個(gè)全局的對(duì)象,用來(lái)配置有關(guān)如何攔截和跟蹤異步回調(diào)的規(guī)則哄芜。Zone 有以下能力:

  • 攔截異步任務(wù)調(diào)度貌亭,如 setTimeout、setInterval认臊、XMLHttpRequest 等
  • 提供了將數(shù)據(jù)附加到 zones 的方法
  • 為異常處理函數(shù)提供正確的上下文
  • 攔截阻塞的方法圃庭,如 alert、confirm 方法

zone.js 內(nèi)部使用 Monkey Patch 方式失晴,攔截 XMLHttpRequest.prototype 對(duì)象中的 open剧腻、send、abort 等方法涂屁。

// zone.js 源碼片段
var openNative = patchMethod(window.XMLHttpRequest.prototype, 'open', function () { 
    return function (self, args) {
        self[XHR_SYNC] = args[2] == false;
        return openNative.apply(self, args);
    }; 
});

Oboe.js

Oboe.js 通過(guò)將 HTTP 請(qǐng)求-應(yīng)答模型封裝在一個(gè)漸進(jìn)流式接口中书在,幫助網(wǎng)頁(yè)應(yīng)用快速應(yīng)答。它將 streaming 和downloading 間的轉(zhuǎn)換與SAX和DOM間JSON的解析整合在一起拆又。它是個(gè)非常小的庫(kù)儒旬,不依賴于其他程序庫(kù)。它可以在 ajax 請(qǐng)求結(jié)束前就開(kāi)始解析 json 變得十分容易帖族,從而提高應(yīng)用的應(yīng)答速度栈源。另外,它支持 Node.js 框架盟萨,還可以讀入除了 http 外的其他流凉翻。

有興趣的讀者,推薦看一下官網(wǎng)的可交互的演示示例 - Why Oboe.js

(備注:該庫(kù)就是文中 - 使用XHR流式傳輸數(shù)據(jù)章節(jié)的實(shí)際應(yīng)用捻激,不信往下看)

// oboe-browser.js 源碼片段
function handleProgress() {            
    var textSoFar = xhr.responseText,
        newText = textSoFar.substr(numberOfCharsAlreadyGivenToCallback);
    if( newText ) {
        emitStreamData( newText );
    } 
    numberOfCharsAlreadyGivenToCallback = len(textSoFar);
}

fetch.js

fetch 函數(shù)是一個(gè)基于 Promise 的機(jī)制,用于在瀏覽器中以編程方式發(fā)送 Web 請(qǐng)求前计。該項(xiàng)目是實(shí)現(xiàn)標(biāo)準(zhǔn) Fetch 規(guī)范的一個(gè)子集的 polyfill 胞谭,足以作為傳統(tǒng) Web 應(yīng)用程序中 XMLHttpRequest 的代替品。

詳細(xì)信息男杈,請(qǐng)參考 - Github - fetch

Fetch API 兼容性丈屹,請(qǐng)參考 - Can I use Fetch

XMLHttpRequest 代碼片段

ArrayBuffer 對(duì)象轉(zhuǎn)為字符串

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint16Array(buf));
}

代碼片段來(lái)源 - ArrayBuffer與字符串的互相轉(zhuǎn)換

字符串轉(zhuǎn) ArrayBuffer對(duì)象

function str2ab(str) {
  var buf = new ArrayBuffer(str.length * 2); // 每個(gè)字符占用2個(gè)字節(jié)
  var bufView = new Uint16Array(buf);
  for (var i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

代碼片段來(lái)源 - ArrayBuffer與字符串的互相轉(zhuǎn)換

創(chuàng)建 XHR 對(duì)象

兼容所有瀏覽器

// Provide the XMLHttpRequest class for IE 5.x-6.x:
// Other browsers (including IE 7.x-8.x) ignore this
//   when XMLHttpRequest is predefined
var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    var aVersions = ["Msxml2.XMLHttp.5.0", "Msxml2.XMLHttp.4.0", 
        "Msxml2.XMLHttp.3.0", "Msxml2.XMLHttp", "Microsoft.XMLHttp"];
    for (var i = 0; i < aVersions.length; i++) {
        try {
            xmlHttp = new ActiveXObject(aVersions[i]);
            break;
        } catch (e) {}
    }
}

精簡(jiǎn)版

var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    try {
       xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (e) {} 
}

sendAsBinary() polyfill

if (!XMLHttpRequest.prototype.sendAsBinary) {
  XMLHttpRequest.prototype.sendAsBinary = function (sData) {
    var nBytes = sData.length, ui8Data = new Uint8Array(nBytes);
    for (var nIdx = 0; nIdx < nBytes; nIdx++) {
      ui8Data[nIdx] = sData.charCodeAt(nIdx) & 0xff;
    }
    this.send(ui8Data);
  };
}

獲取 XMLHttpRequest 響應(yīng)體

function readBody(xhr) {
    var data;
    if (!xhr.responseType || xhr.responseType === "text") {
        data = xhr.responseText;
    } else if (xhr.responseType === "document") {
        data = xhr.responseXML;
    } else {
        data = xhr.response;
    }
    return data;
}

應(yīng)用示例:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
        console.log(readBody(xhr));
    }
}
xhr.open('GET', 'https://www.baidu.com', true);
xhr.send(null);

獲取 responseURL

export function getResponseURL(xhr: any): string {
  if ('responseURL' in xhr) {
    return xhr.responseURL;
  }
  if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
    return xhr.getResponseHeader('X-Request-URL');
  }
  return;
}

代碼片段來(lái)源 - Github - @angular/http - http_utils.ts

驗(yàn)證請(qǐng)求是否成功

export const isSuccess = (status: number): boolean => (status >= 200 && status < 300);

代碼片段來(lái)源 - Github - @angular/http - http_utils.ts

解析查詢參數(shù)為Map對(duì)象

function paramParser(rawParams: string = ''): Map<string, string[]> {
  const map = new Map<string, string[]>();
  if (rawParams.length > 0) {
    const params: string[] = rawParams.split('&');
    params.forEach((param: string) => {
      const eqIdx = param.indexOf('=');
      const [key, val]: string[] =
          eqIdx == -1 ? [param, ''] : [param.slice(0, eqIdx), param.slice(eqIdx + 1)];
      const list = map.get(key) || [];
      list.push(val);
      map.set(key, list);
    });
  }
  return map;
}

代碼片段來(lái)源 - Github - @angular/http - url_search_params.ts

ts 轉(zhuǎn)換為 js 的代碼如下:

   function paramParser(rawParams) {
        if (rawParams === void 0) { rawParams = ''; }
        var map = new Map();
        if (rawParams.length > 0) {
            var params = rawParams.split('&');
            params.forEach(function (param) {
                var eqIdx = param.indexOf('=');
                var _a = eqIdx == -1 ? [param, ''] : 
                    [param.slice(0, eqIdx), param.slice(eqIdx + 1)], key = _a[0], 
                        val = _a[1];
                var list = map.get(key) || [];
                list.push(val);
                map.set(key, list);
            });
        }
        return map;
    }

XHR 下載圖片

var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://avatars2.githubusercontent.com/u/4220799?v=3');
    xhr.responseType = 'blob';

    xhr.onload = function() {
        if (this.status == 200) {
            var img = document.createElement('img');
            img.src = window.URL.createObjectURL(this.response); 
            img.onload = function() {
                window.URL.revokeObjectURL(this.src); 
            };
            document.body.appendChild(img);
        }
    };
    xhr.send();

XHR 上傳數(shù)據(jù)

發(fā)送普通文本

var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string"); 

發(fā)送FormData

var formData = new FormData(); 
formData.append('id', 123456);
formData.append('topic', 'performance');

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

發(fā)送 Buffer

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); 
xhr.send(uInt8Array.buffer);

XHR 上傳進(jìn)度條

progress 元素

<progress id="uploadprogress" min="0" max="100" value="0">0</progress>

定義 progress 事件的回調(diào)函數(shù)

xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
      var complete = (event.loaded / event.total * 100 | 0);
      var progress = document.getElementById('uploadprogress');
      progress.value = progress.innerHTML = complete;
  }
};

注意,progress事件不是定義在xhr伶棒,而是定義在xhr.upload旺垒,因?yàn)檫@里需要區(qū)分下載和上傳,下載也有一個(gè)progress事件肤无。

我有話說(shuō)

1.什么情況下 XMLHttpRequest status 會(huì)為 0先蒋?

XMLHttpRequest 返回 status 時(shí),會(huì)執(zhí)行以下步驟:

  • 如果狀態(tài)是 UNSENT 或 OPENED宛渐,則返回 0
  • 如果錯(cuò)誤標(biāo)志被設(shè)置竞漾,則返回 0
  • 否則返回 HTTP 狀態(tài)碼

另外當(dāng)訪問(wèn)本地文件資源或在 Android 4.1 stock browser 中從應(yīng)用緩存中獲取文件時(shí)眯搭,XMLHttpRequest 的 status 值也會(huì)為0。

示例一:

var xmlhttp;
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET","http://www.w3schools.com/XML/cd_catalog.xml", true);
xmlhttp.onreadystatechange=function() {
  if(xmlhttp.readyState == 4) console.log("status " + xmlhttp.status);
};
xmlhttp.addEventListener('error', function (error) {
   console.dir(error);
});
xmlhttp.send();

以上代碼運(yùn)行后业岁,將會(huì)在控制臺(tái)輸出:

status 0
ProgressEvent # error 對(duì)象

2.為什么 GET 或 HEAD 請(qǐng)求鳞仙,不能通過(guò) send() 方法發(fā)送請(qǐng)求體最铁?

client . send([body = null])

Initiates the request. The optional argument provides the request body. The argument is ignored if request method is GET or HEAD. —— xhr.spec

通過(guò) XMLHttpRequest 規(guī)范跋选,我們知道當(dāng)請(qǐng)求方法是 GET 或 HEAD 時(shí),send() 方法的 body 參數(shù)值將會(huì)被忽略林螃。那么對(duì)于我們常用的 GET 請(qǐng)求允耿,我們要怎么傳遞參數(shù)呢梳玫?解決參數(shù)傳遞可以使用以下兩種方式:

  • URL 傳參 - 常用方式,有大小限制大約為 2KB
  • 請(qǐng)求頭傳參 - 一般用于傳遞 token 等認(rèn)證信息

URL 傳參

var url = "bla.php";
var params = "somevariable=somevalue&anothervariable=anothervalue";
var http = new XMLHttpRequest();

http.open("GET", url+"?"+params, true);
http.onreadystatechange = function()
{
    if(http.readyState == 4 && http.status == 200) {
        alert(http.responseText);
    }
}
http.send(null); // 請(qǐng)求方法是GET或HEAD時(shí)右犹,設(shè)置請(qǐng)求體為空

在日常開(kāi)發(fā)中提澎,我們最常用的方式是傳遞參數(shù)對(duì)象,因此我們可以封裝一個(gè) formatParams() 來(lái)實(shí)現(xiàn)參數(shù)格式念链,具體示例如下:

formatParams() 函數(shù):

function formatParams( params ){
  return "?" + Object
        .keys(params)
        .map(function(key){
          return key+"="+params[key]
        })
        .join("&")
}

應(yīng)用示例:

var endpoint = "https://api.example.com/endpoint";
var params = {
  a: 1, 
  b: 2,
  c: 3
};
var url = endpoint + formatParams(params); // 實(shí)際應(yīng)用中需要判斷endpoint是否已經(jīng)包含查詢參數(shù)
// => "https://api.example.com/endpoint?a=1&b=2&c=3";

一些常用的 AJAX 庫(kù)盼忌,如 jQuery、zepto 等掂墓,內(nèi)部已經(jīng)封裝了參數(shù)序列化的方法 (如:jquery.param)谦纱,我們直接調(diào)用頂層的 API 方法即可。

(備注:以上示例來(lái)源 - stackoverflow - How do I pass along variables with XMLHttpRequest)

請(qǐng)求頭傳參 - (身份認(rèn)證)

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

xhr.setRequestHeader("x-access-token", "87a476494db6ec53d0a206589611aa3f");
xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
       // handle data 
    }
};
xhr.send("foo=bar&lorem=ipsum"); 

詳細(xì)的身份認(rèn)證信息君编,請(qǐng)參考 - JSON Web Tokens

3.XMLHttpRequest 請(qǐng)求體支持哪些格式跨嘉?

send() 方法簽名:

void send();

void send(ArrayBuffer data);

void send(Blob data);

void send(Document data);

void send(DOMString? data);

void send(FormData data);

POST請(qǐng)求示例

發(fā)送 POST 請(qǐng)求通常需要以下步驟:

  • 使用 open() 方法打開(kāi)連接時(shí),設(shè)定 POST 請(qǐng)求方法和請(qǐng)求 URL地址
  • 設(shè)置正確的 Content-Type 請(qǐng)求頭
  • 設(shè)置相關(guān)的事件監(jiān)聽(tīng)
  • 設(shè)置請(qǐng)求體吃嘿,并使用 send() 方法祠乃,發(fā)送請(qǐng)求
var xhr = new XMLHttpRequest();
xhr.open("POST", '/server', true);

//Send the proper header information along with the request
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
        // handle data
    }
}
xhr.send("foo=bar&lorem=ipsum"); 
// xhr.send('string'); 
// xhr.send(new Blob()); 
// xhr.send(new Int8Array()); 
// xhr.send({ form: 'data' }); 
// xhr.send(document);

4.什么是簡(jiǎn)單請(qǐng)求和預(yù)請(qǐng)求 (preflight request) ?

簡(jiǎn)單請(qǐng)求

一些不會(huì)觸發(fā) CORS preflight 的請(qǐng)求被稱為 "簡(jiǎn)單請(qǐng)求"兑燥,雖然 Fetch (定義 CORS的) 不使用這個(gè)術(shù)語(yǔ)亮瓷。滿足下述條件的就是 "簡(jiǎn)單請(qǐng)求":

預(yù)請(qǐng)求

不同于上面討論的簡(jiǎn)單請(qǐng)求挣饥,"預(yù)請(qǐng)求" 要求必須先發(fā)送一個(gè) OPTIONS 方法請(qǐng)求給目的站點(diǎn)除师,來(lái)查明這個(gè)跨站請(qǐng)求對(duì)于目的站點(diǎn)是不是安全的可接受的。這樣做扔枫,是因?yàn)榭缯菊?qǐng)求可能會(huì)對(duì)目的站點(diǎn)的數(shù)據(jù)產(chǎn)生影響汛聚。 當(dāng)請(qǐng)求具備以下條件,就會(huì)被當(dāng)成預(yù)請(qǐng)求處理:

詳細(xì)的信息,請(qǐng)參考 - MDN - HTTP 訪問(wèn)控制 (CORS)

5.XMLHttpRequest 對(duì)象垃圾回收機(jī)制是什么瞄桨?

在以下情況下话速,XMLHttpRequest 對(duì)象不會(huì)被垃圾回收:

  • 如果 XMLHttpRequest 對(duì)象的狀態(tài)是 OPENED 且已設(shè)置 send() 的標(biāo)識(shí)符
  • XMLHttpRequest 對(duì)象的狀態(tài)是 HEADERS_RECEIVED (已獲取響應(yīng)頭)
  • XMLHttpRequest 對(duì)象的狀態(tài)是 LOADING (正在下載響應(yīng)體),并且監(jiān)聽(tīng)了以下一個(gè)或多個(gè)事件:readystatechange芯侥、progress泊交、abort、error柱查、load廓俭、timeout、loadend

如果 XMLHttpRequest 對(duì)象在連接尚存打開(kāi)時(shí)被垃圾回收機(jī)制回收了唉工,用戶代理必須終止請(qǐng)求研乒。

6.GET 和 POST 請(qǐng)求的區(qū)別?

  • 對(duì)于 GET 請(qǐng)求淋硝,瀏覽器會(huì)把 HTTP headers 和 data 一并發(fā)送出去雹熬,服務(wù)器響應(yīng) 200。
  • 而對(duì)于 POST 請(qǐng)求谣膳,瀏覽器會(huì)先發(fā)送 HTTP headers竿报,服務(wù)器響應(yīng) 100 continue ,瀏覽器再發(fā)送 data继谚,服務(wù)器響應(yīng) 200烈菌。

詳細(xì)的信息,請(qǐng)參考 - 99%的人都理解錯(cuò)了HTTP中GET與POST的區(qū)別

7.怎樣防止重復(fù)發(fā)送 AJAX 請(qǐng)求花履?

  • setTimeout + clearTimeout - 連續(xù)的點(diǎn)擊會(huì)把上一次點(diǎn)擊清除掉芽世,也就是ajax請(qǐng)求會(huì)在最后一次點(diǎn)擊后發(fā)出去
  • disable 按鈕
  • 緩存已成功的請(qǐng)求,若請(qǐng)求參數(shù)一致臭挽,則直接返回捂襟,不發(fā)送請(qǐng)求

詳細(xì)的信息,請(qǐng)參考 - 知乎 - 怎樣防止重復(fù)發(fā)送 Ajax 請(qǐng)求

8欢峰、AJAX 站點(diǎn)怎么做 SEO 優(yōu)化

眾所周知,大部分的搜索引擎爬蟲(chóng)都不會(huì)執(zhí)行 JS涨共,也就是說(shuō)纽帖,如果頁(yè)面內(nèi)容由 Ajax 返回的話,搜索引擎是爬取不到部分內(nèi)容的举反,也就無(wú)從做 SEO (搜索引擎優(yōu)化)了懊直。國(guó)外的 prerender.io 網(wǎng)站提供了一套比較成熟的方案,但是需要付費(fèi)的火鼻。接下來(lái)我們來(lái)看一下室囊,怎么 PhantomJS 為我們的站點(diǎn)做 SEO雕崩。

詳細(xì)的信息,請(qǐng)參考 - 用PhantomJS來(lái)給AJAX站點(diǎn)做SEO優(yōu)化

精品文章

參考資源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末融撞,一起剝皮案震驚了整個(gè)濱河市盼铁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尝偎,老刑警劉巖饶火,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異致扯,居然都是意外死亡肤寝,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門抖僵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鲤看,“玉大人,你說(shuō)我怎么就攤上這事耍群∫骞穑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵世吨,是天一觀的道長(zhǎng)澡刹。 經(jīng)常有香客問(wèn)我,道長(zhǎng)耘婚,這世上最難降的妖魔是什么罢浇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮沐祷,結(jié)果婚禮上嚷闭,老公的妹妹穿的比我還像新娘。我一直安慰自己赖临,他們只是感情好胞锰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著兢榨,像睡著了一般嗅榕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吵聪,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天凌那,我揣著相機(jī)與錄音,去河邊找鬼吟逝。 笑死帽蝶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的块攒。 我是一名探鬼主播励稳,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼佃乘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了驹尼?” 一聲冷哼從身側(cè)響起趣避,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扶欣,沒(méi)想到半個(gè)月后鹅巍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡料祠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年骆捧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片髓绽。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敛苇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顺呕,到底是詐尸還是另有隱情枫攀,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布株茶,位于F島的核電站来涨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏启盛。R本人自食惡果不足惜蹦掐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望僵闯。 院中可真熱鬧卧抗,春花似錦、人聲如沸鳖粟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)向图。三九已至泳秀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榄攀,已是汗流浹背晶默。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留航攒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓趴梢,卻偏偏與公主長(zhǎng)得像漠畜,于是被迫代替她去往敵國(guó)和親币他。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容