اكتشف سياق JavaScript غير المتزامن، مع التركيز على تقنيات إدارة المتغيرات المرتبطة بالطلب لبناء تطبيقات قوية وقابلة للتطوير. تعرف على AsyncLocalStorage وتطبيقاته.
سياق JavaScript غير المتزامن: إتقان إدارة المتغيرات المرتبطة بالطلب
تُعد البرمجة غير المتزامنة حجر الزاوية في تطوير JavaScript الحديث، لا سيما في بيئات مثل Node.js. ومع ذلك، يمكن أن تكون إدارة السياق والمتغيرات المرتبطة بالطلب عبر العمليات غير المتزامنة أمرًا صعبًا. غالبًا ما تؤدي الأساليب التقليدية إلى تعليمات برمجية معقدة واحتمال تلف البيانات. يستكشف هذا المقال قدرات السياق غير المتزامن في JavaScript، مع التركيز بشكل خاص على AsyncLocalStorage، وكيف يبسط إدارة المتغيرات المرتبطة بالطلب لبناء تطبيقات قوية وقابلة للتطوير.
فهم تحديات السياق غير المتزامن
في البرمجة المتزامنة، تكون إدارة المتغيرات ضمن نطاق الدالة أمرًا مباشرًا. لكل دالة سياق تنفيذ خاص بها، والمتغيرات المعلنة ضمن هذا السياق تكون معزولة. ومع ذلك، تُدخل العمليات غير المتزامنة تعقيدات لأنها لا تُنفذ خطيًا. تُدخل عمليات الاستدعاء (Callbacks) والوعود (Promises) و async/await سياقات تنفيذ جديدة يمكن أن تجعل من الصعب الحفاظ على المتغيرات المتعلقة بطلب أو عملية معينة والوصول إليها.
لنأخذ سيناريو تحتاج فيه إلى تتبع معرف طلب فريد طوال تنفيذ معالج الطلب. بدون آلية مناسبة، قد تلجأ إلى تمرير معرف الطلب كمعامل لكل دالة تشارك في معالجة الطلب. هذا النهج مرهق وعرضة للأخطاء ويربط التعليمات البرمجية الخاصة بك بشكل وثيق.
مشكلة نشر السياق
- فوضى الكود: يؤدي تمرير متغيرات السياق عبر استدعاءات دوال متعددة إلى زيادة تعقيد الكود بشكل كبير وتقليل قابلية القراءة.
- الترابط الوثيق: تصبح الدوال معتمدة على متغيرات سياق محددة، مما يجعلها أقل قابلية لإعادة الاستخدام وأصعب في الاختبار.
- عرضة للخطأ: يمكن أن يؤدي نسيان تمرير متغير سياق أو تمرير القيمة الخاطئة إلى سلوك غير متوقع ومشكلات يصعب تصحيحها.
- عبء الصيانة: تتطلب التغييرات على متغيرات السياق تعديلات عبر أجزاء متعددة من قاعدة الكود.
تسلط هذه التحديات الضوء على الحاجة إلى حل أكثر أناقة وقوة لإدارة المتغيرات المرتبطة بالطلب في بيئات JavaScript غير المتزامنة.
تقديم AsyncLocalStorage: حل للسياق غير المتزامن
يوفر AsyncLocalStorage، الذي تم تقديمه في Node.js v14.5.0، آلية لتخزين البيانات طوال عمر العملية غير المتزامنة. إنه ينشئ بشكل أساسي سياقًا مستمرًا عبر الحدود غير المتزامنة، مما يتيح لك الوصول إلى المتغيرات الخاصة بطلب أو عملية معينة وتعديلها دون الحاجة إلى تمريرها بشكل صريح.
يعمل AsyncLocalStorage على أساس كل سياق تنفيذ على حدة. تحصل كل عملية غير متزامنة (مثل معالج الطلب) على مساحة تخزين معزولة خاصة بها. وهذا يضمن عدم تسرب البيانات المرتبطة بطلب ما عن طريق الخطأ إلى طلب آخر، مما يحافظ على سلامة البيانات وعزلها.
كيف يعمل AsyncLocalStorage
توفر فئة AsyncLocalStorage التوابع الرئيسية التالية:
getStore(): تُرجع المخزن الحالي المرتبط بسياق التنفيذ الحالي. إذا لم يكن هناك مخزن، فإنها تُرجعundefined.run(store, callback, ...args): تنفذcallbackالمقدمة ضمن سياق غير متزامن جديد. تقوم الوسيطةstoreبتهيئة مساحة تخزين السياق. ستتمكن جميع العمليات غير المتزامنة التي يتم تشغيلها بواسطة الـ callback من الوصول إلى هذا المخزن.enterWith(store): تدخل سياقstoreالمقدم. هذا مفيد عندما تحتاج إلى تعيين السياق بشكل صريح لكتلة معينة من الكود.disable(): تعطل مثيل AsyncLocalStorage. سيؤدي الوصول إلى المخزن بعد تعطيله إلى حدوث خطأ.
المخزن نفسه هو كائن JavaScript بسيط (أو أي نوع بيانات تختاره) يحتفظ بمتغيرات السياق التي تريد إدارتها. يمكنك تخزين معرفات الطلبات أو معلومات المستخدم أو أي بيانات أخرى ذات صلة بالعملية الحالية.
أمثلة عملية على استخدام AsyncLocalStorage
دعنا نوضح استخدام AsyncLocalStorage مع عدة أمثلة عملية.
مثال 1: تتبع معرف الطلب في خادم الويب
لنأخذ خادم ويب Node.js يستخدم Express.js. نريد إنشاء وتتبع معرف طلب فريد لكل طلب وارد تلقائيًا. يمكن استخدام هذا المعرف للتسجيل والتتبع وتصحيح الأخطاء.
const express = require('express');
const { AsyncLocalStorage } = require('async_hooks');
const { v4: uuidv4 } = require('uuid');
const app = express();
const asyncLocalStorage = new AsyncLocalStorage();
app.use((req, res, next) => {
const requestId = uuidv4();
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('requestId', requestId);
console.log(`Request received with ID: ${requestId}`);
next();
});
});
app.get('/', (req, res) => {
const requestId = asyncLocalStorage.getStore().get('requestId');
console.log(`Handling request with ID: ${requestId}`);
res.send(`Hello, Request ID: ${requestId}`);
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
في هذا المثال:
- نقوم بإنشاء مثيل
AsyncLocalStorage. - نستخدم وسيط Express لاعتراض كل طلب وارد.
- داخل الوسيط، نقوم بإنشاء معرف طلب فريد باستخدام
uuidv4(). - نستدعي
asyncLocalStorage.run()لإنشاء سياق غير متزامن جديد. نقوم بتهيئة المخزن باستخدامMap، والذي سيحتوي على متغيرات السياق الخاصة بنا. - داخل دالة الاستدعاء
run()، نقوم بتعيينrequestIdفي المخزن باستخدامasyncLocalStorage.getStore().set('requestId', requestId). - ثم نستدعي
next()لتمرير التحكم إلى الوسيط التالي أو معالج المسار. - في معالج المسار (
app.get('/'))، نستردrequestIdمن المخزن باستخدامasyncLocalStorage.getStore().get('requestId').
الآن، بغض النظر عن عدد العمليات غير المتزامنة التي يتم تشغيلها داخل معالج الطلب، يمكنك دائمًا الوصول إلى معرف الطلب باستخدام asyncLocalStorage.getStore().get('requestId').
مثال 2: مصادقة المستخدم والتفويض
حالة استخدام شائعة أخرى هي إدارة معلومات مصادقة المستخدم والتفويض. لنفترض أن لديك وسيطًا يقوم بمصادقة المستخدم واسترداد معرف المستخدم الخاص به. يمكنك تخزين معرف المستخدم في AsyncLocalStorage بحيث يكون متاحًا للوسطاء ومعالجات المسارات اللاحقة.
const express = require('express');
const { AsyncLocalStorage } = require('async_hooks');
const app = express();
const asyncLocalStorage = new AsyncLocalStorage();
// وسيط المصادقة (مثال)
const authenticateUser = (req, res, next) => {
// محاكاة مصادقة المستخدم (استبدل بمنطقك الفعلي)
const userId = req.headers['x-user-id'] || 'guest'; // الحصول على معرف المستخدم من الترويسة
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('userId', userId);
console.log(`User authenticated with ID: ${userId}`);
next();
});
};
app.use(authenticateUser);
app.get('/profile', (req, res) => {
const userId = asyncLocalStorage.getStore().get('userId');
console.log(`Accessing profile for user ID: ${userId}`);
res.send(`Profile for User ID: ${userId}`);
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
في هذا المثال، يقوم وسيط authenticateUser باسترداد معرف المستخدم (تمت محاكاته هنا عن طريق قراءة ترويسة) وتخزينه في AsyncLocalStorage. يمكن لمعالج المسار /profile بعد ذلك الوصول إلى معرف المستخدم دون الحاجة إلى تلقيه كمعامل صريح.
مثال 3: إدارة معاملات قاعدة البيانات
في السيناريوهات التي تتضمن معاملات قاعدة البيانات، يمكن استخدام AsyncLocalStorage لإدارة سياق المعاملة. يمكنك تخزين اتصال قاعدة البيانات أو كائن المعاملة في AsyncLocalStorage، مما يضمن أن جميع عمليات قاعدة البيانات ضمن طلب معين تستخدم نفس المعاملة.
const express = require('express');
const { AsyncLocalStorage } = require('async_hooks');
const app = express();
const asyncLocalStorage = new AsyncLocalStorage();
// محاكاة اتصال بقاعدة البيانات
const db = {
query: (sql, callback) => {
const transactionId = asyncLocalStorage.getStore()?.get('transactionId') || 'No Transaction';
console.log(`تنفيذ SQL: ${sql} في المعاملة: ${transactionId}`);
// محاكاة تنفيذ استعلام قاعدة البيانات
setTimeout(() => {
callback(null, { success: true });
}, 50);
},
};
// وسيط لبدء معاملة
const startTransaction = (req, res, next) => {
const transactionId = Math.random().toString(36).substring(2, 15); // إنشاء معرف معاملة عشوائي
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('transactionId', transactionId);
console.log(`Starting transaction: ${transactionId}`);
next();
});
};
app.use(startTransaction);
app.get('/data', (req, res) => {
db.query('SELECT * FROM data', (err, result) => {
if (err) {
return res.status(500).send('Error querying data');
}
res.send('Data retrieved successfully');
});
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
في هذا المثال المبسط:
- يقوم وسيط
startTransactionبإنشاء معرف معاملة وتخزينه فيAsyncLocalStorage. - تقوم دالة
db.queryالمحاكاة باسترداد معرف المعاملة من المخزن وتسجيله، مما يوضح أن سياق المعاملة متاح داخل عملية قاعدة البيانات غير المتزامنة.
الاستخدام المتقدم والاعتبارات
الوسيط البرمجي ونشر السياق
يعتبر AsyncLocalStorage مفيدًا بشكل خاص في سلاسل الوسطاء. يمكن لكل وسيط الوصول إلى السياق المشترك وتعديله، مما يتيح لك بناء خطوط أنابيب معالجة معقدة بسهولة.
تأكد من أن دوال الوسيط الخاصة بك مصممة لنشر السياق بشكل صحيح. استخدم asyncLocalStorage.run() أو asyncLocalStorage.enterWith() لتغليف العمليات غير المتزامنة والحفاظ على تدفق السياق.
معالجة الأخطاء والتنظيف
تعد معالجة الأخطاء بشكل صحيح أمرًا بالغ الأهمية عند استخدام AsyncLocalStorage. تأكد من معالجة الاستثناءات بأمان وتنظيف أي موارد مرتبطة بالسياق. ضع في اعتبارك استخدام كتل try...finally لضمان تحرير الموارد حتى في حالة حدوث خطأ.
اعتبارات الأداء
بينما يوفر AsyncLocalStorage طريقة ملائمة لإدارة السياق، من الضروري الانتباه إلى آثاره على الأداء. يمكن أن يؤدي الاستخدام المفرط لـ AsyncLocalStorage إلى زيادة العبء، خاصة في التطبيقات ذات الإنتاجية العالية. قم بتحليل أداء الكود الخاص بك لتحديد الاختناقات المحتملة وتحسينها وفقًا لذلك.
تجنب تخزين كميات كبيرة من البيانات في AsyncLocalStorage. قم بتخزين متغيرات السياق الضرورية فقط. إذا كنت بحاجة إلى تخزين كائنات أكبر، ففكر في تخزين مراجع لها بدلاً من الكائنات نفسها.
بدائل AsyncLocalStorage
بينما يعد AsyncLocalStorage أداة قوية، هناك طرق بديلة لإدارة السياق غير المتزامن، اعتمادًا على احتياجاتك المحددة والإطار الذي تستخدمه.
- التمرير الصريح للسياق: كما ذكرنا سابقًا، يعد تمرير متغيرات السياق بشكل صريح كمعاملات للدوال نهجًا أساسيًا، وإن كان أقل أناقة.
- كائنات السياق: يمكن أن يؤدي إنشاء كائن سياق مخصص وتمريره إلى تحسين قابلية القراءة مقارنة بتمرير المتغيرات الفردية.
- حلول خاصة بالإطار: توفر العديد من الأطر آليات إدارة السياق الخاصة بها. على سبيل المثال، يوفر NestJS مزودات مرتبطة بالطلب (request-scoped providers).
منظور عالمي وأفضل الممارسات
عند العمل مع السياق غير المتزامن في سياق عالمي، ضع في اعتبارك ما يلي:
- المناطق الزمنية: انتبه إلى المناطق الزمنية عند التعامل مع معلومات التاريخ والوقت في السياق. قم بتخزين معلومات المنطقة الزمنية مع الطوابع الزمنية لتجنب الغموض.
- الترجمة والتوطين: إذا كان تطبيقك يدعم لغات متعددة، فقم بتخزين الإعدادات المحلية للمستخدم في السياق لضمان عرض المحتوى باللغة الصحيحة.
- العملة: إذا كان تطبيقك يتعامل مع المعاملات المالية، فقم بتخزين عملة المستخدم في السياق لضمان عرض المبالغ بشكل صحيح.
- تنسيقات البيانات: كن على دراية بتنسيقات البيانات المختلفة المستخدمة في مناطق مختلفة. على سبيل المثال، يمكن أن تختلف تنسيقات التاريخ وتنسيقات الأرقام بشكل كبير.
الخاتمة
يوفر AsyncLocalStorage حلاً قويًا وأنيقًا لإدارة المتغيرات المرتبطة بالطلب في بيئات JavaScript غير المتزامنة. من خلال إنشاء سياق مستمر عبر الحدود غير المتزامنة، فإنه يبسط الكود ويقلل من الترابط ويحسن قابلية الصيانة. من خلال فهم قدراته وقيوده، يمكنك الاستفادة من AsyncLocalStorage لبناء تطبيقات قوية وقابلة للتطوير ومدركة للسياق العالمي.
يعد إتقان السياق غير المتزامن أمرًا ضروريًا لأي مطور JavaScript يعمل مع التعليمات البرمجية غير المتزامنة. تبنى AsyncLocalStorage وتقنيات إدارة السياق الأخرى لكتابة تطبيقات أنظف وأكثر قابلية للصيانة وأكثر موثوقية.