PWA 基礎知識-Fetch API

Fetch API 是目前最新的異步請求解決方案,它在功能上與 XMLHttpRequest(XHR)類似,都是從服務器端異步獲取數據或者資源的方法。 對於有過 AJAX 開發經驗的讀者應該深有體會,基於 XHR 的異步請求方法在實現上比較複雜。 下面簡單演示如何通過 XHR 發送異步請求:

// 實例化 XMLHttpRequest
let xhr = new XMLHttpRequest()
// 定義加載完成回調函數,打印結果
xhr.onload = function(){
  console.log("請求成功");
}
// 定義加載出錯時的回調函數,打印錯誤
xhr.onerror = function(err){
  console.error("請求失敗");
}
// 設置請求目標
xhr.open("GET","/path/to/text", true)
// 開始發起請求
xhr.send()

從上面的代碼當中可以感受到,基於事件回調機制的 XHR 在編程實現的思路上非常反思維, 要實現這樣一個簡單的 GET 請求所需代碼較多,一旦功能變得負責會很容易造成混亂。 因此在實際應用當中,一般會選擇封裝好的函數進行使用, 例如 jQuery 提供的 $.ajax 方法。

接下來使用 Fetch API 來實現上述功能:

fetch("/path/to/text", { method: "GET" })
  .then((response) => {
    console.log("請求成功");
  })
  .catch((err) => {
    console.log("請求失敗");
  });

Fetch API 代碼邏輯清晰,代碼量少。

1. fetch()

Fetch API 提供了 fetch() 用來發起網絡請求並獲得資源響應。它的使用方法非常簡單:

fetch(request).then(response=>{/* 響應結果處理 */})

接受一個 Request 對象作爲參數,fetch() 發起網絡請求,由於網絡請求是一個異步過程,因此 fetch() 返回一個 Promise 對象,當請求響應時 Promise 執行 resolve 並傳回一個 Response 對象。

除了直接以 Request 對象作爲參數之外, fetch() 還支持傳入請求 URL 和請求配置項的方式, fetch() 會自動根據這些參數實例化 Request 對象之後再去發起請求,因此以下代碼是等價的:

fetch(new Request('/path/to/resource',{ method: "GET" }))
// 等價於
fetch('/path/to/resource',{ method: "GET" })

需要注意的是,fetch() 只有在網絡請求終端的時候纔會拋出異常,此時 Promise 對象會執行 reject 並返回錯誤信息。 因此對於 fetch() 來說,服務端返回的 HTTP 404、500 等狀態碼並不認爲是網絡錯誤,因此處理檢查 Promise 是否 resolve 之外,還需要檢查 Response.status、Response.ok 等對象屬性以確保請求是否成功響應。 示例:

fetch('/paht/to/resource/').then(response => {
  if (response.status === 200){
    // 請求成功
  }else{
    // 請求失敗
  }
}).catch(err => {
  // 網絡請求失敗或者請求被中斷
})

2. Request

Request 是一個用於描述資源請求的類,通過 Request() 構造函數,可以實例化一個 Request 對象:

let request = new Request(input,init)

其中,input 代表想要請求的資源,可以是 URL, 或者是描述資源請求的 Request 對象;init 爲可選參數,可以用來定義請求中的其他選項。 可以注意到,Request 構造函數所需要的參數和fetch() 方法的參數是一樣的。 示例:

  1. GET 請求, 請求參數需要寫到 URL 中:

    let getRequest = new Request("/api/hello?name=lilei", {
      method: "GET",
    });
    
  2. POST 請求, 請求參數需要寫到 body 中:

    let postRequest = new Request("/api/hello", {
      method: "POST",
      body: JSON.stringify({
        name: "lilei",
      }),
    });
    
  3. 自定義請求的 Headers 信息

    let customeRequest = new Request("/api/hello", {
      headers: new Headers({
        "Content-Type": "text/plain",
      }),
    });
    
  4. 設置發起資源請求時帶上 cookie

    let cookieRequest = new Request('/api/hello',{
        credentials:'include'
    })
    

init 對象還可配置其他參數,這裏不展開講,介紹以下 Request 對象常用的幾個屬性:

  • url: String 類型,只讀,請求的url;

  • method: String 類型,只讀,請求的方法;

  • headers: Headers 類型,只讀,請求的頭部信息,可通過 get() 方法獲取信息,例如:

    if(request.headers.get('Content-Type') === 'text/html'){
      // ...  
    }
    

3. Response

Response 類,用於描述請求響應數據,通過 Response() 構造函數實例化,

let response = new Response(body,init)

其中 body 參數代表請求響應的資源內容,可以是字符串,FormData, Blob 等等; init爲可選參數對象,可以用來設置響應的 status、statusText、headers 等內容,示例:

// 如何構造一個 iudex.js 的響應:
let jsResponse = new Response(
  // index.js 的內容
  'console.log("Hello World!)',
  {
    status: 200,
    headers: new Headers({
      "Content-Type": "application/x-javascript",
    }),
  },
);

在實際應用當中,我們一般會通過 fetch() 、Cache API 等等獲得請求響應對象,然後再對響應對象進行操作。

3.1 讀取響應體

Response 的 body 屬性 暴露了一個 ReadableStream 類型的響應體內容,Response 提供了一些方法來讀取響應體:

  • text(): 解析爲字符串;
  • json(): 解析爲 JSON 對象;
  • blob(): 解析爲 Blob 對象;
  • formData() : 解析爲 FormData 對象;
  • arrayBuffer(): 解析爲 ArrayBauffer 對象;

這些方法讀取並解析響應體的數據流屬於異步操作,因此這些方法均返回 Promise 對象,當讀取數據流並解析完成時, Promise 對象將 resolve 並同時返回解析好的結果。 示例:

// 溝槽 Response 對象
let response = new Response(JSON.stringify({ name: "lilei" }));

// 通過 response.json() 讀取請求體
response.json().then((data) => {
  console.log(data.nam); //lilei
});

由於Response 的響應體是以數據流的形式存在的,因此只允許進行一次讀取操作。 通過檢查 bodyUsed 屬性可以知道 當前的 Response 對象是否已經被讀取:

let response = new Response(JSON.stringify({ name: "lilei" }));
console.log(response.bodyUsed); //false
response.json().then((data) => {
  console.log(response.bodyUsed);// true
});

由於二次讀取響應體內容會導致報錯,因此爲了保險起見,可以在進行響應體讀取前首先判斷 bodyUsed屬性再決定下一步操作。

3.2 拷貝 Response

Response 提供了 clone() 方法來實現對 Response 對象的拷貝:

let clonedResponse = response.clone()

clone() 是一個同步方法,克隆得到的 新對象再所有方面與原對象都是相同的。 在這裏需要注意的是,如果 Response 對象的響應體已經被讀取, 那麼在調用 clone() 方法的時候會報錯,因此需要在讀取響應體讀取之前進行克隆操作。

4. Fetch API 處理跨域請求

當涉及到前後端通信問題的時候,就不得不提請求跨域的情況。由於受到 Web 同源策略的影響,在使用 Fetch API 默認配置情況下發送異步請求,會受到跨域訪問限制而導致資源請求失敗。

我們通常採用跨域資源共享機制(CORS)來解決這個問題。在跨域服務端支持 CORS 的前提下,通過將 fetch() 的請求模式設置爲“cors”,就可以簡單地實現跨域請求。在這種請求模式下,返回的請求響應是完全可訪問的:

// 假設當前頁面 URL 爲 https://current.com
fetch('https://other.com/data.json', {
  mode: 'cors'
})
.then(response => {
  console.log(response.status) // 200
  console.log(response.type) // 'cors'
  console.log(response.bodyUsed) // false
  return response.json()
})
.then(data => {
  console.log(data.name) // 'lilei'
})

⭐ 對於圖片、JS、CSS 等等這些類型的靜態資源,如果通過對應的 HTML 標籤加載這類跨域資源,是不會受到同源策略限制的,因此一般來說,存放靜態資源的服務器並不需要設置 CORS。這就會對 Fetch API 請求這類靜態資源帶來影響。在默認情況下 fetch() 的請求模式爲“no-cors”,在這種模式下請求跨域資源並不會報錯,但是返回的 Response 對象將變得不透明,type 屬性將變成“opaque”,無論服務端所返回的真實 status 是多少,在這種情況下都會變成 0,其他屬性也都無法正常訪問

// 假設當前頁面 URL 爲 https://current.com
fetch('https://other.com/data.json', {
  mode: 'no-cors'
})
.then(response => {
  console.log(response.status) // 0
  console.log(response.type) // 'opaque'
  console.log(response.headers) // Headers {}
  console.log(response.body) // null
})

此時唯一能正常工作的方法是 clone(),即實現對 Response 對象的拷貝,當然拷貝得到的新對象也同樣是不透明的。這種模式比較適用於在 Service Worker 線程中攔截靜態資源請求並複製一份緩存到本地,只要將這類不透明的請求響應返回主線程,依然是能夠正常工作的。下面的代碼演示了 Service Worker 攔截跨域圖片資源並將資源緩存到本地,然後在 fetch() 出錯的時候再從緩存中讀取資源:

self.addEventListener("fetch", (event) => {
  // 判斷當前攔截到的請求爲跨域圖片資源
  if (event.request.url === "https://other-site.com/pic.jpg") {
    event.respondWith(
      // 優先發送網絡請求獲取最新的資源
      fetch(event.request.url, { mode: "no-cors" })
        .then((response) => {
          // 將請求得到的響應進行緩存
          // 此時緩存的資源是不透明的
          const cloneReq = response.clone();
          caches
            .open("cache-storage")
            .then((cache) => cache.put(event.request.url, cloneReq));

          // 返回請求響應結果
          return response;
        })
        .catch(
          // 請求失敗時在使用緩存資源進行兜底
          () =>
            caches
              .open("cache-storage")
              .then((cache) => cache.match(event.request.url))
              .then((e) => e),
        ),
    );
  }
});

@jayce 原文這裏寫的有點問題,cache.match(event.request.url) 返回的不是 Response 對象,而是一個 Promise ,所以要進一步處理。加一個 .then()

在這種情況下,圖片資源的 Response 對象是不透明的,因此整個操作過程無法對圖片資源響應做任何檢查判斷,只能直存直取。這就有可能將真實狀態碼爲 404、500 等錯誤響應給緩存下來,因此在“no-cors”模式下緩存的跨域資源的可信度不高,最好作爲各類請求策略的兜底資源進行使用。

上面這段代碼,具體在做什麼?

實際上它希望,我先攔截三方站點的 圖片 請求,然後嘗試去請求,然後緩存下來。但是,如果請求不到, fetch() 會報錯,這時候 被 catch 到後,會從先前的緩存中去取出緩存。 注意,緩存的是一個 Response 對象,所以在取得時候直接返回對應的 Response 對象即可。

爲了更加清楚以上的原理,下面用一個實例來說明。

關於Service Worker 的內容,後面有介紹,這裏提前用一下,爲了說明這個地方在做什麼,爲什麼要這麼做,有什麼效果。

有這樣一個頁面,

<img src="http://placeimg.com/640/480/nature" alt="" />

爲了直觀的觀察,這個 API 也有特殊的地方,每次刷新,都會去加載不同的圖片:

170359

這個API 是一個三方站點的 API, 它並不會觸發跨域。 但是會被下面的 Service Worker 攔截:

self.addEventListener("fetch", (event) => {
  // 判斷當前攔截到的請求爲跨域圖片資源
  if (event.request.url === "http://placeimg.com/640/480/nature") {
    event.respondWith(
      // 優先發送網絡請求獲取最新的資源
      fetch(event.request.url, { mode: "no-cors" })
        .then((response) => {
          throw Error()
          // 將請求得到的響應進行緩存
          // 此時緩存的資源是不透明的
          const cloneReq = response.clone();
          caches
            .open("cache-storage")
            .then((cache) => cache.put(event.request.url, cloneReq));

          // 返回請求響應結果
          return response;
        })
        .catch(
          // 請求失敗時在使用緩存資源進行兜底
          () =>
            caches
              .open("cache-storage")
              .then((cache) => cache.match(event.request.url))
              .then((e) => e),
        ),
    );
  }
});

注意,爲了觸發 catch 中的方法,這裏刻意通過 throw Error() 拋出了異常,

171527

可以看到,現在,請求被攔截並且由錯誤處理,返回了緩存中的資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章