เชี่ยวชาญการจัดการตัวแปรตามขอบเขตของ request ใน Node.js ด้วย AsyncLocalStorage ลดปัญหา prop drilling และสร้างแอปพลิเคชันที่สะอาดและตรวจสอบได้ง่ายขึ้นสำหรับผู้ใช้ทั่วโลก
เจาะลึก JavaScript Async Context: การจัดการตัวแปรตามขอบเขตของ Request
ในโลกของการพัฒนาฝั่งเซิร์ฟเวอร์สมัยใหม่ การจัดการ state ถือเป็นความท้าทายพื้นฐาน สำหรับนักพัฒนาที่ทำงานกับ Node.js ความท้าทายนี้ยิ่งทวีความรุนแรงขึ้นจากธรรมชาติของมันที่เป็น single-threaded, non-blocking และ asynchronous แม้ว่าโมเดลนี้จะมีประสิทธิภาพอย่างน่าทึ่งสำหรับการสร้างแอปพลิเคชันที่เน้น I/O และมีประสิทธิภาพสูง แต่มันก็นำมาซึ่งปัญหาเฉพาะตัว: คุณจะรักษา context สำหรับ request หนึ่งๆ ได้อย่างไรในขณะที่มันไหลผ่านการทำงานแบบอะซิงโครนัสต่างๆ ตั้งแต่ middleware ไปจนถึงการ query ฐานข้อมูล และการเรียก API ของบุคคลที่สาม? คุณจะแน่ใจได้อย่างไรว่าข้อมูลจาก request ของผู้ใช้คนหนึ่งจะไม่รั่วไหลไปยังอีกคนหนึ่ง?
เป็นเวลาหลายปีที่ชุมชน JavaScript ต้องต่อสู้กับปัญหานี้ โดยมักจะหันไปใช้รูปแบบที่ยุ่งยากเช่น "prop drilling" ซึ่งคือการส่งผ่านข้อมูลเฉพาะของ request เช่น user ID หรือ a trace ID ผ่านทุกฟังก์ชันในสายการเรียก วิธีการนี้ทำให้โค้ดรก สร้างความผูกพันที่แน่นแฟ้นระหว่างโมดูล และทำให้การบำรุงรักษาเป็นฝันร้ายที่เกิดขึ้นซ้ำแล้วซ้ำเล่า
ขอแนะนำ Async Context ซึ่งเป็นแนวคิดที่ให้โซลูชันที่แข็งแกร่งสำหรับปัญหานี้ที่มีมาอย่างยาวนาน ด้วยการเปิดตัว AsyncLocalStorage API ที่เสถียรใน Node.js ตอนนี้นักพัฒนามีกลไกในตัวที่มีประสิทธิภาพในการจัดการตัวแปรตามขอบเขตของ request อย่างสวยงามและมีประสิทธิภาพ คู่มือนี้จะพาคุณเดินทางอย่างครอบคลุมผ่านโลกของ JavaScript async context โดยอธิบายปัญหา แนะนำโซลูชัน และให้ตัวอย่างที่ใช้งานได้จริงในโลกแห่งความเป็นจริง เพื่อช่วยให้คุณสร้างแอปพลิเคชันที่สามารถขยายขนาดได้ บำรุงรักษาง่าย และตรวจสอบได้ดีขึ้นสำหรับฐานผู้ใช้ทั่วโลก
ความท้าทายหลัก: State ในโลกที่ทำงานพร้อมกันและอะซิงโครนัส
เพื่อให้เข้าใจถึงโซลูชันอย่างถ่องแท้ เราต้องเข้าใจความลึกของปัญหาก่อน เซิร์ฟเวอร์ Node.js จัดการกับ request ที่เกิดขึ้นพร้อมกันนับพันรายการ เมื่อ Request A เข้ามา Node.js อาจเริ่มประมวลผล จากนั้นหยุดชั่วคราวเพื่อรอให้การ query ฐานข้อมูลเสร็จสิ้น ในขณะที่รอมันก็จะหยิบ Request B ขึ้นมาทำงานต่อ เมื่อผลลัพธ์จากฐานข้อมูลสำหรับ Request A กลับมา Node.js ก็จะกลับมาทำงานต่อ การสลับ context อย่างต่อเนื่องนี้คือเวทมนตร์ที่อยู่เบื้องหลังประสิทธิภาพของมัน แต่มันกลับสร้างความหายนะให้กับเทคนิคการจัดการ state แบบดั้งเดิม
ทำไมตัวแปรโกลบอลถึงล้มเหลว
สัญชาตญาณแรกของนักพัฒนามือใหม่อาจเป็นการใช้ตัวแปรโกลบอล (global variable) ตัวอย่างเช่น:
let currentUser; // ตัวแปรโกลบอล
// Middleware สำหรับกำหนด user
app.use((req, res, next) => {
currentUser = await getUserFromDb(req.headers.authorization);
next();
});
// ฟังก์ชันบริการที่อยู่ลึกเข้าไปในแอปพลิเคชัน
function logActivity() {
console.log(`Activity for user: ${currentUser.id}`);
}
นี่คือข้อบกพร่องในการออกแบบที่ร้ายแรงในสภาพแวดล้อมที่ทำงานพร้อมกัน หาก Request A กำหนดค่า currentUser แล้วรอการทำงานแบบ async, Request B อาจเข้ามาและเขียนทับ currentUser ก่อนที่ Request A จะเสร็จสิ้น เมื่อ Request A กลับมาทำงานต่อ มันจะใช้ข้อมูลจาก Request B อย่างไม่ถูกต้อง ซึ่งสร้างบั๊กที่คาดเดาไม่ได้ ข้อมูลเสียหาย และช่องโหว่ด้านความปลอดภัย ตัวแปรโกลบอลไม่ปลอดภัยสำหรับ request (request-safe)
ความเจ็บปวดของ Prop Drilling
ทางออกที่พบบ่อยกว่าและปลอดภัยกว่าคือ "prop drilling" หรือ "parameter passing" ซึ่งเกี่ยวข้องกับการส่งผ่าน context อย่างชัดเจนเป็นอาร์กิวเมนต์ไปยังทุกฟังก์ชันที่ต้องการมัน
ลองจินตนาการว่าเราต้องการ traceId ที่ไม่ซ้ำกันสำหรับการบันทึก log และอ็อบเจกต์ user สำหรับการให้สิทธิ์ตลอดทั้งแอปพลิเคชันของเรา
ตัวอย่างของ Prop Drilling:
// 1. จุดเริ่มต้น: Middleware
app.use((req, res, next) => {
const traceId = generateTraceId();
const user = { id: 'user-123', locale: 'en-GB' };
const requestContext = { traceId, user };
processOrder(requestContext, req.body.orderId);
});
// 2. เลเยอร์ Business logic
function processOrder(context, orderId) {
log('Processing order', context);
const orderDetails = getOrderDetails(context, orderId);
// ... more logic
}
// 3. เลเยอร์ Data access
function getOrderDetails(context, orderId) {
log(`Fetching order ${orderId}`, context);
return db.query('SELECT * FROM orders WHERE id = ?', orderId);
}
// 4. เลเยอร์ Utility
function log(message, context) {
console.log(`[${context.traceId}] [User: ${context.user.id}] - ${message}`);
}
แม้ว่าวิธีนี้จะใช้งานได้และปลอดภัยจากปัญหา concurrency แต่มันก็มีข้อเสียที่สำคัญ:
- โค้ดรก: อ็อบเจกต์
contextถูกส่งไปทุกที่ แม้กระทั่งผ่านฟังก์ชันที่ไม่ได้ใช้มันโดยตรง แต่ต้องส่งต่อไปยังฟังก์ชันที่มันเรียก - ความผูกพันที่แน่นแฟ้น: signature ของทุกฟังก์ชันตอนนี้ผูกติดอยู่กับรูปร่างของอ็อบเจกต์
contextหากคุณต้องการเพิ่มข้อมูลชิ้นใหม่เข้าไปใน context (เช่น A/B testing flag) คุณอาจต้องแก้ไข signature ของฟังก์ชันหลายสิบแห่งทั่วทั้ง codebase ของคุณ - ความสามารถในการอ่านลดลง: จุดประสงค์หลักของฟังก์ชันอาจถูกบดบังด้วย boilerplate ของการส่งผ่าน context ไปรอบๆ
- ภาระในการบำรุงรักษา: การ Refactor กลายเป็นกระบวนการที่น่าเบื่อและเกิดข้อผิดพลาดได้ง่าย
เราต้องการวิธีที่ดีกว่านี้ วิธีที่จะมี "คอนเทนเนอร์วิเศษ" ที่เก็บข้อมูลเฉพาะของ request ซึ่งสามารถเข้าถึงได้จากทุกที่ภายในสายการเรียกแบบอะซิงโครนัสของ request นั้น โดยไม่ต้องส่งผ่านอย่างชัดเจน
ขอแนะนำ `AsyncLocalStorage`: โซลูชันสมัยใหม่
คลาส AsyncLocalStorage ซึ่งเป็นฟีเจอร์ที่เสถียรตั้งแต่ Node.js v13.10.0 เป็นคำตอบอย่างเป็นทางการสำหรับปัญหานี้ มันช่วยให้นักพัฒนาสามารถสร้าง storage context ที่แยกจากกันซึ่งคงอยู่ตลอดสายการทำงานแบบอะซิงโครนัสทั้งหมดที่เริ่มต้นจากจุดเริ่มต้นที่เฉพาะเจาะจง
คุณสามารถคิดว่ามันเป็นรูปแบบหนึ่งของ "thread-local storage" สำหรับโลกของ JavaScript ที่เป็นแบบอะซิงโครนัสและขับเคลื่อนด้วยเหตุการณ์ (event-driven) เมื่อคุณเริ่มการทำงานภายใน context ของ AsyncLocalStorage ฟังก์ชันใดๆ ที่ถูกเรียกจากจุดนั้นเป็นต้นไป ไม่ว่าจะเป็นแบบ synchronous, callback-based, หรือ promise-based ก็จะสามารถเข้าถึงข้อมูลที่เก็บไว้ใน context นั้นได้
แนวคิดหลักของ API
API นั้นเรียบง่ายและทรงพลังอย่างน่าทึ่ง มันหมุนรอบสามเมธอดหลัก:
new AsyncLocalStorage(): สร้าง instance ใหม่ของ store โดยปกติแล้วคุณจะสร้างหนึ่ง instance ต่อประเภทของ context (เช่น หนึ่ง instance สำหรับ HTTP requests ทั้งหมด) และแชร์มันไปทั่วทั้งแอปพลิเคชันของคุณals.run(store, callback): นี่คือตัวทำงานหลัก มันรันฟังก์ชัน (callback) และสร้าง asynchronous context ใหม่ อาร์กิวเมนต์แรกstoreคือข้อมูลที่คุณต้องการให้พร้อมใช้งานภายใน context นั้น โค้ดใดๆ ที่ทำงานภายในcallbackรวมถึงการทำงานแบบ async จะสามารถเข้าถึงstoreนี้ได้als.getStore(): เมธอดนี้ใช้เพื่อดึงข้อมูล (store) จาก context ปัจจุบัน หากเรียกใช้นอก context ที่สร้างโดยrun()มันจะคืนค่าเป็นundefined
การนำไปใช้จริง: คู่มือทีละขั้นตอน
มา refactor ตัวอย่าง prop-drilling ก่อนหน้านี้ของเราโดยใช้ AsyncLocalStorage กัน เราจะใช้เซิร์ฟเวอร์ Express.js มาตรฐาน แต่หลักการเดียวกันนี้ใช้ได้กับเฟรมเวิร์ก Node.js ใดๆ หรือแม้แต่โมดูล http ดั้งเดิม
ขั้นตอนที่ 1: สร้าง Instance กลางของ `AsyncLocalStorage`
เป็นแนวทางปฏิบัติที่ดีที่สุดในการสร้าง instance ของ store ของคุณเพียงตัวเดียวที่ใช้ร่วมกัน และ export มันเพื่อให้สามารถใช้งานได้ทั่วทั้งแอปพลิเคชันของคุณ ลองสร้างไฟล์ชื่อ asyncContext.js กัน
// asyncContext.js
import { AsyncLocalStorage } from 'async_hooks';
export const requestContextStore = new AsyncLocalStorage();
ขั้นตอนที่ 2: สร้าง Context ด้วย Middleware
สถานที่ที่เหมาะที่สุดในการเริ่มต้น context คือที่จุดเริ่มต้นของวงจรชีวิตของ request ซึ่ง middleware เหมาะสำหรับสิ่งนี้ เราจะสร้างข้อมูลเฉพาะของ request ของเราแล้วครอบตรรกะการจัดการ request ที่เหลือไว้ภายใน als.run()
// server.js
import express from 'express';
import { requestContextStore } from './asyncContext.js';
import { v4 as uuidv4 } from 'uuid'; // สำหรับสร้าง traceId ที่ไม่ซ้ำกัน
const app = express();
// middleware มหัศจรรย์
app.use((req, res, next) => {
const traceId = req.headers['x-request-id'] || uuidv4();
const user = { id: 'user-123', locale: 'en-GB' }; // ในแอปจริง ค่านี้จะมาจาก auth middleware
const store = { traceId, user };
// สร้าง context สำหรับ request นี้
requestContextStore.run(store, () => {
next();
});
});
// ... routes และ middleware อื่นๆ ของคุณจะอยู่ที่นี่
ใน middleware นี้ สำหรับทุก request ที่เข้ามา เราสร้างอ็อบเจกต์ store ที่มี traceId และ user จากนั้นเราเรียก requestContextStore.run(store, ...) การเรียก next() ภายในทำให้มั่นใจได้ว่า middleware และ route handlers ทั้งหมดที่ตามมาสำหรับ request เฉพาะนี้จะทำงานภายใน context ที่สร้างขึ้นใหม่นี้
ขั้นตอนที่ 3: เข้าถึง Context ได้จากทุกที่ โดยไม่ต้องทำ Prop Drilling
ตอนนี้โมดูลอื่นๆ ของเราสามารถทำให้เรียบง่ายลงได้อย่างมาก พวกมันไม่จำเป็นต้องมีพารามิเตอร์ context อีกต่อไป พวกมันสามารถ import requestContextStore ของเราและเรียก getStore() ได้เลย
Utility การบันทึกข้อมูลที่ Refactor แล้ว:
// logger.js
import { requestContextStore } from './asyncContext.js';
export function log(message) {
const context = requestContextStore.getStore();
if (context) {
const { traceId, user } = context;
console.log(`[${traceId}] [User: ${user.id}] - ${message}`);
} else {
// กรณีสำรองสำหรับการ log นอก request context
console.log(`[NO_CONTEXT] - ${message}`);
}
}
เลเยอร์ Business และ Data ที่ Refactor แล้ว:
// orderService.js
import { log } from './logger.js';
import * as db from './database.js';
export function processOrder(orderId) {
log('Processing order'); // ไม่จำเป็นต้องใช้ context!
const orderDetails = getOrderDetails(orderId);
// ... more logic
}
function getOrderDetails(orderId) {
log(`Fetching order ${orderId}`); // logger จะดึง context มาใช้โดยอัตโนมัติ
return db.query('SELECT * FROM orders WHERE id = ?', orderId);
}
ความแตกต่างนั้นชัดเจนมาก โค้ดสะอาดขึ้น อ่านง่ายขึ้น และไม่ผูกติดกับโครงสร้างของ context อย่างสิ้นเชิง utility การบันทึกข้อมูล, business logic, และ data access layers ของเราตอนนี้บริสุทธิ์และมุ่งเน้นไปที่งานเฉพาะของตน หากเราต้องการเพิ่ม property ใหม่เข้าไปใน request context ของเรา เราก็แค่ต้องเปลี่ยน middleware ที่มันถูกสร้างขึ้นเท่านั้น ไม่จำเป็นต้องแตะต้อง signature ของฟังก์ชันอื่นใดเลย
กรณีการใช้งานขั้นสูงและมุมมองระดับโลก
Context ตามขอบเขตของ Request ไม่ได้มีไว้สำหรับการบันทึกข้อมูลเท่านั้น มันยังปลดล็อกรูปแบบที่ทรงพลังหลากหลายซึ่งจำเป็นสำหรับการสร้างแอปพลิเคชันระดับโลกที่ซับซ้อน
1. Distributed Tracing และ Observability
ในสถาปัตยกรรมแบบ microservices การกระทำของผู้ใช้เพียงครั้งเดียวสามารถกระตุ้นให้เกิด chain of requests ข้ามบริการต่างๆ ได้ ในการดีบักปัญหา คุณต้องสามารถติดตามการเดินทางทั้งหมดนี้ได้ AsyncLocalStorage เป็นรากฐานของการติดตามสมัยใหม่ request ที่เข้ามายัง API gateway ของคุณสามารถกำหนด traceId ที่ไม่ซ้ำกันได้ จากนั้น ID นี้จะถูกเก็บไว้ใน async context และรวมอยู่ในการเรียก API ขาออกใดๆ โดยอัตโนมัติ (เช่น เป็น HTTP header) ไปยังบริการปลายทาง แต่ละบริการก็จะทำเช่นเดียวกัน คือการส่งต่อ context ต่อไป แพลตฟอร์มการบันทึกข้อมูลแบบรวมศูนย์สามารถนำ log เหล่านี้ไปใช้และสร้างการไหลของ request ทั้งหมดแบบ end-to-end ทั่วทั้งระบบของคุณได้
2. Internationalization (i18n) และ Localization (l10n)
สำหรับแอปพลิเคชันระดับโลก การแสดงวันที่ เวลา ตัวเลข และสกุลเงินในรูปแบบท้องถิ่นของผู้ใช้เป็นสิ่งสำคัญอย่างยิ่ง คุณสามารถเก็บ locale ของผู้ใช้ (เช่น 'fr-FR', 'ja-JP', 'en-US') จาก request headers หรือโปรไฟล์ผู้ใช้ของพวกเขาไว้ใน async context
// utility สำหรับจัดรูปแบบสกุลเงิน
import { requestContextStore } from './asyncContext.js';
function formatCurrency(amount, currencyCode) {
const context = requestContextStore.getStore();
const locale = context?.user?.locale || 'en-US'; // ใช้ค่าเริ่มต้นสำรอง
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
}).format(amount);
}
// การใช้งานลึกเข้าไปในแอป
const priceString = formatCurrency(199.99, 'EUR'); // ใช้ locale ของผู้ใช้โดยอัตโนมัติ
สิ่งนี้ช่วยให้มั่นใจได้ถึงประสบการณ์ผู้ใช้ที่สอดคล้องกันโดยไม่ต้องส่งผ่านตัวแปร locale ไปทุกที่
3. การจัดการทรานแซคชันของฐานข้อมูล
เมื่อ request หนึ่งต้องทำการเขียนฐานข้อมูลหลายครั้งที่ต้องสำเร็จหรือล้มเหลวพร้อมกัน คุณจำเป็นต้องมีทรานแซคชัน คุณสามารถเริ่มทรานแซคชันที่จุดเริ่มต้นของ request handler, เก็บ transaction client ไว้ใน async context, แล้วให้การเรียกฐานข้อมูลครั้งต่อๆ ไปภายใน request นั้นใช้ transaction client เดียวกันโดยอัตโนมัติ ในตอนท้ายของ handler คุณสามารถ commit หรือ roll back ทรานแซคชันได้ตามผลลัพธ์
4. การสลับฟีเจอร์ (Feature Toggling) และ A/B Testing
คุณสามารถกำหนดได้ว่าผู้ใช้คนหนึ่งอยู่ใน feature flags หรือกลุ่ม A/B test ใดที่จุดเริ่มต้นของ request และเก็บข้อมูลนี้ไว้ใน context ส่วนต่างๆ ของแอปพลิเคชันของคุณ ตั้งแต่ API layer ไปจนถึง rendering layer สามารถปรึกษา context เพื่อตัดสินใจว่าจะรันฟีเจอร์เวอร์ชันใดหรือแสดง UI ใด ซึ่งเป็นการสร้างประสบการณ์ที่เป็นส่วนตัวโดยไม่ต้องส่งพารามิเตอร์ที่ซับซ้อน
ข้อควรพิจารณาด้านประสิทธิภาพและแนวทางปฏิบัติที่ดีที่สุด
คำถามที่พบบ่อยคือ: มี overhead ด้านประสิทธิภาพเป็นอย่างไร? ทีมหลักของ Node.js ได้ทุ่มเทความพยายามอย่างมากในการทำให้ AsyncLocalStorage มีประสิทธิภาพสูง มันถูกสร้างขึ้นบน async_hooks API ระดับ C++ และถูกรวมเข้ากับ V8 JavaScript engine อย่างลึกซึ้ง สำหรับเว็บแอปพลิเคชันส่วนใหญ่ ผลกระทบด้านประสิทธิภาพนั้นน้อยมากและถูกชดเชยด้วยประโยชน์มหาศาลในด้านคุณภาพของโค้ดและการบำรุงรักษา
เพื่อใช้งานอย่างมีประสิทธิภาพ ให้ปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดเหล่านี้:
- ใช้ Singleton Instance: ดังที่แสดงในตัวอย่างของเรา ให้สร้าง instance ของ
AsyncLocalStorageที่ถูก export เพียงตัวเดียวสำหรับ request context ของคุณเพื่อให้แน่ใจว่ามีความสอดคล้องกัน - สร้าง Context ที่จุดเริ่มต้น: ใช้ middleware ระดับบนสุดหรือจุดเริ่มต้นของ request handler เพื่อเรียก
als.run()เสมอ สิ่งนี้จะสร้างขอบเขตที่ชัดเจนและคาดเดาได้สำหรับ context ของคุณ - ปฏิบัติต่อ Store ว่าเป็น Immutable: แม้ว่าอ็อบเจกต์ store เองจะเปลี่ยนแปลงได้ แต่ก็เป็นแนวทางปฏิบัติที่ดีที่จะปฏิบัติต่อมันเสมือนว่าไม่สามารถเปลี่ยนแปลงได้ หากคุณต้องการเพิ่มข้อมูลกลางคันใน request การสร้าง nested context ด้วยการเรียก
run()อีกครั้งมักจะสะอาดกว่า แม้ว่านี่จะเป็นรูปแบบที่สูงขึ้น - จัดการกรณีที่ไม่มี Context: ดังที่แสดงใน logger ของเรา utility ของคุณควรตรวจสอบเสมอว่า
getStore()คืนค่าundefinedหรือไม่ สิ่งนี้ช่วยให้พวกมันทำงานได้อย่างราบรื่นเมื่อรันนอก request context เช่น ในสคริปต์เบื้องหลังหรือระหว่างการเริ่มต้นแอปพลิเคชัน - การจัดการข้อผิดพลาดทำงานได้เลย: async context แพร่กระจายผ่าน
Promisechains, บล็อก.then()/.catch()/.finally(), และasync/awaitพร้อมtry/catchได้อย่างถูกต้อง คุณไม่จำเป็นต้องทำอะไรเป็นพิเศษ หากมีข้อผิดพลาดเกิดขึ้น context จะยังคงพร้อมใช้งานในตรรกะการจัดการข้อผิดพลาดของคุณ
สรุป: ยุคใหม่สำหรับแอปพลิเคชัน Node.js
AsyncLocalStorage เป็นมากกว่าแค่ utility ที่สะดวกสบาย มันแสดงถึงการเปลี่ยนแปลงกระบวนทัศน์สำหรับการจัดการ state ใน JavaScript ฝั่งเซิร์ฟเวอร์ มันให้โซลูชันที่สะอาด แข็งแกร่ง และมีประสิทธิภาพสำหรับปัญหาที่มีมาอย่างยาวนานในการจัดการ request-scoped context ในสภาพแวดล้อมที่มีการทำงานพร้อมกันสูง
ด้วยการนำ API นี้มาใช้ คุณสามารถ:
- กำจัด Prop Drilling: เขียนฟังก์ชันที่สะอาดและมุ่งเน้นมากขึ้น
- ลดการผูกมัดของโมดูล: ลดการพึ่งพาและทำให้โค้ดของคุณง่ายต่อการ refactor และทดสอบ
- เพิ่มความสามารถในการตรวจสอบ: นำ distributed tracing และ contextual logging ที่ทรงพลังมาใช้งานได้อย่างง่ายดาย
- สร้างฟีเจอร์ที่ซับซ้อน: ทำให้รูปแบบที่ซับซ้อนเช่นการจัดการทรานแซคชันและ internationalization ง่ายขึ้น
สำหรับนักพัฒนาที่สร้างแอปพลิเคชันที่ทันสมัย ขยายขนาดได้ และรองรับผู้ใช้ทั่วโลกบน Node.js การเชี่ยวชาญ async context ไม่ใช่ทางเลือกอีกต่อไป แต่เป็นทักษะที่จำเป็น ด้วยการก้าวข้ามรูปแบบที่ล้าสมัยและนำ AsyncLocalStorage มาใช้ คุณสามารถเขียนโค้ดที่ไม่เพียงแต่มีประสิทธิภาพมากขึ้น แต่ยังสวยงามและบำรุงรักษาได้ง่ายขึ้นอย่างลึกซึ้ง