ตัวอย่างการเรียก Ajax (XHR) แบบหลายการเชื่อมต่อพร้อมกันแบบมีขีดจำกัด

ในการเรียกใช้ Ajax หรือ XHR นั้นบางครั้งจำเป็นจะต้องเรียกหลาย URL ต่อครั้ง ยกตัวอย่างเช่นการอัพโหลดแบบ chunk เป็นต้น. การเรียก 1 ครั้ง ปกติแล้ว JavaScript จะไม่รอให้ว่าจะสำเร็จก่อนแล้วค่อยไปต่อหรืออย่างไร แต่มีเท่าไหร่มันก็สั่งเรียกให้หมดเลย ทำให้บางครั้งเกิดการเรียก URL เต็มจนท่วม server ส่งผลให้ firewall มองว่าเป็นการโจมตีแล้วทำการบล็อคไปเลย เช่นนี้เป็นต้น.

การจำกัดจำนวนการเรียกใช้ Ajax หลายๆรายการต่อครั้งจึงเป็นสิ่งจำเป็น. ต่อไปนี้เป็นโค้ดตัวอย่าง

/**
 * Function สำหรับเรียก XHR อย่างง่ายๆ จะเปลี่ยนไปใช้ jQuery.ajax() ก็ได้เช่นกัน.
 */
function XHR(url, formData) {
    return new Promise((resolve, reject) => {
        let XHR = new XMLHttpRequest();

        XHR.addEventListener('abort', (event) => {
            reject({'response': '', 'status': (event.currentTarget ? event.currentTarget.status : ''), 'event': event});
        });
        XHR.addEventListener('error', (event) => {
            reject({'response': '', 'status': (event.currentTarget ? event.currentTarget.status : ''), 'event': event});
        });
        XHR.addEventListener('timeout', (event) => {
            reject({'response': '', 'status': (event.currentTarget ? event.currentTarget.status : ''), 'event': event});
        });
        XHR.addEventListener('load', (event) => {
            let response = XHR.response;

            let headers = XHR.getAllResponseHeaders();
            let headerMap = {};
            if (headers) {
                let headersArray = headers.trim().split(/[\r\n]+/);
                headersArray.forEach(function (line) {
                    let parts = line.split(': ');
                    let header = parts.shift();
                    let value = parts.join(': ');
                    headerMap[header] = value;
                });
            }

            if (event.currentTarget && event.currentTarget.status >= 200 && event.currentTarget.status < 300) {
                resolve({'response': response, 'status': event.currentTarget.status, 'event': event, 'headers': headerMap});
            } else {
                reject({'response': response, 'status': event.currentTarget.status, 'event': event, 'headers': headerMap});
            }
        });

        XHR.open('POST', url);
        XHR.responseType = 'json';
        XHR.send(formData);
    });// end new Promise
}// XHR

xhr.js

<?php
// server.php

sleep(1);

$output = [];

$output['round'] = (int) ($_GET['round'] ?? 0);

header('Content-Type: application/json');

echo json_encode($output);

server.php

<!--demo.html-->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>XHR multi concurrent connection</title>
        <style>
            #debug {
                border: 3px dashed #ccc;
                margin: 10px 0;
                padding: 10px;
            }
        </style>
    </head>
    <body>
        <form id="upload-form" method="post">
            <input type="hidden" name="hidden-name" value="hidden-value (unnecessary)">

            <button type="submit">Submit</button>

            <div id="debug"></div>
        </form>

        <script src="xhr.js"></script>
        <script type="application/javascript">
            let debugElement = document.getElementById('debug');
            const thisForm = document.getElementById('upload-form');
            const maxConcurrentConnection = 3;

            thisForm.addEventListener('submit', (event) => {
                event.preventDefault();

                let loopPromise = Promise.resolve();
                let ajaxCons = [];
                for (let i = 0; i < 10; i++) {
                    loopPromise = loopPromise.then(() => {
                        if (ajaxCons.length < maxConcurrentConnection) {
                            debugElement.insertAdjacentHTML('beforeend', '<p>Calling to server round '+ i + '. (number from JavaScript.)</p>');
                            ajaxCons.push(
                                XHR('server.php?round=' + i)
                                .then((responseObject) => {
                                    const response = responseObject.response;
                                    debugElement.insertAdjacentHTML('beforeend', '<p> &nbsp; &gt; Response from server: Round: <code>' + response.round + '</code></p>');
                                    return Promise.resolve();
                                })
                                .catch((responseObject) => {
                                    console.warn('response: ', responseObject);
                                    debugElement.insertAdjacentHTML('beforeend', '<p style="color: red;"> &nbsp; &gt; Error! see console.</p>');
                                    return Promise.reject();
                                })
                                .finally(() => {

                                })
                            );

                            if ((parseInt(ajaxCons.length)) === parseInt(maxConcurrentConnection)) {
                                return new Promise((resolve, reject) => {
                                    Promise.all(ajaxCons)
                                    .then((value) => {
                                        console.log('value', value);
                                        ajaxCons = [];
                                        resolve();
                                    });
                                });
                            }
                        }
                    });
                }

                /*
                // basic example loop make ajax call and wait until finish then next loop...
                let loopPromise = Promise.resolve();
                for (let i = 0; i < 10; i++) {
                    loopPromise = loopPromise.then(() => {
                        console.log('round ' + i);
                        return new Promise((resolve, reject) => {
                            XHR('server.php?round=' + i)
                            .then((responseObject) => {
                                const response = responseObject.response;
                                console.log('response: ', response);
                                resolve();
                            })
                            .catch((responseObject) => {
                                console.warn('response: ', responseObject);
                                reject();
                            });
                        });
                    });
                }
                */
            });
        </script>
    </body>
</html>

demo.html

จากโค้ดตัวอย่างด้านบน มันจะทำการจำลองเรียก URL หลายๆครั้งผ่าน for loop. ทุกๆครั้งที่เรียก จะผลักการเรียกเข้าไปใน array ajaxCons โดยเช็คไม่ให้เกิดจำนวน concurrent หรือการเชื่อมต่อพร้อมกันสูงสุดที่กำหนดไว้คือ 3. ถ้าหากเกินจำนวน ก็จะทำการรอผ่าน Promise object.

เมื่อทุกๆ request ทำงานเสร็จแล้วด้วยการตรวจผ่าน Promise.all() มันจะทำการ resolve เพื่อคลายการรอให้ loop รอบต่อไปทำงานได้อีก 3 การเชื่อมต่อ โดยจะทำซ้ำแบบนี้ไปเรื่อยๆจนกว่าจะครบ 10 รอบ.

ทั้งนี้หากผู้อ่านจะดัดแปลงให้เสร็จเฉพาะ request ใดๆไม่ต้องรอทั้งหมดก็ได้เช่นกัน โดยแก้ไขบรรทัดที่เป็นโค้ด ajaxCons = []; มาเป็นajaxCons.splice(0, 1); และเปลี่ยนจาก Promise.all() มาเป็น Promise.any().

ดูตัวอย่างซอร์สโค้ดเต็มบน GitHub.

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

คุณอาจใช้แท็กHTMLและแอททริบิวต์เหล่านี้: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>