AJAX

Ajax 是 JavaScript 和 Web 開發史上最重要的發展之一,簡單地說,Ajax 是在瀏覽器使用 JavaScript 進行伺服器的後台請求,以便讀取附加資訊或者導致伺服器回應的過程,例如,請求可能是讀取一組待呈現的資料、查看使用者名稱是否可用,或更新資料庫的紀錄等。
參考 W3school AJAX:http://www.w3schools.com/ajax/default.asp

1Ajax 基礎知識

以實際的 JavaScript 程式碼來說,Ajax 請求的執行從下面三個步驟開始:
然後再深入進行如下:
上述內容需要有與 JavaScript 溝通的實際伺服器資源,例如 PHP。

建立 Ajax 物件


Ajax 的處理從一個物件開始,再透過該物件進行互動。這個稱為「Ajax」的物件有些地方會叫作「XHR」物件,XHR 是 XML HTTP 請求(XML HTTP Request)的縮寫,或是使用這些關鍵字的變形。這些術語說明,Ajax 以 JavaScript 和超文本傳輸協定(HTTP)請求其他的資源,最初,回傳的資料是採用可擴充標記語言(XML)格式,現在已經較少使用了。
每個瀏覽器都定義了一個具備必要功能的 XMLHttpRequest 物件,但需注意的是,建立該物件的細節在不同瀏覽器可能稍有不同,透過以下程式,便能在所有的瀏覽器可靠地建立 Ajax 物件:
          function getXMLHttpRequestObject(){      //定義成函數
            var ajax = null;                       //如果無法指派 XMLHttpRequest 物件給 ajax,該變數的值為 null
            if(window.XMLHttpRequest){
              ajax = new XMLHttpRequest();
            }else if(window.ActiveXObject){        //IE 較舊版本
              ajax = new ActiveXObject('MSXML2.XMLHTTP.3.0');
            }
            return ajax;
          }

          var ajax = getXMLHttpRequestObject();    //使用函數
        
更早期的 IE 版本必須藉由建立新的 ActiveXObject,並以 MSXML2.XMLHTTP.3.0 作為參數,上述程式碼還能應用到 IE5 等舊版本(在各種不同地程式碼和其他資源上,該值的內容會有所差別)。
          
Ajax 與漸進式改善
和透過 JavaScript 實作的任何功能一樣,必須留意使用者可能未啟用 JavaScript 的情況,Ajax 需要以 JavaScript 執行請求,再用結果更新頁面。
Ajax 廣泛應用從伺服器讀取資料,並以得到的資訊更新頁面。這種情況是產生另一份 HTML 頁面,並在不透過 JavaScript 的前提下呈現資料。第一個頁面必須連結到其他頁面,再由 JavaScript 讓該連結觸發 Ajax 的呼叫,並將瀏覽器提交給伺服器腳本,而在啟用 JavaScript 的情況下則透過 Ajax 中斷提交。
另一個主要的 Ajax 用途是發送資料給伺服器,例如,從聯絡人表單或註冊腳本送出資料。在這種情況下,必須考慮無法使用 Ajax 時,如何將表單交給伺服器腳本,而在啟用 JavaScript 的情況下則透過 Ajax 中斷提交。
此外請留意,搜尋引擎無法看到 JavaScript 建立的動態內容,如果需讓 Ajax 取得的內容出現在搜尋結果中,必須以非動態的內容方式呈現,例如前文說明地連結輔助頁面。
Ajax 要求使用者上網,但是透過 HTML5 的本地端儲存能力,便可克服此侷限性(假設 HTML5 作為一種選項的話)。再者,除非採取額外措施,否則 JavaScript 產生的動態異動無法存入書籤。

指定結果處理器


有了 Ajax 物件之後,下一步就是指定該物件的結果處理器,結果處理器是在 Ajax 交易期間呼叫的函數。
          ajax.onreadystatechange = handleStateChange;    //此處也允許使用匿名變數
        

發出請求


建立 Ajax 物件並指定回應處理函數之後,就可以執行實際的請求了:

請求類型


最常見的請求類型是 GET 和 POST。GET 是請求任何 HTML 頁面的標準方法,也是點擊連結時瀏覽器發出的請求類型。理論上來說,GET 請求最適合讀取資料;POST 請求是提交表單的標準方法(除了搜尋引擎表單之外,搜尋引擎通常使用 GET)。同樣的,原則上 POST 請求適合發起伺服器的異動或回應,換句話說,GET 用於常見而重複的請求,並可存入書籤;POST則用無需考慮重複性的獨特請求,例如更新特定資料庫紀錄或者提交聯絡人表單(在這兩種情況下,整體概念都一樣,但細節略有不同)。
注意,方法類型必須採用全大寫字母

伺服器資源的 URL


URL 參數可能是絕對路徑或相對路徑,如果連結的是 http://www.example.com/page.html 頁面,假設 page.php 和 page.html 在同一個目錄中,則 http://www.example.com/page.php 或者 page.php 都有效。針對此 URL,有兩個常見的問題和混淆的地方:
  1. 涉及同源策略,作為安全手段,瀏覽器的 JavaScript 不能請求其他網域上的資源。意思是 http://www.example.com/page.html 無法發起對 http://shop.example.com/page.php 或 http://www.domo.com/page.php 的請求。
  2. Ajax 請求必須透過伺服器才能正常運作,這表示 JavaScript 所在的 HTML 頁面也得藉由 URL(http://something)載入,如果請求返回狀態碼 0,或者回傳 PHP 程式碼而非資料,就說明可能不是透過 URL 發出請求。

請求採用同步或非同步


預設值為 true──非同步,不過還是應該明確提供此參數,非同步請求期間可以執行其他的 JavaScript 程式碼(例如處理其他事件),同時等待伺服器回應。同步的請求情況非常少,它阻止 JavaScript 在發起請求和處理的同時進行其他工作,包括處理基於使用者的事件。如果建立的是同步請求,就不需要透過函數來處理 readyState 的變化,因為腳本在進行任何其他工作之前必須等待伺服器回應。

第 4 和第 5 個參數


這兩個參數可有可無,分別代表使用者名稱和密碼,如果資源受到 HTTP 認證保護,則得提供這兩個參數,但使用這些參數時,必須在 JavaScript 程式碼存取參數值,唯一安全的方法是讓使用者輸入值,而不是寫死在頁面的來源程式碼中。

最後一步是實際發送請求:
          ajax.send(null);    //暫時以 null 值作為方法的唯一參數,代表請求中的資料
        

發送請求但尚未完成前,可以呼叫物件的 abort() 方法撤銷,常見的手法是設定一個計時器,當請求耗費的時間過長予以撤銷:
          ajax.open('GET', 'http://www.example.com/page.php', true);
          var ajaxAbortTimer = setTimeout (function(){
            if(ajax){
              ajax.abort();
              ajax = null;
            }
          }, 5000);
        
上述程式碼建立一個在 5 秒之後呼叫的匿名函數。在函數中,如果 ajax 物件值仍然不為假,就能確定請求依舊在進行,可以中止。中止或完成請求後,ajax 變數將變為假值(如 null),代表請求不在處於活動中。此外也能以他種方式向使用者指出這個問題。

處理伺服器回應


發出 Ajax 非同步請求之後,在物件的 readyState 屬性變化時,將呼叫指派 Ajax 物件 onreadystatechange 屬性的函數: readyState 屬性有如下的 5 個值,依照執行的順序排列: 例如,發送請求之後,該值立刻變成 1,然後 2、3,最後變成 4。此刻,onreadystatechange 屬性所指的函數用來處理伺服器回應,在請求過程中的每個階段都將呼叫該函數。
在處理 readyState 變化的函數中,可以檢查 readyState 屬性並作出相對的反應,例如屬性值 4 意味著處理已完成,頁面能夠使用處理後的結果。其他值則表示 Ajax 請求仍在處理中,腳本不需要作任何事情,或是顯示「載入中......」訊息。
也可以在呼叫 open() 方法之後顯示「載入中......」訊息,並在 readyState 等於 4 時隱藏起來。當 readyState 等於 4 時,代表 Ajax 請求已經完成了整個週期,但於試圖處理回應之前還要進行一次檢查:確認回應正常。
物件的 status 屬性能夠完成確認工作,該屬性代表伺服器對資源請求的對應代碼,這些伺服器的 HTTP 代碼包括: 沒有必要記住或者處理各種可能性。當狀態碼為 200 時,說明已經找到並可載入資源;如果狀態碼為 304,表示瀏覽器有一個可用的快取版本;其他所有的代碼都說明出現了問題。
          if(ajax.readyState == 4){
            if((ajax.status >= 200) && (ajax.status < 300) || (ajax.status == 304)){
              //回應完成
            }else{
              //顯示錯誤
            }
          }else{
            //顯示載入中
          }
        

根據 Ajax 的具體使用方法,如果返回有問題的代碼,便可重新啟用預設的瀏覽器行為:重新導向瀏覽器到 Ajax 的頁面,或真正地提交表單等等。
使用完畢 Ajax 物件之後(不再使用腳本時),應該填入 null 值清除:
          ajax = null;      //釋放該物件所需的瀏覽器資源
        

發送資料


從伺服器資源抓取資料後,下一步就是送出資料給伺服器,有些情況下,傳輸資料可能會影響返回的資料,例如為了取得某部門的員工,Ajax 請求可以將部門識別字送到伺服器。其他情況下,發送的資料需通過伺服器的驗證,例如使用者名稱是否存在;或觸發伺服器回應,例如張貼評論置留言板上。
將資料作為請求的一部份送給伺服器有兩種方法:
  1. 將資料附加到 URL 後:以「名稱 = 值」為資料結構配對,然後配對間用「&」隔開,位保證請求資料的安全性,可將其封裝至 encodeURIComponent() 呼叫中
                   
                  ajax.open('GET', 'http://www.example.com/somepage.php?id=' + encodeURIComponent(id), true);
                
  2. 把資料作為 send() 方法(代替 null)的唯一參數
                   
                  var data = 'email=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password);
                  ajax.open('GET', 'http://www.example.com/somepage.php', true);
                  ajax.send(data);
                
兩種方法的最終結果相同,但後者的程式碼更清楚。

然而發出 POST 請求時,必須為 send() 方法提供資料,而不是附加到 URL 之後,而且 POST 方法是為了增加可靠性。
           
          var data = 'email=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password);
          ajax.open('POST', 'http://www.example.com/somepage.php', true);
          ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');   //指定合適的資料編碼
          ajax.send(data);
        
採用 GET 方法,接收資料的 PHP 腳本分別以 $_GET['email']$_GET['password'] 存取發送資料;採用 POST 方法,則接收資料的 PHP 腳本分別以 $_POST['email']&_POST['password'] 存取送出的資料。
不管發起的是 GET 或 POST 請求,資料結構都一樣,兩者各視合適的場合使用。
透過 GET 傳輸的資料量有限制,根據不同瀏覽器,大約為 2~4 KB

向伺服器發送資料的方案之一是建立 FormData 物件,接著呼叫 FormData 物件的 append() 方法新增各個項目(但 FormData 尚未得到所有瀏覽器支援):
          if(typeof FormData == 'function'){    //確定瀏覽器支援 FormData 物件
            var data = new FormData();          //建立 FormData 物件
            //新增各個項目資料
            data.append('email', email);
            data.append('password', password);
          }else{
            //使用前面兩種方法
          }
          data.send(data);
        
注意,由於資料會自動編碼,因此不需執行 encodeURIComponent(),也沒有必要設定 Content-Type
將資料或表單送給伺服器之前,必須先知道安全性方面的影響,在後台進行請求,並不代表資料的傳送是秘密和安全的,在要求較高安全性的場合下,伺服器資源該透過 HTTPS 存取(假設伺服器支援)

            window.onload = function(){
              'use strict';

              var ajax = getXMLHttpRequestObject();         //建立 Ajax 物件

              //建立 onreadystatechange 函數,於物件的 readyState 屬性改變時呼叫
              ajax.onreadystatechange = function(){
                if(ajax.readyState == 4){                   //當狀態為傳送完成(4)時
                  //如果返回正確的狀態碼即回應資料;否則返回錯誤
                  if( (ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304)){
                    document.getElementById('output').innerHTML = ajax.responseText;
                  }else{
                    document.getElementById('output').innerHTML = 'Error: ' + ajax.statusText;
                  }
                }
              };

              //點擊按鈕時發起 Ajax 請求
              document.getElementById('btn').onclick = function(){
                ajax.open('GET', '../resources/test.txt', true);
                ajax.send(null);
              };
            }
          

基本偵錯


Ajax 最困難的方面就是偵錯,依照以下步驟偵錯便可找出原因:

第一件是測試伺服器資源,確認資料正傳回給 JavaScript,不僅做為偵錯的第一步,建議在建立伺服端資源之後立刻進行測試,以便在編寫任何 JavaScript 之前確認沒有問題。許多伺服端資源可以藉由直接在瀏覽器載入之方式進行測試,載入資源的效果往往和透過 JavaScript 接收回應相同。針對 PHP 腳本或其他動態的伺服端工具,可先載入他們以便確認運作正常。最初的 Ajax 嘗試遭到多次失敗,就是因為 PHP 腳本有錯誤,如果先測試腳本檔案,就能發現這些錯誤。在任何情況下,一定要確保透過 URL(http://something)載入 Ajax 頁面以及相關的資源。
如果 PHP 腳本(或其他的伺服端資源)需要接收一些資料才能正常運作,那麼測試會更加困難。倘若腳本採用 GET 方法,則可自行將資料附加到 URL 後面;如果是 POST 送給伺服端資源,就得向伺服器張貼資料,這並不容易,方法之一是送出表單資料給 PHP 腳本。實際上如果採用漸進式改善,就處於此一過程中:在完全不涉及 JavaScript 的情況下,測試以表單提交資料給 PHP 腳本。

第二件是使用網路監視器,例如 Firebug 和其他瀏覽器工具,像是 Opera 的 Dragonfly 以及 Safari 的 Web Inspector 都內建網路監視器,它能夠驗證請求的發生、顯示請求中傳輸的資料,並且顯示回應返回的資料,這些資料幾乎總是能揭示問題的起因,即使無法直接顯露問題,網路監視器也能提示問題是出在請求或回應中,由此便能相對地應用 JavaScript 或者 PHP 的偵錯技術。
開始使用不同的資料格式(如 JSON 和 XML)時,希望驗證資料本身的完整性,有時可能出現這種情況:伺服器返回明確的回應,但是資料未正確格式化,使得無法應用於 JavaScript 程式中。這時可以利用 JSONint 驗證 JSON 資料,或使用 XML Validator 驗證 XML 資料。
我們必須知道同一個頁面何時會發出多個 Ajax 請求。當頁面只有單比請求時,才能使用單個 Ajax 物件,如果要求多個不同的請求(可能在同一個時間)時,就得使用多個 Ajax 物件避免衝突。

最後,瀏覽器快取機制使偵錯變得困難,因為瀏覽器將試圖緩存 Ajax 請求的結果,所以針對伺服端資源的修改就無法立即反應,此一障礙有多種克服手段:
  1. 在瀏覽器中停用快取,只有使用者受到影響
  2. 由伺服器資源指定不應該快取請求,影響相同伺服端資源的所有人
    於 PHP 送出一個 Cache-Control 標頭,就可實作此功能,阻止所有瀏覽器的快取請求:
                  
                
  3. 製造看上去每次都是唯一的請求,常見做法是在請求 URL 上新增一隨機值,例如時間戳記:
                  var url = 'http://www.example.com/somepage.php?stamp=' + new Date.getTime();
                  ajax.open('GET', url, true);
                
    因為每個請求都有不同且唯一的 URL,所以瀏覽器不會使用前面快取的版本,而是再次發起請求。
此處將快取作為偵錯問題來介紹,是因為快取在開發階段容易使人犯錯,但針對大部分情況而言,快取是一種有價值的工具,同時能在客戶端和 Web 伺服端改善性能,不應在未作適當考量的情況下停用此機制。

2處理其他資料

除了接收純文字以外,通常都是在回傳大量資訊或更複雜的資料時,則需要其他資料格式。Ajax 請求的原始資料格式是 XML,但隨著時間流轉,JSON 格式變得越來越流行,因為相較下更容易使用。

XML


XML 格式與 HTML 相似,都是使用包圍值的標記來定義內容。XML 總是有一個圍繞整個資料集的根元素,和 HTML 一樣,XML 標記也可以擁有附加資訊的屬性,但與 HTML 不同的是,XML 標記完全由每個人自行編寫:
        <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
        
          (Re-)Introducing JavaScript
          JavaScript in Action
        
      
建立一個 XML 檔,檔案必須以 XML 宣告,清楚表明該檔包含的是 XML 檔案,然後包含某些 XML 的文字檔案,將其命名為 test.xml
必須確保 XML 有正確格式:所有的開始標記都有對應的結束標記,所有標記的巢狀關係正常,且標記應該使用全小寫字母。

以 XMLHttpRequest 物件擷取 XML 時,返回的資料將自動在 JavaScript 呈現為一棵 DOM 樹。如果 Ajax 請求取得上述的 XML 資料,第一步是先獲得回應的參照:
        var data = ajax.responseXML;
      

由此開始,可以透過 documentElement 存取根元素,然後用 firstChild、nextSibling、childNodes 和其它屬性來瀏覽資料,XML 資料的一個特點是兩個標記之間的空格被視為一個空的文字節點(在前述 XML 中,book 有三個子節點),而且為了獲得節點的內容,必須使用 nodeValue,導致最終的結果可能很冗長:
        documentElement.firstChild.nextSibling.firstChild.nodeValue
      

當 XML 有許多具有相同標記名稱的記錄時,最簡單獲取全部記錄的方式是透過 getElementByTagName() 方法:
        var chapters = data.getElementsByTagName('chapter');
        for (var i = 0, count = chapters.length; i < count; i++){   //遍覽記錄
          //為取得某個屬性值,使用 getAttribute() 方法,chapters[i].getAttribute('id')
          //再以 firstChild.nodeValue 抓取單獨標記的值,chapters[i].firstChild.nodeValue
        }
      

JSON


雖然 XML 曾經是呈現資料的標準格式,但是並非沒有缺點:1.XML 格式比較笨重,2.解析 XML 資料雖不困難,但還是得採用複合結構,例如 chapters[i].firstChild.nodeName,XML 對於 JavaScript 來說不是原生格式,所以性能也可能是個問題。

JavaSciprt 創始人之一 Douglas Crockford 體會到,JavaScript 已經有一種表示資料的好格式,便是 JavaScript 物件標記法(JavaScript's Object Notation,JSON),它既簡潔,又容易透過 JavaScirpt 瀏覽,前面的 XML 可以替換成如下的 JavaScript 物件:
        var chapters = {
          1: {
            title: '(Re-)Introducing JavaScript'
          },
          2: {
            title: 'JavaScript in Action'
          }
        }
      
利用物件標記法,就可以簡單地使用 chapters[1].title 取得 (Re-)Introducing JavaScript 的內容。

理論上,如果沒有將同樣的資料複製給變數,而是放在一個文字檔案中,那麼該檔就是 JSON 格式:
        {
          "1": {"title": "(Re-)Introducing JavaScript"},
          "2": {"title": "JavaScript in Action"}
        }
      
建立一個 JSON 檔,該檔需包含像上述的 JSON 資料,然後將其命名為 test.json
JSON 建議我們用雙引號標記所有的屬性和值

Ajax 只需要一個步驟就能使用 JSON 資料:將其解析為 JavaScript 程式碼
首先從 Ajax 物件的 responseText 屬性取得資料(沒有 responseJSON 屬性):
        var data = ajax.responseText;
      

文字乃透過 eval() 方法轉換為 JavaScript 物件,相關的程式碼如下:
        var data = eval('(' + data ')');
      
為了清楚表明意圖,必須以括號包圍資料,但是使用 eval() 函數不是件好事,因為這個函數將字串當作 JavaScript 程式碼執行,可能會引入重大的安全漏洞,因此 Crockford 帶來更好的解決方案:JSON 程式庫,它能夠將 JSON 資料轉換為可用的 JavaScript 物件,同時讓任何資料仍能順利通過 eval()
最終,這個 JSON 程式庫併入瀏覽器中,現在就能以 JSON.parse() 代替 eval()
        var data = JSON.parse(data);
        //使用 data[1].title
      

如今 JSON 物件及其 parse() 方法已經廣獲大部份瀏覽器中多版本的支援,遺憾的是,IE 8 之後才加入該物件,所以針對舊版的 IE,首先必須含括 JSON 解析程式庫(如 Crockford 更新的 json2.js,可在 www.json.org 上找到),將程式庫複製到 JavaScript 目錄後,就可以在頁面中含括它:
        <script src="js/json2.js"><script>
      
這個程式庫的傑出特性之一是:它僅在 JSON 物件不存在的情況下定義一個新物件,若有需要,也可以在相同的情況下透過 JavaScript 動態載入該程式庫:
        if(typeof JSON == 'undefined'){
          var script = document.createElement('script');
          script.src = 'js/json2.js';
          document.getElementsByTagName('head')[0].appendChild(script);
        }
      
        
傳送 XML 或 JSON 至伺服器
當用 XML 或 JSON 等資料送給伺服器,首先必須向伺服器表明正確的內容類型:
          ajax.setRequestHeader('Content-Type', 'text/xml');
          //or
          ajax.setRequestHeader('Content-Type', 'application/json');
        
接著以正確的格式產生資料。方法是透過建立 XML 資料字串或者元素和節點,以便產生 XML 資料。
後者建立的 XML 更可靠,但是需要更多精力。
如果希望將資料轉換為 JSON 格式,可藉助 JSON.stringify() 方法:
          data = JSON.stringify(data);
        
這個方法由瀏覽器定義,也存在於 json2.js 程式庫。
在任何情況下,資料必須符合伺服端資源能夠理解和預期的格式。

3伺服端腳本

現實世界中,一般均採用更動態的伺服端資源,像是 PHP 腳本,以下將解釋如何編寫伺服端的 PHP,以便返回純文字、XML 或 JSON 等。在所有情況下,最重要的都是:PHP 腳本只能由 JavaScript 存取,所以不應該包含或輸出任何的 HTML(除非腳本將 HTML 當作文字回應的一部份返回),而且繫結 PHP 腳本與 JavaScript 之前要記得測試,確保其正常運作。

返回純文字


從 PHP 腳本返回純文字很簡單,只要讓 PHP 輸出回傳給 JavaScript 的文字即可:
        <?php
          echo 'This is some text being printed';
        ?>
      
通常,PHP 腳本將應用一些邏輯和資料庫的呼叫確定返回的文字,例如,下列文字足以說明使用者名稱的可用性:
        <?php
          if(/* username is available */){
            echo 'AVAILABLE';
          }else{
            echo 'UNAVAILABLE';
          }
        ?>
      
然後 JavaScript 程式碼變能查看 ajax.responseText 等於 AVAILABLE 或 UNAVAILABLE(在這種情況下,一般是以類似常數名稱的全大寫字母返回狀態指示)。

儘管純文字最適用於有限數量的資料,但也可以利用不含在資料中的特殊字元(例如管線符號)分隔多個資料片段:
        <?php
          echo "{row['first_name']}|{row['last_name']}|{row['email']}";
          //驗證此頁接收到員工的 ID,然後從資料庫得到這個員工的資料並回傳
        ?>
      
在 JavaScript 中,可以根據相同的字元分隔文字,以便存取各個單獨的部分:
        var employee = ajax.responseText.split('|');
        //使用 employee[0], employee[1], employee[2]
      

返回 XML


以 PHP 腳本返回 XML 並不困難,首先,腳本必須送出一個 Content-Type 標頭,表示接下來的是 XML 資料:
        <?php
          header('Content-Type: text/xml');                                       //標頭
          echo '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>';        //XML 文件宣告
          echo '';                                                      //所有 XML 都有的根元素
          $q = 'SELECT comment, email, date_submitted FROM comments ORDER BY date_submitted DESC';
          $r = mysqli_query($dbc, $q);
          while ($row = mysqli_fetch_array($r)) {                                 //由 while 迴圈中讀取記錄
            echo "
                    {$row['comment']}
                    {$row['email']}
                    {$row['date']}
                  \n";
          }
          echo '';                                                      //關閉根元素
        ?>
      
version 屬性值為 1 是有效的,但宣告中指出的編碼方式應與 PHP 腳本(建立此腳本的應用程式)所使用的實際編碼相互呼應,基於技術原因,建議利用上述的 PHP 腳本輸出宣告,而不是在腳本中固定寫死這些文字。

返回 JSON


為了從 PHP 腳本返回 JSON,必須先送出相關的 Content-Type 標頭,然後 PHP 腳本輸出 JSON 資料,因難以準確建立 JSON 語法,最簡單的解決方案是建構一個資料陣列,然後轉換為 JSON:
        <?php
          header('Content-Type: application/json');                                       //標頭
          $date = array();
          $q = 'SELECT comment, email, date_submitted FROM comments ORDER BY date_submitted DESC';
          $r = mysqli_query($dbc, $q);
          while ($row = mysqli_fetch_array($r)) {
            $data[] = $row;
          }
          echo json_encode($data);
        ?>
      
從 PHP 5.2 版開始,json_encode() 函數已經成為 PHP 的一部份,如果使用的是舊版 PHP,就必須安裝 JSON 程式庫代替,來自 PECL(http://pecl.php.net)的 JSON 程式庫現已內建於 PHP 中,Zend Framework 也有自己的 JSON 程式庫(http://framework.zend.com),此外還有其他的一些程式庫。在任何情況下,不管採用哪種 JSON 程式庫,都可以將資料轉換為 JSON 並且輸出。

4Ajax 範例

顯示進度


Ajax 的處理必須驗證許多資料,然後傳給伺服器,伺服器也可能返回許多資料,客戶端必須處理大量的 DOM,因此 Ajax 在此一過程可能花費較長的時間,若能為使用者指出發生的情況是一個好主意,尤其是在此期間不希望他進行任何操作的情況下(無論 CSS 或是 DOM 操作的延遲,都容易指出發生的情況)。
首先,應該先確定要採用圖形、文字還是二者,如果需要,甚至可以使用強制回應視窗,接著有兩種選擇: 假設已在網頁上新增了一個動畫 GIF 檔:
        <img src="images/ajax-loader.gif" id="loader">
      
下列 CSS 則隱藏圖形:
        #loader { visibility: hidden; }
      
Ajax 的處理從顯示該圖形開始:
        loader.style.visibility = 'visible';
        ajax.open('GET', 'http://www.example.com/somepage.php', true);
        ajax.send(null);
      
如果資料在請求發出之前進行驗證,便可在驗證程序之前顯示這個動畫 GIF。
於處理 readyState 變化的函數中收到和處理回應之前,則再次隱藏該圖形:
        loader.style.visibility = 'hidden';
      
在自己的電腦上執行這些程式碼或者簡單的展示時,動畫可能只出現極短的時間,為了得到更完整的效果,建議採用計時器延遲 Ajax 的呼叫:
        var ajaxTimer = setTimeout(function(){
          ajax.open('GET', 'http://www.example.com/somepage.php', true);
          ajax.send(null);
        }, 1000);       //等待一秒
        //此處的延遲只是展示用途,實際網站不會用計時器來延遲請求
      
到目前為止,我們已經了解 Ajax 的相關事件只有一個: readyState 值的變化,此外還可以監視如下四個事件: progress 事件在 Ajax 發起請求時觸發,根據完成請求所花費的時間,該事件將被觸發一次或多次,在事件處理器中,事件物件的 total 屬性反應傳輸的位元組數,loaded 屬性代表已經接收的位元組數,因此我們可以使用這兩個屬性確定發生的交易數量:
        ajax.onprogress = function (e){
          if (typeof e == 'undefined')  e = window.event;
          var pct = e.loaded/e.total;
        }
      
Ajax 請求的發生速度通常非常快,監控 progress 事件僅對冗長的請求有意義。load 事件等同於 readyState 變為 4,其他兩個事件可能,也可能不發生,但皆可用來處理錯誤,或者在慢速的請求中止時進行不同的動作。這些處理器和 readystatechange 處理器一樣,必須在呼叫 open() 方法之前指定。
        
發送檔案
Ajax 其實也能夠上傳檔案,解決方案之一是利用 FormData 物件,但 FormData 物件並沒有受到所有瀏覽器支援,因此最常見的解決方法是使用隱藏的 iframe。

登入表單


          function validateForm() {
            'use strict';
            var email = document.getElementById('email');
            var password = document.getElementById('password');

            if ( (email.value.length > 0) && (password.value.length > 0) ) {  //驗證帳號密碼值
            	var ajax = getXMLHttpRequestObject();                         //建立 Ajax 物件

              //指定結果處理器 -> 建立 readyState 變化的處理函數
              ajax.onreadystatechange = function() {
                if (ajax.readyState == 4) {                                   //當讀取 Ajax 資料「完成」時
                  //讀取狀態為正常時
                  if ( (ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304) ) {
                    if (ajax.responseText == 'VALID'){                        //回報結果登入資料的結果
                      alert('您已經登入!');
                    } else {
                      alert('您的帳號密碼有誤!');
                    }
                  } else {
                    //當無法使用 Ajax 時(伺服器回應不正常),表單必須確實提交給伺服器腳本處理
                    document.getElementById('theForm').submit();
                  }
                }
              };

              //發送請求,傳遞資料,發送 Content-Type 標頭,讓後續資料能得到正確處理(使用 POST 傳遞資料必備)
              ajax.open('POST', '../resources/login.php', true);
              ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
              //以「名稱 = 值」配對字串的形式建立資料,並透過 encodeURIComponent() 邊碼其內容以保證安全
              var data = 'email=' + encodeURIComponent(email.value) + '&password=' + encodeURIComponent(password.value);
              ajax.send(data);
              return false;
            } else {
              alert('請完整填寫表單!');
              return false;
            }
          }

          function init() {
              'use strict';
              if (document && document.getElementById) {
                  var loginForm = document.getElementById('loginForm');
                  loginForm.onsubmit = validateForm;
              }
          }

          window.onload = init;
        

聯絡表單


          //定義 handleAjaxResponse() 函數
          function handleAjaxResponse(e) {
            'use strict';
            if (typeof e == 'undefined') var e = window.event;
            var ajax = e.target || e.srcElement;

            if (ajax.readyState == 4) {                       //當讀取 Ajax 資料「完成」時
              if ( (ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304) ) {  //讀取狀態為正常時
                //回報結果傳送資料的結果 -> 覆蓋表單
                document.getElementById('contactForm').innerHTML = ajax.responseText;
              } else {
                //當無法使用 Ajax 時(伺服器回應不正常),表單必須確實提交給伺服器腳本處理
                document.getElementById('theForm').submit();
              }
              ajax = null;                                    //無論何種情況,都清除 Ajax 物件
            }
          }

          window.onload = function() {
            'use strict';
          	var ajax = getXMLHttpRequestObject();            //建立 Ajax 物件
          	ajax.onreadystatechange = handleAjaxResponse;    //指定結果處理器 -> 呼叫 handleAjaxResponse() 函數
            document.getElementById('theForm').onsubmit = function() {
              //利用陣列讀取表單所有欄位,並透過迴圈將資料存放在 data,然後傳送給伺服器
          		var fields = ['name', 'email', 'comments'];
          		var data = [];
          		for (var i = 0, count = fields.length; i < count; i++) {
                //元素值的格式「名稱 = 值」,並透過 encodeURIComponent() 確保資料安全
          			data.push(encodeURIComponent(fields[i]) + '=' + encodeURIComponent(document.getElementById(fields[i]).value));
          		}

              //發送請求,傳遞資料,發送 Content-Type 標頭,讓後續資料能得到正確處理(使用 POST 傳遞資料必備)
          		ajax.open('POST', '../resources/contact.php', true);
          		ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
          		ajax.send(data.join('&'));                   //將 data 三組資料用「&」隔開
          		return false;
          	};
          };
        
        
維護狀態
Ajax 將預先載入內容,並於使用者點擊連結時切換,但瀏覽器並不會維護本身的「狀態」(也就是實際顯示的內容)變化,像是呈現內容的第二頁無法存入書籤、共用或者調校等,而且此系統沒有破壞瀏覽器的預設行為:點擊「後退」和「前進」按鈕不會改變使用者看到的內容。
對此解決之道是使用 URL 的 hash 部分,為每種狀態建立唯一的 URL,例如:http://www.example.com/page.html#model 和 http://www.example.com/page.html#view,然後 JavaScript 必須監視 URL hash 部分的變化,以便改變顯示的內容,為此可以設定一個計時器,在一定時間間隔(可能是 1 秒或 2 秒)內檢查 hash,比較目前與前一個的 hash 值是否相同。

預先載入資料


另一種不同方法操作 Ajax:預先載入使用者可能需要馬上查看的內容。
網頁載入的快慢在某種程度上取決於下載的資料量,所以包含較少資料的頁面能夠更快載入,但如果有些內容不需要立刻查看,不過或許很快就需要,那麼就適合以 Ajax 載入這些內容,代表性的例子有選項卡、摺疊控制項、HTML 表格中的內容、滾動頁面時顯示的內容等。
也就是說,可以先下載頁面上半部份的元素,隨後再下載更多的內容:
          window.onload = function() {
            'use strict';
          	var pages = ['model', 'view', 'controller'];       //按照頁面順序存放於陣列
          	var counter = 0;                                   //計數器初始化為 0,用來追蹤頁面

          	var ajax = getXMLHttpRequestObject();              //建立 Ajax 物件
            //加上 id 值表示請求的內容,本系統採用同步請求,因為在載入新內容之前,點擊連結毫無意義
          	ajax.open('GET', '../resources/content.php?id=' + pages[counter], false);
          	ajax.send(null);

            //擷取資料,由於為同步請求,所以立即讀取資料,而不使用 readyState 函數
            //呼叫方法後面必須加上 [0]:第一個項目的陣列標記法
            //為取出實際值,必須再次使用 firstChild(指向文字節點)以及 nodeValue
          	var title = ajax.responseXML.getElementsByTagName('title')[0].firstChild.nodeValue;
          	var content = ajax.responseXML.getElementsByTagName('content')[0].firstChild.nodeValue;

            //建立連結點擊事件處理器
          	var nextLink = document.getElementById('nextLink');
          	nextLink.onclick = function() {
              //使用新內容更新頁面
          		document.getElementById('title').innerHTML = title;
          		document.getElementById('content').innerHTML = content;
              //遞增並檢查計數器
              counter++;
              if (counter == 3) {   //當計數器等於 3 時 -> 刪除連結,同時清除 Ajax 物件
              	nextLink.parentNode.removeChild(nextLink);
              	ajax = null;
              } else {              //當計數器不等於 3 時 -> 請求下一部份的內容
              	ajax.open('GET', '../resources/content.php?id=' + pages[counter], false);
              	ajax.send(null);
              	title = ajax.responseXML.getElementsByTagName('title')[0].firstChild.nodeValue;
              	content = ajax.responseXML.getElementsByTagName('content')[0].firstChild.nodeValue;
              }
          		return false;         //避免真正請求連結頁面
          	};

          };
        
        
預先載入圖形
如果打算預先載入圖形,可採用更簡單的非 Ajax 解決方案:只需建立一個新的 image 元素,並於 src 指定下載的檔案:
          var temp = document.createElement('image');
          temp.src = 'images/someimage.png';
        
執行上述程式碼之後,瀏覽器立刻載入 someimage.png,因為這段程式可能在載入頁面之後執行,因此載入新圖形的過程不會妨礙原始內容的讀取,然後可以在必要時「換入」新的圖形──新增到頁面,為新圖形連接一個載入事件處理器,便能於載入圖形時做出反應。

使用計時器的股票報價


顯示股票報價的頁面,此處將一個 Google 頁面返回 JSON 格式的股票報價資料,不過由於跨網域策略限制,Ajax 無法直接存取 Google 頁面,這種情況的解決方案是用 JavaScript 連結同一網域的 PHP 腳本,而 PHP 腳本則以 URL 存取公用程式(cURL)讀取 Google 頁面,PHP 和 cURL 沒有跨網域限制,Google 頁面以 JSON 格式返回資料,PHP 腳本則以相同格式比照辦理。
為了在不與任何使用者互動的情況下自動更新股票報價,這裡採用一個計時器,每隔一分鐘呼叫 PHP 腳本:
          window.onload = function() {
            'use strict';
            var ajax = getXMLHttpRequestObject();               //建立 Ajax 物件

            ajax.onreadystatechange = function() {
              if (ajax.readyState == 4) {                       //當讀取 Ajax 資料「完成」時
                if ( (ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304) ) {   //讀取狀態為正常時
                  var data = JSON.parse(ajax.responseText);     //先解析 JSON 資料,使其成為可用的物件
                  var output = document.getElementById('quote');
                  //Google 頁面返回一個物件陣列:每個物件用於請求的一支股票
                  //取得第一個物件的參照──data[0],顯示股票最新價格的具體屬性是小寫的 1
                  if (output.textContent !== undefined) {
                    output.textContent = data[0].l;
                  } else {
                    output.innerText = data[0].l;
                  }
                }
              }
          	};

            //送出 Ajax 請求
          	ajax.open('GET', '../resources/quote.php', true);
          	ajax.send(null);

            //每隔一分鐘再由該函數重新送出 Ajax 請求
          	var stockTimer = setInterval(function() {
          	  ajax.open('GET', '../resources/quote.php', true);
          		ajax.send(null);
          	}, 60000);

          };
        
        
跨網域
瀏覽器本身會阻止 JavaScript 從其他網域讀取資料,此舉能夠保護終端使用者──這始終是首要的目標,但是如果必須讀取另一個網域上可靠的來源資料,倒有一些選擇:
  • 採用的代理腳本:PHP 腳本作為客戶端的 JavaScript 和其他網域資料之間的代理者,這種解法很常見,不過請注意,它破壞了瀏覽器的安全手段,並為伺服器帶來壓力,回傳資料給 JavaScript 之前,最謹慎的做法是由 PHP 腳本盡可能徹底地驗證資料。
  • 在客戶端使用 iframe,頁面則包含源自另一個網域的 iframe
  • 採用所謂的 JSON-P 技術(JSON with Padding 的縮寫,www.json-p.org),雖然 JavaScript 請求另一個網域的資源受到限制,但瀏覽器允許腳本標記引用另一個網域,為了善用此功能,腳本標籤只需載入一個包含 JSON 的資源即可,然而 JSON 無法當成腳本區段的根元素(也就是說,純 JSON 沒有可執行的程式),因應之道是將函數呼叫關聯至請求,當返回 JSON 資料時,隨即呼叫以下函數:
                  <script src="http://other.example.edu/resource?jsonp=parse"><script>
                
上述 parse() 函數定義於 JavaScript 中,一旦伺服器回應時,JSON 資料將立即送往程式碼的 parse() 函數,該函數在必要時解析和處理資料,並進行所有必要的錯誤處理。
跨源頭資源分享(Cross-Origin Resource Sharing,CORS)是正在開發的另一種備選方案,這是提議的 XMLHttpRequest 物件擴充功能,允許在適當情況下進行跨網域請求。
DEMO MEMO