iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Modern Web

給前端新手的圖文故事系列 第 19

學習 API 請求與陳諾的概念

  • 分享至 

  • xImage
  •  

簡單 api 技術探討

API 是 Application Programming Interface 的縮寫,中文得直譯是「應用程式介面」。單看起來是一個非常抽象的字串,但其實在實際應用程面並沒有那麼難以理解,以下讓我們繼續看下去: 介紹影片

API 的基礎概念其實可以理解成橋樑,它連接了用戶端的伺服器端的資料傳輸,並且美化了其中的過程,以現實的範例舉例,您可以把它想成餐廳裡負責與您溝通的服務員,你的任務是負責給他菜單等需求,而他則是會跑去跟後廚的窗口確認內容無誤,並端回你所期望的餐點

簡單範例操作

https://jsonplaceholder.typicode.com/

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => response.json())
  .then(json => console.log(json))

此時網頁 console 將會顯示取回的資料,並且轉換為 JSON 格式,我們可以在第二個 .then 內進行操作

四種從古至今天主要操作方式

01_XMLHttpRequest

基本介紹

此方法為最開始普遍使用的請求方式,但實際上因為其過於複雜的撰寫結構與呼叫函式,導致在 jQuery 開始流行並加入 ajax 方法後,就迅速地退出了歷史的舞台

所遇問題

  • 過於複雜的請求構築,再加入 UI 以及其他呼叫時過於雜亂
  • 需藉由複雜的 callback 與其他操作完善體驗與重構
  • 使用社群基本已漸漸消失(除了較舊專案外)
// XMLHttpRequest
function reqOnload() {
	const data = JSON.parse(this.responseText);
	console.log(data);
}
function reqError(err) {
	console.log('錯誤', err)
}
// 宣告一個 XHR 的物件
var request = new XMLHttpRequest();
// 定義連線方式
request.open('get', 'https://randomuser.me/api/', true);
// 送出請求
request.send();
// 如果成功就執行 reqOnload()
request.onload = reqOnload;
// 失敗就 reqError()
request.onerror = reqError;

02_jQuery ajax

基本介紹

在現在的網頁開發中,其實有很多的框架或函示庫都包含了 Http Request 的操作,但當中最為人所知的,目前還是以 jQuery 為主,因為在當時的開發體驗上,若要將前端網站單獨拆解出來接收資料的話,jQuery 所提供的這套方式,確實是一個最佳首選

官方文件

所遇問題

  • jQuery 作為一個流行一時的前端技術,一些版本存在著資安上的疑慮
  • 函式庫本身太過龐大,會載入大量不必要的程式
  • 基於XHR作為開發基底,整體架構實際上不好理解且不夠清晰
  • ES6 推出了新的解決方法
// jQuery
$.ajax({
  url: 'https://randomuser.me/api/',
  dataType: 'json',
  success: function (data) {
    console.log(data);
  }
});

可引入下方函式庫連結測試:

03_Fetch(ES6 原生)

基本介紹

fetch 是在 ES6 中所提供的 request 的新方法,他使用上相較之前的方法有明顯的優化,同時也有一些與過往設計上不同的地方
MDN 官網介紹

主要差異

  • fetch 將會使用同為 ES6 的 Promise 物件進行回應
  • 因應使用了 Promise 進行回應,所以需要透過 .then 與 .catch 進行後續操作
  • 回傳的資料需要再藉由相應的方法轉為可用的資料型態(JSON,BLOB)

所遇問題

  • 對 400,500 等錯誤訊息都會當成功的請求,需使用 catch 等方法自行處理
  • 預設是沒有攜帶 cookie 等驗證用資料(都需要自行新增)
  • 較早的瀏覽器版本並不支援
  • 無法監聽請求進度與處理 timeout 問題
// Fetch

fetch('https://randomuser.me/api/', {})
  .then((response) => {
    console.log(response);
    return response.json();
  })
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log('錯誤:', err);
  });

04_Axios

基本介紹

Axios 的常用做法與 Fetch 雷同,均可建立在 Promise 的基礎上進行設計的,同時由於底層是使用原生 XHR 進行封裝,因此在 node.js 中也可輕鬆使用也具有較高的支援度,且支持並發請求等,現階段的定位有點像過往技術的整體優化,並獨自提供了蠻多方便的實體建立與攔截器
Axios Github 文件

所遇問題(瑕不掩瑜)

  • 需要學習額外非原生的操作方式
  • 需額外載入一包檔案(約13kb)

所遇問題(瑕不掩瑜)

  • 支援防 CSRF 攻擊
  • 可以在 node.js 中使用
  • 支持 Promise API
  • 提供並發請求功能
  • 易於使用且方便擴展
  • 自動轉換 JSON
// Axios

axios
  .get('https://randomuser.me/api/')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .finally(function () {
    // always executed
    console.log('I always Execued');
  });

可引入下方函式庫連結測試:

GUI 工具簡單介紹

Swagger

Swagger 是許多團隊愛用的 api 開發輔助工具,他可以在有支援的開發語言中直接使用,並且直接產生 api 的相關文件。

官方網站連結:swagger
範例:https://catfact.ninja/

Postman

Postman 是一個老牌的開發者工具,他可以將 api 進行條列與分組等操作,並提供各種參數的調整與紀錄等,是早期許多工程師起初會使用的工具
官方網站連結:Postman

Insomnia

與 Postman 幾乎相同的功能,但勝在目前是 Open Source 的應用,因此目前有很多開發者改為使用這個應用程式
官方網站連結:Insomnia

我們為什麼會需要非同步的操作

以我們過往所學習到的內容來看,會發現 JavaScript 的程式基本上會由上往下進行閱讀,並且逐步地進行操作,而這一切我們就將其稱之為同步操作,也是我們最主要的操作模式。

但在實際上操作時,我們其實會遇到所謂的非同步操作,以我們課程開始的範例來說,就是餐廳的服務生在去後廚確認餐點或送餐時,是無法繼續為客戶提供服務的,但整個進餐過程並不會停止(同步流程將繼續進行),服務生也會在餐點完成後再將其送上(非同步完成作業),而將這一些流程完整串連起來,才會是一個完整的用餐流程(程式流程)。

下面的範例我們設置了一個簡單的計時器,並模擬他在 2000 毫秒(2秒)後執行操作,以模擬 api 在請求時網路的執行時間。

var str = 'init';

const wait = () => {
  str = '我改變了';
  console.log(str); // 我會在兩秒後被印出來
};

setTimeout(wait, 2000);

console.log(str);

此處我們設置了一個兩秒後觸發的計時器進行操作,來模擬並不會即時回傳的 api 內容

過往常用的操作方式

在一段很常的時間內,我們都是採用 jQuery 的 ajax 方式,而原因總結起來其實就是官方所提供的 ajax 實在是太難操作了,因此在 jQuery 推出簡化版本之後,大多數的開發者都轉而使用了這個相對而言合理不少的操作。

先在在 HTML 引入 jQuery

<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>

在 script 或 .js 的外部檔案進行撰寫

// 使用過去常用的 JQuery 的 ajax 送出請求
let data = {};

$.ajax({
  url: 'https://jsonplaceholder.typicode.com/todos/1',
  dataType: 'json',
  // async: false, // 使用 async 將 ajax 請求改為同步
  success: function (status) {
    data = status;
    console.log(data);
  },
});

//data 在同步印出時將會是空物件,而在兩秒後印出的將會帶有資料(如果 api 有正常回傳的話)
console.log(data);

setTimeout(() => {
  console.log(data);
}, 2000);

了解為何會需要新的語法與操作方式

在傳統的 JavaScript 開發中,除非是很熟練的開發者,不然都很容易建造出所謂的毀滅金字塔,而實際上就算是非常熟練的開發者,在時間不足亦或是邏輯太過複雜的情況下,也很難避免他的出現,因此對於非同步語法的更新,其實著實有大大的降低我們開發的難易度

回呼地獄(callback hell)或稱毀滅金字塔(pyramid of doom) 在正規實例中還會更多資料

listen("click", function handler(evt) {
	setTimeout(function request() {
		ajax("http://test.url.1"), function response(text) {
			if (text == "hello") {
				handler()
			} else if (text == "world") {
				request()
			}
		}
	}, 500);
})

建立本範例線性思維

首先(now)
listen("..",function handler(..){
	//...
})

這之後(later)
setTimeout(function request() {
	//...
}, 500);

再之後(later)
ajax("..",function response(..){
	//...
})

最後(end)
if (text == "hello") {
	handler()
} else

暸解 Promise

Promise(承諾)是什麼?他有什麼意義?

在 JavaScript ES6 中,Promise 加入了我們這個大家庭,實際上在過往的 JavaScript 內,我們對於非同步的處理,大多數是採用 callbacks 的操作方式,而這麼做的弊端,我們可以在下面的範例中觀賞一下 回呼地獄(callback hell)或稱毀滅金字塔(pyramid of doom),即使我們可透過封裝函示與重構等方式去將她優雅化,但依舊會有著更多的問題以及作用域產生,最後您的開發體驗就會像是偵探遊戲一樣,只是您同時會扮演偵探與犯人的角色.

相關文件
Promise(承諾) 就如同其名一樣,以現實範例舉例,就像是你去了一間餐廳點送出菜單之後,你會得到一個明細或收據亦或是號碼牌(Promise),而當餐點完成之時,服務生就會將餐點交付給你,而依據即為你手上手持有的票據等.
當然,Promise 有時會得到一些令人沮喪的結果,如在菜單送出之後,店員跑來跟你說您點的餐點售完了等等(ERROR),此時就會進去另一段的流程處理,但那就是後話了.

下圖是 Promise 的生命週期

建立一個最簡單的 Promise(承諾)流程

new Promise((resolve, reject) => {
  console.log('進行初始化操作');
  resolve();
})
  .then(() => {
    throw new Error('Something failed'); // 拋出錯誤讓 catch 去獲取,註解掉將
    console.log('成功操作');
  })
  .catch(() => {
    console.log('失敗操作');
  })
  .then(() => {
    console.log('不管成功或失敗都會執行');
  })
  .then(() => {
    console.log('不管成功或失敗都會執行');
  });

模擬非同步操作方式

// 建立一個新的 Promise 物件並加以操作
// 在這個 Promise 物件後進行 .then (然後)的操作,可以在資料回傳後進行近一步的操作
const newPromise = new Promise((resolve, reject) => {
  // 設置一個三秒的計時器進行非同步的操作
  setTimeout(() => {
    // 做一個布林判斷模擬 Promise 的成功或失敗
    if (Math.random() > 0.5) {
      // 在已經解決(成功)的情況下輸入以下資料
      resolve('changed');
    } else {
      // 在已經被拒絕(失敗)的情況下輸入以下資料
      reject('error');
    }
  }, 3000);
})
  .then((data) => {
    // 顯示 resolve 被輸入的資料
    console.log(data);
  })
  .catch((error) => {
    // 使用 catch 取得 reject 內部輸入的資料
    console.log(error);
  });

一次執行多個承諾項目 Promise.all

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'foo');
});

Promise.all([p1, p2, p3]).then((values) => {
  console.log(values); // [3, 1337, "foo"]
});

API 實際範例操作

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fetch-demo</title>
    <style>
      .todo-list img {
        width: 200px;
      }
    </style>
  </head>
  <body>
    <p class="product"></p>
    <ul class="product-list"></ul>
    <script>
      const productList = document.querySelector('.product-list');
      const product = document.querySelector('.product');

      fetch('https://fakestoreapi.com/products')
        .then((res) => res.json())
        .then((json) => {
          productList.innerHTML = json
            .map(
              ({ id, title, image, description }) =>
                `<li onClick="findProductItem(${id})">${title}<p>${description}</p><img src="${image}" /></li>`,
            )
            .join('');
        });

      const findProductItem = (id) => {
        fetch(`https://fakestoreapi.com/products/${id}`)
          .then((res) => res.json())
          .then((json) => (product.innerHTML = json.title));
      };
    </script>
  </body>
</html>

更加完整的範例操作

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 參考網址:https://jsonplaceholder.typicode.com/guide/
      // // 取得本地的資料
      // fetch('products.json')
      //   .then((response) => response.json())
      //   .then((json) => console.log(json));

      // (GET) 列表頁取得操作 - 預設操作
      fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'GET',
      })
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (GET) 列表頁取得操作 - 預設操作',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      // (POST) 列表頁項目新增
      fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        body: JSON.stringify({
          title: 'foobar',
          body: 'bar',
          userId: 1,
        }),
        headers: {
          'Content-type': 'application/json; charset=UTF-8',
        },
      })
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (POST) 列表頁項目新增',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      // (PUT) 列表頁項目修改
      fetch('https://jsonplaceholder.typicode.com/posts/1', {
        method: 'PUT',
        body: JSON.stringify({
          id: 1,
          title: 'foo',
          body: 'bar',
          userId: 1,
        }),
        headers: {
          'Content-type': 'application/json; charset=UTF-8',
        },
      })
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (PUT) 列表頁項目修改',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      // (PATCH) 列表頁項目修改 - 只修改有傳入的資料
      fetch('https://jsonplaceholder.typicode.com/posts/1', {
        method: 'PATCH',
        body: JSON.stringify({
          title: 'foo',
        }),
        headers: {
          'Content-type': 'application/json; charset=UTF-8',
        },
      })
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (PATCH) 列表頁項目修改 - 只修改有傳入的資料',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      //(DELETE) 列表頁項目刪除
      fetch('https://jsonplaceholder.typicode.com/posts/1', {
        method: 'DELETE',
      })
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (DELETE) 列表頁項目刪除',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      //(GET) 列表頁取得操作 - 帶參數
      fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c (GET) 列表頁取得操作 - 帶參數',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });

      // (GET) 政府 api get 操作
      // 操作範例網址:https://data.gov.tw/dataset/25768
      // 操作範例提供的文件:https://data.wra.gov.tw/openapi/swagger/index.html
      fetch(
        'https://data.wra.gov.tw/OpenAPI/api/OpenData/2A49B760-3C0E-4288-B087-D71D6CB360E6/Data?size=100&page=1',
      )
        .then((response) => response.json())
        .then((json) => {
          console.log(
            '%c 政府 api get 操作',
            'color: orange; font-weight: bold',
          );
          console.log(json);
        });
    </script>
  </body>
</html>

Axios 實際範例操作

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <script>
      const jsonPlaceholderAPI = axios.create({
        baseURL: 'https://jsonplaceholder.typicode.com',
        // 逾時操作:當 api 所設定的時間抵達後還沒回傳資料時,自動中斷操作
        timeout: 5000,
      });

      const apiTodoList = () => jsonPlaceholderAPI.get('/todos');
      const apiTodoListItem = (id) => jsonPlaceholderAPI.get(`/todos/${id}`);
      const apiTodoListRemoveItem = (id) =>
        jsonPlaceholderAPI.delete(`/todos/${id}`);

      apiTodoList().then((data) => console.log(data.data));
      apiTodoListItem(1).then((data) => console.log(data.data));
    </script>
  </body>
</html>

Axios api 管理優化

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <script>
      // 增加請求時的攔截器
      axios.interceptors.request.use(
        function (config) {
          // 在發送請求之前進行操作
          return config;
        },
        function (error) {
          // 對請求的錯誤進行一些操作
          return Promise.reject(error);
        },
      );

      // 增加響應的攔截器
      axios.interceptors.response.use(
        function (response) {
          // 2xx 範圍内的狀態號碼都會出發操作。
          // 對回傳的資料進行一些操作么
          return response;
        },
        function (error) {
          // 超過 2xx 範圍的狀態號碼都會出發操作。
          // 對響應的錯誤進行一些操作
          return Promise.reject(error);
        },
      );

      axios
        .get('https://jsonplaceholder.typicode.com/todos/1')
        .then((json) => console.log(json));
    </script>
  </body>
</html>

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <script>
      const personAPI = axios.create({
        baseURL: 'https://randomuser.me/',
      });

      const login = () => {
        personAPI.defaults.headers.Authorization = `Bearer sssssssssss`;
      };

      const logout = () => {
        delete personAPI.defaults.headers.Authorization;
      };

      // 增加響應的攔截器
      personAPI.interceptors.response.use(
        function (response) {
          // 2xx 範圍内的狀態號碼都會出發操作。
          // 對回傳的資料進行一些操作么
          return response;
        },
        function (error) {
          logout();
          return Promise.reject(error);
        },
      );

      personAPI.get(`/api`).then(function (response) {
        console.log(response.data);
      });
    </script>
  </body>
</html>


上一篇
了解 JSON 以及應用 LocalStorage
下一篇
暸解 SPA 基礎概念與介紹現今的前端架構
系列文
給前端新手的圖文故事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言