أهم 10 أخطاء شائعة ارتكبها مطورو Node.js
نشرت: 2022-03-11منذ اللحظة التي تم فيها الكشف عن Node.js للعالم ، شهدت حصة عادلة من الثناء والنقد. لا يزال النقاش مستمراً ، وقد لا ينتهي في أي وقت قريب. ما نتغاضى عنه غالبًا في هذه المناقشات هو أن كل لغة برمجة ومنصة يتم انتقادها بناءً على مشكلات معينة ، يتم إنشاؤها من خلال كيفية استخدامنا للمنصة. بغض النظر عن مدى صعوبة قيام Node.js بكتابة كود آمن ، ومدى سهولة كتابة كود متزامن للغاية ، فإن النظام الأساسي كان موجودًا منذ فترة طويلة وقد تم استخدامه لبناء عدد كبير من خدمات الويب القوية والمتطورة. تتسع خدمات الويب هذه بشكل جيد ، وقد أثبتت استقرارها من خلال تحملها للوقت على الإنترنت.
ومع ذلك ، مثل أي نظام أساسي آخر ، فإن Node.js عرضة لمشاكل المطورين ومشكلاتهم. تؤدي بعض هذه الأخطاء إلى تدهور الأداء ، في حين أن البعض الآخر يجعل Node.js يظهر بشكل مباشر غير قابل للاستخدام لأي شيء تحاول تحقيقه. في هذه المقالة ، سنلقي نظرة على عشرة أخطاء شائعة غالبًا ما يرتكبها المطورون الجدد على Node.js ، وكيف يمكن تجنبها لتصبح Node.js محترفًا.
الخطأ الأول: منع حلقة الحدث
توفر JavaScript في Node.js (تمامًا كما هو الحال في المتصفح) بيئة مترابطة واحدة. هذا يعني أنه لا يوجد جزءان من التطبيق الخاص بك يعملان بالتوازي ؛ بدلاً من ذلك ، يتحقق التزامن من خلال معالجة عمليات ربط الإدخال / الإخراج بشكل غير متزامن. على سبيل المثال ، الطلب من Node.js إلى محرك قاعدة البيانات لجلب بعض المستندات هو ما يسمح لـ Node.js بالتركيز على جزء آخر من التطبيق:
// Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here })
ومع ذلك ، فإن جزءًا من رمز مرتبط بوحدة المعالجة المركزية في مثيل Node.js مع آلاف العملاء المتصلين هو كل ما يتطلبه الأمر لمنع حلقة الحدث ، مما يجعل جميع العملاء ينتظرون. تتضمن الرموز المرتبطة بوحدة المعالجة المركزية محاولة فرز مجموعة كبيرة وتشغيل حلقة طويلة للغاية وما إلى ذلك. علي سبيل المثال:
function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }
قد يكون استدعاء هذه الوظيفة "sortUsersByAge" أمرًا جيدًا إذا تم تشغيلها على مصفوفة "مستخدمين" صغيرة ، ولكن مع مصفوفة كبيرة ، سيكون لها تأثير رهيب على الأداء الكلي. إذا كان هذا شيئًا يجب القيام به تمامًا ، وأنت متأكد من أنه لن يكون هناك شيء آخر ينتظر في حلقة الحدث (على سبيل المثال ، إذا كان هذا جزءًا من أداة سطر الأوامر التي تقوم ببنائها باستخدام Node.js ، لا يهم إذا كان الأمر برمته يعمل بشكل متزامن) ، فقد لا تكون هذه مشكلة. ومع ذلك ، في مثيل خادم Node.js يحاول خدمة آلاف المستخدمين في وقت واحد ، يمكن أن يكون مثل هذا النمط قاتلاً.
إذا تم استرداد هذه المجموعة من المستخدمين من قاعدة البيانات ، فسيكون الحل المثالي هو جلبها مرتبة بالفعل مباشرة من قاعدة البيانات. إذا تم حظر حلقة الحدث بواسطة حلقة مكتوبة لحساب مجموع التاريخ الطويل لبيانات المعاملات المالية ، فيمكن تأجيلها إلى بعض إعدادات العامل / قائمة الانتظار الخارجية لتجنب استغراق حلقة الحدث.
كما ترى ، لا يوجد حل سحري لهذا النوع من مشاكل Node.js ، بل يجب معالجة كل حالة على حدة. الفكرة الأساسية هي عدم القيام بعمل مكثف لوحدة المعالجة المركزية داخل مثيلات Node.js الأمامية - تلك التي يتصل بها العملاء بشكل متزامن.
الخطأ الثاني: استدعاء رد الاتصال أكثر من مرة
اعتمدت JavaScript على عمليات الاسترجاعات منذ ذلك الحين إلى الأبد. في متصفحات الويب ، يتم التعامل مع الأحداث من خلال تمرير مراجع إلى وظائف (غالبًا مجهولة) تعمل مثل عمليات الاسترجاعات. في Node.js ، كانت عمليات الاسترجاعات هي الطريقة الوحيدة التي تتواصل بها العناصر غير المتزامنة من التعليمات البرمجية الخاصة بك مع بعضها البعض - حتى تقديم الوعود. لا تزال عمليات الاسترجاعات قيد الاستخدام ، ولا يزال مطورو الحزم يصممون واجهات برمجة التطبيقات الخاصة بهم حول عمليات الاسترجاعات. إحدى مشكلات Node.js الشائعة المتعلقة باستخدام عمليات الاسترجاعات هي الاتصال بها أكثر من مرة. عادةً ما تكون الوظيفة التي توفرها الحزمة للقيام بشيء غير متزامن مصممة لتوقع وظيفة باعتبارها الوسيطة الأخيرة لها ، والتي يتم استدعاؤها عند اكتمال المهمة غير المتزامنة:
module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }
لاحظ كيف يوجد بيان عودة في كل مرة يتم استدعاء "تم" ، حتى آخر مرة. هذا لأن استدعاء رد الاتصال لا ينهي تلقائيًا تنفيذ الوظيفة الحالية. إذا تم التعليق على أول "إرجاع" ، فسيظل تمرير كلمة مرور غير سلسلة إلى هذه الوظيفة يؤدي إلى استدعاء "computeHash". اعتمادًا على كيفية تعامل "computeHash" مع مثل هذا السيناريو ، يمكن استدعاء "تم" عدة مرات. قد يتم القبض على أي شخص يستخدم هذه الوظيفة من مكان آخر على حين غرة تمامًا عند استدعاء رد الاتصال الذي يجتازه عدة مرات.
كل ما يتطلبه الأمر هو توخي الحذر لتجنب هذا الخطأ Node.js. يتبنى بعض مطوري Node.js عادة إضافة كلمة رئيسية عائدة قبل كل استدعاء لمعاودة الاتصال:
if(err) { return done(err) }
في العديد من الدالات غير المتزامنة ، لا يكون للقيمة المرتجعة أي أهمية تقريبًا ، لذلك غالبًا ما يجعل هذا الأسلوب من السهل تجنب مثل هذه المشكلة.
الخطأ الثالث: عمليات إعادة الاتصال المتداخلة بعمق
عمليات رد النداء المتداخلة بشكل عميق ، والتي يشار إليها غالبًا باسم "رد الاتصال الجحيم" ، ليست مشكلة Node.js في حد ذاتها. ومع ذلك ، يمكن أن يتسبب ذلك في حدوث مشكلات في جعل الشفرة تخرج عن نطاق السيطرة بسرعة:
function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) }
وكلما كانت المهمة أكثر تعقيدًا ، زاد الأمر سوءًا. من خلال تداخل عمليات الاسترجاعات بهذه الطريقة ، ينتهي بنا الأمر بسهولة مع وجود تعليمات برمجية معرضة للخطأ وصعبة القراءة وصعبة الصيانة. يتمثل أحد الحلول البديلة في إعلان هذه المهام كوظائف صغيرة ، ثم ربطها. على الرغم من أن أحد أنظف الحلول (يمكن القول) هو استخدام حزمة Node.js المساعدة التي تتعامل مع أنماط JavaScript غير متزامنة ، مثل Async.js:
function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }
على غرار "async.waterfall" ، هناك عدد من الوظائف الأخرى التي يوفرها Async.js للتعامل مع الأنماط غير المتزامنة المختلفة. للإيجاز ، استخدمنا أمثلة أبسط هنا ، لكن الواقع غالبًا ما يكون أسوأ.
الخطأ الرابع: توقع تشغيل عمليات الاسترجاعات بشكل متزامن
قد لا تكون البرمجة غير المتزامنة مع عمليات الاسترجاعات شيئًا فريدًا بالنسبة إلى JavaScript و Node.js ، لكنها مسؤولة عن شعبيتها. مع لغات البرمجة الأخرى ، اعتدنا على الترتيب المتوقع للتنفيذ حيث سيتم تنفيذ عبارتين واحدة تلو الأخرى ، ما لم يكن هناك تعليمات محددة للقفز بين الجمل. حتى مع ذلك ، غالبًا ما تقتصر هذه على العبارات الشرطية وعبارات الحلقة واستدعاءات الوظيفة.
ومع ذلك ، في JavaScript ، مع عمليات الاسترجاعات ، قد لا تعمل وظيفة معينة بشكل جيد حتى تنتهي المهمة التي تنتظرها. سيستمر تنفيذ الوظيفة الحالية حتى النهاية دون أي توقف:
function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }
كما ستلاحظ ، فإن استدعاء وظيفة "testTimeout" سيؤدي أولاً إلى طباعة "Begin" ، ثم طباعة "Waiting .." متبوعة بالرسالة "Done!" بعد حوالي ثانية.
يجب استدعاء أي شيء يجب حدوثه بعد تنشيط رد الاتصال من داخله.
الخطأ الخامس: التخصيص إلى "الصادرات" ، بدلاً من "module.exports"
يتعامل Node.js مع كل ملف كوحدة نمطية صغيرة معزولة. إذا كانت الحزمة الخاصة بك تحتوي على ملفين ، ربما "a.js" و "b.js" ، ثم بالنسبة إلى "b.js" للوصول إلى وظيفة "a.js" ، يجب على "a.js" تصديرها عن طريق إضافة خصائص إلى كائن الصادرات:
// a.js exports.verifyPassword = function(user, password, done) { ... }
عند القيام بذلك ، سيتم منح أي شخص يطلب "a.js" كائنًا مع وظيفة الخاصية "checkPassword":

// b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }
ومع ذلك ، ماذا لو أردنا تصدير هذه الوظيفة مباشرة ، وليس كخاصية لكائن ما؟ يمكننا الكتابة فوق الصادرات للقيام بذلك ، ولكن يجب ألا نتعامل معها كمتغير عام:
// a.js module.exports = function(user, password, done) { ... }
لاحظ كيف نتعامل مع "الصادرات" كخاصية لكائن الوحدة. يعتبر التمييز هنا بين "module.exports" و "export" أمرًا مهمًا للغاية ، وغالبًا ما يكون سببًا للإحباط بين مطوري Node.js الجدد.
خطأ # 6: رمي أخطاء من داخل عمليات الاسترجاعات
جافا سكريبت لديها مفهوم الاستثناءات. تقليد بناء الجملة لجميع اللغات التقليدية تقريبًا مع دعم معالجة الاستثناء ، مثل Java و C ++ ، يمكن لـ JavaScript "طرح" الاستثناءات والتقاطها في كتل try-catch:
function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }
ومع ذلك ، لن تتصرف try-catch بالشكل الذي قد تتوقعه في المواقف غير المتزامنة. على سبيل المثال ، إذا كنت ترغب في حماية جزء كبير من التعليمات البرمجية مع الكثير من الأنشطة غير المتزامنة باستخدام كتلة واحدة كبيرة للتجربة ، فلن تنجح بالضرورة:
try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }
إذا تم تشغيل رد الاتصال إلى "db.User.get" بشكل غير متزامن ، فسيكون النطاق الذي يحتوي على كتلة try-catch قد خرج من السياق لفترة طويلة حتى يظل قادرًا على اكتشاف هذه الأخطاء التي تم إلقاؤها من داخل رد الاتصال.
هذه هي الطريقة التي يتم بها التعامل مع الأخطاء بطريقة مختلفة في Node.js ، وهذا يجعل من الضروري اتباع النمط (err،…) على جميع وسيطات دالة رد الاتصال - من المتوقع أن تكون الوسيطة الأولى لجميع عمليات الاسترجاعات خطأ إذا حدث أحدها .
الخطأ السابع: افتراض أن الرقم هو نوع بيانات صحيح
الأرقام في JavaScript هي نقاط عائمة - لا يوجد نوع بيانات صحيح. لا تتوقع أن تكون هذه مشكلة ، لأن الأرقام الكبيرة بما يكفي للتأكيد على حدود التعويم لا تتم مواجهتها كثيرًا. هذا هو بالضبط عندما تحدث أخطاء تتعلق بهذا. نظرًا لأن أرقام الفاصلة العائمة يمكنها فقط الاحتفاظ بتمثيلات أعداد صحيحة تصل إلى قيمة معينة ، فإن تجاوز هذه القيمة في أي حساب سيبدأ على الفور في العبث بها. قد يبدو غريبًا ، ما يلي يتم تقييمه إلى صحيح في Node.js:
Math.pow(2, 53)+1 === Math.pow(2, 53)
لسوء الحظ ، لا تنتهي المراوغات المتعلقة بالأرقام في JavaScript هنا. على الرغم من أن الأرقام هي نقاط عائمة ، فإن العوامل التي تعمل على أنواع البيانات الصحيحة تعمل هنا أيضًا:
5 % 2 === 1 // true 5 >> 1 === 2 // true
ومع ذلك ، على عكس المعاملات الحسابية ، فإن عوامل تشغيل البت وعوامل التحول تعمل فقط على 32 بتًا لاحقة من مثل هذه الأرقام "الصحيحة" الكبيرة. على سبيل المثال ، محاولة إزاحة "Math.pow (2 ، 53)" بمقدار 1 سيتم تقييمها دائمًا إلى 0. محاولة القيام بحساب بت - أو 1 بنفس الرقم الكبير سيتم تقييمه إلى 1.
Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true
نادرًا ما تحتاج إلى التعامل مع الأعداد الكبيرة ، ولكن إذا فعلت ذلك ، فهناك الكثير من مكتبات الأعداد الصحيحة الكبيرة التي تنفذ العمليات الحسابية المهمة على أعداد كبيرة من الدقة ، مثل node-bigint.
الخطأ الثامن: تجاهل مزايا دفق واجهات برمجة التطبيقات
لنفترض أننا نريد إنشاء خادم ويب صغير يشبه الخادم الوكيل يقدم استجابات للطلبات عن طريق جلب المحتوى من خادم ويب آخر. كمثال ، سنقوم ببناء خادم ويب صغير يخدم صور Gravatar:
var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)
في هذا المثال المحدد لمشكلة Node.js ، نجلب الصورة من Gravatar ، ونقرأها في Buffer ، ثم نستجيب للطلب. هذا ليس بالأمر السيئ ، نظرًا لأن صور Gravatar ليست كبيرة جدًا. ومع ذلك ، تخيل ما إذا كان حجم المحتويات التي نقوم بعمل وكلاء لها يبلغ حجمها آلاف الميجابايت. كان من الممكن أن يكون النهج الأفضل هو هذا:
http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)
هنا ، نحضر الصورة ونقوم ببساطة بتوجيه الاستجابة إلى العميل. لا نحتاج في أي وقت إلى قراءة المحتوى بالكامل في مخزن مؤقت قبل تقديمه.
الخطأ التاسع: استخدام Console.log لأغراض التصحيح
في Node.js ، يسمح لك "console.log" بطباعة أي شيء تقريبًا على وحدة التحكم. قم بتمرير كائن إليه وسيطبعه ككائن JavaScript حرفي. يقبل أي عدد تعسفي من الحجج ويطبعها جميعًا مفصولة بمسافات مرتبة. هناك عدد من الأسباب التي تجعل المطور يشعر بإغراء استخدام هذا لتصحيح الكود الخاص به ؛ ومع ذلك ، يوصى بشدة بتجنب "console.log" في التعليمات البرمجية الحقيقية. يجب تجنب كتابة "console.log" في جميع أنحاء الكود لتصحيحها ثم التعليق عليها عندما لا تكون هناك حاجة إليها. بدلاً من ذلك ، استخدم إحدى المكتبات الرائعة التي تم إنشاؤها لهذا الغرض ، مثل debug.
توفر مثل هذه الحزم طرقًا ملائمة لتمكين وتعطيل خطوط تصحيح أخطاء معينة عند بدء تشغيل التطبيق. على سبيل المثال ، مع التصحيح ، من الممكن منع طباعة أي أسطر تصحيح على الجهاز بعدم ضبط متغير بيئة DEBUG. استخدامه بسيط:
// app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')
لتمكين خطوط التصحيح ، ما عليك سوى تشغيل هذا الرمز مع تعيين متغير البيئة DEBUG على "app" أو "*":
DEBUG=app node app.js
الخطأ العاشر: عدم استخدام برامج المشرف
بغض النظر عما إذا كان كود Node.js الخاص بك يعمل في الإنتاج أو في بيئة التطوير المحلية الخاصة بك ، فإن مراقب برنامج المشرف الذي يمكنه تنسيق برنامجك يعد أمرًا مفيدًا للغاية. توصي إحدى الممارسات التي كثيرًا ما يوصى بها المطورون الذين يصممون وينفذون التطبيقات الحديثة بأن التعليمات البرمجية الخاصة بك يجب أن تفشل بسرعة. في حالة حدوث خطأ غير متوقع ، لا تحاول التعامل معه ، بل اترك البرنامج يتعطل واطلب من المشرف إعادة تشغيله في بضع ثوانٍ. لا تقتصر فوائد برامج المشرف على إعادة تشغيل البرامج المعطلة فقط. تتيح لك هذه الأدوات إعادة تشغيل البرنامج عند التعطل ، وكذلك إعادة تشغيله عند تغيير بعض الملفات. هذا يجعل تطوير برامج Node.js تجربة ممتعة أكثر.
هناك عدد كبير من برامج المشرفين المتاحة لـ Node.js. علي سبيل المثال:
مساءا 2
مدى الحياة
nodemon
مشرف
كل هذه الأدوات تأتي مع مزاياها وعيوبها. بعضها جيد للتعامل مع تطبيقات متعددة على نفس الجهاز ، بينما البعض الآخر أفضل في إدارة السجلات. ومع ذلك ، إذا كنت تريد البدء في مثل هذا البرنامج ، فكل هذه خيارات عادلة.
خاتمة
كما يمكنك أن تقول ، يمكن أن يكون لبعض مشكلات Node.js آثار مدمرة على برنامجك. قد يكون البعض سببًا للإحباط أثناء محاولتك تنفيذ أبسط الأشياء في Node.js. على الرغم من أن Node.js جعلت من السهل جدًا على الوافدين الجدد البدء ، إلا أنها لا تزال تحتوي على مناطق يسهل فيها العبث. قد يتمكن المطورون من لغات البرمجة الأخرى من الارتباط ببعض هذه المشكلات ، ولكن هذه الأخطاء شائعة جدًا بين مطوري Node.js الجدد. لحسن الحظ ، من السهل تجنبها. آمل أن يساعد هذا الدليل المختصر المبتدئين في كتابة كود أفضل في Node.js ، وتطوير برامج مستقرة وفعالة لنا جميعًا.