Angular 17+ 高級教程 – HttpClient

前言

HttpClient 是 Angular 對 XMLHttpRequest 和 Fetch 的封裝。

HttpClient 的 DX (Developer Experience) 比 XMLHttpRequest 和 Fetch 都好,只是學習成本比較高,因爲它融入了 RxJS 概念。

要深入理解 HttpClient 最好先掌握 3 個基礎技能:

  1. XMLHttpRequest -- 看這篇
  2. Fetch -- 看這篇
  3. RxJS -- 看這系列 (如果只是爲了 HttpClient 不需要看完,不過 RxJS 其實挺好用的,所以我推薦大家把它學起來)

 

Provide HttpClient

創建 Angular 項目

ng new http-client --ssr=false --routing=false --style=scss --skip-tests

app.config.ts

import { HttpClientModule } from '@angular/common/http';
import {
  ApplicationConfig,
  importProvidersFrom,
} from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    // 1. 添加 HttpClient 相關的 providers
    importProvidersFrom(HttpClientModule),
  ],
};

HttpClient 是一個 Class Provider,我們需要在 appConfig 中提供。

相關源碼在 provider.ts

其它 Service Provider 本篇也會粗略介紹一下哦。

 

Simple Get Request & Response

const response = await fetch('https://192.168.1.152:44300/products');
console.log('status', response.status);
console.log('response', await response.text()); // 注:讀取 response 數據是個異步過程哦,需要 await

對比 XMLHttpRequest 有幾個點:

  1. Fetch 代碼量比 XMLHttpRequest 少了很多
  2. Fetch 默認 request method 是 GET,所以不需要特別聲明
  3. Fetch 用 Promise 方式取代了 XMLHttpRequest 的事件監聽方式
  4. Fetch 的 response 有 Stream 的概念,讀取數據是異步的,需要 await Promise (Stream 還有其它特色,下面會教)。
    另外,面對不同的 response type 需要使用不同的 response 方法做讀取,比如讀取 string 使用 response.text(),JSON 使用 response.json()。
    這類似於 XMLHttpRequest 設置 request.responseType 屬性。

效果

 

Request with Query Parameters

Fetch 和 XMLHttpRequest 一樣,都沒有 built-in 對 Query Parameters 的處理。

我們需要藉助 URLSearchParams。

const searchParams = new URLSearchParams({
  key1: 'value1',
});
const queryString = '?' + searchParams.toString();
const response = await fetch('https://192.168.1.152:44300/products' + queryString);

然後把 Query String 拼接到 request URL 就可以了。

 

Request with Header

fetch 函數的第二參數可以設置 headers

複製代碼
const response = await fetch('https://192.168.1.152:44300/products', {
  headers: {
    Accept: 'application/json',
  },

  // 用 Array Array 類型也是可以
  // headers: [
  //   ['Accept', 'application/json']
  // ],
});
複製代碼

 

Request and Headers 對象

我們也可以先創建 Request 和 Headers 對象,之後再傳入 fetch 函數。

複製代碼
// 創建 headers 對象
const headers = new Headers({
  Accept: 'application/json',
});

// 創建 request 對象,並且傳入 headers
const request = new Request('https://192.168.1.152:44300/products', {
  headers,
});

// 注意,雖然創建 Request 時有傳入 Headers 對象,但是它不是相同的引用哦
// 內部有 clone 的概念
console.log(request.headers === headers); // false

// 再添加一個 header
request.headers.append('Custom-Header', 'value');

// 發送請求
const response = await fetch(request);
複製代碼

fetch 函數的參數和 Request 對象 constructor 是一致的,不管上層接口是什麼,底層實現都是 Request + Headers + fetch 就對了。

Clone Request

Request 有一個 clone 方法,我們可以很簡單的複製出一個請求

複製代碼
const request = new Request('https://192.168.1.152:44300/products', {
  headers: {
    Accept: 'application/json',
  },
});
const request2 = request.clone();
// 注意 clone 是深拷貝
console.log(request.headers === request2.headers); // false
複製代碼

clone 是深拷貝哦。

 

Auto Parse JSON Response

當 XMLHttpRequest 設置 request.responseType = ‘json',response 數據會自動被 JSON.parse 放入 request.response。

Fetch 沒有 request.responseType 屬性,取而代之的是 Response 對象上有各種類型方法。

const response = await fetch('https://192.168.1.152:44300/products');
const products = await response.json(); // [{ id: 1, name: 'iPhone14' }, { id: 2, name: 'iPhone15' }]

Fetch 的 response.text() 相等於 XMLHttpRequest 的 request.responseType = 'text'。

Fetch 的 response.json() 相等於 XMLHttpRequest 的 request.responseType = 'json'。

Fetch 的 response.blob() 相等於 XMLHttpRequest 的 request.responseType = 'blob'。

以此類推...

 

Read Response Body Multiple Times

Fetch 的 Response 有 Stream 的概念,每一個 response 的 stream 只能被讀取一次。

const response = await fetch('https://192.168.1.152:44300/products');
console.log(await response.json()); // ok
console.log(await response.json()); // error

讀取第二次就會報錯。解決方法也非常簡單,clone response 後纔讀取,就可以了。

const response = await fetch('https://192.168.1.152:44300/products');
const response2 = response.clone();

console.log(await response.json());  // ok
console.log(await response2.json()); // ok

 

Response Header

從 response.headers 對象中獲取

複製代碼
const response = await fetch('https://192.168.1.152:44300/products');
console.log(response.headers.get('Content-Type')); // 'application/json; charset=utf-8'

// for loop all response headers
for (const [key, value] of response.headers.entries()) {
  console.log([key, value]); // ['content-type', 'application/json; charset=utf-8']
}
複製代碼

如果遇到重複的 header 那麼它會合並起來

key 是 'cutom',value 是 'a, b'

 

Cross-Origin 跨域請求攜帶 Cookie

跨域主要是服務端的職責,不熟悉的可以看這篇 ASP.NET Core – CORS (Cross-Origin Resource Sharing)

客戶端和跨域有關的是 Cookie,

它有兩種情況:

  1. 請求同跨 (same-origin)
    默認情況下會攜帶 Cookie
    如果不希望攜帶 Cookie 可以設置
    const response = await fetch('https://192.168.1.152:44300/products', {
      credentials: 'omit',
    });
  2. 請求跨域 (cross-origin)
    默認情況下不攜帶 Cookie
    如果希望攜帶 Cookie 可以設置
    const response = await fetch('https://192.168.1.152:44300/products', {
      credentials: 'include',
    });

credentials 還有一個值是 'same-origin'

const response = await fetch('https://192.168.1.152:44300/products', {
  credentials: 'same-origin',
});

它其實就是默認設置,意思是 same-origin 時要攜帶 Cookie。所以我們不設置就相等於設置了 credentials: 'same-origin'。

 

Request Error

Request Error 指的是請求失敗,通常是 offline 造成的。

status 400-5xx 這些可不算 Request Error 哦,因爲這些已經是有 response 成功了,只是在 status 被區分是有問題的。

要處理 Request Error,可以使用 try catch。

複製代碼
try {
  const response = await fetch('https://192.168.1.152:44300/products');
} catch (error) {
  if(error instanceof TypeError) {
    console.log(error.name);    // TypeError
    console.log(error.message); // Failed to fetch
  }
}
複製代碼

其實它就是處理 Promise Rejection,使用 .then().catch 或 .then(() => {}, () => {}) 都可以。

 

Abort Request

我們可以在任意時間終止一個 request。

創建 abort controller

const abortController = new AbortController();

監聽 abort 事件

abortController.signal.addEventListener('abort', () => {
  console.log('The request has been aborted, reason: ' + abortController.signal.reason);
});

signal.reason 是 abort 的原因,這點比 XMLHttpRequest 更好,XMLHttpRequest 無法表述原因。

把 AbortController.signal 設置到 fetch request

await fetch('https://192.168.1.152:44300/products', {
  signal: abortController.signal,
});

兩秒鐘後執行 abortController.abort 方法。

setTimeout(() => {
  abortController.abort('reason of abort'); // abort 時可以聲明原因。
}, 2000);

fetch 內部會監聽 abort 事件,當 abort 時會終止請求,並且通知服務端,請求被終止了。

ASP.NET Core 有一個 cancellationToken,可以隨時查看是否客戶端已經終止請求, 如果已經終止,那就不需要繼續執行了。

Abort 會導致 fetch reject promise

複製代碼
try {
  const response = await fetch('https://192.168.1.152:44300/products', {
    signal: abortController.signal,
  });
} catch (error: unknown) {
  if (error instanceof DOMException && error.name === 'AbortError') {
    console.log(error.message); // The user aborted a request.
    console.log(abortController.signal.reason); // reason of abort
  }
}
複製代碼

通過 catch + error instanceof DOMException,我們可以識別出 error 來自 abort。

 

Request Timeout

XMLHttpRequest 可以通過 request.timeout 設置超時限制,Fetch 沒有類似的 built-in 設置。

我們只可以通過 Abort Request 來實現超時限制。

複製代碼
const timeoutAbortController = new AbortController();

const timeoutNumber = setTimeout(() => {
  timeoutAbortController.abort('Timeout, the request is taking more than five seconds.');
}, 5000);

try {
  await fetch('https://192.168.1.152:44300/products', {
    signal: timeoutAbortController.signal,
  });
  clearTimeout(timeoutNumber);
} catch (error) {
  if (error instanceof DOMException && error.name === 'AbortError' && (timeoutAbortController.signal.reason as string).startsWith('Timeout')
  ) {
    console.log(timeoutAbortController.signal.reason);
  }
}
複製代碼

 

Download File

除了請求 JSON 數據,偶爾我們也會需要下載文件。

download txt file

const response = await fetch('https://192.168.1.152:44300/data.txt');
const memoryStream = await response.arrayBuffer();
const bytes = new Uint8Array(memoryStream);
const textDecoder = new TextDecoder();
const text = textDecoder.decode(bytes);
console.log(text); // 'Hello World'

關鍵是 response.arrayBuffer 方法,它會返回 ArrayBuffer,再通過 Uint8Array 和 TextDecoder 從 ArrayBuffer 讀取 data.txt 的內容。

Download Video

Video 通常 size 比較大,用 ArrayBuffer 怕內存會不夠,所以用 Blob 會比較合適。

const response = await fetch('https://192.168.1.152:44300/video.mp4');
const blob = await response.blob();
console.log(blob.size / 1024); // 124,645 kb
console.log(blob.type);        // video/mp4

download progress

文件大下載慢,最好可以顯示進度條

複製代碼
const response = await fetch('https://192.168.1.152:44300/video.mp4');
const reader = response.body!.getReader();
const totalBytes = +response.headers.get('Content-Length')!;
let loadedBytes = 0;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  loadedBytes += value.length;
  const percentage = ((loadedBytes / totalBytes) * 100).toFixed() + '%';
  console.log(percentage);
}
複製代碼

XMLHttpRequest 有 progress 事件,Fetch 沒有,所以要獲取進度會比較麻煩。

首先需要通過 header Content-Length 獲取 total。

接着利用 response body (類型是 ReadableStream) reader 分段式讀取 response 內容。

然後拿每一次的內容長度做累加,最終計算出進度。

partial data on downloading

XMLHttpRequest 幾乎無法做到分段式讀取 response 內容 (即便做到也需要很多前提條件),但 Fetch 很容易,方案也很完整。

上面計算進度的例子中,我們就用到了分段式讀取。

複製代碼
const response = await fetch('https://192.168.1.152:44300/video.mp4');
const reader = response.body!.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value); // value 的類型是 Uint8Array
}
複製代碼

 

POST Request

POST 和 GET 大同小異

POST JSON Data

複製代碼
const product = { name: 'iPhone12' };
const productJson = JSON.stringify(product);

const response = await fetch('https://192.168.1.152:44300/products', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: productJson,
});

console.log(response.status);       // 201
console.log(await response.json()); // { id: 1, name: 'iPhone12' }
複製代碼

JSON 格式需要添加 request header 'Content-Type',然後 body 放要發送的 JSON 數據就可以了。

POST FormData or FormUrlEncoded (multipart/form-data or application/x-www-form-urlencoded)

POST FormData or FormUrlEncoded 和 POST JSON data 大同小異

複製代碼
// POST multipart/form-data
const productFormData = new FormData();
productFormData.set('name', 'iPhone12');

// POST application/x-www-form-urlencoded
const productFormUrlEncoded = new URLSearchParams({
  name: 'iPhone12',
});

const response = await fetch('https://192.168.1.152:44300/products', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
  },
  body: productFormData,          // POST multipart/form-data
  // body: productFormUrlEncoded, // POST application/x-www-form-urlencoded
});
複製代碼

只是把 send 的數據從 JSON 換成 FormData or FormUrlEncoded 就可以了。

另外,FormData or FormUrlEncoded 不需要設置 request header 'Content-Type',遊覽器會依據發送的數據類型自動添加,JSON 之所以需要是因爲遊覽器把 JSON 視爲 text/plain。

 

POST Binary (Blob)

FormData 支持 Blob 類型的 value,所以我們可以使用 FormData 上傳二進制文件。

複製代碼
const productFormData = new FormData();
productFormData.set('name', 'iPhone12');
const productDocument = 'Product Detail';
const textEncoder = new TextEncoder();
const bytes = textEncoder.encode(productDocument);
const blob = new Blob([bytes], {
  type: 'text/plain',
});
productFormData.set('document', blob);

const response = await fetch('https://192.168.1.152:44300/products', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
  },
  body: productFormData,
});
複製代碼

或者直接發送 Blob 也是可以的。

複製代碼
const productDocument = 'Product Detail';
const textEncoder = new TextEncoder();
const bytes = textEncoder.encode(productDocument);
const blob = new Blob([bytes], {
  type: 'text/plain',
});

const response = await fetch('https://192.168.1.152:44300/products', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
  },
  body: blob,
});
複製代碼

如果二進制沒有明確類型,type 就放 application/octet-stream。

upload progress

非常遺憾,Fetch 完全不支持 upload progress。這也是爲什麼 XMLHttpRequest 還沒有完全被取代的原因。

Chromium 105 推出了 Streaming requests with the fetch API 可以解決這個問題,但是其它遊覽器支持度不高。

  

Request ReadyState

XMLHttpRequest 有 request.readyState 和 readystatechange 事件可以獲知 Request 的不同階段。

Fetch 沒有這些,我們需要自己寫

複製代碼
const response = await fetch('https://192.168.1.152:44300/video.mp4');
console.log('readystatechange', '2. headers received');
const total = +response.headers.get('Content-Length')!;
const reader = response.body!.getReader();
console.log('readystatechange', '3. loading');
let loaded = 0;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  loaded += value!.length;
  console.log('progress', [total, loaded]);
}
console.log('readystatechange', '4. done');
複製代碼

類似這樣。

 

 

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