前端異步 AJAX 請求的重複使用
Tags: javascript, vue
Published: 2020-07-22 01:54:19

在開發前端時，我們非常常使用 AJAX 來異步資料並動態渲染在頁面上，但是在遇到一連串的相同資料都要進行請求時，就有可能對同一個 API Endpoint 發出併發請求，然而，因為這些請求是同時發出，因此 Response 也非常可能是相同的，這樣講可能不夠清楚，直接寫一個簡易的範例來解釋這個情況。

---

## 實際範例

首先我們先撰寫一個 API：

```
https://localhost:3000/api/v1/users/:uuid
```

這個 API 的回傳值如下：

```json
{
    "name":"Username{uuid}",
    "uuid":"{uuid}"
}
```

接著開一個 Vue SPA 專案，並且先透過 Axios 寫一個異步請求的函數：

```js
// 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) 來負責渲染並請求資料：

```vue
<template>
    <div v-if="init">
        <ul>
            <li>{{user.name}}</li>
            <li>{{user.uuid}}</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 中：

```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>
```

接著我們看一下顯示結果：

![顯示結果](https://dev-to-uploads.s3.amazonaws.com/i/4jchqi84ec85rcwqmekl.png)

這樣就正確顯示了，然而這裡有一個問題非常直接注意：

![併發請求](https://dev-to-uploads.s3.amazonaws.com/i/1dh872wdyak28cmol95d.png)

我們打開開發者模式就會發現，每個元件向該 API 發出了請求，因此就產生了 10 次的併發請求，但是在這種情況下，實際上我們僅需要讓一個請求出去，另外 9 個元件等待這個請求的 Response 然後複用即可。

## 改進的方法

接下來將講解要如何實現對於在同一個時間內僅對指定 API 請求一次並複用請求，我們會用到這個元件，這個元件有點類似 Node.js 中的 EventEmitter，主要就是用於收發事件。

https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget


接著我們改寫 fetchUser() 函數：

```js
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));
                reject(e);

            })
        }
        // 當目前指定的 URI 處於請求狀態，則不做任何事情
        else {

            // 向成功的事件監聽器註冊，當完成之後 resolve()
            eventEmitter.addEventListener(uri, (event) => {
                resolve(event.data);
            });

            // 失敗之後 reject()
            errorEmitter.addEventListener(uri, (event) => {
                reject(event.data);
            })
        }
    });
};
```

接著我們重新運行前端應用程式並查看結果：

![顯示結果](https://dev-to-uploads.s3.amazonaws.com/i/4jchqi84ec85rcwqmekl.png)

結果與一開始一模一樣，但這時候我們打開開發者模式就會發現：

![併發請求消失](https://dev-to-uploads.s3.amazonaws.com/i/4qni5gmvv5uthzulqchf.png)

請求已經被減少到剩下一個了，這是因為所有的元件都重複使用了同一個 Response。透過這種方法將可以大大減少伺服器的負載以及前端的運行時間。

## 結論

並不是每一種情況下都可以使用這種方式來請求資料，如：每次請求資料都一定會不一樣的 API 就不能使用這種方式進行 API 呼叫，但是像是上述範例中的用戶資料、電商網站中的商品資料或是部落格的文章等，這類能夠確保在極短時間之內資料都相同的 API 就可以使用這種方式來進行呼叫。