اذهب إلى المحتوى

البحث في الموقع

المحتوى عن 'مقدمة إلى node.js'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 8 نتائج

  1. تُعَدّ Node.js بيئة تشغيل JavaScript التي تعمل من طرف الخادم، وهي مفتوحة المصدر ومتعددة المنصات cross-platform -أي تعمل على أكثر من نظام تشغيل- وهي مبنية على محرك جافاسكربت Chrome V8، وتستعمل أساسيًا لإنشاء خوادم الويب، لكنها ليست محدودةً لهذه المهمة فقط، كما أنها لاقت رواجًا بدءًا من انطلاقها في 2009، وتلعب الآن دورًا مهمًا في عالم تطوير الويب، فإذا عددنا أنّ النجوم التي يحصل عليها المشروع في GitHub معيارًا لشهرة البرمجية، فاعلم أنّ Node.js قد حصدت أكثر من 84 ألفًا من النجوم حتى الآن، ومن الجدير بالذكر أنّ Node.js مبنية على محرك جافاسكربت Chrome V8، كما تُستعمل بصورة أساسية لإنشاء خوادم الويب، لكنها ليست محدودةً لهذه المهمة فقط. هذا المقال جزء من سلسلة حول Node.js: مقدمة إلى Node.js استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js دليلك الشامل إلى مدير الحزم npm في Node.js كيفية تنفيذ الدوال داخليا ضمن Node.js البرمجة غير المتزامنة في Node.js التعامل مع الطلبيات الشبكية في Node.js التعامل مع الملفات في Node.js تعرف على وحدات Node.js الأساسية أفضل ميزات Node.js تتميز Node.js بالمزايا التالية: السرعة إحدى الميزات التي تشتهر بها Node.js هي السرعة، فشيفرة JavaScript التي تعمل على Node.js اعتمادًا على اختبارات الأداء benchmark يمكن أن تكون بضعفي سرعة تنفيذ اللغات المصرَّفة compiled مثل C أو Java، وأضعاف سرعة اللغات المفسَّرة مثل بايثون أو روبي بسبب نموذج عدم الحجب non-blocking الذي تستعمله. بسيطة صدِّقنا عندما نخبرك بأن Node.js بسيطة، بل بسيطة جدًا. تستعمل JavaScript تشغِّل Node.js شيفرة JavaScript، وهذا يعني أنّ ملايين مبرمجي الواجهات الأمامية الذين يستعملون JavaScript في المتصفح سيستطيعون العمل على شيفرات من طرف الخادم ومن طرف الواجهات الأمامية باستخدام اللغة نفسها، فلا حاجة إلى تعلّم لغة جديدة كليًا، حيث ستكون جميع التعابير التي تستعملها في JavaScript متاحةً في Node.js، واطمئن إلى أنّ آخر معايير ECMAScript مستعملة في Node.js، فلا حاجة إلى انتظار المستخدِمين ليحدِّثوا متصفحاتهم، فأنت صاحب القرار بأي نسخة ECMAScript تريد استخدامها في برنامجك باختيار إصدار Node.js المناسب. محرك V8 يمكنك الاستفادة من عمل آلاف المهندسين الذين جعلوا -ويستمروا بجعل- محرك JavaScript الخاص بمتصفح Chrome سريعًا للغاية، وذلك باعتماد Node.js على محرك Chrome V8 المفتوح المصدر. منصة غير متزامنة تُعَدّ جميع الأوامر البرمجية في لغات البرمجة التقليدية حاجبة blocking افتراضيًا مثل سي C وجافا Java وبايثون وبي إتش بي PHP إلا إذا تدخّلتَ بصورة صريحة لإنشاء عمليات غير متزامنة؛ فإذا أجريت مثلًا طلبًا شبكيًا لقراءة ملف JSON، فسيتوقف التنفيذ حتى يكون الرد response جاهزًا. تسمح JavaScript بكتابة شيفرات غير متزامنة asynchronous وغير حاجبة non-blocking بطريقة سهلة جدًا باستخدام خيط thread وحيد ودوال رد النداء callback functions والبرمجة التي تعتمد على الأحداث event-driven، حيث نمرر دالة رد نداء والتي ستُستدعَى حين نتمكن من إكمال معالجة العملية وذلك في كل مرة تحدث عملية تستهلك الموارد، كما أننا لن ننتظر الانتهاء من ذلك قبل الاستمرار في تنفيذ بقية البرنامج. أُخِذت هذه الآلية من المتصفح، فلا يمكننا انتظار تحميل شيء ما عبر طلب AJAX قبل أن نكون قادرين على التعامل مع أحداث النقر على عناصر الصفحة، فيجب حدوث كل شيء في الوقت الحقيقي لتوفير تجربة جيدة للمستخدِم، مما يسمح بمعالجة آلاف الاتصالات بخادم Node.js وحيد دون الدخول في تعقيدات إدارة الخيوط threads، والتي تكون سببًا رئيسيًا للعلل في البرامج. توفِّر Node.js تعاملًا غير حاجب مع الدخل والخرج I/O، وتكون المكتبات في Node.js عمومًا مكتوبةً بمنهجية عدم الحجب، مما يجعل سلوك الحجب في Node.js استثناءً للقاعدة وليس شيئًا طبيعيًا، كما تكمل Node.js العمليات عند وصول الرد عندما تريد إجراء عملية دخل أو خرج مثل القراءة من الشبكة أو الوصول إلى قاعدة البيانات أو نظام الملفات بدلًا من حجب الخيط blocking the thread وإهدار طاقة المعالج بالانتظار. عدد هائل من المكتبات ساعد مدير الحزم npm ببنيته البسيطة النظام العام في node.js، إذ يستضيف npm ما يقرب من 500 ألف حزمة مفتوحة المصدر تستطيع استخدامها بحرّية. مثال عن تطبيق Node.js المثال الأكثر شيوعًا عن تطبيق Node.js هو خادم ويب يعرض العبارة الشهيرة Hello World: const http = require('http') const hostname = '127.0.0.1' const port = 3000 const server = http.createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('Hello World\n') }) server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`) }) احفظ الشيفرة البسيطة السابقة في ملف باسم server.js ثم نفِّذ node server.js في الطرفية الخاصة بك وذلك من أجل تنفيذ تلك الشيفرة. تبدأ الشيفرة السابقة بتضمين وحدة http‏، إذ تمتلك Node.js مكتبةً قياسيةً رائعةً بما في ذلك دعم التعامل مع الشبكات؛ أما التابع createServer()‎ الخاص بوحدة http فيُنشئ خادم HTTP جديد ويعيده، كما أنّ الخادم قد ضُبِط للاستماع إلى منفذ وعنوان شبكي محدَّدين، وعندما يجهز الخادم فستُستدعى دالة رد النداء والتي تخبرنا في هذه الحالة أنّ الخادم جاهز، وكلما استقبل الخادم طلبًا request جديدًا، فسيُطلَق الحدث request الذي يوفِّر كائنين هما الطلب أي كائن http.IncomingMessage والرد أي كائن http.ServerResponse، ويُعَدّ هذان الكائنان أساسًا للتعامل مع استدعاء HTTP. يوفِّر الكائن الأول معلومات الطلبية لكننا لم نستعمله في هذا المثال البسيط، إلا أنه يمكنك الوصول إلى ترويسات الطلب وإلى بيانات الطلب؛ أما الكائن الثاني فيعيد البيانات إلى صاحب الطلب caller، وفي هذه الحالة باستخدامنا للسطر التالي: res.statusCode = 200 ضبطنا قيمة الخاصية statusCode إلى 200 والتي تعني أنّ الرد ناجح، ثم ضبطنا ترويسة Content-Type كما يلي: res.setHeader('Content-Type', 'text/plain') ثم ننهي الطلب بإضافة المحتوى على أساس وسيط argument إلى التابع ‎end()‎: res.end('Hello World\n') أدوات Node.js وأطر عملها تُعَدّ Node.js منصةً منخفضة المستوى، كما توجد آلاف المكتبات المكتوبة باستخدام Node.js لتسهيل الأمور على المطوِّرين وجعلها أكثر متعةً وسلاسةً، إذ أصبح عدد كبير من هذه المكتبات شائعًا بين المطوِّرين، وهذه قائمة غير شاملة للمكتبات التي نراها تستحق التعلم: Express: أحد أبسط وأقوى الطرق لإنشاء خادم ويب، ويكون تركيزه على البساطة وعدم الانحياز والميزات الأساسية لخادم الويب هو مفتاح نجاحه. Meteor: إطار عمل Framework قوي جدًا ومتكامل، يسمح لك ببناء التطبيقات باستخدام JavaScript ومشاركة الشيفرة بين العميل والخادم، كما أصبح الآن يتكامل بسلاسة مع مكتبات واجهة المستخدِم مثل React و Vue و Angular، في حين يمكن استخدامه أيضًا لإنشاء تطبيقات الويب. koa: بناه فريق Express نفسه ويطمح إلى أن يكون أصغر وأبسط اعتمادًا على سنوات الخبرة الطويلة للفريق، إذ بدأ هذا المشروع الجديد للحاجة إلى إنشاء تغييرات غير متوافقة مع ما سبقها دون تخريب ما أُنجِز في المشروع. Next.js: إطار عمل يستعمل التصيير rendering من طرف الخادم لتطبيقات React. Micro: خادم خفيف جدًا لإنشاء خدمات HTTP مصغرة microservices غير متزامنة. Socket.io: محرك تواصل في الوقت الحقيقي لبناء تطبيقات الشبكة. تاريخ موجز عن Node.js لننظر إلى تاريخ Node.js من عام 2009 حتى الآن. أُنشِئت لغة JavaScript في شركة Netscape على أساس أداة لتعديل صفحات الويب داخل متصفحها Netscape Navigator، كما يُعَدّ بيع خوادم الويب جزءًا من نموذج الأعمال في Netscape والتي تحتوي على بيئة باسم Netscape LiveWire التي تستطيع إنشاء صفحات آلية باستخدام JavaScript من طرف الخادم؛ أي أنّ فكرة استخدام JavaScript من طرف الخادم لم تبدأ من Node.js وإنما هي قديمة قدم JavaScript، إلا أنها لم تكن ناجحةً في تلك الفترة. أحد العوامل الرئيسية لاشتهار Node.js هو التوقيت، إذ بدأت لغة JavaScript تُعَدّ لغةً حقيقيةً، وذلك بفضل تطبيقات Web 2.0 التي أظهرت للعالم كيف تكون التجربة الحديثة للويب مثل Google Maps أو GMail، كما أنّ أداء محركات JavaScript قد ارتفع بشدة بفضل حرب المتصفحات، إذ تعمل فرق التطوير خلف كل متصفح رئيسي بجدّ كل يوم لتحسين الأداء، وكان هذا فوزًا عظيمًا لمنصة JavaScript، علمًا أنّ محرك V8 الذي تستعمله Node.js هو محرك متصفح Chrome، وبالطبع لم تكن شهرة Node.js محض صدفة أو توقيت جيد، إذ أضافت مفاهيم ثورية في كيفية البرمجة باستخدام JavaScript من طرف الخادم. عام 2009: ولدت Node.js وأُنشِئت أول نسخة من npm. عام 2010: ولد كل من Express وSocket.io. عام 2011: وصل npm إلى الإصدار 1.0 وبدأت الشركات الكبيرة بتبني Node.js مثل LinkedIn، كما ولد Hapi. عام 2012: استمرت عملية تبنّي Node.js بسرعة كبيرة. عام 2013: أُنشِئت أول منصة تدوين باستخدام Node.js: ولدت Koa. عام 2014: اشتق مشروع IO.js من Node.js بهدف إضافة دعم ES6 والتطوير بوتيرة أسرع. عام 2015: أُسست منظمة Node.js Foundation، ودمج مشروع IO.js مع Node.js مجددًا، كما أصبح npm يدعم الوحدات الخاصة private modules، وأُصدرت نسخة Node 4 (ولم تُصدَر النسخة 1 أو 2 أو 3 من قبل). عام 2016: ولد مشروع Yarn، وأُصدِرت Node 6. عام 2017: ركّز npm على الحماية أكثر، وأُصدرت Node 8، وأضاف محرك V8 وحدة HTTP/2 بنسخة تجريبية. عام 2018: أُصدرت Node 10، وأضيف دعم تجريبي لوحدات ES بلاحقة ‎.mjs. عام 2019: أُصدرت Node 12 وNode 13. عام 2020: أُصدرت Node 14. عام 2021: الإصدار الحالي هو 16 والذي أصبح الإصدار المستقر طويل الدعم LTS حاليًا. كيفية تثبيت Node.js يمكن تثبيت Node.js بطرائق مختلفة، وسنشرح في هذا الفصل أشهر الطرائق وأسهلها لتثبيتها، كما أنّ الحزم الرسمية لجميع أنظمة التشغيل الرئيسية متوافرة على الرابط nodejs.org/en/download. إحدى الطرائق المناسبة لتثبيت Node.js هي استعمال مدير الحزم، حيث يملك كل نظام تشغيل مدير حزم خاص به، ففي نظام macOS يكون مدير الحزم Homebrew هو مدير الحزم الأساسي، ويسمح بعد تثبيته بتثبيت Node.js بسهولة، وذلك بتنفيذ الأمر التالي في سطر الأوامر داخل الطرفية، ولن نذكر بالتفصيل مدراء الحزم المتاحة للينكس أو ويندوز منعًا للإطالة، لكنها موجودة بالتفصيل في موقع nodejs.org: brew install node يُعَدّ nvm الطريق الشائع لتشغيل Node.js، إذ يسمح لك بتبديل إصدار Node.js بسهولة وتثبيت الإصدارات الجديدة لتجربتها ثم العودة إلى الإصدار القديم إذا لم يعمل كل شيء على ما يرام، فمن المفيد جدًا على سبيل المثال تجربة الشيفرة الخاصة ببرنامج على الإصدارات القديمة من Node.js، كما ننصحك بمراجعة github.com/creationix/nvm لمزيد من المعلومات حول هذا الخيار، وننصحك بصورة شخصية باستعمال المثبِّت الرسمي إذا أنت حديث العهد على Node.js ولا تريد استخدام مدير الحزم الخاص بنظام تشغيلك، لكن على أي حال، سيكون البرنامج التنفيذي node متاحًا في سطر الأوامر بعد تثبيت Node.js بأي طريقة من الطرائق السابقة. ماذا عليك معرفته في JavaScript لاستخدام Node.js إذا بدأت لتوّك مع JavaScript، فما مدى عمق المعلومات التي تلزمك لاستخدام Node.js؟ من الصعب الوصول إلى النقطة التي تكون فيها واثقًا من قدراتك البرمجية كفاية، فحين تعلّمك كتابة الشيفرات، فقد تكون محتارًا متى تنتهي شيفرة JavaScript ومتى تبدأ Node.js وبالعكس، لذا ننصحك أن تكون متمكنًا تمكنًا جيدًا من المفاهيم الأساسية في JavaScript قبل التعمق في Node.js: بنية البرنامج. التعابير. الأنواع. المتغيرات الدوال والدوال السهمية. الكلمة المحجوزة this. حلقات التكرار والمجالات scopes. المصفوفات. الفواصل المنقوطة (نعم، هذا المحرف ; ? ) الوضع الصارم. إصدارات ECMAScript مثل ES6 وES2016 وES2017. ستكون بعد تعلّمك للمفاهيم السابقة في طريقك لتصبح مطور JavaScript محترف في بيئة المتصفح وNode.js، وفيما يلي مفاهيم أساسية لفهم البرمجة غير المتزامنة asynchronous programming والتي هي جزء أساسي من Node.js: مفهوم البرمجة غير المتزامنة ورد النداء callbacks. المؤقتات Timers. الوعود Promises. الكلمتان المحجوزتان Async وAwait. التعابير المغلقة Closures. حلقات الأحداث. توجد مقالات كثيرة وكتب في أكاديمية حسوب بالإضافة إلى توثيق JavaScript في موسوعة حسوب عن جميع المواضيع السابقة. الاختلافات بين Node.js والمتصفح كيف تختلف كتابة تطبيقات JavaScript في Node.js عن البرمجة للويب داخل المتصفح؟ يستخدِم كل من المتصفح وNode.js لغة JavaScript للبرمجة؛ لكن بناء التطبيقات التي تعمل في المتصفح مختلف تمامًا عن بناء تطبيقات Node.js، فعلى الرغم من أنهما يستعملان لغة البرمجة نفسها JavaScript، إلا أنه هنالك اختلافات جوهرية تجعل الفرق بينهما كبيرًا،. يملك مطور واجهات المستخدِم الذي يكتب تطبيقات Node.js ميزةً رائعةً ألا وهي استخدامه للغة البرمجة نفسها، فمن المعروف أنه من الصعب تعلّم لغة برمجة جديدة بإتقان، لكن باستعمال لغة البرمجة نفسها لإجراء كل العمل على موقع الويب سواءً للواجهة الأمامية أو الخادم، حيث توجد أفضلية واضحة في هذه النقطة؛ إلا أنّ بيئة العمل هي التي تختلف. ستتعامل أغلب الوقت في المتصفح مع شجرة DOM أو غيرها من الواجهات البرمجية الخاصة بالمتصفح Web Platform APIs مثل ملفات الارتباط Cookies والتي لا توجد في Node.js، وبالطبع لن تكون الكائنات document وwindow وغيرها من كائنات المتصفح متوافرةً، كما لن نحصل على الواجهات البرمجية APIs التي توفرها Node.js عبر وحداتها مثل الوصول إلى نظام الملفات. يوجد اختلاف كبير آخر وهو أنك تستطيع التحكم في البيئة التي تعمل فيها Node.js ما لم تبني تطبيقًا مفتوح المصدر يمكن لأي شخص نشره في أيّ مكان، فأنت تعلم ما هي نسخة Node.js التي سيعمل عليها تطبيقك؛ فليس لديك الحرية في اختيار المتصفح الذي يستخدمه زوار موقعك بموازنة ذلك مع بيئة المتصفح، وهذا يعني أنك يمكنك أن تكتب شيفرات ES6-7-8-9 التي تدعمها نسخة Node.js عندك. ستكون ملزمًا باستخدام إصدارات JavaScript/ECMAScript القديمة على الرغم من تطوّر JavaScript بسرعة كبيرة، وذلك لأن المتصفحات أبطأ منها والمستخدِمون أبطأ بالتحديث، وصحيحٌ أنك تستطيع استخدام Babel لتحويل شيفرتك إلى نسخة موافقة لمعيار ECMAScript 5 قبل إرسالها إلى زوار موقعك، لكنك لن تحتاج إلى ذلك في Node.js. يوجد اختلاف آخر هو استخدام Node.js لنظام الوحدات CommonJS، بينما بدأنا نرى أنّ المتصفحات تستخدم معيار وحدات ES Modules؛ وهذا يعني عمليًا أنه عليك استخدام require()‎ في Node.js، وimport في المتصفح حاليًا. محرك V8 V8 هو اسم محرك JavaScript الذي يُشغِّل متصفح Chrome، حيث يأخذ شيفرات JavaScript وينفذها أثناء التصفح عبر Chrome، إذ يوفر بيئة التشغيل اللازمة لتنفيذ شيفرات JavaScript، في حين يوفِّر المتصفح شجرة DOM وغيرها من الواجهات البرمجية للويب، كما يُعَدّ استقلال محرك JavaScript عن المتصفح الذي يستخدمه الميزة الرئيسية التي سببت بانتشار Node.js. اختار مجتمع مطوري Node.js محرك V8 في 2009، وبعد أن ذاع صيت Node.js صار محرك V8 هو المحرك الذي يشغِّل عددًا غير محصور من شيفرات JavaScript التي تعمل من طرف الخادم، وخذ بالحسبان أنّ بيئة تشغيل Node.js كبيرة جدًا ويعود الفضل إليها في أنّ محرك V8 أصبح يشغِّل تطبيقات سطح المكتب عن طريق مشاريع مثل Electron.js. محركات JavaScript الأخرى تملك المتصفحات الأخرى محركات JavaScript مختلفة منها: يملك متصفح Firefox محرك SpiderMonkey. يملك متصفح Safari محرك JavaScriptCore ويسمى Nitro أيضًا. يملك متصفح Edge محرك Chakra. تطبِّق جميع هذه المحركات معيار ECMA ES-262 والذي يسمى ECMAScript أيضًا، وهو المعيار المستخدَم في لغة JavaScript. السعي إلى الأداء الأفضل كُتِب محرك V8 بلغة C++‎ ويُحسَّن باستمرار، وهو محمول portable ويعمل على ماك ولينكس وويندوز وغيرها من أنظمة التشغيل، كما لن نخوض في تفاصيل محرك V8 لأنها موجودة في مواقع كثيرة مثل موقع V8 الرسمي وتتغير مع مرور الوقت، وعادةً تكون التغييرات كبيرةً، إذ يتطور محرك V8 دومًا كما في غيره في محركات JavaScript لتسريع الويب وبيئة Node.js، كما يجري سباق في عالم الويب للأداء الأفضل منذ سنوات، ونحن -المستخدِمون والمطوِّرون- نستفيد كثيرًا من هذه المنافسة لأنها تعطينا أداءً أسرع وأفضل سنةً بعد سنة. التصريف Compilation تُعَدّ لغة JavaScript عمومًا لغةً مفسَّرةً interpreted؛ لكن محركات JavaScript الحديثة لم تَعُد تفسِّر شيفرات JavaScript وحسب، وإنما تُصرِّفها compile، فقد حدث ذلك منذ عام 2009، عندما أُضيف مُصرِّف SpuderMonkey إلى متصفح Firefox 3.5، ثم اتبع الجميع هذه الفكرة، حيث تُصرَّف JavaScript داخليًا في V8 حين اللزوم بتصريف JIT (اختصارًا لـ just-in-time) لتسريع عملية التنفيذ. قد ترى أنّ ذلك مناف للمنطق، لكن تطورت لغة JavaScript من لغة تنفِّذ عادةً بضعة مئات من الأسطر إلى لغة تشغِّل تطبيقات كاملة تحتوي على آلاف أو حتى مئات الآلاف من الأسطر البرمجية التي تعمل في المتصفح وذلك منذ إطلاق Google Maps في 2004، فيمكن لتطبيقاتنا الآن أن تعمل لساعات في المتصفح بدلًا من سكربتات بسيطة للتحقق من صحة المدخلات أو إجراء أفعال بسيطة معينة، ففي هذا العالم الجديد أصبح من المنطقي تمامًا تصريف شيفرات JavaScript؛ وصحيحٌ أنها قد تأخذ بعض الوقت القليل لجهوزية شيفرة JavaScript، إلا أننا سنرى أداءً أفضل من الشيفرة المفسَّرة فقط. تشغيل تطبيق Node.js والخروج منه الطريقة التقليدية لتشغيل برنامج Node.js هي استخدام الأمر node المتاح في نظام التشغيل بعد تثبيت Node.js ثم تمرير اسم الملف الذي تريد تنفيذه إلى الأمر، فلو كان تطبيق Node.js عندك موجود في ملف باسم app.js، فيمكنك تشغيله بكتابة الأمر التالي في سطر الأوامر: node app.js لنتعلم كيف يمكننا إنهاء تطبيق Node.js بأفضل الطرائق الممكنة، حيث توجد عدة طرق لإنهائه، فعندما تشغِّل برنامج في سطر الأوامر، يمكنك أن توقفه بالضغط على Ctrl+c، لكن ما نريد الحديث عنه هو إنهاء التطبيق برمجيًا، ولنبدأ بأكثر طريقة قاسية لإنهاء التطبيق، ولنرَ لماذا لا يفترض بك استعمالها. توفِّر الوحدة الأساسية core module المسماة process تابعًا يسمح لك بالخروج برمجيًا من تطبيق Node.js وهو process.exit()‎، إذ سيؤدي تشغيل Node.js هذا السطر إلى إغلاق العملية process مباشرةً، وهذا يعني أنه سيتوقف أيّ رد نداء معلَّق، أو أيّ طلب شبكي لا يزال مُرسَلًا، أو أيّ وصول إلى نظام ملفات، أو عمليات تكتب إلى مجرى الخرج القياسي stdout أو الخطأ القياسي stderr توقفًا مباشرًا دون سابق إنذار، وإذا لم تجد حرجًا في ذلك، فيمكنك تمرير عدد صحيح إلى التابع ليخبر نظام التشغيل ما هي حالة الخروج exit code: process.exit(1) تكون حالة الخروج الافتراضية هي 0، والتي تعني نجاح تنفيذ البرنامج، حيث تمتلك حالات الخروج المختلفة معانٍ خاصة والتي يمكنك استخدامها في نظامك للتواصل مع بقية البرامج، كما يمكنك أيضًا ضبط قيمة الخاصية process.exitCode بحيث تُعيد Node.js حالة الخروج المضبوطة إلى هذه الخاصية عند انتهاء تنفيذ البرنامج: process.exitCode = 1 ينتهي البرنامج بسلام عند انتهاء تنفيذ جميع الشيفرات فيه في الحالة الطبيعية، وكثيرًا ما نُنشِئ خوادم باستخدام Node.js مثل خادم HTTP التالي: const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) لن ينتهي هذا البرنامج أبدًا، فإذا استدعيت التابع ‎process.exit()‎‎، فستنتهي جميع الطلبيات قيد التنفيذ أو المعلَّقة، وهذا ليس أمرًا جميلًا صدقًا، حيث ستحتاج في هذه الحالة إلى إرسال الإشارة SIGTERM إلى الأمر، وسنتعامل مع الأمر باستخدام معالج إشارة العملية process signal handler. const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) process.on('SIGTERM', () => { app.close(() => { console.log('Process terminated') }) }) قد تتساءل ما هي الإشارات؟ الإشارات هي نظام تواصل داخلي في معيار POSIX، وهو إشعار يُرسَل إلى العملية لإخبارها أنّ حدثًا ما قد حدث، فالإشارة SIGKILL هي الإشارة التي تخبر العملية بالتوقف عن العمل فورًا، وهي تعمل مثل ‎process.exit()‎؛ أما الإشارة SIGTERM فهي الإشارة التي تخبر العملية بأن تنتهي بلطف، وهي الإشارة التي تُرسَل من مدراء العمليات في أنظمة التشغيل، كما يمكنك إرسال هذه الإشارة من داخل البرنامج باستخدام دالة تابع آخر: process.kill(process.pid, 'SIGTERM') أو من برنامج Node.js آخر، أو من أيّ برنامج يعمل على النظام يعرف مُعرِّف العملية PID الخاص بالعملية التي تريد إنهاءها. متغيرات البيئة: الفرق بين التطوير والإنتاج متغير البيئة هو اصطلاح مُستخدَم على نطاق واسع في المكتبات الخارجية أيضًا، وسنتعلم كيفية قراءة واستخدام متغيرات البيئة في برنامج Node.js، حيث توفِّر الوحدة الأساسية process في Node.js الخاصية env التي تحتوي على جميع متغيرات البيئة المضبوطة في لحظة تشغيل العملية، وفيما يلي مثال يصل إلى متغير البيئة NODE_ENV المضبوط إلى القيمة development افتراضيًا: process.env.NODE_ENV // "development" ضبطه إلى القيمة production قبل تشغيل السكربت سيخبر Node.js أنّ هذه بيئة إنتاجية وليست تطويرية، كما يمكنك بالطريقة نفسها الوصول إلى أيّ متغيرات بيئة خاصة تضبطها. يمكن أن يكون لديك إعدادات مختلفة لبيئات الإنتاج والتطوير، حيث يفترض Node أنه يعمل دائمًا في بيئة تطوير، ولكن يمكنك إعلام Node.js بأنك تعمل في بيئة إنتاج من خلال ضبط متغير البيئة NODE_ENV=production عن طريق تنفيذ الأمر التالي: export NODE_ENV=production لكن يُفضَّل في الصدَفة shell وضعه في ملف إعداد الصدَفة مثل ‎.bash_profile مع صدفة Bash، وذلك لأن الإعداد بخلاف ذلك لا يستمر في حالة إعادة تشغيل النظام، كما يمكنك تطبيق متغير البيئة عن طريق وضعه في بداية أمر تهيئة تطبيقك كما يلي: NODE_ENV=production node app.js يضمن ضبط البيئة على القيمة production ما يلي: الاحتفاظ بتسجيل الدخول إلى المستوى الأدنى الأساسي. إجراء مزيد من مستويات التخبئة أو التخزين المؤقت caching لتحسين الأداء. تطبِّق مكتبة القوالب Pug التي يستخدمها إطار عمل Express على سبيل المثال عملية التصريف في وضع تنقيح الأخطاء، إذا لم يُضبَط المتغير NODE_ENV على القيمة production، حيث تُصرَّف عروض Express في كل طلب في وضع التطوير، بينما تُخزَّن مؤقتًا في وضع الإنتاج، كما يوفِّر إطار Express خطّافات إعداد configuration hooks خاصة بالبيئة تُستدعَى تلقائيًا بناءً على قيمة المتغير NODE_ENV: app.configure('development', () => { //... }) app.configure('production', () => { //... }) app.configure('production', 'staging', () => { //... }) يمكنك استخدام ذلك مثلًا لضبط معالجات أخطاء مختلفة في وضع مختلف كما يلي: app.configure('development', () => { app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); }) app.configure('production', () => { app.use(express.errorHandler()) }) استضافة مشاريع Node.js يمكن استضافة تطبيقات Node.js في أماكن عديدة اعتمادًا على احتياجاتك، وسنذكر لك قائمةً بالخيارات المتاحة أمامك، وهي قائمة غير شاملة بالخيارات التي يمكنك استخدامها لنشر تطبيقك وجعله متاحًا للعامة، وسنرتِّبها من الأبسط والأقل مزايا إلى الأعقد والأقوى: أسهل الخيارات على الإطلاق: نفق محلي local tunnel. نشر التطبيقات دون أي ضبط. Glitch. Codepen. الخيارات عديمة الخوادم Serverless. المنصة على أساس خدمة PAAS. Zeit Now. Nanobox. Heroku. Microsoft Azure. Google Cloud Platform. خادم خاص افتراضي Virtual Private Server أي VPS. خادم حقيقي Bare metal. نفق محلي يمكنك نشر تطبيقك وتُخديم الطلبات من حاسوبك باستخدام نفق محلي local tunnel حتى إذا كان لديك عنوان IP ديناميكي، أو كنت تحت NAT، فهذا الخيار مناسب لإجراء بعض الاختبارات السريعة، أو تجربة المنتج أو مشاركة التطبيق مع مجموعة صغيرة من الأشخاص، وهنالك أداة رائعة لذلك متاحة لجميع المنصات اسمها ngrok، فكل ما عليك فعله لاستعمالها هو كتابة ngrok PORT، إذ إنَّ PORT هو المنفذ الذي تريد نشره على الإنترنت، وستحصل على نطاق من ngrok.io، لكن سيسمح لك الاشتراك المدفوع بالحصول على عنوان URL مخصص إضافةً إلى خيارات حماية إضافة (تذكَّر أنك تفتح جهازك إلى الإنترنت)، وهنالك خدمة أخرى يمكنك استخدامها وهي github.com/localtunnel/localtunnel. نشر التطبيقات دون أي ضبط هناك خيارات متاحة لنشر تطبيقات Node.js دون أيّ ضبط يُذكر، وسنذكر من هذه الخيارات منصة Glitch ومنصة Codepen. Glitch يُعَدّ Glitch بيئةً تسمح لك ببناء تطبيقاتك بسرعة كبيرة، ورؤيتها حيةً على النطاق الفرعي الخاص بك على glitch.com، فلا يمكنك حاليًا الحصول على نطاق مخصص وهنالك بعض المحدوديات، لكن ستبقى مع ذلك بيئةً رائعةً؛ فهي تحتوي على كامل ميزات Node.js وCDN ومكان تخزين آمن للمعلومات الحساسة، بالإضافة إلى الاستيراد والتصدير من GitHub، كما أنّ هذه الخدمة موفَّرة من الشركة التي تقف خلف FogBugz وTrello والمشاركين في إنشاء StackOverflow. Codepen منصة Codepen رائعة، فهي تسمح لك بإنشاء مشروع متعدد الملفات ونشره بنطاق مخصص. الخيارات عديمة الخوادم Serverless إحدى الطرائق لنشر تطبيقك وعدم الحاجة إلى خادم لإدارته هي استخدام إحدى الخيارات عديمة الخوادم Serverless التي هي منهجية لنشر تطبيقاتك على أساس وظائف functions، وهي ترد على نقطة نهاية شبكية network endpoint، وهذا يسمى أيضًا FAAS أي الوظيفة على أساس خدمة Function As A Service، ومن الخيارات الشائعة جدًا هي: Serverless Framework Standard Library يوفِّر كلا الخيارين طبقة تجريدية abstraction layer لنشر التطبيقات على حلول مثل AWS Lambda وغيرها من حلول FAAS المبنية على Azure أو Google Cloud. المنصة على أساس خدمة PAAS تُعَدّ PASS اختصارًا لـ Platform AS A Service أي المنصة على أساس خدمة، وتحمل عنك هذه المنصات عناء التفكير في كثير من الأمور عند نشر تطبيقك. Zeit Now يُعَدّ Zeit خيارًا مثيرًا للاهتمام، فعندما تكتب الأمر now في الطرفية، فسيتولى أمر نشر تطبيقك كله؛ وهنالك نسخة مجانية مع محدوديات ونسخة مدفوعة بميزات أكثر، حيث ستنسى أنّ هنالك خادم وكل ما عليك فعله هو نشر التطبيق. Nanobox سنحيلك إلى موقع Nanobox لتأخذ فكرةً عنه nanobox.io. Heroku يُعَدّ Heroku منصةً رائعةً، وهنالك سلسلة فيديوهات عن كيفية نشر التطبيقات عبر Heroku منها فيديو نشر تطبيق React.js ذو واجهات خلفية Node.js على منصة Heroku. Microsoft Azure خدمة Azure توفرها Microsoft Cloud، وهنالك مقالة أجنبية تفصيلية عن إنشاء تطبيق Node.js في Azure. Google Cloud Platform تُعَدّ منصة Google Cloud خيارًا رائعًا لتنظيم تطبيقاتك، ولديهم توثيق جيد عن Node.js. خادم خاص افتراضي VPS ستجد في هذا القسم الخيارات الشائعة التي قد تعرفها من قبل، وهي مرتبة من أكثرها سهولةً للمستخدِم: DigitalOcean Linode Amazon Web Services، ونذكر خصوصًا خدمة Amazon Elastic Beanstalk فهي تسهل بعضًا من تعقيدات AWS. ولمّا كانت هذه الخدمات توفِّر لك خادم لينكس فارغ يمكنك العمل عليه، فلن نوصي بمقالةٍ أو دليلٍ محدد لهذه الخدمات؛ وهذه ليست جميع الشركات التي توفر خدمات VPS، لكنها هي التي استعملناها سابقًا وننصح بها. خادم حقيقي يوجد خيار آخر هو خادم حقيقي bare metal، بحيث تثبت عليه توزيعة لينكس وتصله بالإنترنت أو يمكنك استئجار واحد شهريًا، كما في خدمة Vultr Bare Metal. ترجمة -وبتصرّف- للفصلين Introduction و Basics من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js بناء تطبيق Node.js باستخدام Docker إعداد تطبيق node.js لسير عمل يعتمد على الحاويات باستخدام Docker Compose تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker نشر تطبيق Node.js على الويب: خدمة هيروكو (Heroku) مثالًا
  2. سنتعرّف من خلال هذا المقال على وحدات Node.js الأساسية مثل وحدة fs وpath وos وevents وتوابعها المتعددة، كما يمكنك بناء وحدة مخصَّصة بالاعتماد على الوحدات الأساسية، حيث سنتعرّف على كيفية استخدام واجهة module.exports البرمجية لتصدير بياناتك، وسنتعرّف على وحدة MySQL للتعامل مع قواعد البيانات. وحدة fs توفّر وحدة fs عمليات متعددة ومفيدة للوصول إلى نظام الملفات والتفاعل معه، وليست هناك حاجة لتثبيتها نظرًا لكونها جزءًا من نواة نود، إذ يمكن استخدامها ببساطة عن طريق طلبها كما يلي: const fs = require('fs') ثم يمكنك الوصول إلى جميع توابعها التي تشمل ما يلي: fs.access()‎: يتحقق من وجود الملف ويمكن لنود الوصول إلى الملف باستخدام أذوناته. fs.appendFile()‎: يُلحِق بيانات بملف، وينشئ الملف إذا كان غير موجود مسبقًا. fs.chmod()‎: يغيّر أذونات الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.lchmod()‎ وfs.fchmod()‎. fs.chown()‎: يغيّر مالك ومجموعة الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.fchown()‎ وfs.lchown()‎. fs.close()‎: يغلق واصف الملف file descriptor. fs.copyFile()‎: ينسخ ملفًا. fs.createReadStream()‎: ينشئ مجرى stream ملف قابل للقراءة. fs.createWriteStream()‎: ينشئ مجرى ملف قابل للكتابة. fs.link()‎: ينشئ رابطًا صلبًا hard link جديدًا إلى ملف. fs.mkdir()‎: ينشئ مجلدًا جديدًا. fs.mkdtemp()‎: ينشئ مجلدًا مؤقتًا. fs.open()‎: يضبط نمط الملف. fs.readdir()‎: يقرأ محتويات مجلد. fs.readFile()‎: يقرأ محتوى ملف، ويتعلق بالتابع fs.read()‎. fs.readlink()‎: يقرأ قيمة الوصلة الرمزية symbolic link. fs.realpath()‎: يُستخدَم لربط resolve مؤشرات مسار الملف النسبي (. و..) مع المسار الكامل. fs.rename()‎: يعيد تسمية ملف أو مجلد. fs.rmdir()‎: يزيل مجلدًا. fs.stat()‎: يعيد حالة الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.fstat()‎ وfs.lstat()‎. fs.symlink()‎: ينشئ وصلةً رمزيةً جديدًا إلى ملف. fs.truncate()‎: يقتطع الملف المحدَّد بواسطة اسم الملف المُمرَّر إلى طول معيَّن، ويتعلق بالتابع fs.ftruncate()‎. fs.unlink()‎: يزيل ملفًا أو وصلةً رمزيةً. fs.unwatchFile()‎: يوقِف مشاهدة التغييرات على ملف. fs.utimes()‎: يغيّر الطابع الزمني timestamp للملف المحدَّد باسم الملف المُمرَّر، ويتعلق بالتابع fs.futimes()‎. fs.watchFile()‎: يبدأ بمشاهدة التغييرات على ملف، ويتعلق بالتابع fs.watch()‎. fs.writeFile()‎: يكتب بيانات في ملف، ويتعلق بالتابع: fs.write()‎. جميع التوابع في وحدة fs غير متزامنة افتراضيًا، ولكن يمكنها العمل بطريقة متزامنة من خلال إلحاق الكلمة Sync باسم التابع كما يلي: fs.rename()‎ fs.renameSync()‎ fs.write()‎ fs.writeSync()‎ ويحدِث ذلك فرقًا كبيرًا في تدفق تطبيقك. لنختبر التابع fs.rename()‎ مثلًا، حيث تُستخدَم واجهة برمجة التطبيقات API غير المتزامنة مع دالة رد نداء callback: const fs = require('fs') fs.rename('before.json', 'after.json', (err) => { if (err) { return console.error(err) } //done }) يمكن استخدام واجهة برمجة تطبيقات متزامنة مثل المثال التالي مع كتلة try/catch لمعالجة الأخطاء: const fs = require('fs') try { fs.renameSync('before.json', 'after.json') //done } catch (err) { console.error(err) } الاختلاف الرئيسي هو إيقاف تنفيذ السكربت الخاص بك في المثال الثاني إلى أن تنجح عملية الملف. للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق التعامل مع نظام الملفات في Node.js في موسوعة حسوب. وحدة المسار path توفِّر وحدة path عمليات متعددة ومفيدة للوصول إلى نظام الملفات والتفاعل معه، وليست هناك حاجة لتثبيتها نظرًا لكونها جزءًا من نواة نود، إذ يمكن استخدامها ببساطة عن طريق طلبها كما يلي: const path = require('path') توفِّر هذه الوحدة محرف path.sep الذي يوفّر فاصل مقاطع المسار path segment separator (\ على نظام ويندوز Windows و/ على نظامي لينكس Linux وmacOS)، بالإضافة إلى محرف path.delimiter الذي يوفّر محدّد المسار path delimiter (; على ويندوز Windows و: على نظامَي لينكس Linux وmacOS). توابع وحدة path هي: path.basename()‎ path.dirname()‎ path.extname()‎ path.isAbsolute()‎ path.join()‎ path.normalize()‎ path.parse()‎ path.relative()‎ path.resolve()‎ للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق وحدة المسار (Path) في Node.js في موسوعة حسوب. التابع path.basename()‎ يعيد هذا التابع الجزء الأخير من المسار، ويمكن للمعامِل الثاني تحديد امتداد الملف لإعطاء الملف دون امتداده كما يلي: require('path').basename('/test/something') //something require('path').basename('/test/something.txt') //something.txt require('path').basename('/test/something.txt', '.txt') //something التابع path.dirname()‎ يعيد هذا التابع جزء المجلد أو الدليل من المسار كما يلي: require('path').dirname('/test/something') // /test require('path').dirname('/test/something/file.txt') // /test/something التابع path.extname()‎ يعيد هذا التابع جزء الامتداد من المسار كما يلي: require('path').dirname('/test/something') // '' require('path').dirname('/test/something/file.txt') // '.txt' التابع path.isAbsolute()‎ يعيد هذا التابع القيمة true إذا كان المسار مسارًا مطلقًا. require('path').isAbsolute('/test/something') // true require('path').isAbsolute('./test/something') // false التابع path.join()‎ يربط هذا التابع جزأين أو أكثر من المسار مع بعضها البعض كما يلي: const name = 'flavio' require('path').join('/', 'users', name, 'notes.txt') //'/users/flavio/notes.txt' التابع path.normalize()‎ يحاول هذا التابع حساب المسار الفعلي عندما يحتوي على محددات نسبية مثل . أو .. أو شرطات مائلة مزدوجة: require('path').normalize('/users/flavio/..//test.txt') ///users/test.txt التابع path.parse()‎ يوزّع هذا التابع مسارًا على كائن يتكون من أجزاء متعددة هي: root: يمثّل الجذر. dir: هو مسار المجلد بداية من الجذر. base: يمثّل اسم الملف مع الامتداد. name: هو اسم الملف. ext: يمثّل امتداد الملف. إليك المثال التالي: require('path').parse('/users/test.txt') وتكون النتيجة كما يلي: { root: '/', dir: '/users', base: 'test.txt', ext: '.txt', name: 'test' } التابع path.relative()‎ يقبل هذا التابع مسارين على أساس وسائط، ويعيد المسار النسبي من المسار الأول إلى المسار الثاني بناءً على مجلد العمل الحالي مثل المثال التالي: require('path').relative('/Users/flavio', '/Users/flavio/test.txt') //'test.txt' require('path').relative('/Users/flavio', '/Users/flavio/something/test.txt') //'something/test.txt' التابع path.resolve()‎ يمكنك حساب المسار المطلق لمسار نسبي باستخدام التابع path.resolve()‎ كما يلي: path.resolve('flavio.txt') //‫'‎/Users/flavio/flavio.txt' إذا شُغِّل من المجلد المحلي إذا حدّدت المعامل الثاني، فسيستخدم التابع resolve المعامِل الأول أساسًا للمعامِل الثاني كما يلي: path.resolve('tmp', 'flavio.txt') // '/Users/flavio/tmp/flavio.txt' إذا شُغِّل من المجلد المحلي إذا بدأ المعامِل الأول بشرطة مائلة، فهذا يعني أنه مسار مطلق كما يلي: path.resolve('/etc', 'flavio.txt')//'/etc/flavio.txt' وحدة os توفِّر هذه الوحدة عمليات متعددة يمكنك استخدامها لاسترداد معلومات من نظام التشغيل الأساسي والحاسوب الذي يعمل عليه البرنامج والتفاعل معه. const os = require('os') هناك بعض الخاصيات المفيدة التي تخبرنا ببعض الأمور الأساسية المتعلقة بمعالجة الملفات مثل: os.EOL التي تعطينا متسلسلة محدّد السطور، وهي ‎\n على نظامَي لينكس Linux وmacOS؛ أما على نظام ويندوز Windows فهي ‎\r\n. os.constants.signals التي تعطينا كل الثوابت المتعلقة بمعالجة إشارات العمليات مثل SIGHUP وSIGKILL وما إلى ذلك، كما يمكنك الاطلاع على جميع هذه الثوابت على /node_os. os.constants.errno التي تضبط الثوابت في تقارير الخطأ مثل EADDRINUSE وEOVERFLOW وغير ذلك. لنتعرّف الآن على التوابع الرئيسية التي توفرها وحدة os وهي: os.arch()‎ os.cpus()‎ os.endianness()‎ os.freemem()‎ os.homedir()‎ os.hostname()‎ os.hostname()‎ os.loadavg()‎ os.networkInterfaces()‎ os.platform()‎ os.release()‎ os.tmpdir()‎ os.totalmem()‎ os.type()‎ os.uptime()‎ os.userInfo()‎ للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الوحدة os في Node.js في موسوعة حسوب. التابع os.arch()‎ يعيد هذا التابع السلسلة النصية التي تحدد البنية الأساسية مثل arm وx64 وarm64. التابع os.cpus()‎ يعيد معلومات وحدات المعالجة المركزية المتوفرة على نظامك، وإليك المثال التالي: [ { model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz', speed: 2400, times: { user: 281685380, nice: 0, sys: 187986530, idle: 685833750, irq: 0 } }, { model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz', speed: 2400, times: { user: 282348700, nice: 0, sys: 161800480, idle: 703509470, irq: 0 } } ] التابع os.endianness()‎ يعيد هذا التابع القيمة BE أو القيمة LE بناءً على طريقة تصريف نود باستخدام تخزين البتات الأقل أهمية أولًا Big Endian أو تخزين البتات الأكثر أهمية أولًا Little Endian. التابع os.freemem()‎ يعيد هذا التابع عدد البايتات التي تمثل الذاكرة المتاحة في النظام. التابع os.homedir()‎ يعيد هذا التابع المسار إلى مجلد المستخدِم الحالي الرئيسي مثل المثال التالي: '/Users/flavio' التابع os.hostname()‎ يعيد هذا التابع اسم المضيف hostname. التابع os.loadavg()‎ يعيد هذا التابع الحساب الذي أجراه نظام التشغيل على متوسط التحميل، حيث يعيد فقط قيمة ذات معنى في نظامَي لينكس Linux وmacOS مثل المثال التالي: [ 3.68798828125, 4.00244140625, 11.1181640625 ] التابع os.networkInterfaces()‎ يعيد هذا التابع تفاصيل واجهات الشبكة المتوفرة على نظامك، وإليك المثال التالي: { lo0: [ { address: '127.0.0.1', netmask: '255.0.0.0', family: 'IPv4', mac: 'fe:82:00:00:00:00', internal: true }, { address: '::1', netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', family: 'IPv6', mac: 'fe:82:00:00:00:00', scopeid: 0, internal: true }, { address: 'fe80::1', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: 'fe:82:00:00:00:00', scopeid: 1, internal: true } ], en1: [ { address: 'fe82::9b:8282:d7e6:496e', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: '06:00:00:02:0e:00', scopeid: 5, internal: false }, { address: '192.168.1.38', netmask: '255.255.255.0', family: 'IPv4', mac: '06:00:00:02:0e:00', internal: false } ], utun0: [ { address: 'fe80::2513:72bc:f405:61d0', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: 'fe:80:00:20:00:00', scopeid: 8, internal: false } ] } التابع os.platform()‎ يعيد هذا التابع المنصة الذي جرى تصريف نود من أجلها مثل: darwin0. freebsd. linux. openbsd. win32. وغيرها الكثير. التابع os.release()‎ يعيد هذا التابع سلسلةً نصيةً تحدِّد رقم إصدار نظام التشغيل. التابع os.tmpdir()‎ يعيد هذا التابع المسار إلى المجلد المؤقت المعيَّن. التابع os.totalmem()‎ يعيد هذا التابع عدد البايتات الذي يمثِّل إجمالي الذاكرة المتوفرة في النظام. التابع os.type()‎ يحدّد هذا التابع نظام التشغيل كما يلي: Linux. Darwin على نظام macOS. Windows_NT على نظام ويندوز. التابع os.uptime()‎ يعيد هذا التابع عدد الثواني التي عمل فيها الحاسوب منذ آخر إعادة تشغيل. التابع os.userInfo()‎ يعيد معلومات عن المستخدِم الفعّال حاليًا. وحدة الأحداث events توفِّر لنا وحدة الأحداث events الصنف EventEmitter، وتُعَدّ أساسًا للعمل مع الأحداث في نود. const EventEmitter = require('events') const door = new EventEmitter() يختبر مستمع الأحداث event listener أحداثه الخاصة ويستخدِم الحدثين التاليين: newListener عند إضافة المستمع. removeListener عند إزالة المستمع. سنشرح فيما يلي التوابع المفيدة التالية: emitter.addListener()‎ emitter.emit()‎ emitter.eventNames()‎ emitter.getMaxListeners()‎ emitter.listenerCount()‎ emitter.listeners()‎ emitter.off()‎ emitter.on()‎ emitter.once()‎ emitter.prependListener()‎ emitter.prependOnceListener()‎ emitter.removeAllListeners()‎ emitter.removeListener()‎ emitter.setMaxListeners()‎ للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الأحداث في Node.js في موسوعة حسوب. التابع emitter.addListener()‎ وهو الاسم البديل للتابع emitter.on()‎. التابع emitter.emit()‎ يصدر هذا التابع حدثًا، حيث يستدعي بصورة متزامنة كل مستمع حدث بالترتيب الذي سُجِّلت به. التابع emitter.eventNames()‎ يعيد هذا التابع مصفوفةً من السلاسل النصية التي تمثِّل الأحداث المُسجَّلة في كائن EventListener الحالي: door.eventNames() التابع emitter.getMaxListeners()‎ يُستخدَم هذا التابع للحصول على الحد الأقصى من المستمعين الذي يمكن إضافته إلى كائن EventListener، حيث يُضبَط هذا العدد افتراضيًا على القيمة 10 ولكن يمكن زيادته أو إنقاصه باستخدام setMaxListeners()‎. door.getMaxListeners() التابع emitter.listenerCount()‎ يُستخدَم هذا التابع للحصول على عدد مستمعي الحدث المُمرَّرين على أساس معامِلات كما يلي: door.listenerCount('open') التابع emitter.listeners()‎ يُستخدَم هذا التابع للحصول على مصفوفة مستمعي الحدث المُمرَّرين على أساس معامِلات كما يلي: door.listeners('open') التابع emitter.off()‎ يمثّل هذا التابع الاسم البديل للتابع emitter.removeListener()‎ المُضاف في الإصدار 10 من نود. التابع emitter.on()‎ يضيف هذا التابع دالة رد النداء التي تُستدعَى عند إصدار حدث، ويُستخدَم هذا التابع كما يلي: door.on('open', () => { console.log('Door was opened') }) التابع emitter.once()‎ يضيف هذا التابع دالة رد النداء التي تُستدعَى عند إصدار حدث لأول مرة بعد تسجيله، حيث ستُستدعَى دالة رد النداء تلك مرةً واحدةً فقط، ولن تُستدعَى مرةً أخرى. const EventEmitter = require('events') const ee = new EventEmitter() ee.once('my-event', () => { //ًاستدعِ دالة رد النداء مرةً واحدة }) التابع emitter.prependListener()‎ يُضاف المستمع الذي تضيفه باستخدام on أو addListener في آخر طابور المستمعين ويُستدعَى أخيرًا كذلك، ولكنه يُضاف ويُستدعَى قبل المستمعين الآخرين باستخدام prependListener. التابع emitter.prependOnceListener()‎ يُضاف المستمع الذي تضيفه باستخدام once في آخر طابور المستمعين ويُستدعَى أخيرًا كذلك، ولكنه يُضاف ويُستدعَى قبل المستمعين الآخرين باستخدام prependOnceListener. التابع emitter.removeAllListeners()‎ يزيل هذا التابع جميع مستمعي الكائن الذي يصدر الأحداث ويستمع إلى حدث محدَّد: door.removeAllListeners('open') التابع emitter.removeListener()‎ يزيل مستمعًا محدَّدًا عن طريق حفظ دالة رد النداء في متغير عند إضافته، بحيث يمكنك الإشارة إليه لاحقًا: const doSomething = () => {} door.on('open', doSomething) door.removeListener('open', doSomething) التابع emitter.setMaxListeners()‎ يضبط الحد الأقصى لعدد المستمعين الذي يمكن إضافته إلى كائن EventListener، حيث يُضبَط هذا العدد افتراضيًا على القيمة 10 ولكن يمكن زيادته أو إنقاصه. door.setMaxListeners(50) وحدة HTTP توفّر وحدة http في Node.js دوالًا وأصنافًا مفيدة لبناء خادم HTTP، وتُعَدّ الوحدة الأساسية لشبكات نود، كما يمكن تضمين وحدة http كما يلي: const http = require('http') توفّر وحدة http بعض الخاصيات properties والتوابع methods والأصناف classes. للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الوحدة HTTP في Node.js في موسوعة حسوب. الخاصيات توفِّر وحدة HTTP الخاصيات التالية: http.METHODS http.STATUS_CODES http.globalAgent الخاصية http.METHODS تعطي هذه الخاصية قائمةً بجميع توابع HTTP المدعومة كما يلي: > require('http').METHODS [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ] الخاصية http.STATUS_CODES تعطي هذه الخاصية قائمةً بجميع رموز حالة HTTP ووصفها كما يلي: > require('http').STATUS_CODES { '100': 'Continue', '101': 'Switching Protocols', '102': 'Processing', '200': 'OK', '201': 'Created', '202': 'Accepted', '203': 'Non-Authoritative Information', '204': 'No Content', '205': 'Reset Content', '206': 'Partial Content', '207': 'Multi-Status', '208': 'Already Reported', '226': 'IM Used', '300': 'Multiple Choices', '301': 'Moved Permanently', '302': 'Found', '303': 'See Other', '304': 'Not Modified', '305': 'Use Proxy', '307': 'Temporary Redirect', '308': 'Permanent Redirect', '400': 'Bad Request', '401': 'Unauthorized', '402': 'Payment Required', '403': 'Forbidden', '404': 'Not Found', '405': 'Method Not Allowed', '406': 'Not Acceptable', '407': 'Proxy Authentication Required', '408': 'Request Timeout', '409': 'Conflict', '410': 'Gone', '411': 'Length Required', '412': 'Precondition Failed', '413': 'Payload Too Large', '414': 'URI Too Long', '415': 'Unsupported Media Type', '416': 'Range Not Satisfiable', '417': 'Expectation Failed', '418': 'I\'m a teapot', '421': 'Misdirected Request', '422': 'Unprocessable Entity', '423': 'Locked', '424': 'Failed Dependency', '425': 'Unordered Collection', '426': 'Upgrade Required', '428': 'Precondition Required', '429': 'Too Many Requests', '431': 'Request Header Fields Too Large', '451': 'Unavailable For Legal Reasons', '500': 'Internal Server Error', '501': 'Not Implemented', '502': 'Bad Gateway', '503': 'Service Unavailable', '504': 'Gateway Timeout', '505': 'HTTP Version Not Supported', '506': 'Variant Also Negotiates', '507': 'Insufficient Storage', '508': 'Loop Detected', '509': 'Bandwidth Limit Exceeded', '510': 'Not Extended', '511': 'Network Authentication Required' } الخاصية http.globalAgent تؤشّر هذه الخاصية إلى نسخة كائن الوكيل Agent العامة، والذي هو نسخة من الصنف http.Agent، حيث يُستخدَم هذا الصنف لإدارة الاتصالات المستمرة وإعادة استخدام عملاء HTTP، وهو المكوّن الأساسي من شبكات HTTP الخاصة بنود Node. التوابع توفِّر وحدة HTTP التوابع التالية: http.createServer()‎ http.request()‎ http.get()‎ التابع http.createServer()‎ يعيد هذا التابع نسخةً جديدةً من الصنف http.Server، حيث يُستخدَم كما يلي: const server = http.createServer((req, res) => { //معالجة كل طلب باستخدام دالة رد النداء هذه }) التابع http.request()‎ ينشئ طلب HTTP إلى خادم، مما يؤدي إلى إنشاء نسخة من الصنف http.ClientRequest. التابع http.get()‎ يشبه التابع http.request()‎، ولكنه يضبط تلقائيًا تابع HTTP على GET ويستدعي التابع req.end()‎ تلقائيًا. الأصناف Classes توفِّر وحدة HTTP خمسة أصناف هي: http.Agent http.ClientRequest http.Server http.ServerResponse http.IncomingMessage الصنف http.Agent نُنشئ نود نسخةً عامةً من الصنف http.Agent لإدارة الاتصالات المستمرة وإعادة استخدام عملاء HTTP، وهو المكوّن الأساسي من شبكات HTTP الخاصة بنود Node، كما يتأكّد هذا الكائن من وضع كل طلب إلى الخادم في طابور ومن إعادة استخدام المقبس socket، كما أنه يحتفظ بمجمّع من المقابس بهدف تحسين الأداء. الصنف http.ClientRequest يُنشَأ الكائن http.ClientRequest عند استدعاء التابع http.request()‎ أو التابع http.get()‎، حيث يُستدعَى حدث response مع الاستجابة عند تلقيها باستخدام نسخة من كائن http.IncomingMessage على أساس وسيط، ويمكن قراءة بيانات الاستجابة المُعادة بطريقتين هما: استدعاء التابع response.read()‎. يمكنك إعداد مستمعٍ للحدث data في معالج الحدث response بحيث يمكنك الاستماع للبيانات المتدفقة إليه. الصنف http.Server تُنشَأ وتُعاد عادةً نسخة من هذا الصنف عند إنشاء خادم جديد باستخدام التابع http.createServer()‎، يمكنك الوصول إلى توابع كائن خادم بعد إنشائه، وهذه التوابع هي: close()‎ الذي يوقِف الخادم من قبول اتصالات جديدة. listen()‎ الذي يشغّل خادم HTTP ويستمع للاتصالات. الصنف http.ServerResponse ينشئه الصنف http.Server ويمرّره على أساس معامل ثانٍ لحدث request الذي يشغّله، حيث يُعرَف هذا الصنف ويُستخدَم في الشيفرة على أنه كائن res كما يلي: const server = http.createServer((req, res) => { //‫res هو كائن http.ServerResponse }) التابع الذي ستستدعيه دائمًا في المعالج هو end()‎ والذي يغلق الاستجابة بعد اكتمال الرسالة ثم يستطيع الخادم إرسالها إلى العميل، إذ يجب استدعاؤه في كل استجابة، وتُستخدَم التوابع التالية للتفاعل مع ترويسات HTTP: getHeaderNames()‎: للحصول على قائمة بأسماء ترويسات HTTP المضبوطة مسبقًا. getHeaders()‎: للحصول على نسخة من ترويسات HTTP المضبوطة مسبقًا. setHeader('headername', value)‎: يحدّد قيمة ترويسة HTTP. getHeader('headername')‎: للحصول على ترويسة HTTP المضبوطة مسبقًا. removeHeader('headername')‎: يزيل ترويسة HTTP المضبوطة مسبقًا. hasHeader('headername')‎: يعيد القيمة true إذا احتوت الاستجابة على هذه الترويسة المضبوطة. headersSent()‎: يعيد القيمة true إذا أُرسِلت الترويسات إلى العميل. مسبقًا يمكنك إرسال الترويسات بعد معالجتها إلى العميل عن طريق استدعاء التابع response.writeHead()‎ الذي يقبل رمز الحالة statusCode على أساس معامل أول، ورسالة الحالة الاختيارية، وكائن الترويسات، كما يمكنك إرسال البيانات إلى العميل في جسم الاستجابة عن طريق استخدام التابع write()‎ الذي سيرسل البيانات المخزَّنة إلى مجرى استجابة HTTP، فإذا لم تُرسَل الترويسات بعد باستخدام التابع response.writeHead()‎، فستُرسَل الترويسات أولًا مع رمز الحالة والرسالة المحدَّدة في الطلب والتي يمكنك تعديلها عن طريق ضبط قيم الخاصيات statusCode وstatusMessage كما يلي: response.statusCode = 500 response.statusMessage = 'Internal Server Error' الصنف http.IncomingMessage يُنشَأ كائن http.IncomingMessage باستخدام: http.Server عند الاستماع إلى الحدث request. http.ClientRequest عند الاستماع إلى الحدث response. يمكن استخدام كائن http.IncomingMessage للوصول إلى خاصيات الاستجابة التالية: الحالة status باستخدام توابع statusCode وstatusMessage الخاصة به. الترويسات باستخدام توابع headers أو rawHeaders الخاصة به. تابع HTTP باستخدام تابع method الخاص به. إصدار HTTP باستخدام تابع httpVersion. عنوان URL باستخدام تابع url. المقبس الأساسي underlying socket باستخدام تابع socket. يمكن الوصول إلى البيانات باستخدام المجاري streams، حيث ينفّذ كائن http.IncomingMessage واجهة المجرى Stream القابلة للقراءة. وحدة MySQL تُعَدّ MySQL واحدةً من أكثر قواعد البيانات العلائقية شيوعًا في العالم، إذ يحتوي نظام نود Node المجتمعي على حزم مختلفة تتيح لك التعامل مع MySQL وتخزين البيانات واسترداد البيانات وما إلى ذلك، كما سنستخدِم حزمة mysqljs/mysql التي تحتوي على أكثر من 12000 نجمة على GitHub وهي موجودة منذ سنوات. تثبيت حزمة نود mysql يمكنك تثبيتها باستخدام الأمر التالي: npm install mysql تهيئة الاتصال بقاعدة البيانات يجب تضمين الحزمة أولًا كما يلي: const mysql = require('mysql') ثم تنشئ اتصالًا كما يلي: const options = { user: 'the_mysql_user_name', password: 'the_mysql_user_password', database: 'the_mysql_database_name' } const connection = mysql.createConnection(options) ثم تهيئ اتصالًا جديدًا عن طريق استدعاء ما يلي: connection.connect(err => { if (err) { console.error('An error occurred while connecting to the DB') throw err } }) خيارات الاتصال احتوى كائن options في المثال السابق على 3 خيارات هي: const options = { user: 'the_mysql_user_name', password: 'the_mysql_user_password', database: 'the_mysql_database_name' } هناك خيارات أخرى متعددة يمكنك استخدامها مثل: host: اسم مضيف قاعدة البيانات، وقيمته الافتراضية هي localhost. port رقم منفذ خادم MySQL، وقيمته الافتراضية هي 3306. socketPath: يُستخدَم لتحديد مقبس يونيكس unix بدلًا من host وport. debug: يمكن استخدامه لتنقيح الأخطاء debugging عند تعطيله افتراضيًا. trace: يطبع تعقبات المكدس stack traces عند حدوث الأخطاء عند تفعيله افتراضيًا. ssl: يُستخدَم لإعداد اتصال SSL إلى الخادم. إجراء استعلام SELECT أصبحتَ الآن جاهزًا لإجراء استعلام SQL في قاعدة البيانات، وسيستدعي الاستعلامُ بمجرد تنفيذه دالةَ رد النداء التي تحتوي على الخطأ المُحتمَل error والنتائج results والحقول fields كما يلي: connection.query('SELECT * FROM todos', (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) يمكنك تمرير القيم التي ستُتجاوز تلقائيًا كما يلي: const id = 223 connection.query('SELECT * FROM todos WHERE id = ?', [id], (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) يمكنك تمرير قيم متعددة من خلال وضع مزيد من العناصر في المصفوفة التي تمررها على أساس معامل ثانٍ كما يلي: const id = 223 const author = 'Flavio' connection.query('SELECT * FROM todos WHERE id = ? AND author = ?', [id, author], (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) إجراء استعلام INSERT يمكنك تمرير كائن كما يلي: const todo = { thing: 'Buy the milk' author: 'Flavio' } connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } }) إذا احتوى الجدول على مفتاح رئيسي primary key مع auto_increment، فستُعاد قيمته ضمن القيمة results.insertId كما يلي: const todo = { thing: 'Buy the milk' author: 'Flavio' } connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => { if (error) { console.error('An error occurred while executing the query') throw error }} const id = results.resultId console.log(id) ) إغلاق الاتصال يمكنك استدعاء التابع end()‎ عند إنهاء الاتصال بقاعدة البيانات كما يلي: connection.end() يعمل ذلك على التأكد من إرسال أي استعلام مُعلَّق وإنهاء الاتصال بأمان. وحدات مخصصة إذا لم تعثر على الوحدات المناسبة لك ضمن الوحدات الأساسية، فيمكنك بناء وحدة مخصَّصة تخدم غرضك بالاعتماد على الوحدات الأساسية ويمكنك أن تصدِّرها وتستوردها حتى أنه يمكنك بناء مكتبة كاملة، حيث سنتعرّف فيما يلي على كيفية استخدام واجهة module.exports البرمجية لتصدير بياناتك إلى ملفات أخرى في تطبيقك أو إلى تطبيقات أخرى. يمتلك نود نظام وحدات مبنيّ مسبقًا، إذ يمكن لملف Node.js استيراد العمليات التي تصدّرها ملفات Node.js الأخرى، فإذا أردت استيراد شيءٍ ما، فاستخدم ما يلي لاستيراد العمليات الظاهرة في ملف library.js الموجود في مجلد الملف الحالي، إذ يجب إظهار العمليات في هذا الملف قبل أن تستوردها ملفات أخرى: const library = require('./library') يكون أيّ كائن أو متغير آخر مُعرَّف في الملف خاصًا private افتراضيًا ولا يظهر لأيّ شيء خارجي، وهذا ما تسمح لنا به واجهة برمجة تطبيقات module.exports التي يوفِّرها [نظام module](https://nodejs.org/api/modules.html)، وإذا أسندتَ كائنًا أو دالةً مثل خاصية exports جديدة، فهذا هو الشيء الذي يظهر، ويمكن استيراده على هذا النحو في أجزاء أخرى من تطبيقك أو في تطبيقات أخرى أيضًا، حيث يمكنك تطبيق ذلك بطريقتين، الأولى هي إسناد كائن لوحدة module.exports، وهو كائن خارجي يوفّره نظام module، وبالتالي سيصدّر ملفك هذا الكائن فقط: const car = { brand: 'Ford', model: 'Fiesta' } module.exports = car //في الملف الآخر‫.. const car = require('./car') أما الطريقة الثانية فهي إضافة الكائن المُصدَّر على أساس خاصية exports، حيث تتيح لك هذه الطريقة تصدير كائنات أو دوال أو بيانات متعددة: const car = { brand: 'Ford', model: 'Fiesta' } exports.car = car أو مباشرةً: const car = { brand: 'Ford', model: 'Fiesta' } كما ستستخدمه في الملف الآخر من خلال الإشارة إلى خاصية الاستيراد كما يلي: const items = require('./items') items.car أو كما يلي: const car = require('./items').car هناك فرق بين module.exports وexports، فالأول يُظهِر الكائن الذي يؤشّر إليه، بينما يُظهِر الثاني خاصيات الكائن الذي يؤشّر إليه. ترجمة -وبتصرّف- للفصل Some essential core modules من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال السابق: التعامل مع الملفات في Node.js البرمجة باستخدام الوحدات مقدمة إلى الوحدات Modules في جافاسكربت
  3. سنتعرّف من خلال هذا المقال على كيفية التعامل مع الملفات والمجلدات في Node.js من خلال شرح واصفات الملفات وإحصائياتها ومساراتها وقراءتها وكتابتها، كما سنتعرّف على مفهوم المجاري streams وميّزاتها وأنواعها. واصفات الملفات File descriptors يمكن التفاعل مع واصفات الملفات باستخدام نود Node، إذ يجب أن تحصل على واصف ملف قبل تمكنك من التفاعل مع ملف موجود في نظام الملفات الخاص بك، وواصف الملف هو ما يُعاد عند فتح الملف باستخدام التابع open()‎ الذي توفره وحدة fs: const fs = require('fs') fs.open('/Users/flavio/test.txt', 'r', (err, fd) => { //‫fd هو واصف الملف }) استخدمنا الراية r على أساس معامِل ثانٍ لاستدعاء fs.open()‎، إذ تعني هذه الراية أننا نفتح الملف للقراءة؛ أما الرايات الأخرى المُستخدَمة فهي: r+‎ فتح الملف للقراءة والكتابة. w+‎ فتح الملف للقراءة والكتابة، مع وضع المجرى stream في بداية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a فتح الملف للكتابة، مع وضع المجرى في نهاية الملف وإنشاء الملف إن لم يكن موجودًا مسبقًا. a+‎ فتح الملف للقراءة والكتابة، مع وضع المجرى في نهاية الملف وإنشاء الملف إن لم يكن موجودًا مسبقًا. يمكنك فتح الملف باستخدام التابع fs.openSync الذي يعيد كائن واصف الملف بدلًا من توفيره في دالة رد نداء: const fs = require('fs') try { const fd = fs.openSync('/Users/flavio/test.txt', 'r') } catch (err) { console.error(err) } يمكنك تنفيذ جميع العمليات المطلوبة مثل استدعاء التابع fs.open()‎ والعديد من العمليات الأخرى التي تتفاعل مع نظام الملفات بمجرد حصولك على واصف الملف بأيّ طريقة تختارها. إحصائيات الملف يأتي كل ملف مع مجموعة من التفاصيل التي يمكننا فحصها باستخدام نود Node باستخدام التابع stat()‎ الذي توفِّره وحدة fs، حيث يمكنك استدعاؤه مع تمرير مسار ملف إليه، حيث سيستدعي نود بعد حصوله على تفاصيل الملف دالة رد النداء التي تمررها مع معاملين هما رسالة خطأ وإحصائيات الملف: const fs = require('fs') fs.stat('/Users/flavio/test.txt', (err, stats) => { if (err) { console.error(err) return } //يمكننا الوصول إلى إحصائيات الملف في‫ `stats` }) كما يوفِّر نود تابعًا متزامنًا يوقِف الخيط thread إلى أن تصبح إحصائيات الملف جاهزةً: const fs = require('fs') try { const stats = fs.stat('/Users/flavio/test.txt') } catch (err) { console.error(err) } تُضمَّن معلومات الملف في المتغير stats، ويمكننا استخراج أنواع معلومات متعددة باستخدام توابع stats كما يلي: استخدم التابع stats.isFile()‎ والتابع stats.isDirectory()‎ لمعرفة إذا كان الملف عبارة عن مجلد أو ملف. استخدم التابع stats.isSymbolicLink()‎ لمعرفة إذا كان الملف وصلةً رمزيةً symbolic link. استخدم التابع stats.size لمعرفة حجم الملف مقدَّرًا بالبايت. هناك توابع متقدمة أخرى، ولكن الجزء الأكبر مما ستستخدمه هو التوابع السابقة. const fs = require('fs') fs.stat('/Users/flavio/test.txt', (err, stats) => { if (err) { console.error(err) return } stats.isFile() //true stats.isDirectory() //false stats.isSymbolicLink() //false stats.size //1024000 //= 1MB }) مسارات الملفات سنتعرّف على كيفية التفاعل مع مسارات الملفات والتعامل معها في نود Node، فلكل ملف في النظام مسار، وقد يبدو المسار في نظامَي لينكس Linux وmacOS كما يلي: /users/flavio/file.txt بينما الحواسيب التي تعمل بنظام ويندوز Windows مختلفة، إذ يكون للمسار بنية كما يلي: C:\users\flavio\file.txt يجب الانتباه عند استخدام المسارات في تطبيقاتك، إذ يجب مراعاة هذا الاختلاف، كما يمكنك تضمين وحدة المسار في ملفاتك كما ما يلي: const path = require('path') ثم يمكنك البدء في استخدام توابعها، كما يمكنك استخراج معلومات من مسار باستخدام التوابع التالية: dirname: للحصول على مجلد الملف الأب. basename: للحصول على جزء اسم الملف. extname: للحصول على امتداد الملف. إليك المثال التالي: const notes = '/users/flavio/notes.txt' path.dirname(notes) // /users/flavio path.basename(notes) // notes.txt path.extname(notes) // .txt يمكنك الحصول على اسم الملف بدون امتداده عن طريق تحديد وسيط ثانٍ للتابع basename كما يلي: path.basename(notes, path.extname(notes)) //notes كما يمكنك ربط جزأين أو أكثر من المسار مع بعضها البعض باستخدام التابع path.join()‎ كما يلي: const name = 'flavio' path.join('/', 'users', name, 'notes.txt') //'/users/flavio/notes.txt' يمكنك حساب مسار الملف المطلق absolute path من مساره النسبي relative path باستخدام التابع path.resolve()‎ كما يلي: path.resolve('flavio.txt') //'/Users/flavio/flavio.txt' if run from my home folder سيُلحِق في هذه الحالة نود Node ببساطة المسارَ النسبي ‎/flavio.txt بدليل أو مجلد العمل الحالي، فإذا حددت مجلدًا على أساس معامل آخر، فسيستخدِم تابع resolve المعامل الأول أساسًا للمعامل الثاني كما يلي: path.resolve('tmp', 'flavio.txt')//‫'‎/Users/flavio/tmp/flavio.txt' إذا شُغِّل من المجلد المحلي إذا بدأ المعامل الأول بشرطة مائلة، فهذا يعني أنه مسار مطلق مثل المثال التالي: path.resolve('/etc', 'flavio.txt')//'/etc/flavio.txt' يُعَدّ path.normalize()‎ تابعًا آخرًا مفيدًا يحسب المسار الفعلي عندما يحتوي على محددات نسبية مثل . أو .. أو شرطة مائلة مزدوجة كما يلي: path.normalize('/users/flavio/..//test.txt') ///users/test.txt لن يتحقق التابعان resolve وnormalize من وجود المسار، وإنما يحسبان المسار فقط بناءً على المعلومات المتاحة. قراءة الملفات أبسط طريقة لقراءة ملف في نود هي استخدام تابع fs.readFile()‎، حيث نمرِّر له مسار الملف ودالة رد النداء التي ستُستدعَى مع بيانات الملف ومع الخطأ كما يلي: const fs = require('fs') fs.readFile('/Users/flavio/test.txt', (err, data) => { if (err) { console.error(err) return } console.log(data) }) يمكنك بدلًا من ذلك استخدام الإصدار المتزامن من التابع السابق وهو التابع fs.readFileSync()‎: const fs = require('fs') try { const data = fs.readFileSync('/Users/flavio/test.txt', 'utf8') console.log(data) } catch (err) { console.error(err) } الترميز الافتراضي هو utf8، ولكن يمكنك تحديد ترميز مُخصَّص باستخدام معامل ثانٍ، كما يقرأ كل من التابعَين fs.readFile()‎ وfs.readFileSync()‎ محتوى الملف الكامل في الذاكرة قبل إعادة البيانات، وهذا يعني أن الملفات الكبيرة سيكون لها تأثير كبير على استهلاك الذاكرة وسرعة تنفيذ البرنامج، وبالتالي يكون الخيار الأفضل في هذه الحالة هو قراءة محتوى الملف باستخدام المجاري streams. كتابة الملفات أسهل طريقة للكتابة في الملفات في Node.js هي استخدام واجهة برمجة تطبيقات fs.writeFile()‎، وإليك المثال التالي: const fs = require('fs') const content = 'Some content!' fs.writeFile('/Users/flavio/test.txt', content, (err) => { if (err) { console.error(err) return } //كُتِب الملف بنجاح }) يمكنك بدلًا من ذلك استخدام الإصدار المتزامن وهو fs.writeFileSync()‎: const fs = require('fs') const content = 'Some content!' try { const data = fs.writeFileSync('/Users/flavio/test.txt', content) //كُتِب الملف بنجاح } catch (err) { console.error(err) } ستبدّل واجهة برمجة التطبيقات هذه افتراضيًا محتويات الملف إذا كان موجودًا مسبقًا، ولكن يمكنك تعديل الإعداد الافتراضي عن طريق تحديد راية كما يلي: fs.writeFile('/Users/flavio/test.txt', content, { flag: 'a+' }, (err) => {}) الرايات التي يمكنك استخدامها هي: r+‎ لفتح الملف للقراءة والكتابة. w+‎ لفتح الملف للقراءة والكتابة مع وضع المجرى في بداية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a لفتح الملف للكتابة مع وضع المجرى في نهاية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a+‎ لفتح الملف للقراءة والكتابة، مع وضع المجرى في نهاية الملف، وإنشاء الملف إن لم يكن موجودًا مسبقًا. يمكنك العثور على المزيد من الرايات على /nodejs. إلحاق محتوى بملف يمكنك إلحاق محتوى بنهاية الملف من خلال استخدام التابع fs.appendFile()‎ ونسخته المتزامنة التابع fs.appendFileSync()‎: const content = 'Some content!' fs.appendFile('file.log', content, (err) => { if (err) { console.error(err) return } //done! }) استخدام المجاري streams تكتب كل التوابع السابقة المحتوى الكامل في الملف قبل إعادة التحكم إلى برنامجك مرةً أخرى، أي تنفيذ دالة رد النداء في النسخة غير المتزامنة، وبالتالي الخيار الأفضل هو كتابة محتوى الملف باستخدام المجاري streams. لنتعرّف على الغرض الأساسي من المجاري streams وسبب أهميتها وكيفية استخدامها، حيث سنقدِّم مدخلًا بسيطًا إلى المجاري، ولكن هناك جوانب أكثر تعقيدًا لتحليلها. مفهوم المجاري streams تُعَدّ المجاري أحد المفاهيم الأساسية التي تعمل على تشغيل تطبيقات Node.js، وهي طريقة للتعامل مع ملفات القراءة/الكتابة أو اتصالات الشبكة أو أيّ نوع من تبادل المعلومات من طرف إلى طرف بطريقة فعالة، كما ليست المجاري مفهومًا خاصًا بنود Node.js، إذ توفّرت في نظام التشغيل يونيكس Unix منذ عقود، ويمكن للبرامج أن تتفاعل مع بعضها البعض عبر تمرير المجاري من خلال معامِل الشريط العمودي أو الأنبوب pipe operator (|). يُقرأ الملف في الذاكرة من البداية إلى النهاية ثم تعالجه، عندما تطلب من البرنامج قراءة ملف بالطريقة التقليدية على سبيل المثال، لكن يمكنك قراءة الملف قطعةً تلو الأخرى باستخدام المجاري، ومعالجة محتواه دون الاحتفاظ به بالكامل في الذاكرة، إذ توفِّر وحدة نود stream الأساس الذي يُبنَى عليه جميع واجهات برمجة التطبيقات ذات المجرى، كما توفِّر المجاري ميزتَين رئيسيتَين باستخدام طرق معالجة البيانات الأخرى هما: فعالية الذاكرة Memory efficiency: لست بحاجة إلى تحميل كميات كبيرة من البيانات في الذاكرة قبل أن تكون قادرًا على معالجتها. فعالية الوقت Time efficiency: تستغرق وقتًا أقل لبدء معالجة البيانات بمجرد حصولك عليها، بدلًا من انتظار اكتمال حمولة البيانات للبدء. يوضِّح المثال التالي قراءة ملفات من القرص الصلب، حيث يمكنك باستخدام وحدة نود fs قراءة ملف وتقديمه عبر بروتوكول HTTP عند إنشاء اتصال جديد بخادم http: const http = require('http') const fs = require('fs') const server = http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', (err, data) => { res.end(data) }) }) server.listen(3000) يقرأ التابع readFile()‎ محتويات الملف الكاملة، ويستدعي دالة رد النداء callback function عند الانتهاء، بينما سيعيد التابع res.end(data)‎ في دالة رد النداء محتويات الملف إلى عميل HTTP، فإذا كان الملف كبيرًا، فستستغرق العملية وقتًا طويلًا، ويمكن تطبيق الأمر نفسه باسخدام المجاري streams كما يلي: const http = require('http') const fs = require('fs') const server = http.createServer((req, res) => { const stream = fs.createReadStream(__dirname + '/data.txt') stream.pipe(res) }) server.listen(3000) يمكننا بث الملف عبر المجاري إلى عميل HTTP بمجرد أن يكون لدينا مجموعة كبيرة من البيانات جاهزة للإرسال بدلًا من انتظار قراءة الملف بالكامل؛ ويستخدم المثال السابق stream.pipe(res)‎، أي استدعاء تابع pipe()‎ في مجرى الملف، حيث يأخذ هذا التابع المصدر، ويضخّه إلى وجهة معينة، كما يُستدعَى هذا التابع على مجرى المصدر، وبالتالي يُضَخ مجرى الملف إلى استجابة HTTP في هذه الحالة، وتكون القيمة المُعادة من التابع pipe()‎ هي مجرى الوجهة، وهذا أمر ملائم للغاية لربط استدعاءات pipe()‎ متعددة كما يلي: src.pipe(dest1).pipe(dest2) الذي يكافئ ما يلي: src.pipe(dest1) dest1.pipe(dest2) تتوفَّر واجهات برمجة تطبيقات API الخاصة بنود Node التي تعمل باستخدام المجاري Streams، إذ توفِّر العديد من وحدات Node.js الأساسية إمكانات معالجة المجرى الأصيلة، ومن أبرزها: process.stdin التي تعيد مجرًى متصلًا بمجرى stdin. process.stdout التي تعيد مجرًى متصلًا بمجرى stdout. process.stderr التي تعيد مجرًى متصلًا بمجرى stderr. fs.createReadStream()‎ الذي ينشئ مجرًى قابلًا للقراءة إلى ملف. fs.createWriteStream()‎ الذي ينشئ مجرًى قابلًا للكتابة إلى ملف. net.connect()‎ الذي يبدأ اتصالًا قائمًا على مجرى. http.request()‎ الذي يعيد نسخة من الصنف http.ClientRequest، وهو مجرى قابل للكتابة. zlib.createGzip()‎ الذي يضغط البيانات باستخدام خوارزمية الضغط gzip في مجرى. zlib.createGunzip()‎ الذي يفك ضغط مجرى gzip. zlib.createDeflate()‎ الذي يضغط البيانات باستخدام خوارزمية الضغط deflate في مجرى. zlib.createInflate()‎ الذي يفك ضغط مجرى deflate. أنواع المجاري المختلفة هناك أربع أصناف من المجاري هي: Readable: هو مجرى يمكن الضخ pipe منه ولكن لا يمكن الضخ إليه، أي يمكنك تلقي البيانات منه ولكن لا يمكنك إرسال البيانات إليه، فإذا دفعتَ بيانات إلى مجرى قابل للقراءة، فستُخزَّن مؤقتًا حتى يبدأ المستهلك في قراءة البيانات. Writable: هو مجرى يمكن الضخ إليه، ولكن لا يمكن الضخ منه، أي يمكنك إرسال البيانات إليه، ولكن لا يمكنك تلقي البيانات منه. Duplex: هو مجرى يمكن الضخ منه وإليه، أي هو مزيج من مجرى Readable ومجرى Writable. Transform: مجرى التحويل مشابه للمجرى Duplex، ولكن خرجه هو تحويل لدخله. كيفية إنشاء مجرى قابل للقراءة يمكن الحصول على مجرى قابل للقراءة من وحدة stream، كما يمكن تهيئته كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() ثم يمكننا إرسال البيانات إليه بعد تهيئته: readableStream.push('hi!') readableStream.push('ho!') كيفية إنشاء مجرى قابل للكتابة يمكنك إنشاء مجرى قابل للكتابة من خلال وراثة كائن Writable الأساسي وتطبيق تابعه ‎_write()‎. أنشئ أولًا كائن Stream كما يلي: const Stream = require('stream') const writableStream = new Stream.Writable() ثم التابع ‎_write كما يلي: writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } يمكنك الآن الضخ إلى مجرى قابل للقراءة كما يلي: process.stdin.pipe(writableStream) كيفية الحصول على بيانات من مجرى قابل للقراءة يمكنك قراءة البيانات من مجرًى قابل للقراءة باستخدام مجرى قابل للكتابة كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() const writableStream = new Stream.Writable() writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } readableStream.pipe(writableStream) readableStream.push('hi!') readableStream.push('ho!') كما يمكنك استهلاك مجرى قابل للقراءة مباشرةً باستخدام الحدث readable كما يلي: readableStream.on('readable', () => { console.log(readableStream.read()) }) كيفية إرسال بيانات إلى مجرى قابل للكتابة استخدم تابع المجرى write()‎ كما يلي: writableStream.write('hey!\n') إعلام مجرى قابل للكتابة بانتهاء الكتابة استخدم التابع end()‎ كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() const writableStream = new Stream.Writable() writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } readableStream.pipe(writableStream) readableStream.push('hi!') readableStream.push('ho!') writableStream.end() التعامل مع المجلدات توفِّر وحدة Node.js الأساسية fs توابعًا متعددةً مفيدةً يمكنك استخدامها للتعامل مع المجلدات. التحقق من وجود مجلد يُستخدَم التابع fs.access()‎ للتحقق مما إذا كان المجلد موجودًا، ويمكن لنود الوصول إلى المجلد باستخدام أذوناته. إنشاء مجلد جديد يُستخدَم التابع fs.mkdir()‎ أو التابع fs.mkdirSync()‎ لإنشاء مجلد جديد. const fs = require('fs') const folderName = '/Users/flavio/test' try { if (!fs.existsSync(dir)){ fs.mkdirSync(dir) } } catch (err) { console.error(err) } قراءة محتوى مجلد يُستخدَم التابع fs.readdir()‎ أو التابع fs.readdirSync لقراءة محتويات مجلد، ويقرأ جزء الشيفرة التالية محتوى مجلد من ملفات ومجلدات فرعية، ويعيد مساراتها النسبية: const fs = require('fs') const path = require('path') const folderPath = '/Users/flavio' fs.readdirSync(folderPath) يمكنك الحصول على المسار الكامل من خلال ما يلي: fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName) } كما يمكنك تصفية النتائج لإعادة الملفات فقط واستبعاد المجلدات كما يلي: const isFile = fileName => { return fs.lstatSync(fileName).isFile() } fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName)).filter(isFile) } إعادة تسمية مجلد يُستخدَم التابع fs.rename()‎ أو التابع fs.renameSync()‎ لإعادة تسمية مجلد، حيث يكون المعامِل الأول هو المسار الحالي، والمعامِل الثاني هو المسار الجديد: const fs = require('fs') fs.rename('/Users/flavio', '/Users/roger', (err) => { if (err) { console.error(err) return } //done }) كما يمكنك استخدام التابع fs.renameSync()‎ الذي هو النسخة المتزامنة كما يلي: const fs = require('fs') try { fs.renameSync('/Users/flavio', '/Users/roger') } catch (err) { console.error(err) } إزالة مجلد يُستخدَم التابع fs.rmdir()‎ أو التابع fs.rmdirSync()‎ لإزالة مجلد، ويمكن أن تكون إزالة مجلد أكثر تعقيدًا إذا تضمّن محتوىً، لذلك نوصي في هذه الحالة بتثبيت وحدة fs-extra التي تحظى بشعبية ودعم كبيرَين، وهي بديل سريع لوحدة fs، وبالتالي تضيف مزيدًا من الميزات عليها، كما ستحتاج استخدام التابع remove()‎. ثبّت وحدة fs-extra باستخدام الأمر: npm install fs-extra، واستخدمها كما يلي: const fs = require('fs-extra') const folder = '/Users/flavio' fs.remove(folder, err => { console.error(err) }) كما يمكن استخدامها مع الوعود promises كما يلي: fs.remove(folder).then(() => { //done }).catch(err => { console.error(err) }) أو مع صيغة async/await كما يلي: async function removeFolder(folder) { try { await fs.remove(folder) //done } catch (err) { console.error(err) } } const folder = '/Users/flavio' removeFolder(folder) للمزيد، يمكنك الرجوع إلى توثيق التعامل مع نظام الملفات في Node.js في موسوعة حسوب. ترجمة -وبتصرّف- للفصل File System من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: تعرف على وحدات Node.js الأساسية المقال السابق: التعامل مع الطلبيات الشبكية في Node.js التعامل مع الملفات في البرمجة مدخل إلى التعامل مع الملفات في جافا كيفية التعامل مع الملفات النصية في بايثون 3 التعامل مع الملفات النصية في لغة سي شارب #C
  4. سنتعرّف من خلال هذا المقال على طريقة إرسال واستقبال الطلبيات بين الخادم والعميل عبر الشبكة في Node.js باستخدام مكتبة Axios، ولكن سنبدأ بشرح وسيلة التواصل الأساسية بين الخادم والمتصفح وهو بروتوكول HTTP، وسنتعرّف على بديل اتصال HTTP في تطبيقات الويب الذي هو مقابس الويب WebSockets. كيفية عمل بروتوكول HTTP يُعَدّ بروتوكول نقل النص الفائق Hyper Text Transfer Protocol -أو HTTP اختصارًا- أحد بروتوكولات تطبيق TCP/IP وهي مجموعة البروتوكولات التي تشغّل شبكة الإنترنت، إذ يُعَدّ البروتوكول الأنجح والأكثر شعبية على الإطلاق، كما يُشغِّل هذا البروتوكول شبكة الويب العالمية World Wide Web، مما يمنح المتصفحات لغة للتواصل مع الخوادم البعيدة التي تستضيف صفحات الويب. وُحِّد بروتوكول HTTP لأول مرة في عام 1991على أساس نتيجة لعمل تيم بيرنرز لي Tim Berners-Lee في المركز الأوروبي للأبحاث النووية European Center of Nuclear Research -أو CERN اختصارًا- منذ عام 1989، وكان الهدف هو السماح للباحثين بتبادل أبحاثهم بسهولة وربطهم ببعضهم بعضًا على أساس وسيلة تحسِّن عمل المجتمع العلمي، كما تكوّنت تطبيقات الإنترنت الرئيسية في ذلك الوقت من بروتوكول FTP أي بروتوكول نقل الملفات File Transfer Protocol والبريد الإلكتروني ونظام يوزنت Usenet أي مجموعات الأخبار newsgroups، ولكنها أصبحت غير مُستخدَمة حاليًا تقريبًا. صدر متصفح موزاييك Mosaic في عام 1993، وهو أول متصفح ويب رسومي، وتطورت الأمور عندها، إذ أصبح الويب التطبيق القاتل في شبكة الإنترنت، حيث سبّب ظهوره ضجةً كبيرةً، كما تطوّر الويب والنظام المجتمعي المحيط به تطوّرًا كبيرًا بمرور الوقت مع بقاء الأساسيات على حالها، وأحد الأمثلة على هذا التطور هو أنّ بروتوكول HTTP يشغّل حاليًا -بالإضافة إلى صفحات الويب- واجهات برمجة تطبيقات REST، وهي إحدى الطرق الشائعة للوصول إلى خدمة عبر الإنترنت برمجيًا. عُدِّل بروتوكول HTTP تعديلًا ثانويًا في عام 1997 في الإصدار HTTP/1.1، وخلفه الإصدار HTTP/2 الذي وُحِّد في عام 2015 ويُطبَّق الآن على خوادم الويب الرئيسية المُستخدَمة في جميع أنحاء العالم، كما يُعَدّ بروتوكول HTTP غير آمن مثل أيّ بروتوكول آخر غير مخدَّم عبر اتصال مشفَّر مثل بروتوكولات SMTP وFTP وغيرها، وهذا هو السبب في التوجّه الكبير حاليًا نحو استخدام بروتوكول HTTPS، وهو بروتوكول HTTP مخدَّم عبر بروتوكول TLS، ولكن بروتوكول HTTP هو حجر الأساس لبروتوكول HTTP/2 وHTTPS. مستندات HTML بروتوكول HTTP هو الطريقة التي تتواصل بها متصفحات الويب web browsers مثل Chrome وFirefox وEdge ومتصفحات أخرى سنسمّيها عملاء clients مع خوادم الويب web servers، كما اُشتق الاسم بروتوكول نقل النص الفائق Hyper Text Transfer Protocol من الحاجة إلى نقل الملفات كما هو الحال في بروتوكول FTP والذي يشير إلى بروتوكول نقل الملفات File Transfer Protocol، بالإضافة إلى النصوص الفائقة hypertexts التي ستُكتَب باستخدام لغة HTML، ثم تُمثَّل رسوميًا باستخدام المتصفح مع عرض جميل وروابط تفاعلية، وساهمت الروابط بقوة في اعتماد بروتوكول HTTP إلى جانب سهولة إنشاء صفحات ويب جديدة، حيث ينقل هذا البروتوكول ملفات النصوص الفائقة بالإضافة إلى الصور وأنواع الملفات الأخرى عبر الشبكة. الروابط والطلبيات يمكن أن يؤشّر مستند إلى مستند آخر باستخدام الروابط ضمن متصفح الويب، حيث يحدِّد جزء الرابط الأول كل من البروتوكول وعنوان الخادم من خلال إما اسم نطاق domain name أو عنوان IP، وليس هذا الجزء خاصًا ببروتوكول HTTP؛ أما الجزء الثاني فهو جزء المستند الذي يتبع جزء العنوان ويمثِّل مسار المستند مثل https://flaviocopes.com/http/‎ الذي يتكوّن مما يلي: https هو البروتوكول. flaviocopes.com هو اسم النطاق الذي يؤشر إلى الخادم. /http/ هو عنوان URL النسبي للمستند إلى مسار الخادم الجذر. يمكن أن يتداخل المسار مثل https://academy.hsoub.com/files/c5-programming/‎، حيث يكون عنوان URL للمستند هو ‎/files/c5-programming؛ أما خادم الويب فيُعَدّ مسؤولًا عن تفسير الطلب وتقديم الاستجابة الصحيحة بعد تحليل الطلب، كما يمكن أن يكون الطلب عنوان URL الذي رأيناه سابقًا، فإذا أدخلنا عنوانًا وضغطنا Enter من لوحة المفاتيح في المتصفح، فسيرسل الخادم طلبًا في الخلفية إلى عنوان IP الصحيح مثل الطلب التالي: GET /a-page حيث ‎‎‎‎/a-page‎ هو عنوان URL الذي طلبته، كما يمكن أن يكون الطلب تابع HTTP ويُسمّى فعلًا verb أيضًا، حيث حدّد بروتوكول HTTP سابقًا ثلاثةً من هذه التوابع وهي: GET. POST. HEAD. وقدّم الإصدار HTTP/1.1 التوابع: PUT. DELETE. OPTIONS. TRACE. سنتحدّث عنها لاحقًا، وقد يكون الطلب مجموعة ترويسات HTTP، فالترويسات Headers هي مجموعة من أزواج المفتاح-القيمة key: value التي تُستخدَم للتواصل مع المعلومات الخاصة بالخادم المُحدَّدة مسبقًا ليتمكّن الخادم من فهم ما نعنيه، كما أنّ جميع الترويسات اختيارية باستثناء الترويسة Host. توابع HTTP أهم توابع HTTP هي: GET: هو التابع الأكثر استخدامًا، وهو الخيار الذي يُستخدَم عند كتابة عنوان URL في شريط عنوان المتصفح، أو عند النقر على رابط، كما يطلب هذا التابع من الخادم إرسال المورد المطلوب على أساس استجابة. HEAD: يتشابه هذا التابع مع التابع GET تمامًا، ولكن HEAD يخبر الخادم بعدم إرسال جسم الاستجابة response body، بل إرسال الترويسات فقط. POST: يستخدِم العميل هذا التابع لإرسال البيانات إلى الخادم، حيث يُستخدَم عادةً في النماذج forms مثلًا، وعند التفاعل مع واجهة برمجة تطبيقات REST. PUT: يهدف هذا التابع إلى إنشاء مورد في عنوان URL المحدَّد باستخدام المعاملات المُمرَّرة في جسم الطلب، كما يُستخدم استخدامًا رئيسيًا في واجهات برمجة تطبيقات REST. DELETE: يُستدعَى هذا التابع مع عنوان URL لطلب حذف المورد المقابل لهذا العنوان، كما يُستخدَم استخدامًا رئيسيًا في واجهات برمجة تطبيقات REST. OPTIONS: يجب أن يرسِل الخادم قائمة توابع HTTP المسموح بها إلى عنوان URL المحدَّد عندما يتلقى طلب OPTIONS. TRACE: يعيد هذا التابع إلى العميل الطلب المُستلَم، حيث يُستخدَم هذا التابع لتنقيح الأخطاء debugging أو لأغراض التشخيص. اتصال HTTP خادم/عميل بروتوكول HTTP هو بروتوكول عديم الحالة stateless مثل معظم البروتوكولات التي تنتمي إلى مجموعة بروتوكولات TCP/IP، حيث ليس لدى الخوادم أيّ فكرة عن حالة العميل الحالية، فكل ما يهم الخوادم هو أن تتلقى طلبات ثم تلبيتها، كما لا يكون لطلب مسبق أيّ معنى في هذا السياق، وبالتالي يمكن أن يكون خادم الويب سريعًا جدًا، مع وجود قليل من المعالجة وحيز نطاق تراسلي bandwidth مناسب لمعالجة كثير من الطلبات المتزامنة. يُعَدّ بروتوكول HTTP مرنًا واتصاله سريعًا جدًا اعتمادًا على حِمل الشبكة، وهذا يتناقض مع البروتوكولات الأكثر استخدامًا في وقت صدوره مثل TCP وPOP/SMTP وبروتوكولات البريد التي تتضمن كثيرًا من عمليات المصافحة handshaking والتأكيدات على النهايات المُستقبَلة، كما تجرِّد المتصفحات الرسومية هذا الاتصال، ولكن يمكن توضيحه كما يلي، إذ يبدأ سطر الرسالة الأول بتابع HTTP ثم مسار المَورد النسبي وإصدار البروتوكول كما يلي: GET /a-page HTTP/1.1 ثم يجب إضافة ترويسات طلبات HTTP، إذ توجد هناك ترويسات متعددة، ولكن الترويسة الإلزامية الوحيدة هي Host: GET /a-page HTTP/1.1 Host: flaviocopes.com يمكنك اختبار ذلك باستخدام أداة telnet، وهي أداة سطر أوامر تتيح لنا الاتصال بأي خادم وإرسال الأوامر إليه، والآن افتح طرفيتك terminal واكتب telnet flaviocopes.com 80 مثلًا، حيث سيؤدي ذلك إلى فتح طرفية تعرض ما يلي: Trying 178.128.202.129... Connected to flaviocopes.com. Escape character is '^]'. أنت الآن متصل بخادم الويب Netlify، ثم اكتب ما يلي: GET /axios/ HTTP/1.1 Host: flaviocopes.com اضغط بعد ذلك على زر Enter في سطر فارغ لتشغيل الطلب، وستكون الاستجابة كما يلي: HTTP/1.1 301 Moved Permanently Cache-Control: public, max-age=0, must-revalidate Content-Length: 46 Content-Type: text/plain Date: Sun, 29 Jul 2018 14:07:07 GMT Location: https://flaviocopes.com/axios/ Age: 0 Connection: keep-alive Server: Netlify Redirecting to https://flaviocopes.com/axios/ وهذه هي استجابة HTTP التي حصلنا عليها من الخادم، وهي طلب 301‎ Moved Permanently الذي يخبرنا بانتقال المَورد إلى موقع آخر انتقالًا دائمًا، وذلك لأننا اتصلنا بالمنفذ 80 وهو المنفذ الافتراضي لبروتوكول HTTP، ولكننا ضبطنا الخادم على إعادة التوجيه التلقائي إلى HTTPS، كما حُدِّد الموقع الجديد في ترويسة استجابة HTTP التي هي Location، وهناك ترويسات استجابة أخرى سنتحدث عنها لاحقًا، كما يفصل سطر فارغ ترويسة الطلب عن جسمه في كل من الطلب والاستجابة، حيث يحتوي جسم الطلب في مثالنا على السلسلة النصية التالية: Redirecting to https://flaviocopes.com/axios/ يبلغ طول هذه السلسلة النصية 46 بايتًا كما هو محدَّد في ترويسة Content-Length، إذ تظهر هذه السلسلة في المتصفح عند فتح الصفحة ريثما يُعاد توجيهك إلى الموقع الصحيح تلقائيًا، كما نستخدم أداة telnet في مثالنا، وهي أداة منخفضة المستوى يمكننا استخدامها للاتصال بأي خادم، لذلك لا يمكننا الحصول على أي نوع من إعادة التوجيه التلقائي، فلنتصل الآن بالمنفذ 443 وهو المنفذ الافتراضي لبروتوكول HTTPS، حيث لا يمكننا استخدام أداة telnet بسبب مصافحة SSL التي يجب أن تحدث،فولنستخدم الآن أداة curl وهي أداة سطر أوامر أخرى، إذ لا يمكننا كتابة طلب HTTP مباشرةً، لكننا سنرى الاستجابة: curl -i https://flaviocopes.com/axios/ سنحصل في المقابل على ما يلي: HTTP/1.1 200 OK Cache-Control: public, max-age=0, must-revalidate Content-Type: text/html; charset=UTF-8 Date: Sun, 29 Jul 2018 14:20:45 GMT Etag: "de3153d6eacef2299964de09db154b32-ssl" Strict-Transport-Security: max-age=31536000 Age: 152 Content-Length: 9797 Connection: keep-alive Server: Netlify <!DOCTYPE html> <html prefix="og: http://ogp.me/ns#" lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>HTTP requests using Axios</title> .... لن ينقل خادم HTTP ملفات HTML فقط، وإنما يمكنه نقل ملفات أخرى مثل ملفات CSS و JS و SVG و PNG و JPG وأنواع ملفات متعددة أخرى، إذ يعتمد ذلك على الإعداد، فبروتوكول HTTP قادر تمامًا على نقل هذه الملفات، وسيعرف العميل نوع الملف، وبالتالي سيفسرها بالطريقة الصحيحة، وهذه هي الطريقة التي يعمل بها الويب عند استرداد صفحة HTML بواسطة المتصفح، إذ تُفسَّر هذه الصفحة وأي مَورد آخر يحتاجه المتصفح لعرض خاصية (CSS و JavaScript والصور وغير ذلك) مُسترَدّة عبر طلبات HTTP إضافية إلى الخادم نفسه. بروتوكول HTTPS والاتصالات الآمنة يُعَدّ بروتوكول HTTPS امتدادًا لبروتوكول HTTP -أي بروتوكول نقل النص الفائق- والذي يوفِّر اتصالًا آمنًا، فبروتوكول HTTP غير آمن في تصميمه، فإذا فتحتَ متصفحك وطلبتَ من خادم الويب إرسال صفحة ويب لك، فستسير بياناتك ضمن رحلتين تكون الأولى من المتصفح إلى خادم الويب، والأخرى من خادم الويب إلى المتصفح، وقد تحتاج بعد ذلك إلى مزيد من الاتصالات -اعتمادًا على محتوى صفحة الويب- للحصول على ملفات CSS وملفات JavaScript والصور وما إلى ذلك، كما يمكن فحص بياناتك والتلاعب بها خلال مرورها في الشبكة أثناء أيّ من هذه الاتصالات. قد تكون العواقب وخيمةً، فقد يراقب ويسجّل طرف ثالث كل أنشطة شبكتك دون علمك، وقد تحقن بعض الشبكات إعلانات، وقد تكون عرضةً لهجوم الوسيط man-in-the-middle، وهو تهديد أمني يستطيع المهاجم من خلاله التلاعب ببياناتك وحتى انتحال شخصية حاسوبك عبر الشبكة، إذ يمكن لأي شخص الاستماع بسهولة إلى حزم HTTP المُرسَلة عبر شبكة واي فاي Wi-Fi عامة وغير مشفَّرة، حيث يهدف بروتوكول HTTPS إلى حل هذه المشكلة من خلال تشفير الاتصال الكامل بين متصفحك وخادم الويب. تُعَدّ كل الخصوصية والأمن مصدر قلق كبير في شبكة الإنترنت حاليًا، فقد كان الأمر مختلفًا قبل بضع سنوات، حيث كان بإمكانك توفير الأمن من خلال استخدام اتصال مشفر فقط في الصفحات المحمية بتسجيل الدخول أو أثناء عمليات الدفع في المتاجر الإلكترونية، كما أنّ معظم مواقع الويب قد استخدَمت بروتوكول HTTP بسبب أسعار شهادات SSL وتعقيداتها. يُعَدّ استخدام HTTPS إلزاميًا على جميع المواقع في الوقت الحالي، إذ يستخدِمه حاليًا أكثر من 50% من مواقع الويب، وقد بدأ Google Chrome مؤخرًا في تمييز مواقع HTTP بأنها غير آمنة، لمنحك سببًا وجيهًا في جعل بروتوكول HTTPS إلزاميًا على جميع مواقع الويب الخاصة بك. يكون منفذ الخادم الافتراضي هو 80 عند استخدام بروتوكول HTTP، في حين يكون 443 عند استخدام بروتوكول HTTPS، وليست إضافته بصورة صريحة أمرًا إلزاميًا إذا استخدَم الخادم المنفذ الافتراضي، كما يُطلق على بروتوكول HTTPS أحيانًا اسم HTTP عبر SSL أو HTTP عبر TLS، حيث يكون بروتوكول TLS خلَفًا لبروتوكول SSL؛ أما الشيء الوحيد غير المشفَّر عند استخدام بروتوكول HTTPS، فهو نطاق خادم الويب ومنفذ الخادم، بينما تُشفَّر كل المعلومات الأخرى بما في ذلك مسار المَورد والترويسات وملفات تعريف الارتباط cookies ومعامِلات الاستعلام. لن نشرح تفاصيل تحليل كيفية عمل بروتوكول TLS الداخلي، لكنك قد تعتقد أنه يضيف قدرًا كبيرًا من الحِمل على الشبكة، وربما هذا صحيح، إذ تتسبب أيّ عملية حسابية مُضافَة إلى معالجة موارد الشبكة في زيادة الحِمل على العميل والخادم وحجم الرُزم المرسَلة على حد سواء. يتيح HTTPS استخدام أحدث بروتوكول وهو HTTP/2 الذي يحتوي على ميزة إضافية يتفوق بها على الإصدار HTTP/1.1، وهذه الميزة هي أنه أسرع لعدة أسباب مثل ضغط الترويسة وتعدُّد الموارد، كما يمكن للخادم زجّ مزيد من الموارد عند طلب أحدها، فإذا طلب المتصفح صفحةً، فسيتلقى جميع الموارد اللازمة مثل الصور وملفات CSS وJS، كما يُعَدّ HTTP/2 تحسّنًا كبيرًا على HTTP/1.1 ويتطلب بروتوكول HTTPS، وهذا يعني أنّ HTTPS أسرع من HTTP بكثير إذا ضُبِط كل شيء ضبطًا صحيحًا باستخدام إعداد حديث على الرغم من وجود عبء التشفير الإضافي. كيفية عمل طلبات HTTP سنشرح ما يحدث عند كتابة عنوان URL في المتصفح من البداية إلى النهاية، حيث سنوضّح كيف تطبّق المتصفحات طلبات الصفحة باستخدام بروتوكول HTTP/1.1، إذ ذكرنا HTTP على وجه الخصوص لأنه يختلف عن اتصال HTTPS. إذا أجريت مقابلةً عمل من قبل، فقد تُسأَل ماذا يحدث عندما تكتب شيئًا ما في مربع بحث جوجل ثم تضغط مفتاح Enter؟ فهو أحد الأسئلة الأكثر شيوعًا التي ستُطرَح عليك، لمعرفة ما إذا كان بإمكانك شرح بعض المفاهيم الأساسية وما إذا كان لديك أيّ فكرة عن كيفية عمل الإنترنت، حيث سنحلّل ما يحدث عندما تكتب عنوان URL في شريط عنوان متصفحك ثم تضغط على Enter، وتُعَدّ هذه التقنية نادرة التغيّر وتشغّل أحد أكثر الأنظمة المجتمعية التي بناها الإنسان تعقيدًا واتساعًا. تحليل طلبات URL تملك المتصفحات الحديثة القدرة على معرفة ما إذا كان الشيء الذي كتبته في شريط العناوين هو عنوان URL فعلي أو مصطلح بحث، حيث سيستخدم المتصفح محرّك البحث الافتراضي إذا لم يكن عنوان URL صالحًا، فلنفترض أنك كتبتَ عنوان URL فعليًا، حيث ينشئ المتصفح أولًا عنوان URL الكامل عند إدخال العنوان ثم الضغط على مفتاح Enter، فإذا أدخلت نطاقًا مثل flaviocopes.com، فسيضيف المتصفح إلى بدايته HTTP://‎ افتراضيًا اعتمادًا على بروتوكول HTTP. مرحلة بحث DNS يبدأ المتصفح عملية بحث DNS للحصول على عنوان IP الخادم، ويُعَدّ اسم النطاق اختصارًا مفيدًا للبشر، ولكن الإنترنت منظَّم بطريقة تمكّن الحواسيب من البحث عن موقع الخادم الدقيق من خلال عنوان IP الخاص به، وهو عبارة عن مجموعة من الأعداد مثل 222.324.3.1 في الإصدار IPv4، حيث يتحقق المتصفح أولًا من ذاكرة DNS المخبئية المحلية، للتأكد من أن النطاق قد جرى تحليله resolved مؤخرًا، كما يحتوي كروم Chrome على عارض مفيد لذاكرة DNS المخبئية الذي يمكنك رؤيته من خلال chrome://net-internals/#dns، فإذا لم تعثر على أي شيء هناك، فهذا يعني استخدام المتصفح محلّل DNS عن طريق استدعاء نظام ‎gethostbyname POSIX لاسترداد معلومات المضيف. gethostbyname يبحث استدعاء النظام gethostbyname أولًا في ملف المضيفِين hosts المحلي، والذي يوجد في نظامَي macOS أو لينكس Linux ضمن ‎/etc/hosts، للتأكد من أن النظام يوفِّر المعلومات محليًا، فإذا لم يقدّم ملف المضيفِين المحلي أيّ معلومات عن النطاق، فسيقدّم النظام طلبًا إلى خادم DNS، حيث يُخزَّن عنوان خادم DNS في تفضيلات النظام، كما يُعَدّ الخادمان التاليان خادمي DNS شهيرين: 8.8.8.8: خادم DNS العام الخاص بجوجل. 1.1.1.1: خادم CloudFlare DNS. يستخدِم معظم الأشخاص خادم DNS الذي يوفِّره مزوّد خدمة الإنترنت الخاص بهم، كما يطبّق المتصفح طلب DNS باستخدام بروتوكول UDP، فالبروتوكولان TCP وUDP من بروتوكولات الشبكات الحاسوبية الأساسية ويتواجدان بالمستوى نفسه، لكن بروتوكول TCP موجَّه بالاتصال، بينما بروتوكول UDP عديم الاتصال وأخف، ويُستخدَم لإرسال الرسائل مع قليل من الحِمل على الشبكة. قد يحتوي خادم DNS على عنوان IP النطاق في الذاكرة المخبئية، فإذا لم يكن كذلك، فسيسأل خادم DNS الجذر، إذ يتكون هذا النظام من 13 خادم حقيقي موزع في أنحاء العالم، حيث يقود هذا النظام شبكة الإنترنت بأكملها، كما لا يعرف خادم DNS عنوان كل اسم نطاق على هذا الكوكب، ولكن يكفي معرفة مكان وجود محلّلي DNS من المستوى الأعلى، إذ يُعَدّ نطاق المستوى الأعلى top-level domain امتداد النطاق مثل ‎.com و‎.it و‎.pizza وغير ذلك. يَعيد خادم DNS توجيه الطلب عند تلقّيه إلى خادم DNS الخاص بنطاق المستوى الأعلى TLD، ولنفترض أنك تبحث عن موقع flaviocopes.com، حيث يعيد خادم DNS الخاص بالنطاق الجذر عنوان IP الخاص بخادم نطاق المستوى الأعلى ‎.com، ويخزّن بعدها محلّل DNS الخاص بنا عنوان IP لخادم نطاق المستوى الأعلى، بحيث لا يتعيّن عليه أن يسأل خادم DNS الجذر مرةً أخرى عنه. سيمتلك خادم DNS الخاص بنطاق المستوى الأعلى عناوين IP لخوادم الأسماء الرسمية الخاصة بالنطاق الذي نبحث عنه، إذ عند شرائك لنطاقٍ يرسل مسجل النطاق domain registrar نطاق المستوى الأعلى المناسب TDL إلى خوادم الأسماء.، فإذا حدّثتَ خوادم الأسماء عند تغيير مزود الاستضافة مثلًا، فسيُحدِّث مسجّل النطاق الخاص بك هذه المعلومات تلقائيًا، ونوضِّح فيما يلي أمثلةً عن خوادم DNS لمزود الاستضافة التي تكون أكثر من خادم عادةً لاستخدامها على أساس نسخة احتياطية: ns1.dreamhost.com ns2.dreamhost.com ns3.dreamhost.com يبدأ محلل DNS بالخادم الأول، ويحاول طلب عنوان IP الخاص بالنطاق مع النطاق الفرعي أيضًا الذي تبحث عنه، وهو المصدر النهائي لعنوان IP. إنشاء اتصال/مصافحة handshaking طلب TCP يمكن للمتصفح الآن بدء اتصال TCP عند توفر عنوان IP الخادم، حيث يتطلب اتصال TCP عملية مصافحة handshaking قبل تهيئته بالكامل والبدء بإرسال البيانات، إذ يمكننا إرسال الطلب بعد إنشاء الاتصال. إرسال الطلب يكون الطلب عبارةً عن مستند نصي منظَّم بطريقة دقيقة يحدّدها بروتوكول الاتصال، ويتكون من 3 أجزاء هي: سطر الطلب request line. ترويسة الطلب request header. جسم الطلب request body. يضبط سطر الطلب ما يلي في سطر واحد: تابع HTTP. موقع المَورد. إصدار البروتوكول. إليك المثال التالي: GET / HTTP/1.1 تتكون ترويسة الطلب من مجموعة من أزواج الحقل-القيمة field: value التي تحدِّد قيمًا معينةً، وهناك حقلان إلزاميان هما Host وConnection، بينما جميع الحقول الأخرى اختيارية: Host: flaviocopes.com Connection: close يشير الحقل Host إلى اسم النطاق الذي نريد الوصول إليه، بينما يُضبَط الحقل Connection على القيمة close دائمًا إلّا في حالة إبقاء الاتصال مفتوحًا، وبعض حقول الترويسة الأكثر استخدامًا هي: Origin Accept Accept-Encoding Cookie Cache-Control Dnt وهناك غيرها الكثير، ويُنهَى جزء الترويسة بسطر فارغ. أما جسم الطلب فهو اختياري ولا يُستخدَم في طلبات GET، ولكنه يُستخدَم بكثرة في طلبات POST وفي أفعال أخرى في بعض الأحيان، كمايمكن أن يحتوي على بيانات بتنسيق JSON، وبما أننا الآن نحلّل طلب GET، فإن الجسم فارغ. الاستجابة Response يعالِج الخادم الطلب بعد إرساله ويرسل استجابةً، حيث تبدأ الاستجابة برمز الحالة status code ورسالة الحالة status message، فإذا كان الطلب ناجحًا ويعيد القيمة 200، فستبدأ الاستجابة بما يلي: 200 OK قد يعيد الطلب رمز ورسالة حالة مختلفَين مثل الأمثلة التالية: 404 Not Found 403 Forbidden 301 Moved Permanently 500 Internal Server Error 304 Not Modified 401 Unauthorized تحتوي الاستجابة بعد ذلك على قائمة بترويسات HTTP وجسم الاستجابة الذي سيكون HTML لأننا ننفّذ الطلب في المتصفح. تحليل HTML تلقّى المتصفح الآن ملف HTML وبدأ في تحليله، وسيكرّر العملية نفسها بالضبط على جميع الموارد التي تطلبها الصفحة مثل: ملفات CSS. الصور. الأيقونة المفضلة أو رمز الموقع favicon. ملفات جافا سكريبت. وغير ذلك. الطريقة التي تصيّر render بها المتصفحاتُ الصفحةَ خارج نطاق مناقشتنا، ولكن يجب فهم أن العملية التي شرحناها غير مقتصرة على صفحات HTML فقط، بل يمكن تطبيقها على أيّ عنصر مُقدَّم عبر بروتوكول HTTP. بناء خادم HTTP باستخدام Node.js خادم ويب HTTP الذي سنستخدِمه هو الخادم نفسه الذي استخدمناه سابقًا مثل تطبيق Node Hello World. const http = require('http') const port = 3000 const server = http.createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('Hello World\n') }) server.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`) }) لنحلّل المثال السابق بإيجاز: ضمّنا وحدة http التي نستخدمها لإنشاء خادم HTTP، وضُبِط الخادم للاستماع على المنفذ المحدَّد 3000، حيث تُستدعَى دالة رد النداء listen عندما يكون الخادم جاهزًا، فدالة رد النداء التي نمررها هي الدالة التي ستُنفَّذ عند وصول كل طلب، ويُستدَعى حدث request عند تلقّي طلب جديد، مما يوفّر كائنين هما: طلب (كائن http.IncomingMessage) واستجابة ( كائن http.ServerResponse). يوفّر الطلب request تفاصيل الطلب، حيث نصل من خلاله إلى ترويسات الطلبات وبياناتها؛ أما الاستجابة response فتُستخدَم لتوفير البيانات التي سنعيدها إلى العميل، كما ضبطنا خاصية statusCode على القيمة 200 في مثالنا، للإشارة إلى استجابة ناجحة. res.statusCode = 200 وضبطنا ترويسة Content-Type كما يلي: res.setHeader('Content-Type', 'text/plain') ثم أغلقنا الاستجابة في النهاية بإضافة المحتوى على أساس وسيط للتابع end()‎: res.end('Hello World\n') إجراء طلبات HTTP سنشرح كيفية إجراء طلبات HTTP في Node.js باستخدام GET و POST و PUT و DELETE. إجراء طلب GET const https = require('https') const options = { hostname: 'flaviocopes.com', port: 443, path: '/todos', method: 'GET' } const req = https.request(options, (res) => { console.log(`statusCode: ${res.statusCode}`) res.on('data', (d) => { process.stdout.write(d) }) }) req.on('error', (error) => { console.error(error) }) req.end() إجراء طلب POST const https = require('https') const data = JSON.stringify({ todo: 'Buy the milk' }) const options = { hostname: 'flaviocopes.com', port: 443, path: '/todos', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } } const req = https.request(options, (res) => { console.log(`statusCode: ${res.statusCode}`) res.on('data', (d) => { process.stdout.write(d) }) }) req.on('error', (error) => { console.error(error) }) req.write(data) req.end() PUT وDELETE تستخدِم طلبات PUT وDELETE تنسيق طلب POST نفسه مع تغيير قيمة options.method فقط. مكتبة Axios تُعَدّ Axios مكتبة جافاسكربت يمكنك استخدامها لإجراء طلبات HTTP، وتعمل في المنصَّتين المتصفح Browser ونود Node.js. تدعم هذه المكتبة جميع المتصفحات الحديثة بما في ذلك الإصدار IE8 والإصدارات الأحدث، كما تستند على الوعود، وهذا يتيح لنا كتابة شيفرة صيغة عدم التزامن/الانتظار async/await لإجراء طلبات XHR بسهولة، كما يتمتع استخدام مكتبة Axios ببعض المزايا بالموازنة مع واجهة Fetch API الأصيلة، وهذه المزايا هي: تدعم المتصفحات القديمة، حيث تحتاج Fetch إلى تعويض نقص دعم المتصفحات polyfill. لديها طريقة لإبطال طلب. لديها طريقة لضبط مهلة الاستجابة الزمنية. تحتوي على حماية CSRF مبنية مسبقًا. تدعم تقدّم التحميل. تجري تحويل بيانات JSON تلقائيًا. تعمل في Node.js. تثبيت Axios يمكن تثبيت Axios باستخدام npm: npm install axios أو باستخدام yarn: yarn add axios أو يمكنك تضمينها ببساطة في صفحتك باستخدام unpkg.com كما يلي: <script src="https://unpkg.com/axios/dist/axios.min.js"></script> واجهة برمجة تطبيقات Axios يمكنك بدء طلب HTTP من كائن axios: axios({ url: 'https://dog.ceo/api/breeds/list/all', method: 'get', data: { foo: 'bar' } }) لكنك ستستخدم التوابع التالية كما هو الحال في jQuery، حيث يمكنك استخدام ‎$.get()‎ و‎$.post()‎ بدلًا من ‎$.ajax()‎: axios.get()‎ axios.post()‎ توفِّر مكتبة Axios توابعًا لجميع أفعال HTTP، والتي تُعَدّ أقل شيوعًا ولكنها لا تزال مُستخدَمة: ‎axios.delete()‎ axios.put()‎ axios.patch()‎ axios.options()‎ axios.head()‎: وهو تابع يُستخدَم للحصول على ترويسات HTTP لطلب ما مع تجاهل الجسم. إرسال واستقبال الطلبات إحدى الطرق الملائمة لاستخدام مكتبة Axios هي استخدام صيغة async/await الحديثة في الإصدار ES2017، حيث يستعلم مثال Node.js التالي عن واجهة Dog API لاسترداد قائمة بجميع سلالات الكلاب dogs breeds باستخدام التابع axios.get()‎، ويحصي هذه السلالات: const axios = require('axios') const getBreeds = async () => { try { return await axios.get('https://dog.ceo/api/breeds/list/all') } catch (error) { console.error(error) } } const countBreeds = async () => { const breeds = await getBreeds() if (breeds.data.message) { console.log(`Got ${Object.entries(breeds.data.message).length} breeds`) } } countBreeds() إذا لم ترغب في استخدام صيغة async/await، فيمكنك استخدام صيغة الوعود Promises: const axios = require('axios') const getBreeds = () => { try { return axios.get('https://dog.ceo/api/breeds/list/all') } catch (error) { console.error(error) } } const countBreeds = async () => { const breeds = getBreeds() .then(response => { if (response.data.message) { console.log( `Got ${Object.entries(response.data.message).length} breeds` ) } }) .catch(error => { console.log(error) }) } countBreeds() يمكن أن تحتوي استجابة GET على معامِلات في عنوان URL مثل https://site.com/?foo=bar، حيث يمكنك تطبيق ذلك في مكتبة Axios عن طريق استخدام عنوان URL كما يلي: axios.get('https://site.com/?foo=bar') أو يمكنك استخدام خاصية params في الخيارات كما يلي: axios.get('https://site.com/', { params: { foo: 'bar' } }) يشبه إجراء طلب POST تمامًا إجراء طلب GET مع استخدم axios.post بدلًا من axios.get: axios.post('https://site.com/') الكائن الذي يحتوي على معامِلات POST هو الوسيط الثاني: axios.post('https://site.com/', { foo: 'bar' }) مقابس الويب Websockets مقابس الويب WebSockets هي بديل لاتصال HTTP في تطبيقات الويب، إذ توفِّر قناة اتصال ثنائية الاتجاه طويلة الأمد بين العميل والخادم، كما تبقى القناة مفتوحة بمجرد إنشائها، مما يوفر اتصالًا سريعًا جدًا مع زمن انتقال وحِمل منخفضَين، كما تدعم جميع المتصفحات الحديثة مقابس WebSockets. قد تتساءل، ما وجه الاختلاف بين WebSockets وبين HTTP؟ حسنًا، يُعَدّ HTTP بروتوكولًا وطريقة تواصل مختلفة تمامًا، فهو بروتوكول طلب/استجابة request/response، إذ يعيد الخادم البيانات التي يطلبها العميل، بينما تفيد مقابس WebSockets فيما يلي: يمكن للخادم إرسال رسالة إلى العميل دون أن يطلب العميل صراحةً شيئًا ما. يمكن للعميل والخادم التحدث مع بعضهما البعض في الوقت نفسه. في حالة تبادل كمية قليلة من البيانات الإضافية لإرسال الرسائل، وهذا يعني اتصالًا ذو زمن انتقال منخفض. تُعَدّ مقابس WebSockets مناسبةً للاتصالات طويلة الأمد في الوقت الحقيقي، بينما يُعَدّ بروتوكول HTTP مفيدًا لتبادل البيانات والتفاعلات المؤقتة التي يبدأها العميل، كما يُعَدّ بروتوكول HTTP أبسط بكثير في التطبيق، بينما تتطلب مقابس WebSockets مزيدًا من العبء الإضافي. مقابس الويب الآمنة استخدم دائمًا البروتوكول الآمن والمشفّر لمقابس الويب أي wss://‎، ويشير ws://‎ إلى إصدار مقابس WebSockets غير الآمن -مثل http://‎ في مقابس WebSockets- الذي يجب تجنبه. إنشاء اتصال WebSockets جديد إليك المثال التالي: const url = 'wss://myserver.com/something' const connection = new WebSocket(url) يُعَدّ connection كائن WebSocket، كما يُشغَّل حدث open عند إنشاء الاتصال بنجاح، ويمكنك الاستماع إلى الاتصال عن طريق إسناد دالة رد نداء callback إلى خاصية onopen الخاصة بكائن connection كما يلي: connection.onopen = () => { //... } إذا كان هناك أي خطأ، فستُشغَّل دالة رد النداء onerror كما يلي: connection.onerror = error => { console.log(`WebSocket error: ${error}`) } إرسال البيانات إلى الخادم باستخدام WebSockets يمكنك إرسال البيانات إلى الخادم بمجرد فتح الاتصال، حيث يمكنك إرسال البيانات بسهولة ضمن دالة رد النداء onopen كما يلي: connection.onopen = () => { connection.send('hey') } استقبال البيانات من الخادم باستخدام WebSockets استمع إلى الاتصال باستخدام دالة رد النداء onmessage التي تُستدعَى عند تلقي حدث message كما يلي: connection.onmessage = e => { console.log(e.data) } تطبيق خادم WebSockets في Node.js تُعَدّ مكتبة ws مكتبة WebSockets شائعةً ومُستخدَمةً مع Node.js، كما سنستخدمها لبناء خادم WebSockets، ويمكن استخدامها أيضًا لتطبيق العميل مع استخدام مقابس WebSockets للتواصل بين خدمَتين من الخدمات الخلفية، كما يمكنك تثبيت هذه المكتبة بسهولة باستخدام الأمر التالي: yarn init yarn add ws ليست الشيفرة التي تحتاج إلى كتابتها كبيرةً كما يلي: const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 8080 }) wss.on('connection', ws => { ws.on('message', message => { console.log(`Received message => ${message}`) }) ws.send('ho!') }) تُنشئ الشيفرة السابقة خادمًا جديدًا على المنفذ 8080 وهو المنفذ الافتراضي لمقابس الويب WebSockets-، وتضيف دالة رد نداء عند إنشاء اتصال، مما يؤدي إلى إرسال ho!‎ إلى العميل، وتسجيل الرسائل التي يتلقاها. شاهد مثالًا حيًا لخادم مقابس الويب WebSockets ومثالًا حيًا لعميل WebSockets يتفاعل مع الخادم على Glitch. ترجمة -وبتصرّف- للفصل Networking من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: التعامل مع الملفات في Node.js المقال السابق: البرمجة غير المتزامنة في Node.js تطبيقات الشبكات الحاسوبية: البريد الإلكتروني طبقة النقل في بروتوكول TCP/IP استكشاف عملية توصيل الرزم عند بناء الشبكات
  5. تُعَدّ الحواسيب غير متزامنةً في تصميمها، ويعني المصطلح غير متزامن Asynchronous أنّ الأشياء يمكن حدوثها حدوثًا مستقلًا عن تدفق البرنامج الرئيسي، إذ يُشغَّل كل برنامج لفتحة زمنية محدَّدة في الحواسيب الاستهلاكية الحالية، ثم يتوقف تنفيذه للسماح لبرنامج آخر بمواصلة التنفيذ، حيث تجري هذه العملية ضمن دورة سريعة جدًا بحيث لا يمكن ملاحظتها، وبالتالي نعتقد أن الحواسيب تشغّل برامجًا متعددةً في الوقت نفسه، لكن ذلك وهم باستثناء الأجهزة متعددة المعالجات. تستخدم البرامج المقاطعات interrupts داخليًا، فالمقاطعة هي إشارة تنبعث من المعالج لجذب انتباه النظام، ولن نخوض في التفاصيل الداخلية، ولكن ضع في بالك أن عدم تزامن البرامج أمرٌ طبيعي، إذ توقف تنفيذها إلى أن تتنبّه مرةً أخرى، بحيث يمكن للحاسوب تنفيذ أشياء أخرى في هذه الأثناء، فإذا انتظر برنامج استجابةً من الشبكة، فلا يمكن إيقاف المعالج إلى أن ينتهي الطلب. تكون لغات البرمجة متزامنةً عادةً، وتوفِّر بعضها طريقةً لإدارة عدم التزامن في اللغة نفسها أو من خلال المكتبات، فاللغات C و Java و C#‎ و PHP و Go و Ruby و Swift و Python متزامنة افتراضيًا، كما تعالجِ بعضها عدم التزامن باستخدام الخيوط threads، مما ينتج عنه عملية جديدة، فلغة جافاسكربت متزامنة افتراضيًا وتعمل على خيط وحيد، وهذا يعني أنّ الشيفرة لا يمكنها إنشاء خيوط جديدة وتشغيلها على التوازي، إذ تُنفَّذ سطور الشيفرة تسلسليًا سطرًا تلو الآخر كما في المثال التالي: const a = 1 const b = 2 const c = a * b console.log(c) doSomething() نشأت جافاسكربت داخل المتصفح، وكانت وظيفتها الرئيسية في البداية الاستجابة لإجراءات المستخدِم مثل onClick وonMouseOver وonChange وonSubmit وما إلى ذلك، ولكن بيئتها ساعدتها في التعامل مع نمط البرمجة المتزامن من خلال المتصفح الذي يوفّر مجموعةً من واجهات برمجة التطبيقات APIs التي يمكنها التعامل مع هذا النوع من العمليات، كما قدّم Node.js في الآونة الأخيرة بيئة إدخال/إخراج دون توقف لتوسيع هذا المفهوم ليشمل الوصول إلى الملفات واستدعاءات الشبكة وغير ذلك. دوال رد النداء Callbacks لا يمكنك معرفة الوقت الذي سينقر فيه المستخدِم على زر، لذلك تعرِّف معالج أحداث لحدث النقر الذي يقبل دالةً تُستدعَى عند بدء الحدث كما يلي: document.getElementById('button').addEventListener('click', () => { //نُقِر العنصر }) وهذا ما يسمى دالة رد النداء، وهي دالة بسيطة تُمرَّر على أساس قيمة إلى دالة أخرى وستُنفَّذ عند وقوع الحدث فقط، إذ يمكن ذلك لأن للغة جافاسكربت دوالًا من الصنف الأول، والتي يمكن إسنادها للمتغيرات وتمريرها إلى دوال أخرى تسّمى دوال الترتيب الأعلى higher-order functions، كما تُغلَّف شيفرة العميل في مستمع حدث load على الكائن window الذي يشغِّل دالة رد النداء عندما تكون الصفحة جاهزة فقط مثل المثال التالي: window.addEventListener('load', () => { //حُمِّلت الصفحة //افعل ما تريده }) تُستخدَم دوال رد النداء في كل مكان، ولا تقتصر على أحداث DOM فقط، فأحد الأمثلة الشائعة هو استخدام المؤقتات: setTimeout(() => { // تشغيل بعد 2 ثانية }, 2000) تقبَل طلبات XHR دالة رد نداء عن طريق إسناد دالة لخاصية في المثال التالي، إذ ستُستدعَى هذه الدالة عند وقوع حدث معيّن -أي حدث تغيّرات حالة الطلب في مثالنا-: const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send() معالجة الأخطاء في دوال رد النداء تتمثَّل إحدى الإستراتيجيات الشائعة جدًا في استخدام ما يعتمده Node.js وهو المعامل الأول في أيّ دالة رد نداء هي كائن الخطأ، وبالتالي تُسمّى دوال رد النداء مع معامل الأخطاء الأول error-first callbacks، فإذا لم يكن هناك خطأً، فستكون قيمة الكائن null، وإذا كان هناك خطأ، فسيحتوي هذا الكائن وصفًا للخطأ ومعلومات أخرى. fs.readFile('/file.json', (err, data) => { if (err !== null) { //عالِج الخطأ console.log(err) return } //لا يوجد خطأ، إذَا عالِج البيانات console.log(data) }) مشكلة دوال رد النداء تُعَدّ دوال رد النداء رائعةً في الحالات البسيطة، ولكن تضيف كل دالة رد نداء مستوىً من التداخل nesting، وبالتالي تتعقَّد الشيفرة بسرعة كبيرة عند وجود كثير من دوال رد النداء كما يلي: window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => { setTimeout(() => { items.forEach(item => { //أضِف شيفرتك هنا }) }, 2000) }) }) تُعَدّ الشيفرة السابقة بسيطةً، إذ تتألف من 4 مستويات فقط، لكنك قد تصادف مستويات أكثر بكثير من التداخل وبالتالي سيزداد تعقيد الشيفرة. بدائل دوال رد النداء قدّمت جافاسكربت بدءًا من الإصدار ES6 ميزات متعددةً تساعدنا في التعامل مع الشيفرة غير المتزامنة التي لا تتضمن استخدام دوال رد النداء مثل: الوعود Promises في الإصدار ES6. صيغة عدم التزامن/الانتظار Async/Await في الإصدار ES8. الوعود Promises الوعود هي إحدى طرق التعامل مع الشيفرات غير المتزامنة في جافاسكربت دون كتابة كثير من دوال رد النداء في الشيفرة. مدخل إلى الوعود يُعرَّف الوعد Promise عمومًا على أنه وكيل لقيمة ستتوفر في وقت لاحق، فالوعود موجودة منذ سنوات، لكن وُحِّدت وقُدِّمت في الإصدار ES2015، واُستبدِلت دوال عدم التزامن Async functions في الإصدار ES2017 بها والتي تستخدِم واجهة برمجة تطبيقات الوعود أساسًا لها، لذلك يُعَدّ فهم الوعود أمرًا أساسيًا حتى في حالة استخدام دوال عدم التزامن في الشيفرة الأحدث عوضًا عن الوعود، وإليك شرح مختصر عن كيفية عمل الوعود. يبدأ الوعد عند استدعائه في حالة انتظار pending state، أي أن الدالة المستدعِية تواصل التنفيذ في الوقت الذي تنتظر به الوعد لينفّذ معالجته الخاصة ويستجيب لها، حيث تنتظر الدالة المستدعِية إما إعادة الوعد في حالة التأكيد أو الحل resolved state أو في حالة الرفض rejected state، ولكن لغة جافاسكربت غير متزامنة، لذلك تتابع الدالة تنفيذها ريثما ينتهي الوعد من عمله، كما تستخدِم واجهات برمجة تطبيقات الويب المعيارية الحديثة الوعود بالإضافة إلى شيفرتك ومكتباتها، ومن هذه الواجهات البرمجية: Battery API. Fetch API. Service Workers. ستستخدَم الوعود بالتأكيد في جافاسكربت الحديثة، لذلك يجب فهمها جيدًا. إنشاء وعد تُظهِر واجهة برمجة الوعد Promise API باني وعد Promise constructor يمكن تهيئته باستخدام الدالة new Promise()‎: let done = true const isItDoneYet = new Promise( (resolve, reject) => { if (done) { const workDone = 'Here is the thing I built' resolve(workDone) } else { const why = 'Still working on something else' reject(why) } } ) يتحقّق الوعد من الثابت العام done، فإذا كانت قيمته صحيحة true، فإننا نعيد قيمة وعد مؤكَّد، وإلا فسنعيد وعدًا مرفوضًا، كما يمكننا إعادة قيمة باستخدام القيم resolve وreject، حيث أعدنا سلسلةً نصيةً فقط في المثال السابق، لكنها يمكن أن تكون كائنًا أيضًا. استهلاك وعد لنرى الآن كيفية استهلاك أو استخدام وعد. const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet .then((ok) => { console.log(ok) }) .catch((err) => { console.error(err) }) } سيؤدي تشغيل الدالة checkIfItsDone()‎ إلى تنفيذ الوعد isItDoneYet()‎ وستنتظر إلى أن يُؤكَّد الوعد باستخدام دالة رد النداء then، وإذا كان هناك خطأ، فستعالجه في دالة رد النداء catch. إذا أردت مزامنة وعود مختلفة، فسيساعدك التابع Promise.all()‎ على تحديد قائمة وعود، وتنفيذ شيء ما عند تأكيد هذه الوعود جميعها، وإليك المثال التالي: const f1 = fetch('/something.json') const f2 = fetch('/something2.json') Promise.all([f1, f2]).then((res) => { console.log('Array of results', res) }) .catch((err) => { console.error(err) }) تتيح لك صيغة إسناد الهدم destructuring assignment syntax الخاصة بالإصدار ES2015 تنفيذ ما يلي: Promise.all([f1, f2]).then(([res1, res2]) => { console.log('Results', res1, res2) }) ليس الأمر مقتصرًا على استخدام fetch بالطبع، إذ يمكنك استخدام أيّ وعد، كما يُشغَّل التابع Promise.race()‎ عند تأكيد أول وعد من الوعود التي تمرّرها إليه، ويشغِّل دالةَ رد النداء المصاحبة للوعد مرةً واحدةً فقط مع نتيجة الوعد الأول المُؤكَّد resolved، وإليك المثال التالي: const first = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result) // second }) سلسلة الوعود Chaining promises يمكن أن يُعاد وعدٌ إلى وعد آخر، وبالتالي ستنشأ سلسلة من الوعود، إذ تقدِّم واجهة Fetch API -وهي طبقة فوق واجهة برمجة تطبيقات XMLHttpRequest- مثالًا جيدًا عن سلسلة وعود، إذ يمكننا استخدام هذه الواجهة للحصول على مورد ووضع سلسلة من الوعود في طابور لتنفيذها عند جلب المورد، كما تُعَدّ واجهة Fetch API آليةً قائمةً على الوعود، حيث يكافئ استدعاءُ الدالة fetch()‎ تعريف وعد باستخدام new Promise()‎، وإليك المثال التالي عن كيفية سلسلة الوعود: const status = (response) => { if (response.status >= 200 && response.status < 300) { return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('/todos.json') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) }) نستدعي في المثال السابق التابع fetch()‎ للحصول على قائمة من عناصر TODO من ملف todos.json الموجود في نطاق الجذر، وننشئ سلسلة من الوعود، كما يعيد تشغيل التابع fetch()‎ استجابةً لها خاصيات منها: status وهي قيمة عددية تمثِّل رمز حالة HTTP. statusText وهي رسالة حالة تكون قيمتها OK إذا نجح الطلب. تحتوي الاستجابة response أيضًا على تابع json()‎ الذي يعيد وعدًا سيُؤكد ويُربَط مع محتوى الجسم المُعالَج والمُحوَّل إلى JSON. الوعد الأول في السلسلة هو الدالة التي حدّدناها وهي status()‎ التي تتحقق من حالة الاستجابة، فإذا لم تكن استجابةً ناجحةً -أي قيمتها بين 200 و299، فسترفِض الوعد، إذ ستؤدي هذه العملية إلى تخطي جميع الوعود المتسلسلة المدرجَة في سلسلة الوعود وستنتقل مباشرةً إلى تعليمة catch()‎ في الأسفل، مما يؤدي إلى تسجيل نص فشل الطلب Request failed مع رسالة الخطأ؛ أما إذا نجحت الاستجابة، فستُستدعَى دالة json()‎ التي حدّدناها، وبما أنّ الوعد السابق يعيد كائن الاستجابة response عند النجاح، فسنحصل عليه على أساس دخل للوعد الثاني، وبالتالي نُعيد بيانات JSON المُعالَجة في هذه الحالة، لذا فإن الوعد الثالث يتلقى JSON مباشرةً مع تسجيله ببساطة في الطرفية كما يلي: .then((data) => { console.log('Request succeeded with JSON response', data) }) معالجة الأخطاء ألحقنا في المثال السابق تعليمة catch بسلسلة وعود، فإذا فشل أيّ شيء في سلسلة الوعود مسببًا خطأً أو رفض وعد، فسينتقل التحكم إلى أقرب تعلمية catch()‎ أسفل السلسلة. new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) // أو new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) }) إذا ظهر خطأ ضمن تعليمة catch()‎، فيمكنك إلحاق تعليمة catch()‎ ثانية لمعالجة الخطأ وهلم جرًا وهذا ما يسمى بعملية معالجة توريث الأخطاء Cascading errors. new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) إذا ظهر الخطأ Uncaught TypeError: undefined is not a promise في الطرفية، فتأكد من استخدام new Promise()‎ بدلًا من استخدام Promise()‎. صيغة عدم التزامن/الانتظار async/await تطوّرت لغة جافاسكربت في وقت قصير جدًا من دوال رد النداء callbacks إلى الوعود promises في الإصدار ES2015، وأصبحت لغة جافاسكربت غير المتزامنة منذ الإصدار ES2017 أبسط مع صيغة عدم التزامن/الانتظار async/await، فالدوال غير المتزامنة هي مزيج من الوعود والمولِّدات generators، وهي في الأساس ذات مستوىً أعلى من الوعود من ناحية التجريد، فصيغة async/await مبنية على الوعود. سبب ظهور صيغة async/await هو أنها تقلل من الشيفرة التكرارية أو المتداولة boilerplate الموجودة في الوعود، وتقلّل من محدودية قيود عدم كسر السلسلة في تسلسل الوعود، فقد كان الهدف من تقديم الوعود في الإصدار ES2015 حلَّ مشكلة التعامل مع الشيفرة غير المتزامنة، وقد حلّت هذه المشكلة حقًا، ولكن كان واضحًا على مدار العامين اللذين فصلا بين الإصدارين ES2015 وES2017 أن الوعود ليست الحل النهائي. اُستخدِمت الوعود لحل مشكلة جحيم دوال رد النداء callback hell الشهيرة، لكنها أدخلت التعقيد فيها بالإضافة إلى تعقيد الصيغ، وقد كانت عناصرًا أوليةً جيدةً يمكن من خلالها إظهار صيغة أفضل للمطورين، لذلك حصلنا على دوال غير متزامنة في الوقت المناسب، إذ تظهر الشيفرة على أنها متزامنة، لكنها غير متزامنة وغير قابلة للتوقّف non-blocking في الحقيقة. كيفية عمل صيغة async/await تعيد الدالة غير المتزامنة وعدًا كما في المثال التالي: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) } إذا أردت استدعاء هذه الدالة، فستضيف الكلمة await في البداية، وستتوقف شيفرة الاستدعاء حتى تأكيد أو رفض الوعد. إليك المثال التالي: const doSomething = async () => { console.log(await doSomethingAsync()) } إليك المثال التالي أيضًا والذي يوضِّح استخدام صيغة async/await لتشغيل دالة تشغيلًا غير متزامن: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) } const doSomething = async () => { console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After') ستطبع الشيفرة السابقة ما يلي في طرفية المتصفح: Before After I did something //after 3s تطبيق الوعود على كل شيء تعني إضافة الكلمة المفتاحية async في بداية أيّ دالة أنّ هذه الدالة ستعيد وعدًا، وإذا لم تفعل ذلك صراحةً، فستعيد وعدًا داخليًا، وهذا سبب كون الشيفرة التالية صالحةً valid: const aFunction = async () => { return 'test' } aFunction().then(alert) // سيؤدي هذا إلى تنبيه‫ 'test' وكذلك الشيفرة التالية: const aFunction = async () => { return Promise.resolve('test') } aFunction().then(alert) // سيؤدي هذا إلى تنبيه‫ 'test' تبدو الشيفرة السابقة بسيطةً للغاية إذا وازنتها مع الشيفرة التي تستخدِم وعودًا صريحةً مع الدوال المتسلسلة ودوال رد النداء، كما يُعَدّ المثال السابق بسيطًا للغاية، لذلك ستظهر الفوائد جليةً عندما تكون الشيفرة أكثر تعقيدًا، وإليك المثال التالي الذي يوضّح كيفية الحصول على مورد JSON وتحليله parse باستخدام الوعود: const getFirstUserData = () => { return fetch('/users.json') // الحصول على قائمة المستخدِمين .then(response => response.json()) // تحليل‫ JSON .then(users => users[0]) // التقاط المستخدِم الأول .then(user => fetch(`/users/${user.name}`)) // الحصول على بيانات المستخدِم .then(userResponse => response.json()) // تحليل‫ JSON } getFirstUserData() وإليك المثال التالي الذي ينفّذ ما يفعله المثال السابق ولكن باستخدام صيغة await/async: const getFirstUserData = async () => { const response = await fetch('/users.json') // الحصول على قائمة المستخدِمين const users = await response.json() // تحليل‫ JSON const user = users[0] // التقاط المستخدِم الأول const userResponse = await fetch(`/users/${user.name}`) // الحصول على بيانات المستخدِم const userData = await user.json() // تحليل‫ JSON return userData } getFirstUserData() استخدام دوال متعددة غير متزامنة ضمن سلسلة يمكن وضع الدوال غير المتزامنة ضمن سلسلة بسهولة باستخدام صيغة أكثر قابلية للقراءة من الوعود الصرفة كما يلي: const promiseToDoSomething = () => { return new Promise(resolve => { setTimeout(() => resolve('I did something'), 10000) }) } const watchOverSomeoneDoingSomething = async () => { const something = await promiseToDoSomething() return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => { const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { console.log(res) }) ستطبع الشيفرة السابقة ما يلي: I did something and I watched and I watched as well سهولة تنقيح الأخطاء يُعَدّ تنقيح أخطاء Debugging الوعود أمرًا صعبًا لأن منقِّح الأخطاء لن يتخطى الشيفرة غير المتزامنة، بينما تجعل صيغة Async/await هذا الأمر سهلًا لأنها تُعَدّ مجرد شيفرة متزامنة بالنسبة للمصرِّف compiler. ترجمة -وبتصرّف- للفصل Asynchronous programming من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: التعامل مع الطلبيات الشبكية في Node.js المقال السابق: كيفية تنفيذ الدوال داخليا ضمن Node.js البرمجة غير المتزامنة في جافاسكريبت مقدّمة إلى البرمجة غير المتزامنة في Xamarin المدخل الشامل لتعلم علوم الحاسوب
  6. سنتعرّف في هذا المقال على مفهوم حلقة الأحداث وكيفية سير عملية تنفيذ الدوال تنفيذًا غير متزامن ضمن نود، كما سنوضِّح كيفية التعامل مع الأحداث المخصَّصة من خلال الصنف EventEmitter الذي يُستخدَم لمعالجة الأحداث. حلقة الأحداث event loop تُعَدّ حلقة الأحداث Event Loop أحد أهم جوانب جافاسكربت التي يجب فهمها. مدخل إلى حلقة الأحداث سنشرح التفاصيل الداخلية لكيفية عمل جافاسكربت باستخدام خيط thread واحد، وسنوضّح كيفية معالجة الدوال غير المتزامنة. تُشغَّل شيفرة جافاسكربت الخاصة بك ضمن خيط واحد، أي أن هناك شيئًا واحدًا فقط يحدث في الوقت نفسه، هذا القيد مفيد جدًا لأنه يبسّط كثيرًا من عملية البرمجة دون القلق بشأن مشاكل التزامن، فما عليك إلا التركيز على كيفية كتابة شيفرتك الخاصة وتجنب أي شيء يمكن أنه إيقاف الخيط مثل استدعاءات الشبكة المتزامنة أو الحلقات اللانهائية. توجد حلقة أحداث لكل تبويب في معظم المتصفحات لعزل العمليات عن بعضها البعض وتجنب صفحة الويب ذات الحلقات اللانهائية أو ذات المعالجة الكبيرة التي تؤدي إلى توقّف المتصفح بأكمله، كما تدير البيئة حلقات أحداث متزامنة متعددة لمعالجة استدعاءات واجهة API مثلًا، كما تُشغَّل عمَّال الويب Web Workers في حلقة الأحداث الخاصة بها أيضًا، إذ يجب عليك الاهتمام فقط بتشغيل شيفرتك ضمن حلقة أحداث واحدة، وكتابة شيفرتك مع وضع ذلك في الحسبان لتجنب توقفها. إيقاف حلقة الأحداث ستوقِف شيفرة جافاسكربت التي تستغرق وقتًا طويلًا لإعادة التحكم إلى حلقة الأحداث مرةً أخرى تنفيذَ أيّ شيفرة جافاسكربت في الصفحة، إذ يمكن أن توقِف خيط واجهة المستخدِم، وبالتالي لا يمكن للمستخدِم تمرير الصفحة أو النقر عليها وغير ذلك، كما تُعَدّ جميع عناصر الدخل/الخرج الأولية في جافاسكربت غير قابلة للإيقاف non-blocking تقريبًا مثل طلبات الشبكة وعمليات نظام ملفات Node.js وما إلى ذلك، ولكن الاستثناء هو توقّفها، وهذا هو سبب اعتماد جافاسكربت الكبير على دوال رد النداء callbacks واعتمادها مؤخرًا على الوعود promises وصيغة عدم التزامن/الانتظار async/await. مكدس الاستدعاءات call stack مكدس الاستدعاءات هو طابور LIFO أي القادم أخيرًا يخرج أولًا Last In First Out، حيث تتحقّق حلقة الأحداث باستمرار من مكدس الاستدعاءات للتأكد من وجود دالة يجب تشغيلها، حيث تضيف حلقة الأحداث عندها أي استدعاء دالة تجده إلى مكدس الاستدعاءات وتنفّذ كل استدعاء بالترتيب. قد تكون على دراية بتعقّب مكدس الأخطاء في منقِّح الأخطاء debugger أو في وحدة تحكم المتصفح، حيث يبحث المتصفح عن أسماء الدوال في مكدس الاستدعاءات لإعلامك بالدالة التي تنشئ الاستدعاء الحالي: شرح بسيط لحلقة الأحداث افترض المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() الذي يطبع ما يلي: foo bar baz تُستدعَى الدالة foo()‎ أولًا عند تشغيل الشيفرة السابقة، ثم نستدعي الدالة bar()‎ أولًا ضمن الدالة foo()‎، ثم نستدعي الدالة baz()‎، ويبدو مكدس الاستدعاءات في هذه المرحلة كما يلي: تتأكد حلقة الأحداث في كل تكرار من وجود شيء ما في مكدس الاستدعاءات، وتنفِّذه كما يلي إلى أن يصبح مكدس الاستدعاءات فارغًا: تنفيذ طابور الدوال لا يوجد شيء مميز في المثال السابق، حيث تعثر شيفرة جافاسكربت على الدوال لتنفيذها وتشغيلها بالترتيب، ولنشاهد كيفية تأجيل تنفيذ دالة إلى أن يصبح المكدس فارغًا، حيث تُستخدَم حالة الاستخدام setTimeout(() => {}), 0)‎ لاستدعاء دالة، ولكنها تُنفَّذ عند كل تنفيذ لدالة أخرى في الشيفرة، وإليك المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() تطبع الشيفرة السابقة ما يلي: foo baz bar تُستدعَى الدالة foo()‎ أولًا عند تشغيل الشيفرة، ثم نستدعي setTimeout أولًا ضمن الدالة foo()‎، ونمرّر bar على أساس وسيط، ونطلب منه العمل على الفور بأسرع ما يمكنه، ونمرر القيمة 0 على أساس مؤقت timer، ثم نستدعي الدالة baz()‎، حيث يبدو مكدس الاستدعاءات في هذه المرحلة كما يلي: يوضِّح الشكل التالي ترتيب تنفيذ جميع الدوال في البرنامج: طابور الرسائل Message Queue يبدأ المتصفح أو Node.js المؤقت timer عند استدعاء الدالة setTimeout()‎، ثم توضَع دالة رد النداء callback function في طابور الرسائل Message Queue بمجرد انتهاء صلاحية المؤقت عالفور مثل حالة وضع القيمة 0 على أساس مهلة زمنية timeout. يُعَدّ طابور الرسائل المكان الذي توضَع الأحداث التي بدأها المستخدِم مثل أحداث النقر أو أحداث لوحة المفاتيح أو جلب الاستجابات الموجودة في طابور قبل أن تتاح لشيفرتك فرصة الرد عليها أو أحداث DOM مثل onLoad. لا يتعيّن علينا انتظار دوال مثل الدالة setTimeout أو انتظار جلب أو تنفيذ أشياء أخرى لهذه الدوال لأن المتصفح يوفّرها وتتقيّد بخيوطها الخاصة، فإذا ضبطتَ مهلة setTimeout الزمنية على 2 ثانية مثلًا، فلن تضطر إلى الانتظار لمدة 2 ثانية، بل يحدث الانتظار في مكان آخر. طابور العمل Job Queue الخاص بالإصدار ES6 قدّم المعيار ECMAScript 2015 مفهوم طابور العمل Job Queue الذي تستخدمه الوعود Promises التي قُدِّمت أيضًا ضمن الإصدار ES6/ES2015، ويُعَدّ هذا المفهوم طريقةً لتنفيذ نتيجة دالة غير متزامنة بأسرع ما يمكن بدلًا من وضعها في نهاية مكدس الاستدعاءات. ستُنفَّذ الوعود المؤكَّدة قبل انتهاء الدالة الحالية بعدها مباشرةً، حيث يشبه ذلك ركوب الأفعوانية في مدينة ملاهي، إذ يضعك طابور الرسائل بعد جميع الأشخاص الآخرين الموجودين في هذا الطابور، بينما طابور العمل هو مثل تذكرة Fastpass تتيح لك الركوب في رحلة أخرى في الأفعوانية بعد الانتهاء من الرحلة السابقة مباشرةً، وإليك المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) => resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() تطبع الشيفرة السابقة ما يلي: foo baz should be right after baz, before bar bar يشكّل ذلك فرقًا كبيرًا بين الوعود Promises وصيغة Async/await المبنيّة على الوعود والدوال القديمة غير المتزامنة من خلال الدالة setTimeout()‎ أو واجهات API للمنصات الأخرى. المؤقتات Timers: التنفيذ غير المتزامن في أقرب وقت ممكن تُعَدّ الدالة process.nextTick()‎ جزءًا مهمًا من حلقة أحداث Node.js، حيث نسمّي كل دورة كاملة تدورها حلقة الأحداث بالاسم نبضة tick، كما يؤدي تمرير دالة إلى process.nextTick()‎ إلى استدعاء هذه الدالة في نهاية العملية الحالية وقبل بدء نبضة حلقة الأحداث التالية. process.nextTick(() => { //افعل شيئًا ما }) حلقة الأحداث مشغولة بمعالجة شيفرة الدالة الحالية، كما يشغّل محرك JS عند انتهاء هذه العملية جميع الدوال المُمرَّرة إلى استدعاءات nextTick خلال تلك العملية، وهي الطريقة التي يمكننا من خلالها إخبار محرك JS بمعالجة دالة بطريقة غير متزامنة بعد الدالة الحالية في أقرب وقت ممكن دون وضعها في طابور، كما سيؤدي استدعاء setTimeout(() => {}, 0)‎ إلى تنفيذ الدالة في النبضة التالية بعد وقت أطول من استخدام الدالة nextTick()‎، واستخدم الدالة nextTick()‎ عندما تريد التأكد من تنفيذ الشيفرة في تكرار حلقة الأحداث التالي. الدالة setTimeout()‎ قد ترغب في تأخير تنفيذ دالة عند كتابة شيفرة جافاسكربت، وهذه هي مهمة الدالة setTimeout، حيث تحدِّد دالة رد نداء لتنفيذها لاحقًا مع قيمة تعبِّر عن مقدار التأخير لتشغيلها لاحقًا مقدَّرةً بالميلي ثانية: setTimeout(() => { // تشغيل بعد 2 ثانية }, 2000) setTimeout(() => { // تشغيل بعد 50 ميلي ثانية }, 50) تحدِّد هذه الصيغة دالةً جديدةً، حيث يمكنك استدعاء أيّ دالة أخرى تريدها هناك، أو يمكنك تمرير اسم دالة موجودة مسبقًا مع مجموعة من المعاملات كما يلي: const myFunction = (firstParam, secondParam) => { // افعل شيئًا ما } // تشغيل بعد 2 ثانية setTimeout(myFunction, 2000, firstParam, secondParam) تعيد الدالة setTimeout معرِّف المؤقت timer id، وهذا المعرِّف غير مُستخدَم، ولكن يمكنك تخزينه ومسحه إذا أردت حذف تنفيذ الدوال المجدولة: const id = setTimeout(() => { // يجب تشغيله بعد 2 ثانية }, 2000) //غيّرنا رأينا clearTimeout(id) الدالة setImmediate إذا أردت تنفيذ جزء من الشيفرة بطريقة غير متزامنة ولكن في أقرب وقت ممكن، فإنّ أحد الخيارات هو استخدام الدالة setImmediate()‎ التي يوفّرها Node.js: setImmediate(() => { //شغّل شيئًا ما }) تمثِّل الدالة المُرَّرة على أساس وسيط للدالة setImmediate()‎ دالةَ رد نداء تُنفَّذ في تكرار حلقة الأحداث التالي، كما تختلف setImmediate()‎ عن setTimeout(() => {}, 0)‎ مع تمرير مهلة زمنية مقدارها 0 ميلي ثانية وعن process.nextTick()‎، إذ تُنفَّذ الدالة المُمرَّرة إلى process.nextTick()‎ في تكرار حلقة الأحداث الحالي بعد انتهاء العملية الحالية، وهذا يعني أنها ستُنفَّذ دائمًا قبل setTimeout وsetImmediate، كما تشبه دالةُ رد النداء setTimeout()‎ مع تأخير 0 ميلي ثانية الدالةَ setImmediate()‎، في حين يعتمد ترتيب التنفيذ على عوامل مختلفة، ولكنهما ستُشغَّلان في تكرار حلقة الأحداث التالي. التأخير الصفري Zero delay إذا حدّدت تأخير المهلة الزمنية بالقيمة 0، فستُنفَّذ دالة رد النداء في أقرب وقت ممكن ولكن بعد تنفيذ الدالة الحالية: setTimeout(() => { console.log('after ') }, 0) console.log(' before ') ستطبع الدالة السابقة before after. يُعَدّ هذا مفيدًا لتجنب إيقاف وحدة المعالجة المركزية CPU في المهام المكثفة والسماح بتنفيذ الدوال الأخرى أثناء إجراء عملية حسابية ثقيلة عن طريق وضع الدوال ضمن طابور في المجدوِل scheduler. الدالة setInterval()‎ تُعَدّ setInterval دالةً مشابهةً للدالة setTimeout، مع اختلاف أنّ الدالة setInterval ستشغِّل دالةَ رد النداء إلى الأبد ضمن الفاصل الزمني الذي تحدِّده مقدَّرًا بالميلي ثانية بدلًا من تشغيلها مرةً واحدةً: setInterval(() => { // تشغيل كل 2 ثانية }, 2000) تُشغَّل الدالة السابقة كل 2 ثانية ما لم تخبرها بالتوقف باستخدام clearInterval من خلال تمرير معرِّف id الفاصل الزمني الذي تعيده الدالة setInterval: const id = setInterval(() => { // تشغيل كل 2 ثانية }, 2000) clearInterval(id) يشيع استدعاء clearInterval ضمن دالة رد نداء الدالة setInterval، للسماح لها بالتحديد التلقائي إذا وجب تشغيلها مرةً أخرى أو إيقافها، حيث تشغّل الشيفرة التالية شيئًا على سبيل المثال إذا لم تكن قيمة App.somethingIWait هي arrived: const interval = setInterval(() => { if (App.somethingIWait === 'arrived') { clearInterval(interval) return } // وإلّا افعل شيئًا ما }, 100) دالة setTimeout العودية تبدأ setInterval دالةً كل n ميلي ثانية، دون الأخذ في الحسبان موعد انتهاء تنفيذ هذه الدالة، فإذا استغرقت الدالة القدر نفسه من الوقت دائمًا، فلا بأس بذلك: قد تستغرق الدالة أوقات تنفيذ مختلفة اعتمادًا على ظروف الشبكة مثلًا: وقد يتداخل وقت تنفيذ دالة طويل مع وقت تنفيذ الدالة التالية: يمكن تجنب ذلك من خلال جدولة دالة setTimeout العودية لتُستدعَى عند انتهاء دالة رد النداء: const myFunction = () => { // افعل شيئًا ما setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) بهدف تحقيق السيناريو التالي: يتوفَّر كل من setTimeout وsetInterval في Node.js من خلال وحدة المؤقتات Timers module، كما يوفِّر Node.js أيضًا الدالة setImmediate()‎ التي تعادل استخدام setTimeout(() => {}, 0)‎ المستخدَمة للعمل مع حلقة أحداث Node.js في أغلب الأحيان. مطلق الأحداث Event Emitter الخاص بنود Node إذا استخدمت جافاسكربت في المتصفح سابقًا، فلا بد أنك تعرف مقدار تفاعلات المستخدِم المُعالَجة من خلال الأحداث مثل نقرات الفأرة وضغطات أزرار لوحة المفاتيح والتفاعل مع حركة الفأرة وغير ذلك، وهنالك الكثير من الأحداث الأساسية في المتصفح ولكن قد تحتاج في وقت ما إلى أحداث مخصَّصة غير تلك الأساسية لتطلقها وفقًا لوقوع حدث ما ثم تعالجها بما يناسبك. يوفِّر نود على جانب الواجهة الخلفية خيارًا لإنشاء نظام مماثل باستخدام وحدة الأحداث events module، إذ تقدّم هذه الوحدة الصنف EventEmitter الذي يُستخدَم لمعالجة الأحداث، كما يمكنك تهيئته كما يلي: const eventEmitter = require('events').EventEmitter() يُظهِر هذا الكائن التابعين on وemit من بين أشياء متعددة. emit الذي يُستخدَم لبدء حدث. on الذي يُستخدَم لإضافة دالة رد نداء والتي ستُنفَّذ عند بدء الحدث. لننشئ حدث start مثلًا ثم نتفاعل معه من خلال تسجيل الدخول إلى الطرفية: eventEmitter.on('start', () => { console.log('started') }) فإذا شغّلنا ما يلي: eventEmitter.emit('start') فستُشغَّل دالة معالج الأحداث، وسنحصل على سجل طرفية. يمكنك تمرير الوسائط إلى معالج الأحداث من خلال تمريرها على أساس وسائط إضافية إلى التابع emit()‎ كما يلي: eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23) أو من خلال تمرير وسائط متعددة كما يلي: eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100) يظهِر كائن EventEmitter توابعًا متعددةً أخرى للتفاعل مع الأحداث مثل: once()‎: يضيف مستمعًا لمرة واحدة. removeListener()‎ أو off()‎: يزيل مستمع حدث من الحدث. removeAllListeners()‎: يزيل جميع المستمعين لحدث ما. يمكنك قراءة جميع التفاصيل الخاصة بهذه التوابع في صفحة وحدة الأحداث events module على Node.js. ترجمة -وبتصرّف- للفصل Working with the event loop من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: البرمجة غير المتزامنة في Node.js المقال السابق: دليلك الشامل إلى مدير الحزم npm في Node.js إعداد تطبيق node.js لسير عمل يعتمد على الحاويات باستخدام Docker Compose تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js
  7. يمثِّل مدير حزم نود npm -اختصارًا إلى Node Package Manager- أساس نجاح Node.js، فقد صدر تقرير في شهر 1 من عام 2017 بوجود أكثر من 350000 حزمة مُدرجَة في سجل npm، مما يجعله أكبر مستودع لشيفرات لغة على الأرض، فكُن على ثقة أنك ستجد فيه حزمةً لكل شيء تقريبًا. يُعَدّ npm مدير حزم Node.js المعياري، فقد اُستخدِم في البداية على أساس طريقة لتنزيل وإدارة اعتماديات حزم Node.js، لكنه أصبح بعدها أداةً تُستخدَم في واجهة جافاسكربت الأمامية أيضًا. إدارة تنزيل الحزم والمكتبات والاعتماديات يدير npm تنزيلات جميع اعتماديات مشروعك. تثبيت جميع الاعتماديات Dependencies يمكنك استخدام الأمر التالي إذا احتوى المشروع على ملف packages.json: npm install سيثبِّت كل ما يحتاجه المشروع في المجلد node_modules مع إنشاء هذا المجلد إذا لم يكن موجودًا مسبقًا. تثبيت حزمة واحدة يمكنك أيضًا تثبيت حزمة معينة عن طريق تشغيل الأمر: npm install <package-name> سترى في أغلب الأحيان مزيدًا من الرايات flags المضافة إلى هذا الأمر مثل: ‎--save التي تثبّت وتضيف مدخلة إلى اعتماديات ملف package.json وهي الافتراضية فلا داعي لإضافتها في كل مرة تثبت فيها حزمة في مشروعك. ‎--save-dev التي تثبّت وتضيف مدخلة إلى اعتماديات تطوير devDependencies ملف package.json. يتمثل الاختلاف الأساسي بينهما في أن اعتماديات التطوير devDependencis هي أدوات تطوير مثل مكتبة الاختبار، بينما تُجمَّع الاعتماديات dependencies مع التطبيق الذي يكون قيد الإنتاج. يمكن تثبيت إصدار أقدم من حزمة npm أو تثبيت إصدار محدد بعينه، وهو شيء قد يكون مفيدًا في حل مشكلة التوافق، كما يمكنك تثبيت إصدار قديم من حزمة npm باستخدام صيغة @ كما يلي: npm install <package>@<version> يثبّت الأمر التالي الإصدار الإصدار الأخير الأحدث من حزمة cowsay: npm install cowsay يمكنك تثبيت الإصدار 1.2.0 من خلال الأمر التالي: npm install cowsay@1.2.0 يمكن تطبيق الشيء نفسه مع الحزم العامة كما يلي: npm install -g webpack@4.16.4 وقد تكون مهتمًا بسرد جميع إصدارات الحزمة السابقة من خلال استخدام الأمر npm view <package> versions كما يلي: npm view cowsay versions [ '1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.1.0', '1.1.1', '1.1.2', '1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7', '1.1.8', '1.1.9', '1.2.0', '1.2.1', '1.3.0', '1.3.1' ] مكان تثبيت npm للحزم يمكنك إجراء نوعين من التثبيت، عند تثبيت حزمة باستخدام npm أو yarn: تثبيت محلي local install. تثبيت عام global install. إذا كتبتَ أمر تثبيت npm install مثل الأمر التالي، فستُثبَّت الحزمة في شجرة الملفات الحالية ضمن المجلد الفرعي node_modules افتراضيًا، ويضيف عندها npm أيضًا المدخلة lodash في خاصية الاعتماديات dependencies الخاصة بملف package.json الموجود في المجلد الحالي: npm install lodash يُطبَّق التثبيت العام باستخدام الراية ‎-g: npm install -g lodash لن يثبِّت npm الحزمة ضمن المجلد المحلي وإنما سيستخدم موقعًا عامًا، إذ سيخبرك الأمر npm root -g بمكان هذا الموقع الدقيق على جهازك، حيث يمكن أن يكون هذا الموقع ‎/usr/local/lib/node_modules في نظام macOS أو لينكس، ويمكن أن يكون C:\Users\YOU\AppData\Roaming\npm\node_modules على نظام ويندوز، لكن إذا استخدمت nvm لإدارة إصدارات Node.js، فقد يختلف هذا الموقع، حيث استخدمنا nvm على سبيل المثال وكان موقع الحزم هو ‎/Users/flavio/.nvm/versions/node/v8.9.0/lib/node_modules. كيفية استخدام أو تنفيذ حزمة مثبتة باستخدام npm هل تساءلت عن كيفية تضمين واستخدام حزمة مثبَّتة في مجلد node_modules في شيفرتك الخاصة؟ حسنًا، لنفترض أنك ثبَّت مكتبة أدوات جافاسكربت الشائعة lodash باستخدام الأمر التالي: npm install lodash سيؤدي ذلك إلى تثبيت الحزمة في مجلد node_modules المحلي التي يمكنك استخدامها في شيفرتك الخاصة من خلال استيرادها في برنامجك باستخدام require: const _ = require('lodash) إذا كانت حزمتك الخاصة قابلة للتنفيذ، فسيوضَع الملف القابل للتنفيذ ضمن المجلد node_modules/.bin/‎، وإحدى طرق إثبات ذلك هي استخدام الحزمة cowsay، حيث توفِّر هذه الحزمة برنامج سطر أوامر يمكن تنفيذه لإنشاء بقرة تقول شيئًا -وحيوانات أخرى أيضًا-، حيث ستثبِّت هذه الحزمة نفسها وعددًا من الاعتماديات في المجلد node_modules عند تثبيتها باستخدام الأمر npm install cowsay: يوجد مجلد ‎.bin‎‎ مخفي يحتوي على روابط رمزية إلى ملفات cowsay الثنائية: يمكنك تنفيذ هذه الحزمة من خلال كتابة ‎./node_modules/.bin/cowsay لتشغيلها، لكن يُعَدّ npx المُضمَّن في الإصدارات الأخيرة من npm -منذ الإصدار 5.2- الخيار الأفضل، فما عليك إلا تشغيل الأمر التالي: npx cowsay وسيجد npx موقع الحزمة. تحديث الحزم أصبح التحديث سهلًا أيضًا عن طريق تشغيل الأمر: npm update سيتحقّق npm من جميع الحزم بحثًا عن إصدار أحدث يلبي قيود إدارة الأصدارات Versioning الخاصة بك، كما يمكنك تحديد حزمة واحدة لتحديثها أيضًا باستخدام الأمر: npm update <package-name> إذا أردت تحديث جميع اعتماديات npm المخزَّنة في الملف package.json -الذي سنشرحه بعد قليل- إلى أحدث إصدار متاح لها، فثبَّتَ حزمةً باستخدام الأمر npm install <packagename>‎ الذي سينزِّل أحدث إصدار متاح من الحزمة ويوضَع هذا الإصدار في مجلد node_modules، وستُضاف مدخلة مقابلة إلى الملف package.json والملف package-lock.json الموجودَين في مجلدك الحالي، إذ يحسب npm الاعتماديات ويثبّت أحدث إصدار متاح منها أيضًا. لنفترض أنك ثبَّتَ الحزمة cowsay، وهي أداة سطر أوامر رائعة تتيح لك إنشاء بقرة تقول أشياء، فإذا ثبَّتَها باستخدام الأمر npm install cowsay، فستضاف المدخلة التالية إلى ملف package.json: { "dependencies": { "cowsay": "^1.3.1" } } يمثّل ما يلي جزءًا من ملف package-lock.json، حيث أزلنا الاعتماديات المتداخلة للتوضيح: { "requires": true, "lockfileVersion": 1, "dependencies": { "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz", "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } } } } يوضّح هذان الملفان أننا ثبَّتنا الإصدار 1.3.1 من الحزمة cowsay باستخدام قاعدة التحديثات ‎^1.3.1، والتي تعني بالنسبة لقواعد إدارة إصدارات npm أنه يمكن تحديث npm إلى إصدار حزمة التصحيح patch والإصدار الثانوي minor، أي 0.13.1 و0.14.0 وما إلى ذلك، فإذا كان هناك إصدار ثانوي أو إصدار حزمة تصحيح جديد وكتبنا الأمر npm update، فسيُحدَّث الإصدار المثبَّت، وسيُملَأ ملف package-lock.json بالإصدار الجديد، بينما يبقى الملف package.json دون تغيير، كما يمكنك اكتشاف إصدارات الحزم الجديدة من خلال تشغيل الأمر npm outdated، وفيما يلي قائمة ببعض الحزم القديمة في مستودع واحد لم نحدِّثها لفترة طويلة: تُعَدّ بعض هذه التحديثات إصدارات رئيسية، إذ لن يؤدي تشغيل الأمر npm update إلى تحديثها، فالإصدارات الرئيسية لا تُحدَّث بهذه الطريقة أبدًا لأنها حسب التعريف تقدِّم تغييرات جذرية، ولأن npm يريد توفير المتاعب عليك، في حين يمكنك تحديث جميع الحزم إلى إصدار رئيسي جديد من خلال تثبيت الحزمة تثبيتًا عامًا باستخدام الأمر npm-check-updates كما يلي: npm install -g npm-check-updates ثم تشغيلها باستخدام الأمر التالي: ncu -u سيؤدي ذلك إلى ترقية جميع تلميحات الإصدار في ملف package.json إلى الاعتماديات dependencies وdevDependencies، لذلك يستطيع npm تثبيت الإصدار الرئيسي الجديد، ويمكنك الآن تشغيل أمر التحديث كما يلي: npm update إذا حمّلتَ المشروع بدون اعتماديات node_modules وأردت تثبيت الإصدارات الجديدة أولًا، فما عليك إلا تشغيل الأمر التالي: npm install إدارة الإصدارات وسرد إصدارات الحزم المثبتة يدير npm أيضًا -بالإضافة إلى التنزيلات العادية- عملية الأصدَرة versioning، بحيث يمكنك تحديد إصدار معيّن من الحزمة، أو طلب إصدار أحدث أو أقدم مما تحتاجه، وستجد في كثير من الأحيان أنّ المكتبة متوافقة فقط مع إصدار رئيسي لمكتبة أخرى، أو قد تجد خطأً غير مُصحَّح بعد في الإصدار الأخير من مكتبة، مما يسبِّب مشاكلًا، كما يساعد تحديد إصدار صريح من مكتبة أيضًا في إبقاء كل فريق العمل على إصدار الحزمة الدقيق نفسه، بحيث يشغّل الفريق بأكمله الإصدار نفسه حتى تحديث ملف package.json. تساعد عملية تحديد الإصدار كثيرًا في جميع الحالات السابقة، حيث يتبع npm معيار إدارة الإصدارات الدلالية semantic versioning - أو semver اختصارًا- والذي سنشرحه تاليًا في قسم منفصل، وقد تحتاج عمومًا إلى معرفة إصدار حزمة معينة ثبَّتها في تطبيقك، وهنا يمكنك استخدم الأمر التالي لمعرفة الإصدار الأحدث من جميع حزم npm المثبَّتة بالإضافة إلى اعتمادياتها: npm list إليك المثال التالي: npm list /Users/flavio/dev/node/cowsay └─┬ cowsay@1.3.1 ├── get-stdin@5.0.1 ├─┬ optimist@0.6.1 │ ├── minimist@0.0.10 │ └── wordwrap@0.0.3 ├─┬ string-width@2.1.1 │ ├── is-fullwidth-code-point@2.0.0 │ └─┬ strip-ansi@4.0.0 │ └── ansi-regex@3.0.0 └── strip-eof@1.0.0 يمكنك فتح ملف package-lock.json فقط، ولكنه يحتاج بعض الفحص البصري، حيث يطبق الأمر npm list -g الشيء نفسه ولكن للحزم المثبَّتة تثبيتًا عامًا، كما يمكنك الحصول على حزم المستوى الأعلى فقط، أي الحزم التي طلبتَ من npm تثبيتها وأدرجتَها في ملف package.json، من خلال تشغيل الأمر npm list --depth=0 كما يلي: npm list --depth=0 /Users/flavio/dev/node/cowsay └── cowsay@1.3.1 يمكنك الحصول على إصدار حزمة معينة عن طريق تحديد اسمها كما يلي: npm list cowsay /Users/flavio/dev/node/cowsay └── cowsay@1.3.1 وتعمل هذه الطريقة أيضًا مع اعتماديات الحزم التي ثبَّتها كما يلي: npm list minimist /Users/flavio/dev/node/cowsay └─┬ cowsay@1.3.1 └─┬ optimist@0.6.1 └── minimist@0.0.10 إذا أردت معرفة أحدث إصدار متوفر من الحزمة في مستودع npm، فشغّل الأمر npm view [package_name] version كما يلي: npm view cowsay version 1.3.1 إلغاء تثبيت حزم npm قد تسأل نفسك ماذا لو أردت إلغاء تثبيت حزمة npm المُثبَّتة تثبيتًا محليًا أو عامًا؟ يمكنك إلغاء تثبيت حزمة مثبَّتة مسبقًا محليًا locally باستخدام الأمر npm install <packagename>‎ في مجلد node_modules من خلال تشغيل الأمر التالي في مجلد جذر المشروع، أي المجلد الذي يحتوي على مجلد node_modules: npm uninstall <package-name> تُستخدَم الراية ‎-S أو ‎--save لإزالة جميع المراجع في ملف package.json، فإذا كانت الحزمة عبارة عن اعتمادية تطوير مُدرَجة في اعتماديات devDependencies الخاصة بملف package.json، فيجب عليك استخدام الراية ‎-D أو الراية ‎--save-dev لإزالتها من الملف كما يلي: npm uninstall -S <package-name> npm uninstall -D <package-name> إذا ثُبِّتت الحزمة تثبيتًا عامًا globally، فيجب إضافة الراية ‎-g أو الراية ‎--global كما يلي: npm uninstall -g <package-name> إليك المثال التالي: npm uninstall -g webpack كما يمكنك تشغيل هذا الأمر من أي مكان تريده على نظامك لأن المجلد الذي تتواجد فيه حاليًا غير مهم. تشغيل مهام وتنفيذ سكربتات من سطر الأوامر يدعم ملف package.json تنسيقًا لتحديد مهام سطر الأوامر التي يمكن تشغيلها باستخدام الأمر التالي: npm run <task-name> فمثلًا: { "scripts": { "start-dev": "node lib/server-development", "start": "node lib/server-production" }, } يشيع استخدام هذه الميزة لتشغيل Webpack: { "scripts": { "watch": "webpack --watch --progress --colors --config webpack.conf.js", "dev": "webpack --progress --colors --config webpack.conf.js", "prod": "NODE_ENV=production webpack -p --config webpack.conf.js", }, } يمكنك تشغيل الأوامر التالية بدلًا من كتابة الأوامر الطويلة السابقة التي يسهل نسيانها أو كتابتها بصورة خاطئة: $ npm run watch $ npm run dev $ npm run prod الملف package.json نقطة ارتكاز المشروع يُعَدّ ملف package.json عنصرًا أساسيًا في كثير من قواعد شيفرات التطبيقات المستندة إلى نظام Node.js المجتمعي، فإذا استخدمت سابقًا لغة جافاسكريبت أو تعاملت مع مشروع JavaScript أو Node.js أو مشروع واجهة أمامية، فلا بد أنك صادفت ملف package.json، كما يُعَدّ ملف package.json بيانًا manifest لمشروعك، إذ يمكنه تطبيق أشياء غير مرتبطة متعددة، فهو مستودع مركزي لإعداد الأدوات مثلًا، كما أنه المكان الذي يخزّن فيه npm وyarn أسماء وإصدارات الحزم المُثبَّتة. معمارية الملف package.json فيما يلي مثال لملف package.json: { } هذا الملف فارغ، إذ لا توجد متطلبات ثابتة لما يجب تواجده في ملف package.json خاص بتطبيقٍ ما، فالشرط الوحيد هو أنه يجب أن يتبع تنسيق JSON، وإلّا فلا يمكن أن تقرأه البرامج التي تحاول الوصول إلى خصائصه برمجيًا، وإذا أردت بناء حزمة Node.js التي ترغب في توزيعها عبر npm، فسيتغيّر كل شيء جذريًا، إذ يجب أن يكون لديك مجموعة من الخصائص التي ستساعد الأشخاص الآخرين على استخدام هذا الملف، حيث سنتحدّث عن ذلك لاحقًا، وإليك مثال آخر عن ملف package.json: { "name": "test-project" } يعرِّف الملف السابق خاصية الاسم name والتي تعطي اسم التطبيق أو الحزمة الموجودة في المجلد نفسه الذي يوجد فيه هذا الملف، وإليك المثال التالي الأكثر تعقيدًا والمُستخرَج من عينة تطبيق Vue.js: { "name": "test-project", "version": "1.0.0", "description": "A Vue.js project", "main": "src/main.js", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "unit": "jest --config test/unit/jest.conf.js --coverage", "test": "npm run unit", "lint": "eslint --ext .js,.vue src test/unit", "build": "node build/build.js" }, "dependencies": { "vue": "^2.5.2" }, "devDependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1", "babel-eslint": "^8.2.1", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-jest": "^21.0.2", "babel-loader": "^7.1.1", "babel-plugin-dynamic-import-node": "^1.2.0", "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", "babel-plugin-transform-runtime": "^6.22.0", "babel-plugin-transform-vue-jsx": "^3.5.0", "babel-preset-env": "^1.3.2", "babel-preset-stage-2": "^6.22.0", "chalk": "^2.0.1", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", "eslint": "^4.15.0", "eslint-config-airbnb-base": "^11.3.0", "eslint-friendly-formatter": "^3.0.0", "eslint-import-resolver-webpack": "^0.8.3", "eslint-loader": "^1.7.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-vue": "^4.0.0", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^1.1.4", "friendly-errors-webpack-plugin": "^1.6.1", "html-webpack-plugin": "^2.30.1", "jest": "^22.0.4", "jest-serializer-vue": "^0.3.0", "node-notifier": "^5.1.2", "optimize-css-assets-webpack-plugin": "^3.2.0", "ora": "^1.2.0", "portfinder": "^1.0.13", "postcss-import": "^11.0.0", "postcss-loader": "^2.0.8", "postcss-url": "^7.2.1", "rimraf": "^2.6.0", "semver": "^5.3.0", "shelljs": "^0.7.6", "uglifyjs-webpack-plugin": "^1.1.1", "url-loader": "^0.5.8", "vue-jest": "^1.0.2", "vue-loader": "^13.3.0", "vue-style-loader": "^3.0.1", "vue-template-compiler": "^2.5.2", "webpack": "^3.6.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-dev-server": "^2.9.1", "webpack-merge": "^4.1.0" }, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } هناك خاصيات متعددة يجب شرحها في المثال السابق: name التي تضبط اسم التطبيق أو الحزمة. version التي تشير إلى الإصدار الحالي. description وهي وصف مختصَر للتطبيق أو للحزمة. main التي تضبط نقطة الدخول للتطبيق. private التي تمنع نشر التطبيق أو الحزمة عن طريق الخطأ على npm إذا ضُبِطت على القيمة true. scripts التي تحدّد مجموعة من سكربتات نود التي يمكنك تشغيلها. dependencies التي تضبط قائمة بحزم npm المثبَّتة كاعتماديات. devDependencies التي تضبط قائمة بحزم npm المثبَّتة كاعتماديات تطوير. engines التي تحدّد إصدار نود الذي تعمل عليه هذه الحزمة أو التطبيق. browserslist التي تُستخدَم لمعرفة المتصفحات وإصداراتها التي تريد دعمها. تُستخدَم جميع هذه الخصائص إما باستخدام npm أو باستخدام أدوات أخرى. خاصيات الملف package.json يشرح هذا القسم الخاصيات التي يمكنك استخدامها ضمن الملف package.json بالتفصيل، حيث سنطبّق كل شيء على الحزمة، ولكن يمكن تطبيق الشيء نفسه على التطبيقات المحلية التي لا تستخدِمها على أساس حزم، كما تُستخدَم معظم هذه الخاصيات فقط على npm، ويُستخدَم البعض الآخر بواسطة السكربتات التي تتفاعل مع شيفرتك مثل npm أو غيره. name تضبط هذه الخاصية اسم الحزمة مثل المثال التالي: "name": "test-project" يجب أن يتضمّن الاسم أقل من 214 محرفًا وألا يحتوي على مسافات، كما لا يمكن أن يحتوي إلّا على أحرف صغيرة أو واصلات - أو شرطات سفلية _، وذلك لأن الحزمة تحصل على عنوان URL الخاص بها بناءً على هذه الخاصية عند نشرها على npm، إذا نشرتَ هذه الحزمة علنًا على GitHub، فستكون القيمة المناسبة لهذه الخاصية هي اسم مستودع GitHub. author تعطي هذه الخاصية اسم مؤلف الحزمة مثل المثال التالي: { "author": "Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)" } يمكن استخدامها أيضًا بالتنسيق التالي: { "author": { "name": "Flavio Copes", "email": "flavio@flaviocopes.com", "url": "https://flaviocopes.com" } } contributors يمكن أن يكون للمشروع مساهم أو أكثر بالإضافة إلى المؤلف، وهذه الخاصية هي مصفوفة تعطي قائمة المساهمين مثل المثال التالي: { "contributors": [ "Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)" ] } كما يمكن استخدام هذه الخاصية أيضًا بالتنسيق التالي: { "contributors": [ { "name": "Flavio Copes", "email": "flavio@flaviocopes.com", "url": "https://flaviocopes.com" } ] } bugs تُستخدَم هذه الخاصية للربط بمتتبّع مشاكل الحزمة، أي بصفحة مشاكل GitHub مثلًا كما يلي: { "bugs": "https://github.com/flaviocopes/package/issues" } homepage تضبط هذه الخاصية صفحة الحزمة الرئيسية مثل المثال التالي: { "homepage": "https://flaviocopes.com/package" } version تشير هذه الخاصية إلى إصدار الحزمة الحالي مثل المثال التالي: "version": "1.0.0" تتبع هذه الخاصية صيغة إدارة الإصدارات الدلالية semver، مما يعني أنّ الإصدار يُعبَّر عنه دائمًا بثلاثة أعداد: x.x.x، حيث يمثِّل العدد الأول الإصدار الرئيسي، ويمثِّل العدد الثاني الإصدار الثانوي؛ أما العدد الثالث فهو إصدار حزمة التصحيح patch version، فالإصدار الذي يصلح الأخطاء فقط هو إصدار حزمة التصحيح، والإصدار الذي يقدّم تغييرات متوافقة مع الإصدارات السابقة هو الإصدار الثانوي، كما يمكن أن يحتوي الإصدار الرئيسي على تغييرات جذرية. license تشير إلى رخصة الحزمة مثل المثال التالي: "license": "MIT" keywords تحتوي هذه الخاصية على مصفوفة من الكلمات المفتاحية المرتبطة بما تفعله حزمتك مثل المثال التالي: "keywords": [ "email", "machine learning", "ai" ] تساعد هذه الخاصية في العثور على حزمتك عند التنقل بين حزم مماثلة، أو عند تصفح موقع npm. description تحتوي هذه الخاصية على وصف مختصَر للحزمة مثل المثال التالي: "description": "A package to work with strings" هذه الخاصية مفيدة إذا قرّرت نشر حزمتك على npm لمعرفة معلومات الحزمة. repository تحدّد هذه الخاصية مكان وجود مستودع الحزمة مثل المثال التالي: "repository": "github:flaviocopes/testing", لاحظ البادئة github، وهناك خدمات شائعة أخرى مثل gitlab: "repository": "gitlab:flaviocopes/testing", وأيضًا bitbucket: "repository": "bitbucket:flaviocopes/testing", يمكنك ضبط نظام التحكم بالإصدارات بصورة صريحة كما يلي: "repository": { "type": "git", "url": "https://github.com/flaviocopes/testing.git" } كما يمكنك استخدام أنظمة مختلفة للتحكم بالإصدارات كما يلي: "repository": { "type": "svn", "url": "..." } main تضبط هذه الخاصية نقطة الدخول إلى الحزمة، وهي المكان الذي سيبحث فيه التطبيق عن عمليات تصدير الوحدة عند استيرادها في أحد التطبيقات مثل المثال التالي: "main": "src/main.js" private إذا ضُبِطت هذه الخاصية على القيمة true، فستمنع نشر التطبيق أو الحزمة عن طريق الخطأ على npm كما يلي: "private": true scripts تحدّد هذه الخاصية مجموعة سكربتات نود التي يمكنك تشغيلها مثل المثال التالي: "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "unit": "jest --config test/unit/jest.conf.js --coverage", "test": "npm run unit", "lint": "eslint --ext .js,.vue src test/unit", "build": "node build/build.js" } تُعَدّ هذه السكربتات تطبيقات سطر الأوامر، حيث يمكنك تشغيلها عن طريق استدعاء الأمر npm run XXXX أو yarn XXXX، حيث XXXX هو اسم الأمر مثل npm run dev، كما يمكنك إعطاء الأمر أيّ اسم تريده، ويمكن للسكربتات فعل أيّ شيء تريده. dependencies تضبط هذه الخاصية قائمة حزم npm المثبَّتة على أساس اعتماديات. إذا ثبَّتَ حزمةً باستخدام npm أو yarn كما يلي: npm install <PACKAGENAME> yarn add <PACKAGENAME> ستُدخَل تلك الحزمة في هذه القائمة تلقائيًا، وإليك المثال التالي: "dependencies": { "vue": "^2.5.2" } devDependencies تضبط هذه الخاصية قائمة حزم npm المثبَّتة على أساس اعتماديات تطوير، وهي تختلف عن الخاصية dependencies لأنها مُخصَّصة للتثبيت على آلة تطوير فقط، وليست ضرورية لتشغيل الشيفرة في عملية الإنتاج، فإذا ثبَّتَ حزمةً باستخدام npm أو yarn: npm install --dev <PACKAGENAME> yarn add --dev <PACKAGENAME> فستُدخَل تلك الحزمة في هذه القائمة تلقائيًا، وإليك المثال التالي: "devDependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1" } engines تضبط هذه الخاصية إصدارات Node.js والأوامر الأخرى التي تعمل عليها هذه الحزمة أو التطبيق مثل المثال التالي: "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0", "yarn": "^0.13.0" } browserslist تُستخدَم هذه الخاصية لمعرفة المتصفحات وإصداراتها التي تريد دعمها، وقد أشارت إليها أدوات Babel وAutoprefixer وأدوات أخرى أنها تُستخدَم لإضافة تعويض نقص دعم المتصفحات polyfills والنسخ الاحتياطية fallbacks اللازمة للمتصفحات التي تستهدفها، وإليك المثال التالي: "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] يعني الإعداد السابق أنك تريد دعم آخر إصدارين رئيسيين من جميع المتصفحات باستخدام 1‎%‎ على الأقل -من إحصائيات موقع caniuse- باستثناء الإصدار IE8 والإصدارات الأقدم (اطلع على المزيد من موقع حزمة). خصائص خاصة بالأوامر يمكن أن يستضيف ملف package.json أيضًا إعدادًا خاصًا بالأوامر مثل Babel وESLint وغير ذلك، فلكل منها خاصية معينة مثل eslintConfig وbabel وغيرها، وهذه هي الخصائص الخاصة بالأوامر، كما يمكنك العثور على كيفية استخدامها في توثيق الأمر أو المشروع المرتبط بها. إصدارات الحزم رأيت في الوصف أعلاه أرقام الإصدارات مثل: ‎~3.0.0 أو ‎^0.13.0، حيث يحدِّد الرمز الموجود على يسار رقم الإصدار التحديثات التي تقبلها الحزمة من تلك الاعتمادية، ولنفترض استخدام semver -الأصدَرة الدلالية semantic versioning-، حيث تتضمن جميع الإصدارات 3 خانات عددية، أولها هو الإصدار الرئيسي وثانيها هو الإصدار الثانوي وثالثها هو إصدار حزمة التصحيح patch release، وبالتالي لديك القواعد التالية: ~: إذا كتبت ‎~0.13.0، فهذا يعني أنك تريد فقط تحديث إصدارات حزمة التصحيح، أي أنّ الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. ^: إذا كتبت ‎^0.13.0، فهذا يعني أنك تريد تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. *: إذا كتبت *، فهذا يعني أنك تقبل جميع التحديثات بما في ذلك ترقيات الإصدارات الرئيسية. <: أي أنك تقبل أي إصدار أعلى من الإصدار الذي تحدِّده. =‎<: أي أنك تقبل أي إصدار مساوي أو أعلى من الإصدار الذي تحدِّده. =>: أي أنك تقبل أي إصدار مساوي أو أدنى من الإصدار الذي تحدِّده. >: أي أنك تقبل أي إصدار أدنى من الإصدار الذي تحدِّده. وهناك قواعد أخرى هي: بدون رمز: أي أنك تقبل فقط الإصدار الذي تحدّده. latest: أي أنك تريد استخدام أحدث إصدار متاح. كما يمكنك دمج معظم ما سبق ضمن مجالات مثل 1.0.0‎ || >=1.1.0 <1.2.0 لاستخدم إما الإصدار 1.0.0 أو أحد الإصدارات الأعلى أو المساوية للإصدار 1.1.0 والأدنى من الإصدار 1.2.0. الملف package-lock.json ودوره في إدارة الإصدارات يُنشَأ الملف package-lock.json تلقائيًا عند تثبيت حزم نود، حيث قدَّم npm ملف package-lock.json في الإصدار رقم 5، والهدف من هذا الملف هو تتبّع الإصدار الدقيق لكل حزمة مثبَّتة، وبالتالي فإن المنتج قابل لإعادة الإنتاج بنسبة 100% بالطريقة نفسها حتى إذا حدّث القائمون على الصيانة الحزم، كما يحل ذلك مشكلةً تركَها ملف package.json دون حل، إذ يمكنك في ملف package.json ضبط الإصدارات التي تريد الترقية إليها -أي إصدار حزمة التصحيح أو الإصدار الثانوي- باستخدام صيغة semver كما يلي: إذا كتبت ‎~0.13.0، فهذا يعني أنك تريد فقط تحديث إصدار حزمة التصحيح، أي أن الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. إذا كتبت ‎^0.13.0، فهذا يعني أنك تريد تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. إذا كتبت 0.13.0، فهذا يعني الإصدار الدقيق الذي سيُستخدَم. لستَ ملزَمًا بتوزيع مجلد node_modules الضخم باستخدام برنامج جت Git، وإذا حاولتَ نسخ المشروع على جهاز آخر باستخدام الأمر npm install -إذا حدّدتَ الصيغة ~ مع إصدار حزمة التصحيح الخاص بالحزمة- فسيثبَّت هذا الإصدار، كما يحدث الأمر ذاته مع الصيغة ^ والإصدارات الثانوية. قد تحاول أنت أو أي شخص آخر تهيئة المشروع على الجانب الآخر من العالم عن طريق تشغيل الأمر npm install، لذلك فإن مشروعك الأصلي والمشروع المُهيَّأ حديثًا مختلفان فعليًا، إذ يجب ألّا يدخِل إصدار حزمة التصحيح أو الإصدار الثانوي تغييرات معطِّلة، ولكننا نعلم أن الأخطاء ممكنة الحدوث وستحدث بالفعل. يضبط ملف package-lock.json الإصدار المثبَّت حاليًا من كل حزمة باستخدام رمز stone، وسيستخدِم npm هذه الإصدارات المحدَّدة عند تشغيل الأمر npm install، كما أنّ هذا المفهوم ليس بجديد، إذ يستخدِم مديرو حزم لغات البرمجة الأخرى -مثل مكتبات Composer في لغة PHP- نظامًا مشابهًا منذ سنوات. يجب أن يكون ملف package-lock.json ملتزمًا بمستودع Git الخاص بك حتى يجلبه أشخاص آخرون إذا كان المشروع عامًا أو لديك متعاونون أو إذا استخدمت Git على أساس مصدر لعمليات النشر، كما ستُحدَّث إصدارات الاعتماديات في ملف package-lock.json عند تشغيل الأمر npm update. يوضِّح المثال التالي معمارية ملف package-lock.json التي نحصل عليها عند تشغيل الأمر npm install cowsay في مجلد فارغ: { "requires": true, "lockfileVersion": 1, "dependencies": { "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz", "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "minimist": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" } }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" } } } ثبّتنا حزمة cowsay التي تعتمد على الحزم التالية: get-stdin. optimist. string-width. strip-eof. تتطلب هذه الحزم حزمًا أخرى مثل الحزم الموجودة في الخاصية requires كما يلي: ansi-regex. is-fullwidth-code-point. minimist. wordwrap. strip-eof. تُضاف هذه الحزم إلى الملف بالترتيب الأبجدي، ولكل منها حقل version، وحقل resolved يؤشّر إلى موقع الحزمة، وسلسلة نصية integrity يمكننا استخدامها للتحقق من الحزمة. قواعد الإدارة الدلالية لنسخ الاعتماديات تُعَدّ الإدارة الدلالية لنسخ الاعتماديات Semantic Versioning اصطلاحًا يُستخدَم لتوفير معنى للإصدارات، فإذا كان هناك شيء رائع في حزم Node.js، فهو اتفاق الجميع على استخدام هذا المفهوم لترقيم إصداراتهم، كما يُعَدّ مفهوم الإدارة الدلالية للنسخ بسيطًا للغاية، فلكل الإصدارات 3 خانات عددية x.y.z: العدد الأول هو الإصدار الرئيسي. العدد الثاني هو الإصدار الثانوي. العدد الثالث هو إصدار التصحيح. إذا أردت إنشاء إصدار جديد، فلن تزيد عددًا كما يحلو لك، بل لديك قواعد يجب الالتزام بها وهي: يُحدَّث الإصدار الرئيسي عند إجراء تغييرات غير متوافقة مع واجهة برمجة التطبيقات API. يُحدَّث الإصدار الثانوي عند إضافة عمليات بطريقة متوافقة مع الإصدارات السابقة. يُحدَّث إصدار تصحيح عند إجراء إصلاحات أخطاء متوافقة مع الإصدارات السابقة. اُعتمِد هذا المفهوم في جميع لغات البرمجة ومن المهم أن تلتزم بها كل حزمة npm لأن النظام بأكمله يعتمد على ذلك، إذ وضَع npm بعض القواعد التي يمكننا استخدامها في ملف package.json لاختيار الإصدارات التي يمكن تحديث حزمنا إليها عند تشغيل الأمر npm update، وتستخدِم هذه القواعد الرموز التالية: ^: إذا كتبت ‎^0.13.0 عند تشغيل الأمر npm update، فهذا يؤدي إلى تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. ~: إذا كتبت ‎~0.13.0 عند تشغيل الأمر npm update، فهذا يؤدي إلى تحديث إصدارات حزمة التصحيح، أي أن الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. <: أي أنك تقبل أي إصدار أعلى من الإصدار الذي تحدده. =‎<: أي أنك تقبل أي إصدار مساوي أو أعلى من الإصدار الذي تحدّده. =>: أي أنك تقبل أي إصدار مساوي أو أدنى من الإصدار الذي تحدّده. >: أي أنك تقبل أي إصدار أدنى من الإصدار الذي تحدّده. =: أي أنك تقبل الإصدار المحدَّد. -: أي أنك تقبل مجالًا من الإصدارات مثل المجال مثل: 2.1.0‎ - 2.6.2. ||: يُستخدَم لدمج مجموعات من الإصدارات مثل: ‎< 2.1 || > 2.6. يمكنك دمج بعض القواعد السابقة مثل: 1.0.0‎ || >=1.1.0 <1.2.0 لاستخدام إما الإصدار 1.0.0 أو أحد الإصدارات الأعلى أو المساوية للإصدار 1.1.0 والأدنى من الإصدار 1.2.0. هناك قواعد أخرى أيضًا هي: بدون رمز: أي أنك تقبل فقط الإصدار الذي تحدّده مثل: 1.2.1. latest: أي أنك تريد استخدام أحدث إصدار متاح. أنواع الحزم تُصنَّف الحزم وفقًا لمجال نطاق رؤيتها، أي المكان الذي تُرَى الحزمة منه ويمكن استخدامها فيه، وتنقسم إلى حزمة عامة وخاصة أو محلية، كما تصنَّف أيضًا وفقًا لبيئة استخدامها وتكون إما اعتماديات أساسية ضرورية للمشروع في بيئة الإنتاج والتطوير معًا، وإما اعتماديات خاصة ببيئة التطوير فقط، أي مطلوبة في وقت تطوير المشروع وغير مطلوبة في بيئة الإنتاج. الحزم العامة والحزم المحلية الفرق الرئيسي بين الحزم المحلية والعامة هو: تُثبَّت الحزم المحلية في المجلد أو المسار حيث تشغّل الأمر npm install <package-name>‎، وتوضَع في مجلد node_modules ضمن هذا المجلد أو المسار. توضَع جميع الحزم العامة في مكان واحد في نظامك بالاعتماد على إعدادك الخاص بغض النظر عن مكان تشغيل الأمر npm install -g <package-name>‎. وكلاهما مطلوب بالطريقة نفسها في شيفرتك الخاصة كما يلي: require('package-name') يجب تثبيت جميع الحزم محليًا، إذ يضمن ذلك أنه يمكنك الحصول على عشرات التطبيقات على حاسوبك، وتشغِّل جميعها إصدارًا مختلفًا من كل حزمة إذ لزم الأمر، بينما سيجعل تحديث حزمة عامة جميع مشاريعك تستخدِم الإصدار الجديد، مما قد يسبّب مشاكلًا ضخمةً في عملية الصيانة، حيث قد تخرِّب بعض الحزم التوافق بمزيد من الاعتماديات وما إلى ذلك. تحتوي جميع المشاريع على نسختها المحلية الخاصة من الحزمة، فقد يبدو ذلك ضياعًا للموارد، ولكنه ضياع ضئيل بالموازنة مع العواقب السلبية المحتمَلة، كما يجب تثبيت الحزمة العامة عندما توفِّر هذه الحزمة أمرًا قابلًا للتنفيذ بحيث تشغّله من الصدفة shell أي واجهة سطر الأوامر CLI، ويُعاد استخدام هذه الحزمة عبر المشاريع، كما يمكنك أيضًا تثبيت الأوامر القابلة للتنفيذ محليًا وتشغيلها باستخدام npx، ولكن تثبيت الحزم العامة أفضل بالنسبة لبعض الحزم. فيما يلي أمثلة رائعة عن الحزم العامة الشائعة التي قد تعرفها: npm. create-react-app. vue-cli. grunt-cli. mocha. react-native-cli. gatsby-cli. forever. nodemon. يُحتمَل أن تكون لديك بعض الحزم العامة المثبَّتة على نظامك التي يمكنك رؤيتها عن طريق تشغيل الأمر التالي في سطر الأوامر الخاص بك: npm list -g --depth 0 الاعتماديات الأساسية واعتماديات التطوير إذا ثبَّتَ حزمة npm باستخدام الأمر npm install <package-name>‎، فهذا يعني أنك ثبّتها على أساس اعتمادية dependency، حيث تُدرَج الحزمة تلقائيًا في ملف package.json ضمن قائمة dependencies بدءًا من الإصدار npm 5، إذ احتجنا سابقًا إلى تحديد الراية ‎--save يدويًا، فإذا أضفتَ الراية ‎-D أو الراية ‎--save-dev، فهذا يعني أنك تثبّتها على أساس اعتمادية تطوير، وبالتالي ستُضاف إلى قائمة devDependencies. يُقصَد باعتماديات التطوير أنها حزم للتطوير فقط، وهي غير ضرورية في عملية الإنتاج مثل حزم الاختبار أو حزم webpack أو Babel، فإذا كتبت الأمر npm install واحتوى المجلد على ملف package.json في عملية الإنتاج، فستُثبَّت اعتماديات التطوير، حيث يفترض npm أنّ هذه عملية نشر تطوير، كما يجب ضبط الراية ‎--production من خلال الأمر npm install --production لتجنب تثبيت اعتماديات التطوير. npx يُعَدّ npx طريقةً رائعةً جدًا لتشغيل شيفرة نود، كما يوفِّر ميزات مفيدةً متعددةً، إذ كان متاحًا في npm بدءًا من الإصدار 5.2، الذي صدر في شهر 7 من عام 2017. يتيح لك npx تشغيل الشيفرة المُنشَأة باستخدام نود والمنشورة من خلال سجل npm، كما يتميز npx بالميزات التالية: تشغيل الأوامر المحلية بسهولة اعتاد مطورو نود على نشر معظم الأوامر القابلة للتنفيذ على أساس حزم عامة لتكون هذه الأوامر في المسار الصحيح وقابلةً للتنفيذ مباشرةً، إذ كان هذا أمرًا صعبًا جدًا، لأن تثبيت إصدارات مختلفة من الأمر نفسه غير ممكن، كما يؤدي تشغيل الأمر npx commandname تلقائيًا إلى العثور على مرجع الأمر الصحيح ضمن مجلد node_modules الخاص بالمشروع دون الحاجة إلى معرفة المسار الدقيق، ودون الحاجة إلى تثبيت الحزمة على أنها حزمة عامة وفي مسار المستخدِم. تنفيذ الأوامر دون تثبيتها هناك ميزة أخرى رائعة في npm تسمح بتشغيل الأوامر دون تثبيتها أولًا، حيث يُعَدّ ذلك مفيدًا جدًا للأسباب التالية: لا تحتاج إلى تثبيت أي شيء. يمكنك تشغيل إصدارات مختلفة من الأمر نفسه باستخدام صيغة ‎.@version يمكن توضيح استخدام npx من خلال الأمر cowsay الذي سيطبع بقرةً تقول ما تكتبه ضمن الأمر، حيث سيطبع الأمر cowsay "Hello"‎ ما يلي على سبيل المثال: _______ < Hello > ------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || يحدث ذلك إذا كان الأمر cowsay مثبَّتًا تثبيتًا عامًا من npm سابقًا، وإلا فستحصل على خطأ عند محاولة تشغيل الأمر، كما يسمح لك npx بتشغيل الأمر npm السابق دون تثبيته محليًا كما يلي: npx cowsay "Hello" يُعَدّ الأمر السابق للتسلية فقط ودون فائدة، ولكن يمكنك استخدام npx في حالات مهمة أخرى مثل: تشغيل أداة واجهة سطر الأوامر vue لإنشاء تطبيقات جديدة وتشغيلها باستخدام الأمر npx vue create myvue-app إنشاء تطبيق React جديد باستخدام الأمر npx create-react-app my-react-app. و حالات أخرى أيضًا، كما ستُمسَح الشيفرة المُنزَّلة لهذه الأوامر بمجرد تنزيلها. تشغيل شيفرة باستخدام إصدار نود Node مختلف استخدم الرمز @ لتحديد الإصدار، وادمج ذلك مع حزمة npm التي هي node: npx node@6 -v #v6.14.3 npx node@8 -v #v8.11.3 يساعد ذلك في تجنب استخدام أدوات مثل أداة nvm أو أدوات إدارة إصدارات نود الأخرى. تشغيل أجزاء شيفرة عشوائية مباشرة من عنوان URL لا يقيّدك npx بالحزم المنشورة في سجل npm، إذ يمكنك تشغيل الشيفرة الموجودة في GitHub gist مثل المثال التالي: npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32 يجب أن تكون حذرًا عند تشغيل شيفرة لا تتحكم بها، فالقوة العظمى تستوجب مسؤولية عظمى أيضًا. ترجمة -وبتصرّف- للفصل Node modules and npm من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: كيفية تنفيذ الدوال داخليا ضمن Node.js المقال السابق: استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js مقدمة إلى Node.js تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker
  8. سنتعرّف في هذا المقال على الوضع التفاعلي REPL في Node.js الذي نستخدمه في الطرفية لتقييم تعبير برمجي مكتوب بلغة javascript، كما سنتعلّم كيفية التعامل مع سطر الأوامر في Node.js من تمرير وسائط منه وإرسال مخرجات إليه وقبول مدخلات منه. استخدام الوضع التفاعلي REPL يسمى الوضع التفاعلي في Node.js باسم REPL اختصارًا إلى اقرأ-قيّم-اطبع-كرر Read-Evaluate-Print-Loop، أي اقرأ وقدِّر قيمة التعبير البرمجي ثم اطبع الناتج وكرر العملية، وهو طريقة رائعة لاستكشاف ميزات Node.js بطريقة سريعة. استعملنا الأمر node سابقًا لتشغيل أحد السكربتات: node script.js أما إذا حذفنا اسم الملف، فسندخل في الوضع التفاعلي REPL: node وعندما تجرب الأمر السابق في الطرفية عندك، فسيحدث ما يلي وسيبقى الأمر في وضع السكون وينتظر منك إدخال تعبير ما: $ node Welcome to Node.js v14.15.0. Type ".help" for more information. > إذا أردنا أن نكون أكثر دقةً، فإن الوضع التفاعلي ينتظر منّا إدخال شيفرة JavaScript، لذا لنبدأ بسطر بسيط: > console.log('test') test undefined > القيمة الأولى المطبوعة test هي ناتج الأمر الذي طلبنا منه طباعة سلسلة نصية إلى الطرفية، ثم حصلنا على القيمة undefined وهي القيمة المعادة من تنفيذ console.log()‎، ويمكننا الآن إدخال سطر JavaScript جديد. استخدام الزر tab للإكمال التلقائي من أجمل مزايا الوضع التفاعلي REPL هو أنه تفاعلي، ففي أثناء كتابك للشيفرة إذا ضغطت على زر tab، فسيحاول الوضع التفاعلي الإكمال التلقائي لما كتبته ليوافق اسم متغير عرفته مسبقًا. استكشاف كائنات JavaScript جرِّب إدخال اسم كائن من كائنات JavaScript مثل Number وأضف إليه نقطةً ثم اضغط على tab، إذ سيعرض لك الوضع التفاعلي جميع الخاصيات والتوابع التي يمكنك الوصول إليها في ذاك الكائن. استكشاف الكائنات العامة يمكنك معاينة الكائنات العامة بكتابة global.‎ ثم الضغط على الزر tab: المتغير الخاص _ إذا كتبت _ بعد شيفرة ما، فسيؤدي ذلك إلى طباعة ناتج آخر عملية. الأوامر ذوات النقط يملك الوضع التفاعلي REPL بعض الأوامر الخاصة وكلها تبدأ بنقطة . وهي: ‎.help: يظهر المساعدة للأوامر ذوات النقط. ‎.editor: تفعيل وضع المحرر، وذلك لكتابة شيفرة JavaScript متعددة الأسطر بسهولة، وبعد دخولك في هذا الوضع وكتابتك للشيفرة المطلوبة، فيمكنك إدخال Ctrl+D لتنفيذ الأمر الذي كتبت. ‎.break: إذا كتبت الأمر ‎.break عند كتابتك لتعبير متعدد الأسطر، فستلغي أي مدخلات قادمة، ومثله مثل الضغط على Ctrl+C. ‎.clear: إعادة ضبط سياق الوضع التفاعلي إلى كائن فارغ وإزالة أي تعابير متعددة الأسطر جرت كتابتها. ‎.load: تحميل ملف JavaScript بمسار نسبي إلى مجلد العمل الحالي. ‎.save: حفظ ما أدخلته في الجلسة التفاعلية إلى ملف مع تمرير مسار الملف بعد كتابة ‎.save. ‎‎.exit: الخروج من الوضع التفاعلي وهو يماثل الضغط على Ctrl+C مرتين. يعرِف الوضع التفاعلي REPL متى تكتب تعبيرًا متعدد الأسطر دون الحاجة إلى تشغيل الأمر ‎.editor، فإذا بدأت مثلًا بكتابة حلقة تكرار مثل هذه: [1, 2, 3].forEach(num => { ثم ضغطت على زر enter، فسيعرف الوضع التفاعلي أنك تكتب تعبيرًا متعدد الأسطر ويبدأ سطرًا جديدًا بثلاث نقط …، مما يشير إلى أنك ما زلت تعمل على القسم أو المقطع نفسه: ... console.log(num) ... }) إذا كتبت ‎.break في آخر السطر، فسيتوقف وضع تعدد الأسطر ولن يُنفَّذ التعبير الذي كتبته. تمرير الوسائط من سطر الأوامر إلى Node.js سنشرح في هذا القسم كيفية استقبال وسائط arguments في برنامج Node.js مُمرَّرة من سطر الأوامر، إذ يمكنك تمرير أي عدد من الوسائط أثناء تشغيل برنامج Node.js باستخدام الأمر: node app.js يمكن أن تكون الوسائط بمفردها، أو على شكل زوج من المفاتيح والقيم مثل: node app.js ahmed أو node app.js name=ahmed يجعل هذا طريقة الحصول على القيمة مختلفةً في شيفرة Node.js. إذ أنّ طريقة الحصول على الوسائط هي استخدام الكائن process المبني في Node.js، ففيه الخاصية argv التي هي مصفوفة تحتوي على جميع الوسائط الممررة عبر سطر الأوامر؛ ويكون الوسيط الأول هو المسار الكامل للأمر node، بينما الوسيط الثاني هو مسار الملف المُنفَّذ كاملًا، وجميع الوسائط الإضافية ستكون موجودةً من الموضع الثالث إلى آخره، كما يمكنك المرور على جميع المعاملات بما في ذلك مسار node ومسار الملف المُنفَّذ باستخدام حلقة تكرار: process.argv.forEach((val, index) => { console.log(`${index}: ${val}`) }) يمكنك الحصول على الوسائط الإضافية من خلال إنشاء مصفوفة جديدة تستثني أول قيمتين: const args = process.argv.slice(2) إذا كان لديك معامل بمفرده دون مفتاح مثل: node app.js ahmed فيمكنك الوصول إليه كما يلي: const args = process.argv.slice(2) args[0] أما في حالة كان الوسيط هو مفتاح وقيمة كما في: node app.js name=ahmed فإن قيمة args[0]‎ هي name=ahmed، وستحتاج إلى تفسيرها، وأفضل طريقة هي استخدام المكتبة minimist التي تساعدنا بالتعامل مع الوسائط: const args = require('minimist')(process.argv.slice(2)) args['name'] // ahmed إرسال تطبيق Node.js المخرجات إلى سطر الأوامر سنتعلم كيفية الطباعة إلى سطر الأوامر باستخدام Node.js بدءًا من الاستخدام الأساسي للتابع console.log حتى وصولنا إلى الأمور المعقدة. طباعة المخرجات باستخدام الوحدة console توفِّر Node.js الوحدة console التي توفر عددًا كبيرًا من الطرائق المفيدة في التعامل مع سطر الأوامر، وهي تشبه إلى حد ما الكائن console الموجود في المتصفحات، والتابع الأساسي الأكثر استخدامًا في هذه الوحدة هو التابع console.log()‎، الذي يطبع السلسلة النصية التي تمررها إليه إلى الطرفية، وإذا مررت كائنًا فسيعرضه على أساس سلسلة نصية، كما يمكنك تمرير قيمًا متعددةً إلى التابع console.log()‎ كما يلي، إذ ستطبع Node.js القيمتين x وy معًا: const x = 'x' const y = 'y' console.log(x, y) يمكنك أيضًا تنسيق السلاسل النصية بتمرير القيم ومُحدِّد التنسيق كما في المثال التالي: console.log('My %s has %d years', 'cat', 2) إذ أنّ محددات التنسيق format specifiers هي: ‎%s: تنسيق المتغير على أساس سلسلة نصية. ‎%d أو ‎%i: تنسيق المتغير على أساس عدد صحيح. ‎%f: تنسيق المتغير على أساس عدد ذي فاصلة عشرية. ‎%O: تنسيق المتغير على أساس كائن كما في المثال البسيط الآتي: console.log('%O', Number) مسح محتوى الطرفية يمسح التابع console.clear()‎ محتوى الطرفية، لكن يختلف سلوكه اعتمادًا على الطرفية المستخدَمة. عد العناصر يُعَدّ التابع console.count()‎ مفيدًا في بعض الحالات التي تريد فيها عدّ المرات التي طُبِعَت فيها السلسلة النصية وإظهار الرقم بجوارها، خذ هذا المثال: const x = 1 const y = 2 const z = 3 console.count( 'The value of x is ' + x + ' and has been checked .. how many times?' ) console.count( 'The value of x is ' + x + ' and has been checked .. how many times?' ) console.count( 'The value of y is ' + z + ' and has been checked .. how many times?' ) يمكننا إحصاء عدد البرتقالات والتفاحات التي لدينا: const oranges = ['orange', 'orange'] const apples = ['just one apple'] oranges.forEach(fruit => { console.count(fruit) }) apples.forEach(fruit => { console.count(fruit) }) طباعة تتبع مكدس الاستدعاء قد تكون هنالك حالات من المفيد فيها طباعة تتبع مكدس الاستدعاء call stack trace لإحدى الدوال، وذلك للإجابة مثلًا على السؤال التالي: كيف وصلنا إلى هذا الجزء من الشيفرة، إذ يمكنك استخدام التابع console.trace()‎: const function2 = () => console.trace() const function1 = () => function2() function1() سيطبع مكدس الاستدعاء ما يلي، وهذا هو الناتج إذا جربنا الشيفرة السابقة في النمط التفاعلي REPL في Node.js: Trace at function2 (repl:1:33) at function1 (repl:1:25) at repl:1:1 at ContextifyScript.Script.runInThisContext (vm.js:44:33) at REPLServer.defaultEval (repl.js:239:29) at bound (domain.js:301:14) at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:440:10) at emitOne (events.js:120:20) at REPLServer.emit (events.js:210:7) طباعة الزمن المستغرق يمكنك ببساطة حساب مقدار الوقت الذي أخذته الدالة للتنفيذ باستخدام التابعين time()‎ وtimeEnd()‎ كما في المثال التالي: const doSomething = () => console.log('test') const measureDoingSomething = () => { console.time('doSomething()') // افعل شيئًا وقس الوقت المستغرق لتنفيذه doSomething() console.timeEnd('doSomething()') } measureDoingSomething() مجرى الخرج القياسي stdout والخطأ القياسي stderr رأينا أنّ التابع console.log()‎ رائع لطباعة الرسائل في الطرفية، وهذا ما يسمى مجرى الخرج القياسي standard output stream أو stdout؛ أما التابع console.error()‎ فسيطبع الرسائل المرسَلة إلى مجرى الخطأ القياسي stderr والتي لن تظهر في الطرفية وإنما ستظهر في سجل الخطأ. طباعة المخرجات بألوان مختلفة يمكنك أن تغيير لون المخرجات النصية في الطرفية باستخدام تسلسلات التهريب escape sequences، وتسلسل التهريب هو مجموعة من المحارف التي تُعرِّف لونًا معينًا في الطرفية مثل: console.log('\x1b[33m%s\x1b[0m', 'hi!') يمكنك تجربة ذلك باستخدام النمط التفاعلي في Node.js وستُعرَض السلسلة النصية hi!‎ باللون الأصفر، لكن هذه الطريقة ذات مستوى منخفض، فأبسط طريقة لتلوين المخرجات في الطرفية هي باستخدام مكتبة مثل Chalk، حيث يمكنها أيضًا إجراء عمليات التنسيق الأخرى إضافةً إلى تلوينها للنص مثل جعل النص غامقًا أو مائلًا أو مسطرًا تحته، كما يمكنك تثبيتها باستخدام npm install chalk ثم استخدامها: const chalk = require('chalk') console.log(chalk.yellow('hi!')) من المؤكد أن استخدام chalk.yellow أكثر راحةً من محاولة تذكُّر شيفرات التهريب وستكون قابلية قراءة الشيفرة أفضل. إنشاء شريط تقدم في الطرفية تُعَدّ حزمة Progress حزمةً رائعةً لإنشاء شريط تقدُّم في الطرفية، حيث يمكنك تثبيتها باستخدام npm install progress ببساطة. تُنشِئ الشيفرة التالية شريط تقدُّم بعشر خطوات، حيث ستكتمل خطوة كل 100 ميللي ثانية، وحين اكتمال الشريط سنصفِّر العداد: const ProgressBar = require('progress') const bar = new ProgressBar(':bar', { total: 10 }) const timer = setInterval(() => { bar.tick() if (bar.complete) { clearInterval(timer) } }, 100) قبول المدخلات من سطر الأوامر يمكننا جعل تطبيق Node.js الذي يعمل من سطر الأوامر تفاعليًا باستخدام وحدة readline المبنية في أساس Node.js. أصبحت Node.js بدءًا من الإصدار السابع توفِّر الوحدة readline التي تحصل على المدخلات من مجرى قابل للقراءة مثل process.stdin (مجرى الدخل القياسي stdin) سطرًا بسطر والذي يكون أثناء تنفيذ برنامج Node.js من سطر الأوامر هو الدخل المكتوب في الطرفية. const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }) readline.question(`What's your name?`, (name) => { console.log(`Hi ${name}!`) readline.close() }) تسأل الشيفرة السابقة المستخدِم عن اسمه، وبعد كتابة المستخدِم الاسم والضغط على enter سنرسل تحيةً له. يُظهِر التابع question()‎ المعامل parameter الأول -أي السؤال- وينتظر مدخلات المستخدِم، ثم يستدعي دالة رد نداء عندما يضغط المستخدِم على زر enter. لاحظا أننا أغلقنا واجهة readline في دالة رد النداء، كما توفِّر الواجهة readline توابعًا كثيرةً متعددةً وسنتركك لاستكشافها من توثيقها؛ أما إذا احتجت إلى إدخال كلمة مرور، فمن الأفضل إظهار رمز * بدلًا منها، وأسهل طريقة لذلك هو استخدام الحزمة readline-sync التي تشبه readline في الواجهة البرمجية وتستطيع التعامل مع هذه الحالة دون عناء، والخيار الممتاز والمتكامل هو الحزمة inquirer.js، حيث يمكنك تثبيتها باستخدام npm install inquirer، كما يمكنك إعادة كتابة الشيفرة السابقة كما يلي: const inquirer = require('inquirer') var questions = [{ type: 'input', name: 'name', message: "What's your name?", }] inquirer.prompt(questions).then(answers => { console.log(`Hi ${answers['name']}!`) }) تسمح لك مكتبة inquirer.js بفعل أمور كثيرة مثل الأسئلة متعددة الخيارات وأزرار الانتقاء والتأكيدات وغير ذلك، كما من المفيد معرفة جميع الخيارات المتاحة أمامنا خصوصًا التي توفِّرها Node.js، لكن إذا أردت التعامل مع مدخلات سطر الأوامر كثيرًا، فالخيار الأمثل هو مكتبة inquirer.js. ترجمة -وبتصرّف- للفصل Command Line من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: دليلك الشامل إلى مدير الحزم npm في Node.js المقال السابق: مقدمة إلى Node.js تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker نشر تطبيق Node.js على الويب: خدمة هيروكو (Heroku) مثالًا
×
×
  • أضف...