Join MDN and developers like you at Mozilla's View Source conference, 12-14 September in Berlin, Germany. Learn more at https://viewsourceconf.org

使用XMLHttpRequest

XMLHttpReques讓送出HTTP請求十分簡單,建立XMLHttpRequest物件,開啟URL,送出請求。請求完成後,HTTP狀態以及內容都可以從XMLHttpRequest物件取得,本教學將說明XMLHttpRequest一些使用範例。

function reqListener () {
  console.log(this.responseText);
};

var oReq = new XMLHttpRequest();
oReq.onload = reqListener;
oReq.open("get", "yourFile.txt", true);
oReq.send();

請求種類

XMLHttpRequest有同步和非同步兩種請求,同步和非同步請求的決定取決於XMLHttpRequest的open()方法的async參數,若async值為true,則進行非同步請求,否則則進行同步請求,請參照同步和非同步請求相關說明。大致上,很少情況需要用同步請求。

回應處理

W3C規範了數個XMLHttpRequest回應屬性,我們可以從其中得知XMLHttpRequest狀態,下面說明了某些回傳非文字回應的案例會需要一些分析和操作。

分析和操作responseXML屬性

當透過XMLHttpRequest取得XML文件,那麼responseXML屬性將會是一個由解析XML文件而來的DOM物件,這樣可能會造成分析和操作上的一些困難,以下有幾個方法可以幫助分析XML文件:

  1. 利用XPath指向需要部份。
  2. 利用JXON轉換成Javascript物件樹。
  3. 手動解析與序列化XML字串或物件。
  4. Using XMLSerializer to serialize DOM trees to strings or to files.
  5. 如果事先知道文件內容,可利用正規表達式(RegExp),但是這項方法應該是最後不得已的方法,因為一旦XML文件內容稍有變動,這項方法就會失敗。如果換行符號會影響正規表達式掃描結果,那麼你要移除換行符號。

分析與操作包含HTML文件的responseText屬性

Note: W3C的XMLHttpRequest規範原本只定義解析XML文件,後來又加入支援解析HTML文件,這項新增支援讓我們可以從XMLHttpRequest.responseXML取得解析成DOM物件的HTML,詳情請參照HTML in XMLHttpRequest

當透過XMLHttpRequest取得HTML文件,那麼responseText屬性會包含一串HTML標籤的文字,所以可能在分析和操作上會造成困難,以下幾個方法幫忙分析和操作:

  1. 利用reponseXML
  2. 利用nsIScriptableUnescapeHTML安全解析HTML文件可以快速轉換文件成DOM,不過這也會去除Javascript和其他進階元素,例如<head>。
  3. 如果事先知道文件內容,可利用正規表達式(RegExp),但是這項方法應該是最後不得已的方法,因為一旦XML文件內容稍有變動,這項方法就會失敗。如果換行符號會影響正規表達式掃描結果,那麼你要移除換行符號。
document.getElementById("hiddenXULiframe").contentWindow.document.body.innerHTML = req.responseText

隱藏二進位資料

XMLHttpRequest不只能夠收發文字資料,透過利用overrideMimeType()方法,我們也能夠收發二進位資料。

var oReq = new XMLHttpRequest();
oReq.open("GET", url, true);
// retrieve data unprocessed as a binary string
oReq.overrideMimeType("text/plain; charset=x-user-defined");
/* ... */

XMLHttpRequest Level 2規範加入responseType屬性讓收發二進位資料變得容易許多。

var oReq = new XMLHttpRequest();

oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.onload = function(e) {
  var arraybuffer = oReq.response; // not responseText
  /* ... */
}
oReq.send();

詳細請參照收發二進位資料一文。

監視進度

XMLHttpRequest提供當請求進行時的各類事件,包括定期進度通知、錯誤通知等等。

XMLHttpRequest的DOM進度事件遵從了Web API的進度事件規範;這些事件實作ProgressEvent介面。

var oReq = new XMLHttpRequest();

oReq.addEventListener("progress", updateProgress, false);
oReq.addEventListener("load", transferComplete, false);
oReq.addEventListener("error", transferFailed, false);
oReq.addEventListener("abort", transferCanceled, false);

oReq.open();

// ...

// progress on transfers from the server to the client (downloads)
function updateProgress (oEvent) {
  if (oEvent.lengthComputable) {
    var percentComplete = oEvent.loaded / oEvent.total;
    // ...
  } else {
    // Unable to compute progress information since the total size is unknown
  }
}

function transferComplete(evt) {
  alert("The transfer is complete.");
}

function transferFailed(evt) {
  alert("An error occurred while transferring the file.");
}

function transferCanceled(evt) {
  alert("The transfer has been canceled by the user.");
}

第3~6行註冊了當進行XMLHttpRequest資料收發時的各類事件。

Note: 必須在呼叫open()前就註冊好事件,否則事件將不會被觸發。

本例中的updateProgress()能從事件的total和loaded屬性收到總傳輸位元與目前已傳輸位元,但這是在lengthComputable屬性不為false時,否則總長度未知且為零。

上下傳皆有進度事件;下載事件觸發在XMLHttpRequest物件上,如上例所示,上傳事件件觸發在XMLHttpRequest.upload物件上,如下所示:

var oReq = new XMLHttpRequest();

oReq.upload.addEventListener("progress", updateProgress, false);
oReq.upload.addEventListener("load", transferComplete, false);
oReq.upload.addEventListener("error", transferFailed, false);
oReq.upload.addEventListener("abort", transferCanceled, false);

oReq.open();
Note: 進度事件無法用於file協定。

Gecko 9.0 備註
(Firefox 9.0 / Thunderbird 9.0 / SeaMonkey 2.6)

自Gecko 9.0起,每一塊資料接收進度事件都會被觸發,即使連線在最後一筆資料封包接收進度事件觸發前就關閉了,進度事件還是會在封包載入事件發生時觸發,所以我們可以放心透過進度事件監控進度。

Gecko 12.0 備註
(Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9)

如果responseType為moz-blob,那麼進度事件觸發時的response會是目前所收到的Blob

我們也可以用loadend事件偵測下載結束狀況(同時包含到abort, load或error這三種下載結束狀況)。

req.addEventListener("loadend", loadEnd, false);

function loadEnd(e) {
  alert("The transfer finished (although we don't know if it succeeded or not).");
} 

loadend事件下無從得知是abort, load或error哪一種結束狀況,不過還是可以用loadend事件處理不分結束狀況下需要做的事。

送出表單與上傳檔案

XMLHttpRequest有兩種方法送出表單:

第二種FormData API方法簡單快速,但有個資料無法字串化的缺點。
第一種純AJAX複雜但彈性;資料可以字串化和重複使用,例如,操作瀏覽器歷史紀錄時產生狀態物件等等。

純AJAX

不用FormData API傳送表單便不需要其他API,除非我們要上傳的是檔案,哪麼就需要用到FileReader API。

傳送方法簡單說明

有4種送出html <form>方法:

  • 採用POST方法,enctype屬性設為application/x-www-form-urlencoded(預設)。
  • 採用POST方法,enctype屬性設為text/plain。
  • 採用POST方法,enctype屬性設為multipart/form-data。
  • 採用GET方法,於此方法下enctype屬性將被忽略。

假想我們送出一份表單,這個表單有foo和bar兩個欄位,如果送出方法是POST,那麼依據所設定的enctype,伺服器會分別收到以下字串:

  • 方法: POST; 編碼型態: application/x-www-form-urlencoded (預設):

    Content-Type: application/x-www-form-urlencoded
    
    foo=bar&baz=The+first+line.&#37;0D%0AThe+second+line.%0D%0A
  • 方法: POST; 編碼型態: text/plain:

    Content-Type: text/plain
    
    foo=bar
    baz=The first line.
    The second line.
  • 方法: POST; 編碼型態: multipart/form-data:

    Content-Type: multipart/form-data; boundary=---------------------------314911788813839
    
    -----------------------------314911788813839
    Content-Disposition: form-data; name="foo"
    
    bar
    -----------------------------314911788813839
    Content-Disposition: form-data; name="baz"
    
    The first line.
    The second line.
    
    -----------------------------314911788813839--

如果送出方法是GET,那麼如下所示的字串將被附加到URL上:

?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.

框架範例

當我們送出<form>瀏覽器自動幫我們做了許多工作送出表單,不過若是純AJAX用Javascript送出表單,我們就必須自己處理這些工作了。至於這些工作的說明由於過於複雜,這裡不方便解釋,所以我們改提供一組完整、教學用的框架,這個框架適用於全部4種送出方法,包括上傳檔案:

<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Sending forms with pure AJAX &ndash; MDN</title>
<script type="text/javascript">

"use strict";

/*\
|*|
|*|  :: XMLHttpRequest.prototype.sendAsBinary() Polyfill ::
|*|
|*|  https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#sendAsBinary()
\*/

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;
    }
    /* send as ArrayBufferView...: */
    this.send(ui8Data);
    /* ...or as ArrayBuffer (legacy)...: this.send(ui8Data.buffer); */
  };
}

/*\
|*|
|*|  :: AJAX Form Submit Framework ::
|*|
|*|  https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest
|*|
|*|  This framework is released under the GNU Public License, version 3 or later.
|*|  http://www.gnu.org/licenses/gpl-3.0-standalone.html
|*|
|*|  Syntax:
|*|
|*|   AJAXSubmit(HTMLFormElement);
\*/

var AJAXSubmit = (function () {

  function ajaxSuccess () {
    /* console.log("AJAXSubmit - Success!"); */
    alert(this.responseText);
    /* you can get the serialized data through the "submittedData" custom property: */
    /* alert(JSON.stringify(this.submittedData)); */
  }

  function submitData (oData) {
    /* the AJAX request... */
    var oAjaxReq = new XMLHttpRequest();
    oAjaxReq.submittedData = oData;
    oAjaxReq.onload = ajaxSuccess;
    if (oData.technique === 0) {
      /* method is GET */
      oAjaxReq.open("get", oData.receiver.replace(/(?:\?.*)?$/, oData.segments.length > 0 ? "?" + oData.segments.join("&") : ""), true);
      oAjaxReq.send(null);
    } else {
      /* method is POST */
      oAjaxReq.open("post", oData.receiver, true);
      if (oData.technique === 3) {
        /* enctype is multipart/form-data */
        var sBoundary = "---------------------------" + Date.now().toString(16);
        oAjaxReq.setRequestHeader("Content-Type", "multipart\/form-data; boundary=" + sBoundary);
        oAjaxReq.sendAsBinary("--" + sBoundary + "\r\n" + oData.segments.join("--" + sBoundary + "\r\n") + "--" + sBoundary + "--\r\n");
      } else {
        /* enctype is application/x-www-form-urlencoded or text/plain */
        oAjaxReq.setRequestHeader("Content-Type", oData.contentType);
        oAjaxReq.send(oData.segments.join(oData.technique === 2 ? "\r\n" : "&"));
      }
    }
  }

  function processStatus (oData) {
    if (oData.status > 0) { return; }
    /* the form is now totally serialized! do something before sending it to the server... */
    /* doSomething(oData); */
    /* console.log("AJAXSubmit - The form is now serialized. Submitting..."); */
    submitData (oData);
  }

  function pushSegment (oFREvt) {
    this.owner.segments[this.segmentIdx] += oFREvt.target.result + "\r\n";
    this.owner.status--;
    processStatus(this.owner);
  }

  function plainEscape (sText) {
    /* how should I treat a text/plain form encoding? what characters are not allowed? this is what I suppose...: */
    /* "4\3\7 - Einstein said E=mc2" ----> "4\\3\\7\ -\ Einstein\ said\ E\=mc2" */
    return sText.replace(/[\s\=\\]/g, "\\$&");
  }

  function SubmitRequest (oTarget) {
    var nFile, sFieldType, oField, oSegmReq, oFile, bIsPost = oTarget.method.toLowerCase() === "post";
    /* console.log("AJAXSubmit - Serializing form..."); */
    this.contentType = bIsPost && oTarget.enctype ? oTarget.enctype : "application\/x-www-form-urlencoded";
    this.technique = bIsPost ? this.contentType === "multipart\/form-data" ? 3 : this.contentType === "text\/plain" ? 2 : 1 : 0;
    this.receiver = oTarget.action;
    this.status = 0;
    this.segments = [];
    var fFilter = this.technique === 2 ? plainEscape : escape;
    for (var nItem = 0; nItem < oTarget.elements.length; nItem++) {
      oField = oTarget.elements[nItem];
      if (!oField.hasAttribute("name")) { continue; }
      sFieldType = oField.nodeName.toUpperCase() === "INPUT" ? oField.getAttribute("type").toUpperCase() : "TEXT";
      if (sFieldType === "FILE" && oField.files.length > 0) {
        if (this.technique === 3) {
          /* enctype is multipart/form-data */
          for (nFile = 0; nFile < oField.files.length; nFile++) {
            oFile = oField.files[nFile];
            oSegmReq = new FileReader();
            /* (custom properties:) */
            oSegmReq.segmentIdx = this.segments.length;
            oSegmReq.owner = this;
            /* (end of custom properties) */
            oSegmReq.onload = pushSegment;
            this.segments.push("Content-Disposition: form-data; name=\"" + oField.name + "\"; filename=\""+ oFile.name + "\"\r\nContent-Type: " + oFile.type + "\r\n\r\n");
            this.status++;
            oSegmReq.readAsBinaryString(oFile);
          }
        } else {
          /* enctype is application/x-www-form-urlencoded or text/plain or method is GET: files will not be sent! */
          for (nFile = 0; nFile < oField.files.length; this.segments.push(fFilter(oField.name) + "=" + fFilter(oField.files[nFile++].name)));
        }
      } else if ((sFieldType !== "RADIO" && sFieldType !== "CHECKBOX") || oField.checked) {
        /* field type is not FILE or is FILE but is empty */
        this.segments.push(
          this.technique === 3 ? /* enctype is multipart/form-data */
            "Content-Disposition: form-data; name=\"" + oField.name + "\"\r\n\r\n" + oField.value + "\r\n"
          : /* enctype is application/x-www-form-urlencoded or text/plain or method is GET */
            fFilter(oField.name) + "=" + fFilter(oField.value)
        );
      }
    }
    processStatus(this);
  }

  return function (oFormElement) {
    if (!oFormElement.action) { return; }
    new SubmitRequest(oFormElement);
  };

})();

</script>
</head>
<body>

<h1>Sending forms with pure AJAX</h1>

<h2>Using the GET method</h2>

<form action="register.php" method="get" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Registration example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" />
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

<h2>Using the POST method</h2>
<h3>Enctype: application/x-www-form-urlencoded (default)</h3>

<form action="register.php" method="post" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Registration example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" />
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

<h3>Enctype: text/plain</h3>

<form action="register.php" method="post" enctype="text/plain" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Registration example</legend>
    <p>
      Your name: <input type="text" name="user" />
    </p>
    <p>
      Your message:<br />
      <textarea name="message" cols="40" rows="8"></textarea>
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

<h3>Enctype: multipart/form-data</h3>

<form action="register.php" method="post" enctype="multipart/form-data" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Upload example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" /><br />
      Sex:
      <input id="sex_male" type="radio" name="sex" value="male" /> <label for="sex_male">Male</label>
      <input id="sex_female" type="radio" name="sex" value="female" /> <label for="sex_female">Female</label><br />
      Password: <input type="password" name="secret" /><br />
      What do you prefer:
      <select name="image_type">
        <option>Books</option>
        <option>Cinema</option>
        <option>TV</option>
      </select>
    </p>
    <p>
      Post your photos:
      <input type="file" multiple name="photos[]">
    </p>
    <p>
      <input id="vehicle_bike" type="checkbox" name="vehicle[]" value="Bike" /> <label for="vehicle_bike">I have a bike</label><br />
      <input id="vehicle_car" type="checkbox" name="vehicle[]" value="Car" /> <label for="vehicle_car">I have a car</label>
    </p>
    <p>
      Describe yourself:<br />
      <textarea name="description" cols="50" rows="8"></textarea>
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

</body>
</html>

下面我們創造了範例表單action 屬性指向的register.php。

<?php

  /* register.php */

  header("Content-type: text/plain");

  echo ":: data received via GET ::\n\n";
  print_r($_GET);

  echo "\n\n:: Data received via POST ::\n\n";
  print_r($_POST);

  echo "\n\n:: Data received as \"raw\" (text/plain encoding) ::\n\n";
  if (isset($HTTP_RAW_POST_DATA)) { echo $HTTP_RAW_POST_DATA; }

  echo "\n\n:: Files received ::\n\n";
  print_r($_FILES);

?>

框架使用語法如下:

AJAXSubmit(myForm);
Note: 上面框架裡,當有檔案上傳,而且表單方法採用POST、enctype是multipart/form-data下,我們用了FileReader API,可是這是新的API,所以說純AJAX上傳檔案尚處於實驗性質技巧,不過不需要上傳檔案的話,我們的框架不會用到其他新的API。傳送二進位資料最佳的方法是使用send()方法送出ArrayBuffersBlobs,如果可以,搭配FileReader API的readAsArrayBuffer()。不過因為本程式碼的目標是要處理可字串化原始資料,我們使用FileReader API的sendAsBinary()readAsBinaryString()方法,當處理相對少量需要字串化以利重複使用的資料,這是最佳方法,然而使用字串而非型態陣列隱含占用更多資源,所以本程式碼比較適合小型檔案(如影像、文件、mp3等),否則,如果你不想要字串化送出或上傳資料,除了型態陣列,還可以考慮使用FormDataAPI。

使用FormData物件

FormData建構子讓我們組合出一組key/value配對資料好透過XMLHttpRequest送出。FormData主要目的是用來傳送表單資料,不過也可以獨立用來傳送索引資料。傳送出去的資料格式會等同於表單submit()方法所使用的格式,只要表單編碼型態為” multipart/form-data”。FormData物件和XMLHttpRequest有數種搭配使用方式(請參照Using FormData Objects一文),這裡我們改寫前例換成使用FormData API:

<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Sending forms with FormData &ndash; MDN</title>
<script type="text/javascript">
"use strict";

function ajaxSuccess () {
  alert(this.responseText);
}

function AJAXSubmit (oFormElement) {
  if (!oFormElement.action) { return; }
  var oReq = new XMLHttpRequest();
  oReq.onload = ajaxSuccess;
  if (oFormElement.method.toLowerCase() === "post") {
    oReq.open("post", oFormElement.action, true);
    oReq.send(new FormData(oFormElement));
  } else {
    var oField, sFieldType, nFile, sSearch = "";
    for (var nItem = 0; nItem < oFormElement.elements.length; nItem++) {
      oField = oFormElement.elements[nItem];
      if (!oField.hasAttribute("name")) { continue; }
      sFieldType = oField.nodeName.toUpperCase() === "INPUT" ? oField.getAttribute("type").toUpperCase() : "TEXT";
      if (sFieldType === "FILE") {
        for (nFile = 0; nFile < oField.files.length; sSearch += "&" + escape(oField.name) + "=" + escape(oField.files[nFile++].name));
      } else if ((sFieldType !== "RADIO" && sFieldType !== "CHECKBOX") || oField.checked) {
        sSearch += "&" + escape(oField.name) + "=" + escape(oField.value);
      }
    }
    oReq.open("get", oFormElement.action.replace(/(?:\?.*)?$/, sSearch.replace(/^&/, "?")), true);
    oReq.send(null);
  }
}
</script>
</head>
<body>

<h1>Sending forms with FormData</h1>

<h2>Using the GET method</h2>

<form action="register.php" method="get" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Registration example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" />
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

<h2>Using the POST method</h2>
<h3>Enctype: application/x-www-form-urlencoded (default)</h3>

<form action="register.php" method="post" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Registration example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" />
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

<h3>Enctype: text/plain</h3>

<p>The text/plain encoding is not supported by the FormData API.</p>

<h3>Enctype: multipart/form-data</h3>

<form action="register.php" method="post" enctype="multipart/form-data" onsubmit="AJAXSubmit(this); return false;">
  <fieldset>
    <legend>Upload example</legend>
    <p>
      First name: <input type="text" name="firstname" /><br />
      Last name: <input type="text" name="lastname" /><br />
      Sex:
      <input id="sex_male" type="radio" name="sex" value="male" /> <label for="sex_male">Male</label>
      <input id="sex_female" type="radio" name="sex" value="female" /> <label for="sex_female">Female</label><br />
      Password: <input type="password" name="secret" /><br />
      What do you prefer:
      <select name="image_type">
        <option>Books</option>
        <option>Cinema</option>
        <option>TV</option>
      </select>
    </p>
    <p>
      Post your photos:
      <input type="file" multiple name="photos[]">
    </p>
    <p>
      <input id="vehicle_bike" type="checkbox" name="vehicle[]" value="Bike" /> <label for="vehicle_bike">I have a bike</label><br />
      <input id="vehicle_car" type="checkbox" name="vehicle[]" value="Car" /> <label for="vehicle_car">I have a car</label>
    </p>
    <p>
      Describe yourself:<br />
      <textarea name="description" cols="50" rows="8"></textarea>
    </p>
    <p>
      <input type="submit" value="Submit" />
    </p>
  </fieldset>
</form>

</body>
</html>
Note: FormData物件不可字串化,所以請採用前面所述的純AJAX方法當需要字串化資料時。另外請注意,本例中有一些檔案<input>欄位,不過我們不用呼叫FileReader API,因為透過FormData API送出表單時,檔案將自動載入上傳。

取得最後修改日期

function getHeaderTime () {
  alert(this.getResponseHeader("Last-Modified"));  /* A valid GMTString date or null */
}

var oReq = new XMLHttpRequest();
oReq.open("HEAD" /* use HEAD if you only need the headers! */, "yourpage.html", true);
oReq.onload = getHeaderTime;
oReq.send();

當最後修改日期改變時做一些事情

先建立兩個函數:

function getHeaderTime () {

  var

    nLastVisit = parseFloat(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*lm_" + escape(this.filepath).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")),
    nLastModif = Date.parse(this.getResponseHeader("Last-Modified"));

  if (isNaN(nLastVisit) || nLastModif > nLastVisit) {
    document.cookie = "lm_" + escape(this.filepath) + "=" + Date.now() + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
    isFinite(nLastVisit) && this.callback(nLastModif, nLastVisit);
  }

}

function ifHasChanged (sURL, fCallback) {
  var oReq = new XMLHttpRequest();
  oReq.open("HEAD" /* use HEAD - we only need the headers! */, sURL, true);
  oReq.callback = fCallback;
  oReq.filepath = sURL;
  oReq.onload = getHeaderTime;
  oReq.send();
}

測試:

/* Let's test the file "yourpage.html"... */

ifHasChanged("yourpage.html", function (nModif, nVisit) {
  alert("The page \"" + this.filepath + "\" has been changed on " + (new Date(nModif)).toLocaleString() + "!");
});
Note: 上面程式碼有用到the JavaScript cookie API,其中以測試網頁路徑作為cookie id,所以如果有兩個網頁皆會送出請求到測試網頁,一個是以測試網頁絕對路徑,另一個以測試網頁相對路徑,那麼cookie的id會不一樣,產生兩組cookie。

如果想要知道網頁何時改變,請參照document.lastModified一文。

跨站XMLHttpRequest請求

最近新版的瀏覽器開始支援網頁應用工作小組制定的跨站請求存取控制標準,只要伺服器允許來自你的網路應用請求,那麼XMLHttpRequest請求就會執行,否則會產生INVALID_ACCESS_ERR例外錯誤。

避開快取

有一個跨瀏覽器避開快取的方法,那就是附加時間戳記於URL之後,例如:

http://foo.com/bar.html -> http://foo.com/bar.html?12345
http://foo.com/bar.html?foobar=baz -> http://foo.com/bar.html?foobar=baz&12345

因為本地快取的索引是基於URL,所以一旦URL改變就會發送新請求,避開快取。

我們可以如下調整URL加入時間戳記:

var oReq = new XMLHttpRequest();

oReq.open("GET", url + ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(), true);
oReq.send(null);

安全性

Firefox 3 note

Firefox 3之前的版本支援設定偏好 capability.policy.XMLHttpRequest.open為 allAccess來允許特定跨站存取,不過現已不再支援。

Firefox 5 note

Firefox 5之前的版本支援呼叫netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");來執行跨站存取,不過現已不再支援(請注意允許請求對話框依然會出現,也不會有警告,但實際上不再支援)。

開啟跨存取建議方式為利用 Access-Control-Allow-OriginHTTP標頭回應XMLHttpRequest請求。

XMLHttpRequests中止

如果你發現XMLHttpRequests的status=0還有statusText=null,這代表請求不被允許執行,並未送出。會被中止有可能是因為XMLHttpRequests來源改變(在建立XMLHttpRequests之時),然後呼叫XMLHttpRequests的open(),例如在window的onunload事件發生時發出XMLHttpRequests請求的情況下: XMLHttpRequests物件產生時當即將關閉的widnow尚存,然後呼叫open()送出請求卻是在新widnow取得焦點、舊widnow尚失焦點下。要避開這個問題可以在舊window onunload事件觸發時註冊新window的activate事件。

從Javascript模組/XPCOM元件使用XMLHttpRequests

Javascript模組/XPCOM元件產生XMLHttpRequests物件在做法上會有些許不同,我們無法用XMLHttpRequests()建構子,因為這個建構子在元件並沒有被定義,比較好的作法是使用XPCOM元件建構子。

const XMLHttpRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest");

不過在Gecko 16之前透過這種方式發送請求存在一個請求會被無條件取消的問題,所以Gecko 15以前我們需要從隱藏的DOM window取得XMLHttpRequests建構子,作法如下:

const { XMLHttpRequest } = Components.classes["@mozilla.org/appshell/appShellService;1"]
                                     .getService(Components.interfaces.nsIAppShellService)
                                     .hiddenDOMWindow;
var oReq = new XMLHttpRequest();

延伸閱讀

  1. MDN AJAX introduction
  2. HTTP access control
  3. How to check the security state of an XMLHTTPRequest over SSL
  4. XMLHttpRequest - REST and the Rich User Experience
  5. Microsoft documentation
  6. Apple developers' reference
  7. "Using the XMLHttpRequest Object" (jibbering.com)
  8. The XMLHttpRequest Object: W3C Specification
  9. Web Progress Events specification

文件標籤與貢獻者

 此頁面的貢獻者: teoli, foxbrush
 最近更新: teoli,