******** 向伺服器取得多項資料─使用 XML ********

一般的 Web Application,後端程式通常只回傳一項資料,
例如回傳帶有 HTML 的文字檔案,用來更新前端瀏覽器的畫面;
使用 Ajax,通常只傳回純文字資料(不帶 HTML),
再透過 DOM 操作將資料更新到畫面中的某一個區塊內。

如果前端想取得多項資料時該怎麼辦?由於回傳資料通常是文字檔案,
所以一般會想到使用 CSV(Comma Separated Values)之類的作法,
以逗號(,)或分號(;)或 or 號(|)之類的特殊符號表示資料分隔處,
前端再用 String.split(String separator) 將資料切割存成陣列
可是會遇到一個問題,就是資料內如果有這些分隔符號時該怎麼處理?
另外,要處理這些資料,後端跟前端的設計人員必須事先溝通好,
否則會搞不清楚字串切割成陣列後,陣列的第幾項資料代表的是什麼。

解決辦法之一──除了文字資料外,後端可以傳回 XML 形態的資料;
將資料包在 XML 中回傳給前端瀏覽器,
前端再使用 DOM 的操作方式取出 XML 內的個別資料就可以了。
例如後端要傳回個人資料的姓名、年齡及電話,XML 可以產生如下:


<?xml version="1.0" encoding="utf-8"?>
<profile>
  <name>王大明</name>
  <age>28</age>
  <phone>0212345678</phone>
</profile>

後端在輸出資料之前,必須先在 header 中指定 Content-Typetext/xml
例如 PHP:header(“Content-Type: text/xml”);
Servlet:res.setContentType(“text/xml”);
或者 res.setHeader(“Content-Type”,”text/xml”);
前端接收資料時,使用 request.responseXML,將收到的資料化成 DOM Tree,
接著就可以使用 DOM 的操作方式取出節點中的資料:

  1. 使用 getElementsByTagName 取出某個 Node 陣列
    以上例 <profile> 來說,取法為 getElementsByTagName(“profile”)
    如果要取得 <name>,則是 getElementsByTagName(“name”)
  2. 使用 childNodes 來取得子節點陣列。Tag 內的純文字資料也算是子節點。
    以上例 <name> 來說,要取得該節點,除了上述方法外,也可以用
    getElementsByTagName(“profile”)[0].childNodes[0] 或者
    getElementsByTagName(“profile”)[0].firstChild 取得。
  3. 使用 nodeValue 取得 Tag 內的純文字資料。
    以上例 <name> 來說,要取得該 Tag 內的資料,可以用
    getElementsByTagName(“name”)[0].childNodes[0].nodeValue 或者
    getElementsByTagName(“profile”)[0].childNodes[0].childNodes[0].nodeValue

基本上使用 getElementsByTagName、childNodes、nodeValue 取得 XML 資料,
而後端必須要產生格式良好(Well-Formed)的 XML,前端才有辦法正確接收資料,
經過測試,有幾個地方如果沒有標示正確,前端會收不到資料:

  • XML 文件開頭的宣告 <?xml version=”1.0″ encoding=”utf-8″?>
    如果沒有宣告這一行,前端可能會收不到正確的資料,而取得 null
    如果後端使用 PHP,由於 ?> 也是 PHP 區段的結尾,如果沒特別處理好,
    則 PHP 程式可能會在 XML 宣告後結束,因此前端可能會出現難以 debug 的錯誤。
  • 如果是同名的多數資料,最好前面加上一個父標籤包住,例如:

    <books>
    <book>Servlet</book>
    <book>Ruby</book>
    <book>PHP</book>
    </books>

    如果這個 XML 文件沒有加上 <books> 包起同名的 <book>,則前端會收不到資料。

另外,由於 XML 並沒有固定 id 這個屬性,
故無法使用 getElementById 方法來取得 Element 物件,
(使用 IE 會出現找不到 getElementById method 的錯誤訊息)
還是只能使用 getElementsByTagName method 來取出資料。

總結 XML 使用,可以依照以下步驟操作:

  1. 後端建立 Well-Formed XML 文件,將要回傳的多項資料包成 XML 文字檔案
  2. 後端輸出資料前,在 header 宣告此輸出資料的 Content-Typetext/xml
  3. 後端輸出資料。
  4. 前端用 request.responseXML 取得後端傳來的 XML 資料,並轉成 DOM Tree 物件。
  5. 前端使用 DOM Tree 物件的 getElementsByTagName 取得節點物件的陣列。
  6. 前端使用 DOM Tree 節點物件的 childNodes 取得子節點陣列。
  7. 前端使用 DOM Tree 節點物件的 nodeValue 取得 Tag 內的純文字資料。
  8. 若是節點操作不正確就無法取得正確的資料。

=== DOM Tree 物件測試心得 ===

由於 request.responseXML 是將傳回的 XML 檔案轉成 DOM Tree 物件,
因此嘗試在後端建立 HTML 檔案,並將 header 指定成 text/xml 文件格式後傳回,
前端用 request.responseXML 接收之後,原本以為已經取得 HTML 資料,
想透過 DOM 操作取出 Node 之後,再用 appendChild 將其加入某 <div> 中,
結果出現「不支援此種介面」之錯誤訊息。測試方式如下:

  • 測試在後端將 XML 或 HTML 文件的 Content-Type 設定成 text/html 後送出,
    結果前端用 request.responseXML 收到的 DOM Tree 是 null
  • 測試將簡單的一行「<word>Test</word>」文字插入網頁區塊中:
    測試在前端建立上述 XML,透過 document.createElement 建立 Element 後,
    再用 document.createTextNode 建立純文字內容後加入 Element 中,
    最後再 appendChild(或 replaceChild)到某個 <div> 中,結果是可行的;
    程式碼如下:

    var word=document.createElement(“word”);
    var txt=document.createTextNode(“Test”);
    word.appendChild(txt);
    var blk=document.getElementById(“blk”);
    blk.replaceChild(word,blk.childNodes[0]);

    可是同樣的 XML 內容,若是透過 request.responseXML 接收,
    則無法加到(appendChild 或 replaceChild)網頁區塊內,
    會出現「不支援此種介面」之錯誤訊息。

因此得到的結論是─後端傳過來的 HTML 或 XML 文字資料即使化成 DOM Tree 物件,
也無法直接將 Node 附加到網頁區塊內。
另外,使用 document.createTextNode 所建立的資料會變成純文字
如果要產生真正的 HTML Tag,就要使用 document.createElement 建立,
否則就連簡單的 <br /> 用 createTextNode 建立的話,
也會呈現純文字而不會進行換行動作。
如果後端回傳的是巢狀的 HTML 資料,更是要費上一番工夫才能轉成 HTML Tag,
還是使用 innerHTML 會比較方便,雖然 innerHTML 不是標準功能‧‧‧