前端異步 AJAX 請求的重複使用
在開發前端時,我們非常常使用 AJAX 來異步資料並動態渲染在頁面上, 但是在遇到一連串的相同資料都要進行請求時, 就有可能對同一個 API Endpoint 發出併發請求, 因為這些請求是同時發出, 因此 Response 也是相同的 本文章將透過寫一個簡易的範例來解釋這個情況。
實際範例
首先我們先撰寫一個 API:
https://localhost:3000/api/v1/users/:uuid
這個 API 的回傳值如下:
{
"name":"Username{uuid}",
"uuid":"{uuid}"
}
接著開一個 Vue SPA 專案,並且先透過 Axios 寫一個異步請求的函數:
// fetch-user.js
const axios = require('axios');
module.exports = (uuid) => {
let uri = `http://localhost:3000/users/${uuid}`;
return new Promise(resolve => {
axios.get(uri).then(resolve);
})
};
然後我們在 Vue 專案中新增一個 User Component(User.vue) 來負責渲染並請求資料:
<template>
<div v-if="init">
<ul>
<li></li>
<li></li>
</ul>
</div>
</template>
<script>
const fetchUser = require('../lib/fetch-user');
export default {
name: 'User',
data: function() {
return {
init: false,
user: null
}
},
props: {
uuid: String
},
async mounted() {
const response = await fetchUser(this.uuid);
this.init = true;
this.user = response.data;
}
}
</script>
最後將 user component 放入 App.vue 中:
<template>
<div id="app">
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
<user uuid="user-uuid"></user>
</div>
</template>
<script>
import User from './components/User';
export default {
name: 'App',
components: {
User
}
}
</script>
接著我們看一下顯示結果:
這樣就正確顯示了,然而這裡有一個問題非常直接注意:
我們打開開發者模式就會發現,每個元件向該 API 發出了請求,因此就產生了 10 次的併發請求,但是在這種情況下,實際上我們僅需要讓一個請求出去,另外 9 個元件等待這個請求的 Response 然後複用即可。
改進的方法
接下來將講解要如何實現對於在同一個時間內僅對指定 API 請求一次並複用請求: 我們會用到這個元件,這個元件有點類似 Node.js 中的 EventEmitter,主要就是用於收發事件。
EventTarget Documentation | MDN web docs
https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget
接著我們改寫 fetchUser() 函數:
const axios = require('axios');
/**
* 這個 class 是用於儲存 Response Data 的 Event 衍生類
*/
class FetchCompleteEvent extends Event
{
constructor(type, data) {
super(type);
this.data = data;
}
}
// 用於請求成功時使用的事件監聽器
const eventEmitter = new EventTarget();
// 用於請求失敗時使用的事件監聽器
const errorEmitter = new EventTarget();
/**
* 用於儲存 URI 以及是否當前正在請求的對應,如:
* http://localhost:8000/users/foo => true 代表已經發出請求,正在等待 Response
* http://localhost:8000/users/bar => false 代表當前沒有請求在路上
*/
const requestingList = new Map();
module.exports = (uuid) => {
let uri = `http://localhost:3000/users/${uuid}`;
return new Promise((resolve, reject) => {
// 如果沒有紀錄,或者尚未處於請求狀態
if(!requestingList.has(uri) || !requestingList.get(uri)) {
// 進入之後立即將請求狀態設為 true
requestingList.set(uri, true);
// 請求 URI
axios.get(uri).then(response => {
// 完成請求之後將請求狀態設為 false
requestingList.set(uri, false);
// 發出一個事件通知來告訴 callback 請求完成了
eventEmitter.dispatchEvent(new FetchCompleteEvent(uri, response));
resolve(response);
}).catch((e) => {
// 請求失敗也算是請求完成,將請求狀態設為 false
requestingList.set(uri, false);
// 發出一個事件通知來告訴 callback 請求失敗了
errorEmitter.dispatchEvent(new FetchCompleteEvent(uri, e));
resolve(e);
})
}
// 當目前指定的 URI 處於請求狀態,則不做任何事情
else {
// 向成功的事件監聽器註冊,當完成之後 resolve()
eventEmitter.addEventListener(uri, (event) => {
resolve(event.data);
});
// 失敗之後 reject()
errorEmitter.addEventListener(uri, (event) => {
reject(event.data);
})
}
});
};
接著我們重新運行前端應用程式並查看結果:
結果與一開始一模一樣,但這時候我們打開開發者模式就會發現:
請求已經被減少到剩下一個了,這是因為所有的元件都重複使用了同一個 Response。透過這種方法將可以大大減少伺服器的負載以及前端的運行時間。
結論
並不是每一種情況下都可以使用這種方式來請求資料,如:每次請求資料都一定會不一樣的 API 就不能使用這種方式進行 API 呼叫,但是像是上述範例中的用戶資料、電商網站中的商品資料或是部落格的文章等,這類能夠確保在極短時間之內資料都相同的 API 就可以使用這種方式來進行呼叫。