WEB 開發

Fetch API — JavaScript 發送 HTTP 請求 (III)

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


 

Fetch API

Fetch API [fɛtʃ] 獲取、取得之意,是 WHATWG 近年來推動的新標準,
其擁有良好的設計、Promise 的實作、API 相容性 (e.g., Service Worker、Cache) …,
儘管 規範 尚未穩定,且許多功能仍處實驗中,仍令許多開發者趨之若鶩 !
 


 

基本使用

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

Polyfill

不同於 jQuery 需額外載入,
Fetch API 是標準的 Web API,因此能夠 直接使用
 
但別忘記啦 ! 新的 API 也說明 瀏覽器相容性 可能不普及,
MDN 提供了表格 (以撰文日期 2017/05/30 為準) :
 
mdn-fetch-compatibility
 
可謂慘不忍睹,像 iOS safari 就要 10.1 以上才能使用,QQ 😢
 
 
不過沒關係,可以使用 自動補完函式庫 (polyfill) 解決,
polyfill-service 是個方便簡單的選擇 :
(在 HTML 的 head 元素內加入 <script src=…)

<head>
    <title>Fetch API Demo</title>
    <meta charset="utf-8">
    
    <script src="//cdn.polyfill.io/v2/polyfill.min.js?features=fetch"></script>
    
</head>

 
 

fetch()

首先,呼叫全域的 fetch 方法,參數放置的是 目標 URI :
(當然,還有還有其他實例方式 — — 可選參數、多載函式…,詳見 MDN)

fetch('https://gank.io/api/random/data/福利/20')

 
fetch() 函式會回傳一個 Promise,並在解析/完成 (resolve) 後,回傳 Response 物件,
因此,能直接以 .then(onFulfilled, onRejected) 串接解析 完成 或 拒絕 的回調函式,
且能使用 Response 物件 提供的 json() 方法,將回應解析為 JSON 物件 !

fetch('https://gank.io/api/random/data/福利/20')
    .then(function (res) {
        return res.json();
    })
    .then(function (data) {
        jsonHandler(data);
    });

(此例沿用第一集的 jsonHandler 函式)
 
 
完成 ! (一切都是為了學習),
可前往 這裡 觀看成果 :
gank-io-ex
 
 

Async/Await

說到 Promise ,怎麼能忘記Async/Await !
再進一步改寫,跟回調函式說掰掰 ~

async function fetchGank() {

    let res = await fetch('https://gank.io/api/random/data/福利/20');

    // 等待 parse json
    let data = await res.json();

    jsonHandler(data);
}

 
啊啊啊啊 ~~ 變好清楚啊
bekas
 
 
需注意的是,json() 方法回傳的也是 Promise
在其解析完成後,會回傳 JSON 物件。
 
因此 務必 加上 await 等待解析的結果,
否則 data 會是 undefined !
 
 

二進制回應

Fetch 處理二進制回應,也是毫無壓力,
直接使用 res.blob(),回應就解析完成 :

async function blobDemo() {
    try {
        let res = await fetch('Nobita.jpg');

        if (res.ok) {
            // 等待 parse blob
            let blob = await res.blob();

            let img = document.createElement("img");
            let link = document.createElement("a");

            // 建立 blob URI (Object URI)
            var blobUri = window.URL.createObjectURL(blob);

            img.src = blobUri;
            link.href = blobUri;
            link.innerHTML = "Download"; // 下載提示文字
            link.download = "Nobita.jpg"; // 檔案名稱

            // 新增 img 與 a 元素至頁面
            document.body.appendChild(img);
            document.body.appendChild(link);

        } else {
            let text = await res.text();
            console.log(text);
        }
    } catch (e) {
        console.log(e);
    }
}

 
Result:

 
不熟稔 Blob 的話,請先看 第一集 喔 !
 


 

關注點分離
(Separation of Concerns)

 

不同於 XMLHttpRequest 或 jQuery Ajax,所有職責都在單一類別。

 
Fetch API 擁有良好的 關注點分離 (Separation of Concerns, SOC)
除了已經見過的 全域 fetch 方法,以及回應的 Response 物件
尚有 Body Headers 以及 Request 等介面。
 
 
其實,我們也已使用過 Body 介面了 !
 
 
還記得上例中,在回應的 Response 物件上,
使用 res.json() 來讀取 訊息主體 (Body) 並解析為 JSON 物件嗎 ?
那就是 Body 介面所提供的屬性與方法喔 !
 
 

Body

串流 (Stream)

訊息主體 (Body) 被 Request 與 Response 所實作,內容是 Byte Stream 的串流形式,
因此,一旦透過 json()text()formData()arrayBuffer() 讀取後,
Body 的 bodyUsed 唯讀屬性,會設置為 true。
 
這代表:

Request、Response 物件,只能被讀取一次 !

 
以下範例,欲分別以 json 與 text 的方式讀取回應:

async function willError() {

    let res = await fetch('https://gank.io/api/random/data/福利/20');
    
    let data = await res.json();
    let text = await res.text();

    console.log(data);
    console.log(text);
}

 
看似合理,卻會拋出 Body has already been consumed. 錯誤 ! 使用上需多加注意。
 

副本 (Clone)

復用 請求 與 回應 Body 是很常見的需求,
解決 只讀一次 最簡單的方式,就是做一個 副本 啦 !
因此 RequestResponse 介面,都提供了 clone 方法
 
接著,改寫一下上方範例 :

async function WillSuccess() {

    let res = await fetch('https://gank.io/api/random/data/福利/20');

    // 製作 Response 物件副本
    let resCopy = res.clone();

    let data = await res.json();
    let text = await resCopy.text();

    console.log(data);
    console.log(text);
}

 
 
成功 !
 
 

Headers

Headers 表頭,是 Fetch API 中最簡單的介面了 !
首先使用 Headers 建構元實例物件 :

var myHeaders = new Headers(init);

 
init 參數是可選的,可以是 ByteString 形式的物件 :

var myHeaders = new Headers({
    'Content-Type': 'image/jpeg',
    'User-Agent': 'Hellow, World!'
});

 
Result:

 
 
或是另一個 Headers :

var myHeaders1 = new Headers({
    'Content-Type': 'image/jpeg',
    'User-Agent': 'Hellow, World!'
});

var myHeaders2 = new Headers(myHeaders1);

 
也可以在實例完成之後,用 append() 方法添加:

var headers = new Headers();
headers.append('How-Are-You', 'I\'m fine, Thank you.');

 
欲讀取 回應 (Response) 的 表頭,則是 :

var myHeaders = response.headers;

 
 

Forbidden_header_name

想取得某個表頭欄位的值,可以透過 get(),且不區分大小寫,
若沒有該欄位,則回傳 空值 (null) :

var type = response.headers.get('Content-Type');

然而,與 XMLHttpRequest 相同,
基於安全考量,這些表頭欄位: Forbidden_header_name 是禁止添加的喔,
會由 使用者代理 (e.g., 瀏覽器) 幫你搞定 !
 
且 Headers 其實有一個隱藏的 Guard 屬性,
限制了可以操作的表頭欄位,完整的 Headers 用法 詳見 MDN
 
 

內容類型

別忘記啦 ! HTML5 表單 預設編碼方式 為 application/x-www-form-urlencoded
POST 表單 在送出資料前,需設置 Content-Type 表頭
否則 Server 可能不會鳥你 !
 
最重要的是 — 開發時,務必記得處理 百分比編碼 (Percent-Encoding)
你或許會想使用 犀牛本 作者 David 寫的簡易函式 — encodeFormData :

function encodeFormData(data) {
    if (!data) return "";    // Always return a string
    var pairs = [];          // To hold name=value pairs
    for (var name in data) {                                  // For each name
        if (!data.hasOwnProperty(name)) continue;            // Skip inherited
        if (typeof data[name] === "function") continue;      // Skip methods
        var value = data[name].toString();                   // Value as string
        name = encodeURIComponent(name.replace(" ", "+"));   // Encode name
        value = encodeURIComponent(value.replace(" ", "+")); // Encode value
        pairs.push(name + "=" + value);   // Remember name=value pair
    }
    return pairs.join('&'); // Return joined pairs separated with &
}

 
Input:

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

Output:

name=%E4%B8%AD%E5%8B%9D&city=Taipei

 
 

Request

在介紹 Request 之前,先看看完整的 fetch() 語法 :

fetch(input[, init]);

 
再來看看 Request() 的語法 :

Request(input, input[, init]);

 
沒錯,除了一個回傳值是 Promise<Response>,
另一個是 Request,他們的實例方式一模一樣 !
 

input

input 參數,除了 基本使用 介紹過的 目標 URI 以外,
也能是一個 Request 物件 !
 
還記得剛才用 clone 方法,解決 Body 只讀一次 的問題嗎 ?
也能如法炮製,直接使用建構元製作副本喔 !

var myRequest = new Request('https://example.com');

// 製作 Request 物件副本
var myRequest2 = new Request(myRequest);

 

init

可選的 init 參數,則是包含所有請求的進階設定,
例如,欲使用的 請求方法 — method、
請求 訊息主體 — body (GET、HEAD、DELETE…等請求方法 不應設置),
甚至 第一集 所提到的 同源政策 (Same-origin policy) 模式 — mode …,完整用法詳見 MDN
 
 
最後,統整本節的所有介面,
一個 POST 請求就嚕出來囉 😃 :

async function post() {
    try {

        // 設置表頭欄位 (以一個客製化表頭為例)
        var headers = new Headers({'x-just-test': 'This is a test.'});
        headers.append('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');

        // 準備 資料酬載 (Payload)
        var data = {name: "勝", city: "Taipei"};
        // 以 application/x-www-form-urlencoded 的內容類型送出
        // 酬載 (Payload) 需使用百分比編碼
        var encodedData = encodeFormData(data);

        // 請求參數 init:
        //
        // 請求方法 POST
        // 設置表頭
        // 將編碼後的酬載 置於訊息主體 (Message Body) 中
        var myInit = {
            method: 'POST',
            headers: headers,
            body: encodedData
        };

        // 實例請求 -- Request,設置 目標 URI 與 請求參數 init
        var myRequest = new Request('demo_test_post.asp', myInit);

        // 非同步執行 fetch
        let res = await fetch(myRequest);

        // 判斷回應狀態碼
        if (res.ok) {

            alert(await res.text());

        } else {
            let text = await res.text();
            console.log(text);
        }

    } catch (e) {
        console.log(e);
    }
}

 


 

Fetch vs. jQuery

錯誤狀態

不同於 jQuery,無論回應狀態碼是 4xx 的客戶端錯誤5xx 的伺服端錯誤
Fetch 都視為 解析成功 (Resolved Promise)
僅在網路錯誤 或 請求被拒 才會拒絕 Promise,
因此實務開發時,回應狀態碼的判斷 必不能省 :

if (res.ok) {
    ...成功...
} 

 
其等價於 :

if (200 <= res.status && res.status <= 299) {
    ...成功...
} 

 
 

Cookie

MDN: 默認情況下,fetch在伺服端不會發送或接收任何 cookies,
如果站點依賴於維護一個用戶會話,則導致未經認證的請求(要發送 cookies,必須發送憑證表頭) 。
 


 

總結

Fetch 是用於獲取資源的低階 API,
涵蓋比 XMLHttpRequest 還全面的範疇,
儘管目前仍缺少對於請求進度的處理 (e.g., 中斷)。
 
另外,仍有許多的 HTTP Client Library,例如我愛用的 Request
或是對 XHR 、 Fetch 的進一步封裝 (e.g., Vue-Resource) …。
(建議閱讀 上集 結尾的 XHR 封裝)
 
也因為 Promise、Async/Await 的盛行,使用上皆大同小異,
依照需求、相容性 選個最舒服來用即可 😀,
但千萬別忘記 同源政策 (Same-origin policy)
這客戶端 JavaScript 的最大的限制 !
 
 
最後,我要感謝 Gank IO 免費提供這麼好的 API,
不僅豐富了我的人生,也促使我撰寫了三集 JavaScript 發送 HTTP 請求
希望大家多多支持它們…還有我 😂。
 
範例原始檔: Github
 
給 S & N 的話 :

希望你們看完這三集後,
再去看之前很紅的 『在 2016 年學 JavaScript 是一種什麼樣的體驗?』,
應該已能看懂 2/3 😆 ,JS 並沒有想像中的可怕,加油 !

 

 
 

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

在《Fetch API — JavaScript 發送 HTTP 請求 (III)》中有 2 則留言

  1. 文中提到「使用 res.json() 來讀取 訊息主體 (Body) 並解析為 JSON 物件嗎 ?
    那就是 Body 介面所提供的屬性與方法喔 !」

    但是透過 console 觀察,看起來 .json() 方法 應該是繼承自 Response 的原型物件,而非 body 屬性 對應的 ReadableStream 物件,想請問這部分我是否理解有誤?

發表迴響