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 內進行操作
此方法為最開始普遍使用的請求方式,但實際上因為其過於複雜的撰寫結構與呼叫函式,導致在 jQuery 開始流行並加入 ajax 方法後,就迅速地退出了歷史的舞台
// 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;
在現在的網頁開發中,其實有很多的框架或函示庫都包含了 Http Request 的操作,但當中最為人所知的,目前還是以 jQuery 為主,因為在當時的開發體驗上,若要將前端網站單獨拆解出來接收資料的話,jQuery 所提供的這套方式,確實是一個最佳首選
// jQuery
$.ajax({
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function (data) {
console.log(data);
}
});
可引入下方函式庫連結測試:
fetch 是在 ES6 中所提供的 request 的新方法,他使用上相較之前的方法有明顯的優化,同時也有一些與過往設計上不同的地方
MDN 官網介紹
// Fetch
fetch('https://randomuser.me/api/', {})
.then((response) => {
console.log(response);
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log('錯誤:', err);
});
Axios 的常用做法與 Fetch 雷同,均可建立在 Promise 的基礎上進行設計的,同時由於底層是使用原生 XHR 進行封裝,因此在 node.js 中也可輕鬆使用也具有較高的支援度,且支持並發請求等,現階段的定位有點像過往技術的整體優化,並獨自提供了蠻多方便的實體建立與攔截器
Axios Github 文件
// 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');
});
可引入下方函式庫連結測試:
Swagger 是許多團隊愛用的 api 開發輔助工具,他可以在有支援的開發語言中直接使用,並且直接產生 api 的相關文件。
官方網站連結:swagger
範例:https://catfact.ninja/
Postman 是一個老牌的開發者工具,他可以將 api 進行條列與分組等操作,並提供各種參數的調整與紀錄等,是早期許多工程師起初會使用的工具
官方網站連結:Postman
與 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
在 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"]
});
<!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>
<!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>
<!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>