Vue.js และการทำ Request Cancellation กับ axios

ปัจจุบันการส่ง XMLHttpRequest ไปยัง Backend API นั้นเป็นเรื่องพื้นฐานที่มีการใช้งานกันแทบทุกเว็บแอพ แต่ด้วยธรรมชาติของ Asynchronous ใน JavaScript นั้นทำให้การแสดงผลลัพธ์ของ Response อาจไม่เป็นไปตามลับดับในการส่ง Request เสมอไป แล้วถ้าการแสดงผลลัพธ์ของ Response ไม่ได้เป็นไปตามลำดับที่ควรจะเป็นหล่ะ จะเกิดปัญหาอย่างไร

เคสตัวอย่างปัญหาเช่น ในการทำระบบแสดงสินค้าตามหมวดหมู่นั้น ในขั้นต้นผู้ใช้อาจจะคลิกเลือกแสดงสินค้าในหมวด A แต่เกิดเปลี่ยนใจกระทันหัน ไปคลิกเลือกแสดงสินค้าในหมวด B แทน ในขณะที่ผลลัพธ์ของหมวด A ยังไม่มีการ Response กลับมา

สิ่งที่นักพัฒนาอาจจะเข้าใจไปเองคือ เมื่อมีการส่ง Request ครั้งใหม่เกิดขึ้น axios ก็จะไม่สนใจ Response ของ Request ก่อนหน้านั้นอีก แต่ในความเป็นจริงแล้ว Response ทุกตัวยังคงถูกจัดการตามลำดับของการ Response อยู่ดี (ไม่ใช่ลำดับของการส่ง Request) แสดงว่าหากโชคไม่ดี Response ของสินค้าของหมวด B ดันกลับมาถึงซะก่อน หน้าจอแสดงผลของผู้ใช้จะแสดงผลสินค้าในหมวด A (เพราะดันกลับมาถึงทีหลัง จึงไปแทนที่ผลลัพธ์ที่กลับมาถึงก่อน) ทั้งๆ ที่ผู้ใช้เปลี่ยนใจ และได้คลิกแสดงสินค้าหมวด B ไปแล้ว

ดังนั้นสิ่งที่นักพัฒนาควรทำก็คือให้แอพของเราเพิกเฉยที่จะไปจัดการกับ Response ของ Request ก่อนหน้าทันที ที่มีการส่ง Request ใหม่

เตรียมไฟล์สำหรับเขียนโปรแกรม

ในตัวอย่างโค้ดของบทความนี้ เราจะเลือกใช้ Vue.js เนื่องจากเป็นไลบรารี่ยอดนิยมและใช้งานง่าย โดยจะใช้ axios เป็นตัวส่ง XMLHttpRequest และจัดการกับ Response ครับ

axios คือไลบรารี่ที่ใช้ในการส่ง XMLHttpRequest สำหรับการพัฒนาแอพฝั่ง Client และยังสามารถใช้ส่ง HTTP Request สำหรับการพัฒนาแอพด้วย Node.js ด้วยเช่นกันครับ

Path

|-package.json
+-src
|   |-main.js
|   |-App.vue

ติดตั้ง axios

พิมพ์คำสั่งที่ Terminal

npm install axios --save

ใช้งาน Vue.js และ axios

เพื่อความง่ายในการสาธิต เราจะเพียงแค่ทดลองส่ง Request เรียงลำดับกันจำนวนหนึ่งเพื่อให้เห็นปัญหาว่าการ Response ของผลลัพธ์นั้นอาจจะไม่เรียงลำดับตามลำดับของการส่ง Request เสมอไป โดยเราจะใช้ Backend API ของ https://jsonplaceholder.typicode.com และใช้ Endpoint https://jsonplaceholder.typicode.com/users ในการเขียนโค้ด

Edit Vue Template

ไฟล์ src/App.vue

<button @click="makeMultipleRequests()">Make Multiple Requests</button>
import axios from 'axios'

const uri = 'https://jsonplaceholder.typicode.com/users'

export default {
    methods: {
        async makeRequest(id) {
            const { data } = await axios.get(`${ uri }/${ id }`)

            console.log(`ID: ${ data.id } Name: ${ data.name }`)
        },

        makeMultipleRequests() {
            const userIds = [1, 2, 3]

            // loop เพื่อส่ง request แต่ละ id
            userIds.forEach(id => {
                this.makeRequest(id)
            })
        }
    }
}

***

จากผลลัพธ์ console.log() ของตัวอย่างข้างต้นจะเห็นว่าลำดับการกลับมาของ Response นั้น อาจจะไม่ได้เรียงลำดับตามการส่ง Request เสมอไปครับ

ใช้งาน cancelToken

สิ่งที่นักพัฒนาควรทำคือการทำให้ axios เพิกเฉยกับ Response ของ Request ก่อนหน้าทั้งหมด (Response ยังคงกลับมาตามปกติ เพียงแต่ถูกเพิกเฉยไม่นำผลลัพธ์มาแสดง) ซึ่ง axios ก็มีตัวช่วยเรื่องนี้ให้เราพร้อมแล้ว โดยใช้หลักการว่าเราสามารถใช้ Object CancelToken ในการสร้าง cancelToken เพื่อแนบไปกับ Request ในการส่งแต่ละครั้ง เพื่อให้ axios สามารถอ้างอิง Request แต่ละตัวได้อย่างถูกต้อง และทุกครั้งที่จะส่ง Request ก็จะตรวจสอบก่อนว่ามี Request ก่อนหน้ารึเปล่า ถ้ามีก็ยกเลิกตัวก่อนหน้า เพราะ axios รู้จักแล้วว่าตัวไหนคือตัวไหน

ในขั้นตอนนี้เราจะแก้ไขโค้ดของเราเพียงแค่ Import Object CancelToken มาใช้ และปรับปรุง Method makeRequest() ส่วนอื่นๆ เหมือนเดิมทั้งหมดครับ

Edit Vue Template

ไฟล์ src/App.vue

import axios, { CancelToken } from 'axios'

export default {
    methods: {
        async makeRequest(id) {
            // เพิกเฉย request ก่อนหน้า
            if (this.source) {
                this.source.cancel()
            }

            // สร้าง source ใหม่
            this.source = CancelToken.source()
            // รับค่า token จาก source
            const cancelToken = this.source.token

            // ส่ง request โดยอ้างอิง cancelToken
            const { data } = await axios.get(`${ uri }/${ id }`, { cancelToken })

            console.log(`ID: ${data.id} Name: ${data.name}`)
        }
    }
}

***

จากผลลัพธ์ console.log() ของตัวอย่างข้างต้นจะเห็นว่า Request ก่อนหน้าทั้งหมดถูกเพิกเฉยแอพจึงแสดงผลลัพธ์ล่าสุดเท่านั้นครับ

โค้ดทั้งหมด

ไฟล์ src/App.vue

<button @click="makeMultipleRequests()">Make Multiple Requests</button>
import axios, { CancelToken } from 'axios'

const uri = 'https://jsonplaceholder.typicode.com/users'

export default {
    methods: {
        async makeRequest(id) {
            // เพิกเฉย request ก่อนหน้า
            if (this.source) {
                this.source.cancel()
            }

            // สร้าง source ใหม่
            this.source = CancelToken.source()
            // รับค่า token จาก source
            const cancelToken = this.source.token

            // ส่ง request โดยอ้างอิง cancelToken
            const { data } = await axios.get(`${ uri }/${ id }`, { cancelToken })

            console.log(`ID: ${data.id} Name: ${data.name}`)
        },

        makeMultipleRequests() {
            const userIds = [1, 2, 3]

            // loop เพื่อส่ง request แต่ละ id
            userIds.forEach(id => {
                this.makeRequest(id)
            })
        }
    }
}

つづく