邊做邊學 jQuery 系列 14- 呼叫總部-jQuery.ajax()

邊做邊學 jQuery 系列 14- 呼叫總部-jQuery.ajax() 教學影片

> [!VIDEO https://www.microsoft.com/zh-tw/videoplayer/embed/37a434ef-8574-47d0-abe6-a0fe98689809]

 

到目前為止,我們已能用jQuery自由自在地操弄網頁元素,但如果網頁只能活在自己的小框框裡,不能即時與伺服器端溝通取得資訊或更新資料,就不能稱為AJAX化的設計。AJAX代表"Asynchronous Javascript And Xml",其中的Asynchronous意味著不採行傳統Postback整個網頁表單送回伺服器的做法,而是透過XmlHttpRequest物件與伺服器溝通,再以Javascript解析伺服器端所傳回的資料。這種做法,避免了傳統Postback期間,網頁會歷經消失、等待、重新建立的過程,給予使用者更流暢的操作感受。

jQuery對與後端溝通的相關作業提供了一系統的函數,其中最基層的函數叫做jQuery.ajax()。如同animate()是彈性最大的基本函數,針對常見的應用方式,則另外又衍生出slideDown/fadeIn/show等簡便函數。jQuery.ajax()也衍生了一系列的常用函數,如jQuery.get(), jQuery.post(), jQuery.getScript(), jQuery.getJSON(),能更簡便地滿足常見的需求,只有需要進一步控制溝通細節時,才需回頭使用jQuery.ajax()。

首先介紹get(), post(), getScript()與getJSON()這一系列的函數,接收的參數形式很相似,可用的參數有url, data, callback及type。

url是要連線的伺服器程式網址,由於瀏覽器預設並不允許XmlHttpRequest物件跨網域呼叫另一台伺服器上的程式(主要基於資安考量),因此url的網址多半與網頁在同一主機,寫成相對網址即可。唯一可以突破跨網域限制的是getScript()(或get()時指定type="script"),當發現要存取的網址不在同一台主機時,它會透過在<head>新增<script src="...">的技巧載入Javascript,達成執行遠端網站Javascript程式的效果。

data是發出GET或POST請求時要傳遞的參數,此處可以直接給字串,則字串會被附加在url後方或當成POST內容;或者我們也可以傳入匿名物件,則物件屬性名稱及屬性值會被轉換成字串,例如: { a:1, b:2, c:[3,4] }會變成a=1&b=2&c=3&c=4。getScript()不支援data參數,如有需要指定時,請自行在URL中以QueryString方式表示。

callback是呼叫成功時要執行的事件函數,其中會傳入兩個參數: data及textStatus,其中data是伺服器回傳的結果,視指定的型別不同,可能是字串、XML物件、JSON轉換成的物件等;而textStatus則是狀態字串,正常情況下會傳回success,若資料型別為XML物件而解析XML失敗時則可能得到parsererror。callback只有在傳輸成功時才會被呼叫,若發生錯誤時,並不會產生任何例外,如果要掌握錯誤時的處理,則要回歸改用jQuery.ajax()才能做進階控制。

另外,get()與post()則還多了第四個參數type,type是一個字串,可以為xml, html, script, json, jsonp, text其中之一,type會決定傳回結果的解析方式。

接下來我們先一一介紹get(), post(), getScript()與getJSON()的使用,最後再介紹ajax()。

【基本應用】

簡單來說,get()就是透過XmlHttpRequest物件,發出一個GET請求,取回網頁執行結果。post()的原理與用途完全相同,只差別在發出的是POST請求。

為了檢視傳送結果,我們先寫一個簡單的ASPX來承接我們的請求:

<%@ Page Language="C#" %>
<script runat="server">
    protected void Page_Load(object sender, EventArgs e) 
    {
        Response.ContentType = "text/plain";
        Response.Write("Method: " + Request.HttpMethod + "\n");
        Response.Write("QueryString: " + Request.Url.Query + "\n");
        Response.Write("PostData: ");
        foreach (string key in Request.Form.Keys)
            Response.Write("\n" + key + "->" + Request.Form[key]);
        Response.End();
    }
</script>

ReturnString.aspx將HttpMethod(GET或POST)、QueryString及POST的內容簡單地以純文字顯示出來,讓我們可以用一小段Javascript驗證傳送的結果:

function showResult(data, textStatus) { 
    alert(data); 
}
$.get("../ReturnString.aspx", "a=1&b=2", function(d) { alert(d); });
$.get("../ReturnString.aspx", {a:1, b:2, c:[3,4]}, showResult);
$.post("../ReturnString.aspx", "a=1&b=2", showResult);
$.post("../ReturnString.aspx", {a:1, b:2, c:[3,4]}, showResult);

如下圖,我們在Mini jQuery Lab裡執行,可以由alert看到四次執行的效果:

先前提到get()及post()時可以指定type,適用於伺服器會傳回特定資料型別時。為了驗證指定type的效果,我們再寫另一個ReturnDiffType.aspx:

<%@ Page Language="C#" %>
<script runat="server">
    public class Class4Json 
    {
        public string Name;
        public int Rating;
    }
    
    protected void Page_Load(object sender, EventArgs e) 
    {
        if (Request["t"] == "xml")
        {
            System.Xml.XmlDocument xd = new System.Xml.XmlDocument();
            xd.LoadXml("<Data><Item>AJAX</Item></Data>");
            Response.ContentType = "text/xml";
            Response.Write(xd.OuterXml);
        }
        else if (Request["t"] == "json")
        {
            System.Web.Script.Serialization.JavaScriptSerializer jss =
                new System.Web.Script.Serialization.JavaScriptSerializer();
            Class4Json darkthread = new Class4Json();
            darkthread.Name = "Darkthread";
            darkthread.Rating = 99;
            Response.ContentType = "text/plain";
            Response.Write(jss.Serialize(darkthread));
        }
        else if (Request["t"] == "html")
        {
            Response.ContentType = "text/html";
            Response.Write("<span style='color:red'>Hello World!</span>");
        }
        else if (Request["t"] == "script")
        {
            Response.ContentType = "text/javascript";
            Response.Write("alert('Hello World!');");
        }                
        Response.End();
    }
</script>

這個ASP.NET網頁提供四種傳回內容,分別是XML、JSON字串、HTML以及Javascript程式。比較有趣的是JSON部分(第20-26列),它會將.NET裡的Class轉成一個JSON字串:

{"Name":"Darkthread","Rating":99}

而這個字串會在Javascript端還原成相對的物件,換句話說,在.NET裡有Name及Rating兩個屬性的darkthread物件,在Javascript中也是一個物件,而且一樣具備.Name、.Rating兩個屬性值。如此將提高Server與Client間資料傳遞程式的可讀性,與以往用XML格式包裝複雜資料相比,更為輕巧方便。

接著我們寫一小段程式來測試不同內容類別的處理,其中提到另一個新函數load()跟AJAX也有點相關,但作用的對象是jQuery物件,我們可以用它從伺服器端程式動態取得HTML內容,變更jQuery物件的html()。

//XML時解析成XMLDOM,如傳回時已宣告ContentType=text/xml,type="xml"可省略
$.get("../ReturnDiffType.aspx?t=xml", {}, function(x) { alert($(x).find("Item").text()); }, "xml");
//JSON時直接解析成物件
$.get("../ReturnDiffType.aspx", {t:"json"}, function(o) { alert(o.Name + "->" + o.Rating); }, "json");
//上述寫法也可寫成以下形式
$.getJSON("../ReturnDiffType.aspx", {t:"json"}, function(o) { alert(o.Name + "->" + o.Rating); });
//Script時直接執行
$.get("../ReturnDiffType.aspx", {t:"script"}, null, "script");
//上述寫法較冗長,故多寫成以下形式
$.getScript("../ReturnDiffType.aspx?t=script", null);
//將HTML內容載入<div id="x"></div>中
$("#x").load("../ReturnDiffType.aspx", {t:"html"}, function(d) { alert('Loaded!'); });

由上述程式我們不難發現,getScript()、getJSON()其實只是特定get()呼叫的簡化寫法而已,更進一步,get()、post()、getJSON()、getScript()也只是特殊ajax()呼叫的簡化寫法,當我們需要做更進階控制時,就要回頭使用ajax()。

【jQuery.ajax()】

jQuery.ajax(options)與前述的簡化型API不同,只有一個參數options,但options有眾多屬性,足以進行精細的控制:

async設定false時,程式會等到傳輸結束才繼續執行,但等待期間瀏覽器會卡住不動,這就是AJAX的第一個A,Asynchronous(非同步),所要避免的情境,因此我們很少去更動它,多半就採用預設值true,以免變成黑心AJAX。beforeSend開始傳送前的呼叫事件,可由this取存完整的options參數,可進行一些前置轉換或檢查,萬一發現不妥,還可return false阻止傳送。cache是否接受被Cache下來的讀內容,script/json時預設為false,其餘為true。complete呼叫完成後執行的函數(在sucess或error之後執行),有XMLHttpRequest與textStatus兩個參數可用。contentType預設為application/x-www-form-urlencoded,原則上不需要修改。data要傳送的文字內容,用法已在先前說明過。dataFilter可對XMLHttpRequest接收的原始資料進行前置處理,函數會接入data及type兩個參數,將data處理過再return回去即可。dataType為字串參數,可設為xml, html, script, json, text或jsonp等。jsonp的用法稍後再做說明errorXMLHttpRequest發生錯誤時呼叫的處理函數,共有XMLHttpRequest, textStatus及errorThrown三個參數。textStatus可能的值有timeout, error, notmodified或parsererror等,若為XMLHttpRequest.send()引發的錯誤,例外物件會放在errornThrown中。global是否要啟用共用AJAX處理函數,如: ajaxStart, ajaxStop, ajaxError... 等。預設為true。ifModified設為true時,只有伺服器傳回結果有變更(以Last-Modified標頭為準)時才算成功。預設為false。jsonp當dataType為jsonp時,這個函數會取代URL中callback=隨機函數名稱中的callback,例如: {jsonp:'onJsonPLoad'}會產生onJsonPLoad=隨機函數名稱的QueryString。password指定XMLHttpRequest連線時使用的認證密碼。processData一般來說,data中的資料會被轉換成適用於application/x-www-form-urlencoded的格式,如要傳送的是DOMDocument或其他不希望被處理轉換的資料,可將processData設為false。scriptCharset適用於dataType=script及jsonp時,可指定script的語系編碼,多用於跨不同網站呼叫時的語系調整。successXMLHttpRequest呼叫成功時執行,共有data與textStatus兩個參數。即前述get()、post()的callback。timeout指定送出請求的逾時期限(以0.001秒為單位),可壓過$.ajaxSetup()所設定的期限,適用在較耗時的請求上。type字串參數,可為GET或POST。PUT/DELETE等延伸指令也可使用,但並非所有瀏覽器都支援。username指定XMLHttpRequest連線時使用的認證帳號。xhr為一callback函數,可自行產生XMLHttpRequest物件後傳回,取代內建的XMLHttpRequest物件。

【AJAX事件】

我們可以在全網頁設定共用的AJAX事件,包含了ajaxStart(有人呼叫ajax()且沒有其他傳輸在處理中)、ajaxSend(xhr.send()前一刻)、ajaxSuccss(傳送成功)、ajaxError(傳送失敗)、ajaxComplete(傳送完成,不論成功或失敗都會執行)、ajaxStop(所有的傳送都結束時觸發)。我們用以下的範例示範如何應用ajaxSend及ajaxComplete在網頁上顯示傳送狀態,並用ajaxError及error捕捉錯誤。

...略...
<script type='text/javascript'>
    $(function() {

        $.ajaxSetup({ cache: false });
        $("#xhrStatus").ajaxSend(function() { $(this).text("傳送中,請稍候..."); });
        $("#xhrStatus").ajaxComplete(function() { $(this).text("傳送完成!"); });
        $().ajaxError(function(event, XMLHttpRequest, ajaxOptions, thrownError) {
            alert("Fail on: " + ajaxOptions.url);
        });
        $("#btnUpdate").click(function() {
            $.get("../Lab3_Delay.aspx", null, function(d) { $("#txtTime").val(d); });
        });
        $("#btnUpdateFail").click(function() {
            $.ajax({
                url: "../NOT_Lab3_Delay.aspx",
                error: function(xhr, textStatus, errorThrown) { alert(textStatus); }
            });
        });

    });
</script>
...略...
<body>
<div id="xhrStatus"></div>
<input type="button" id="btnUpdate" value="更新時間" />
<input type="button" id="btnUpdateFail" value="更新時間(誤)" />
<input type="text" id="txtTime" />
</body>

【JSONP】

我們都已看過getScript()動態載入Javascript執行,而getScript()可以克服跨網站限制是其一大特色。但在某些情境下,我們希望載入的Javascript可以回頭呼叫我們指定的函數,並將要傳遞的資料以Javascript物件參數方式傳入。例如: 有一個天氣預報的Web API,透過GetWeather.aspx?city=Taiwan的方式會傳回台灣北中南部的天氣資料,如果用Javascript物件陣列表示會像[{Area:"北部", Report:"晴時多雲 26-31度"}, {Area:"中部", Report:"多雲 24-32度"}, {Area:"南部", Report:"晴 28-34度"}]。但因為Web API位於另一個網站,$.get()無法成功將JSON物件取回,所以有另一種做法是我們呼叫時傳入接收函數的名稱,例如: GetWeather.aspx?city=Taiwan&callback=getData,Web API在產生Script時,會帶入我們指定的函數名稱,則執行的Script變成getData([{Area:"北部", .... }]),即可將兩邊串起來。

讓我們來做個小測試,先寫一隻ASPX,會依傳入的callback參數,產生一段呼叫該函數的Javascript,並帶入我們要傳遞的匿名物件:

<%@ Page Language="C#" %>
<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        string funcName = Request["callback"];
        Response.Write(funcName + "({ Name:'Darkthread', Rating: 5 });");
    }
</script>

接著我們寫一個$.ajax(),指定dataType="jsonp",加上beforeSend事件觀察url被修改的情況,加上dataFilter事件觀察ASPX傳回的結果,最後由success取得ASPX傳來的匿名物件並顯示出來。

$.ajax({ 
url:"../Lab5_JSONP.aspx", dataType: "jsonp",
beforeSend: function() { alert(this.url); },
dataFilter: function(data, type) { alert("(" + type + ")" + data); return data; },
success: function(d) { alert(d.Name + "=" + d.Rating); }
});

我們發現url變成了"../Lab5_JSONP.aspx?callback=jsonp1238089172708&_=1238089172722",jsonp1238089172708是由success函數轉化而成的,改成隨機名稱才不會在同時呼叫多次時發生錯亂,而_=1238089172722則用來防止讀到Cache中的版本。

由這個QueryString,Lab5_JSONP.aspx的傳回結果不意外的是jsonp1238089172708({ Name:'Darkthread', Rating: 5 }); 然後我們得到Darkthread=5。

在上述的例子中,ASPX知道由callback=?來取用函數名稱,但若API由第三者提供,指定了如cbfunc=?來讀取函數名稱,我們可使用前面介紹過的ajax參數裡的jsonp屬性指定為cbfunc即可自訂函數名所用的QueryString參數名稱。

【範例檔案下載】