WEB 開發

jQuery Ajax — JavaScript 發送 HTTP 請求 (II)

此系列主要討論 JavaScript 發送請求的方式,
若對 XMLHttpRequest 不熟稔,建議閱讀 上集
 
就算不會 JavaScript/jQuery,閱讀本篇後,您將擁有基本實作能力,
不清楚如何執行 JavaScript 的話,可參閱 W3SChools,或利用 CodePen 進行測試。
 

 


 

jQuery Ajax

jQuery [dʒeˋkwɪrɪ] 是快速、小巧、功能豐富 的 JavaScript 函式庫
然而,前端的快速發展, React、Angular、Vue …等框架的出現,
讓我們不再依賴 DOMEvent,自然也漸漸不需 jQuery。
 
例如,Vue響應式系統 (reactivity system)
將資料 綁定 (binding) 到 DOM 的文本或結構,
讓我們能專注在底層邏輯,而非冗余的操作:
vue-reactivity-system
 
儘管 jQuery 被認為過時,而且我也很少用 😂,
但說過啦 : 仍值得了解其用法
對於理解 其他 API、維護舊系統 皆能有所幫助。
 
別擔心,本篇重點不在 jQuery 過時與否,
而是 jQuery Ajax 的使用 與 非同步概念 (ES6 Promise, ES7 Async/Await),
因此,就算您不使用 jQuery,也能有所收穫 …吧 😂。
 


 

基本使用

同樣的,以 jQueryhttps://gank.io/api/random/data/福利/20
送出 GET 請求,並將回應提供的 影像 URI,添加至頁面為例:
 
 

安裝

安裝方式 非常多種,這裡我使用最簡易的 Max CDN :
(在 HTML 的 head 元素內加入 <script src=…)

<head>
    <title>jQuery-Ajax-Demo</title>
    <meta charset="utf-8">
    
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"
            integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
            crossorigin="anonymous"></script>
    
</head>

 
 

jQuery.get()

首先,呼叫 get 方法,參數放置的是 目標 URI,
其後伴隨著 done() 方法,設置回應完成後要做的事 — 回調函式 (callback) :
(當然,還有還有其他實例方式 — — 可選參數、多載函式…,詳見 官網)

$.get("https://gank.io/api/random/data/福利/20")
    .done(res => jsonHandler(res));

(此例沿用上集的 jsonHandler 函式)
 
[註]:
上例為使用 箭頭函數 (Arrow Function) 的寫法,相當於:

$.get( "https://gank.io/api/random/data/福利/20")
            .done(function(res){jsonHandler(res)});

 
[註]:
$」是 jQuery 全域函式的捷徑符號
 
 
完成 ! (一切都是為了學習),
可前往 這裡 觀看成果 :
gank-io-ex-2
 
 
是否覺得超快、超方便 😂?

實際上 jQuery 就是把 XMLHttpRequest (XHR) 再包一層 !
並幫你處理許多麻煩的事情 (e.g., 請求生命週期、回應解析)。

 


 

Promise

如果只是簡單的封裝,構不成 jQuery Ajax 屌壓 XMLHttpRequest 的理由,
重要的是 jQuery 在 1.5 版時,實作了 Promise 語法結構
且在 3.0 版時,已相容於 Promises/A+ 標準規範,
藉此擺脫可怕的 回調函式地獄 (Callback Hell) !
 
Promise 是一個 非同步/異步 (asynchronous) [eˋsɪŋkrənəs] 操作的執行 結果
它使 jQuery 能輕易在單個請求上分配多個 回調函式 (callback),甚至在請求完成後才分配。
 

而 Promise 在 jQuery 中的實作者即 Deferred 物件,
與其子裔 — — jqXHR 物件

 

方法鏈

標準的 Promise 共有三種狀態 (MDN):

  • pending: 等待中,為初始之狀態,即不是 fulfilled 也不是 rejected。
  • fulfilled: 已實現,表示操作完成,又稱 resolved
  • rejected: 已拒絕,表示操作失敗。

 
promise
 
[註]:
此處對 MDN 示意圖 做了些微簡化。
推薦閱讀 從Promise開始的JavaScript異步生活 了解更多 😄。
 
穩定 (settled) 代表的是:

Promise 只會 完成 或 拒絕 一次,
且不會再改變 (完成變拒絕 or 拒絕變完成) 。

 
因此 jqXHR 物件 每次執行方法,都會回傳 新的 Promise 物件
根據 非同步的執行 狀態,執行 對應的處理
 
直接看例子:

$.get("qq")
    .fail(function () {
        alert("error1");
    })
    .fail(function () {
        alert("error2");
    })
    .done(function () {
        alert("success");
    })
    .always(function () {
        alert("complete");
    });

請求 成功 則 (done) 顯示: “success” 與 “complete”,不會 顯示 “errorX” ;
請求 失敗 則 (fail) 顯示: “error1” 與 “error2” 與 “complete”,不會 顯示 “success” 😲 !
 
 
以下為 jQuery 常用的 jqXHR 方法 (大部分為擴展的非標準方法) :

  • done(x) — 當 jqXHR 狀態為 Resolved 才執行 x,並回傳新的 Resovled jqXHR。
  • fail(x) — 當 jqXHR 狀態為 Rejected 才執行 x,並回傳新的 Rejected jqXHR。
  • always(x) — 總是執行 x,並回傳新的 Resovled jqXhr。
  • … (請參考 官方文件)

 


 

Async/Await

儘管 ES6 的 Promise,已大大改善 非同步 的寫法,
(ECMAScript 為 JavaScript 標準)
回調函式 (callback) 的影子還是很重… 😥。
 
於是 ES7 推出了 神器 :
async/await
 
Async/Await 仍處於草案, 卻已被稱為 非同步的終極解決方案
在開始之前,請再瞄一下 Promise 狀態圖 :
 
promise
 
 

async function

async/await 用法是以 關鍵字 async function 宣告一個 非同步函式
並在 非同步函式 內部,加上 await 運算式,以等待 Promise 的解析 :

async function test(param) {
    // await 會暫停此 async function 的執行,
    // 並等待 doSomethingAsync() 回傳 Promise 解析 (Resolved) 的 『值 (Value)』
    // 若 Promise 解析失敗 (被拒絕 rejected) 則拋出例外,並附加 『拒絕理由 (reason) 』
    var data = await doSomethingAsync();
   
    // Promise 解析完成之後,會繼續此 async function 的執行
    console.log(data);
}

 
 

使用範例

覺得太文言文,就看個例子吧 !
 
一個簡易的 同步函式,參數可以帶入布林值,
代表直銷拉人 成功 or 失敗,並回傳 Promise 的解析結果 :

function 你聽過安利嗎(flag) {
    return new Promise(function (resolve, reject) {

        var value = "一起年薪百萬啦 !";
        var reason = "先來用我家 賀寶寶奶昔 啦 !";

        if (flag) {
            // 解析/接受/完成 Promise 並回傳 value
            resolve(value)
        } else {
            // 拒絕 Promise 並回傳 拒絕 reason 理由
            reject(reason);
        }
    })
}

 
接著,一個 同步函式 test ,
await 等待拉人的結果 🤓 :

async function test() {
    console.log("測試 1. 拉人成功:");
    let test = await 你聽過安利嗎(true);
    console.log("Result: " + test);

    console.log("-----------------\n");

    console.log("測試 2. 拉人失敗:");
    test = await 你聽過安利嗎(false);
    console.log("Result: " + test);
}

 
開始執行 (請注意 第三行的小陷阱) !

console.log("測試開始:");
test();
console.log("測試結束了 ?");

 
 
想想結果吧 🤔
 
 
 
 
 
 
 
 
 
時間到 ~ Result:
 
 
await-ex
 
 
" 測試結束了 ? " 出現在第三行,是因為執行 test() 時,
遇到了 : await 你聽過安利嗎(true);
因此暫停 async function 的執行,優先執行 同步 的 "測試結束了 " !

async function test() {
    console.log("測試 1. 拉人成功:");
    let test = await 你聽過安利嗎(true);
    ...

 
接著,等待回傳 Promise 解析 (Resolved) 的 『值 (Value)』:

if (flag) {
    // "一起年薪百萬啦 !"
    resolve(value)

    ...略...

 
若 Promise 解析失敗 (被拒絕 rejected) 則拋出例外,並附加 『拒絕理由 (reason) 』

// "先來用我家 賀寶寶奶昔 啦 !"
reject(reason);

 
 

非 Promise

更狂的是,await 之後所接的內容,不一定要實作 Promise
await 會透過 Promise.resolve() 解析它 !
這代表 : 你能安全地在任何 預期等待的地方 使用 await !
 
例如,此非同步函式,『20』並非一個 Promise 物件,
因此直接轉換其值為 resolved Promise,並等待之 (by MDN) :

async function f2() {
  var y = await 20;
  console.log(y); // 20
}
f2();

 
 
對喔 ! 你好棒棒 :

async/await 是操作 Promise 的 語法糖 (syntactic sugar) [sɪnˋtæktɪk] !

 
 

瀏覽器相容性

需注意的是許多瀏覽器 (尤其是行動裝置) 尚未支援 Promise 與 Async Function,
可以使用 Babel 等編譯器,轉換為相容的程式碼喔 !
詳見 Babel 安裝補完函式庫 (Polyfill) 教學。
 
 

改寫 promise 鏈

MDN 的 範例說明 寫得太好了 ! 這裡擷取部分內容說明,
強烈建議,對 async/await 有興趣的話一定要看看 !
 
 

回調函式地獄

以往的 (ES5) 非同步運算,容易墮入可怕的「金字塔惡夢」… :

 
 

Promise

使用 Promise (es6) 後,蘇湖啊 !

 
 

箭頭函式 (Arrow Function)

結合 箭頭函式 後,又進化了 :

 
 

Async/Await 降臨

最後,終極解決方案 async/await :
mdn-async-ex
Quietly
 
 
不難發現,async/await 最強大之處在於:

能以 同步 的流程,敘述 非同步/異步 的操作 !

 
這不僅僅是簡潔的層次了,
它讓 非同步 返璞歸真,能用最直覺的方式撰寫程式 !
當然,採用何種方式見仁見智,選個最舒服的吧 😄 。
 
 

改寫範例

使用 async/await 改寫最初的範例:

// 使用關鍵字 async 宣告 非同步函式
async function enjoy() {

    // 等待獲取回應
    var data = await $.get("https://gank.io/api/random/data/福利/20");

    // 回應 (Promise) 完成時,自動執行此行
    jsonHandler(data)
}

 
Result:

 
async/await 萬歲 !
Bravo
 
 


 

POST 請求

jQuery Ajax 要送出 POST 請求,
僅需將 請求方法 換成 $.post(),並 置放資料 (如果有的話) 即可,
第一個參數放置的是 目標 URI,後伴隨著欲發送的 資料酬載 (payload):

$.post("demo_test_post.asp", {name: "勝", city: "Taipei"})
    .done(function (data) {
        console.log("success");
        document.body.innerHTML = data;
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        console.error(errorThrown);
    })
    .always(function () {
        console.log("finished");
    });

 
請求成功即顯示: “success” + “finished”,
請求失敗則顯示: “error” + “finished”,
請至 W3schools 嘗試。
 
 
當然,可以使用 async/await 改寫 :

async function post(url) {
    try {
        var data = await $.post(url, {name: "勝", city: "Taipei"});
		
        console.log("success");
        document.body.innerHTML = data;

    } catch (jqXHR) {
        console.error(jqXHR.statusText);
    }
    console.log("finished");
}

 
是否覺得邏輯更清晰呢 🤔 ?
 
 

內容類型 (Content-Type)

(延續上例)
咦 ! 不用處理 酬載 的編碼嗎 !?
 
Ans:

可能不用

 

XHR vs. jQuery

不同於 XMLHttpRequest,jQuery 請求的 編碼方式 (enctype) 預設即為:

application/x-www-form-urlencoded; charset=UTF-8

且會 自動 為資料進行 百分比編碼 (Percent-Encoding) 喔 !
 
讓我們看一下,一樣的資料:

var data = {name: "勝", city: "Taipei"};

 
以 jQuery.post() 送出,已 自動編碼 :

name=%E5%8B%9D&city=Taipei

 
以 xhr.send(data) 送出,這是尛啦 :

[object Object]

 
 
儘管如此,還是有許多機會需要手工處理 資料酬載 (payload) 喔 !
例如,常見的 JSON.stringify() 方法。
 


 

ajax() 方法

包含常使用的 $.get()$.post()
另外還有 getJSON()、getScript()、load(),共五種 快捷方法 (Shorthand Methods)
其實最後都是呼叫較低階的 API — — $.ajax() 方法,格式為:

// 回傳 jqXHR 物件 (實作 Promise)

jQuery.ajax( url [, settings ] )

 
例如,欲使用 PUT 方法傳送 JSON 資料時,
需變更 methodcontentType 屬性:

$.ajax({
    url: "user.php",
    method: "PUT",
    contentType: "application/json",
    data: JSON.stringify({
        "name": "Jason",
        "id": 1,
        "handsome": true
    })
})
    .done(function (data, textStatus, jqXHR) {
        console.log("success");
        console.log(data);
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        console.log(errorThrown);
    })
    .always(function () {
        console.log("finished");
    });

 
使用 jQuery 1.9.0 以前的版本,需將 method 以 type 屬性替換,
詳盡的設定列表,請參閱 官方文件
 
 
使用 async/await 改寫 :

async function ajax(setting) {
    try {
        var data = await $.ajax(setting);
        console.log("success");
        console.log(data);

    } catch (jqXHR) {
        console.error(jqXHR.statusText);
    }
    console.log("finished");
}

ajax(setting);

 


 

封裝 XHR

既然 jQuery 能 封裝 XMLHttpRequest (XHR)Promise,那我們呢?
 
Ans:

當然也可以

 

function GET(url) {

    // 使用 Promise() 建構元 建立 promise 物件
    return new Promise(function (resolve, reject) {

        var xhr = new XMLHttpRequest();
        xhr.open('GET', url);

        xhr.onload = function () {
            if (200 <= xhr.status && xhr.status <= 299) {

                // 解析/接受/完成 Promise 並回傳 回應 value
                resolve(xhr.responseText);
            }
            else {
                // 拒絕 Promise 並回傳 拒絕 reason 理由
                reject(Error(xhr.statusText));
            }
        };

        xhr.onerror = function () {
            reject(Error("Network error."));
        };

        xhr.send();
    });
}

 
 
於是,能如此使用 :

async function enjoy() {
    let res = await GET('https://gank.io/api/random/data/福利/20');
    let data = JSON.parse(res);
    jsonHandler(data)
}

 
Oops…😨 找不到用 jQuery Ajax 的理由了…
 
範例原始檔: Github
 
下集: (5-7) 發送 HTTP 請求 (III) Fetch API

 
 

作者: 鄭中勝
喜愛音樂,但不知為何總在打程式 ? 期許能重新審視、整理自身所學,幫助有需要的人。

在《jQuery Ajax — JavaScript 發送 HTTP 請求 (II)》中有 4 則留言

  1. 這篇太神了!
    深入淺出的介紹
    所有前端開發都會遇到的 ajax
    也更瞭解 ES6 的重點 promise

    大推!

發表迴響