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

Hassan Hedr

الأعضاء
  • المساهمات

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

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    38

كل منشورات العضو Hassan Hedr

  1. يوجد نوعين من المواقع أو خوادم الويب، وهي الثابتة static والمتغيرة dynamic، ففي المواقع الثابتة كل صفحة من الموقع يقابلها ملف صفحة مخصص لها بمحتوى ثابت لا يتغير، فعند تطبيق هذه الطريقة على موقع سيحوي الكثير من الصفحات والمحتوى مثل موقع الأكاديمية لن يكون هذا الحل عمليًا، هنا يُبنى هذا النوع من المواقع بطريقة متغيرة أو ديناميكية، أي بدلًا من إنشاء صفحة مخصصة لكل سؤال مثلًا، يتم تخزين محتويات تلك الأسئلة في مكان ما، مثل قاعدة البيانات، وعند طلب صفحة سؤال ما يُجلب قالب واجهة أمامية ثابت لصفحات الأسئلة ويملأه الموقع بمحتوى السؤال والإجابات ما يولد صفحة السؤال كما تشاهدها وترسل للمتصفح لديك، أي أنه يوجد صفحة واحدة ضمن موقع الأكاديمية خاصة بالأسئلة، تُملئ بالمحتوى المناسب عند طلبك لصفحة سؤال ما، يمكنك القراءة أكثر عن نوعي خادم الويب الديناميكي والثابت من المقال التالي: ويمكنك الاطلاع بنظرة شاملة عن أقسام تعلم الويب ومجالاتها لفهم كيف تُقسم المهام لتخديم موقع متكامل من المقال التالي:
  2. سننشئ في هذا المقال صورة دوكر مخصصة لتشغيل تطبيقات بايثون بداخلها، حيث يمكننا باستخدام ملف Dockerfile تخصيص وإنشاء صور دوكر جديدة، يمكن لمطوري التطبيقات أو المستخدمين أيضًا الاستفادة من ذلك، حيث سنستعين بصورة بايثون بسيطة وخفيفة، وبعد إجراء التعديلات على تلك الصورة ستصبح عملية تشغيل تطبيقات بايثون أسهل، ولن نقلق بشأن تشغيل تلك التطبيقات على أنظمة التشغيل المختلفة، حيث يمكن بتنفيذ أمر دوكر بسيط تشغيل تطبيق بايثون، ودون الحاجة للاستعانة ببرامج تثبيت التطبيقات التي توفرها الاستضافات عادةً. سنستخدم مينيكوندا Miniconda وهو مُثبّت كوندا مجاني يُنشئ gkh نسخة صغيرة ومجهزة من أناكوندا فيها الاحتياجات الأساسية لتطبيقات بايثون. لماذا نستخدم مينيكوندا؟ لعدة أسباب حيث يستبدل مينيكوندا إصدار بايثون الذي يوفره مدير حزم نظام التشغيل، بإصدار بايثون يُثبته هو في مكان منفصل وله بيئته الخاصة، فنحصل على طبقة عزل إضافية عند استخدامه ضمن حاوية دوكر، ما يوفر لنا مزايا إضافية، فبما أننا نستخدم كوندا المُثبت من قبل مينيكوندا، يمكن استخدام تلك الأداة لتحديد إصدار بايثون الذي يحتاجه التطبيق، ما يفيد المطورين خصوصًا للعمل على عدة نسخ من بايثون معًا على نفس الجهاز، حيث يمكن مثلًا استخدام عدة إصدارات مختلفة من بايثون 3، مثل 3.6 أو 3.7 أو 3.8 أو 3.9 أو إصدارات أقدم منها، ومثلًا لو كان الإصدار الافتراضي الذي نستخدمه هو بايثون 3.9، ونعمل على تطبيق يحتاج لإصدار بايثون 3.7 واعتمادياته الخاصة، يمكن الاستفادة من كوندا لتغيير إصدار بايثون وتثبيت كافة اعتمادياته وذلك بتنفيذ الأمر conda install python=3.7. يسمح مينيكوندا بتثبيت تطبيقات لكل من بايثون 2 و بايثون 3، ومع أن إصدار بايثون 2 قد توقف دعمه لكن لا زال بإمكاننا اختبار التطبيقات القديمة باستخدامه دون الحاجة للتعديل عليها لدعم إصدار بايثون 3 بأدوات مثل 2to3. أحيانًا تعتمد تطبيقات بايثون المُشغلة ضمن مينيكوندا على اعتماديات خارجية بغير لغة بايثون مثبتة على الجهاز المضيف، مثل الاعتماد على g++‎، وهنا تبرز قوة استخدام مينيكوندا ضمن دوكر كحل لتلك المشكلة. يمكن أيضًا إنشاء وتفعيل بيئات تطبيق بايثون مخصصة باستخدام كوندا، وذلك بالاعتماد على العزل. يمكن التبديل بين إصدار بايثون الافتراضي ضمن حاوية دوكر، وبين الإصدار ضمن مينيكوندا في أي وقت، ما يوفر سهولة أكبر في إجراء التعديلات على التطبيق وإعادة بناء الصورة، والآن سنبدأ بإنشاء صورة دوكر جديدة لتطبيق بايثون مع مينيكوندا. المستلزمات أولًا يجب تثبيت دوكر بحسب توزيعة لينكس التي تعمل عليها، ولا ننسى إضافة المستخدم الخاص بنا إلى مجموعة دوكر حتى نتمكن من تنفيذ أوامر دوكر دون استخدام الأمر sudo، ويجب توفر اتصال بالإنترنت لنتمكن من تنزيل صورة دوكر الأساسية التي سنستخدمها. ولاختبار عملية تشغيل التطبيق باستخدام مينيكوندا داخل دوكر، سنستخدم تطبيق بسيط بالاسم python-app.py يطبع عبارة "Hello World!‎"، وبعدها يمكن تبديل ذلك التطبيق بتطبيق بايثون كامل يستخدم العديد من مكتبات بايثون، والاستفادة من بيئة مينيكوندا التي توفر عدة نسخ من الاعتماديات. الخطوة الأولى: جلب صورة دوكر (اختياري) سنستخدم صورة بايثون ذات الوسم slim بدلًا من وسم لينكس apline الخفيفة، لأنها تقدم أداء أفضل لتشغيل التطبيقات، وحجمها تقريبًا 40 ميجابايت مبنية على نسخة ديبيان Buster وإصدار بايثون 3.9.1، هذه الخطوة اختيارية والهدف منها توضيح الفرق بين استخدام نسخة جاهزة أو إنشاء نسخة مخصصة يدويًا باستخدام ملف دوكر Dockerfile، ويمكن تنزيل آخر إصدار من صورة بايثون slim باستخدام أمر docker pull كالتالي: docker pull python:slim الخطوة الثانية: إنشاء ملف دوكر Dockerfile بالتخصيصات المطلوبة ننشئ أولًا مجلد جديد لاحتواء تطبيق دوكر، ثم نُنشئ داخله ملف جديد فارغ ونسميه Dockerfile باستخدام الأمر touch كالتالي: mkdir python-docker cd python-docker touch Dockerfile سنشرح خطوات عملية بناء صورة دوكر خطوة بخطوة، بعدها سنذكر المحتوى الكامل النهائي لملف Dockerfile. تحضير الصورة الأساسية التحديث إلى آخر إصدارات من الحزم الافتراضية باستخدام صورة بايثون slim: FROM python:slim RUN apt-get update && apt-get -y upgrade \ تثبيت حزم الاعتماديات الخارجية نثبت الاعتماديات الخارجية المكتوبة بغير لغة بايثون بحسب ما يحتاجه التطبيق، مثل g++‎: && apt-get install -y --no-install-recommends \ git \ wget \ g++ \ gcc \ ca-certificates \ && rm -rf /var/lib/apt/lists/* يمكن الاستفادة من Git و Wget لجلب تطبيقات بايثون من المستودعات والعناوين المختلفة، أخيرًا يمكننا تقليص حجم صورة دوكر الناتجة عبر مسح قوائم الحزم باستخدام الأمر rm -rf /var/lib/apt/lists/*‎. تثبيت مينيكوندا يُعدل مينيكوندا بعد تثبيته الملف ‎.bashrc ليبدل إصدار بايثون المستخدم إلى الإصدار الخاص به: ENV PATH="/root/miniconda3/bin:${PATH}" ARG PATH="/root/miniconda3/bin:${PATH}" RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ && mkdir /root/.conda \ && bash Miniconda3-latest-Linux-x86_64.sh -b \ && rm -f Miniconda3-latest-Linux-x86_64.sh \ يٌعيّن متغير البيئة ضمن مسار الحاوية داخل النظام، حيث أن التعليمة ENV خاصة بالحاويات التي ستعمل وفقًا للصورة التي يتم بنائها، بينما ARG للاستخدام بين الحاويات الوسيطة التي تُنشئ خلال عملية بناء الصورة لأول مرة، فالفرق بين تعليمتي ENV و ARG في الشيفرة السابقة هو أن التعليمة الأخيرة متاحة خلال عملية بناء الصورة فقط، ونٌنزل باستخدام الأمر wget الإصدار الأخير من مينيكوندا من مستودع أناكوندا الرسمي، بعدها يتم إنشاء مجلد الضبط ثم تثبيته ثم حذف المُثبت. إعداد مينيكوندا مع صدفة باش بعد تثبيت مينيكوندا نعرض رقم الإصدار المثبت لتأكيد نجاح عملية التثبيت، ثم نُهيئه للعمل ضمن صدفة باش داخل الحاوية، السطر الثاني يُحدّث ملف ‎.bashrc الافتراضي: && echo "Running $(conda --version)" && \ conda init bash && \ إعادة تحميل باش لتطبيق التغييرات نعيد تحميل باش داخل دوكر للتبديل من إصدار بايثون الافتراضي المثبت على نظام التشغيل الأساسي المٌستخدم في ديبيان، إلى إصدار بايثون الخاص بمينيكوندا: . /root/.bashrc && \ ونحدث الحزم الافتراضية داخل حزمة مينيكوندا: conda update conda && \ تحضير بيئة كوندا للتطبيق نُنشئ ونُفعل بيئة كوندا منفصلة خاصة بتطبيق بايثون: conda create -n python-app && \ conda activate python-app && \ نثبت إصدار بايثون المطلوب من قبل التطبيق، وذلك بفرض أن التطبيق مطور ليعمل على إصدار بايثون 3.6 نعيّن ذلك الإصدار ضمن البيئة الافتراضية الجديدة، إضافة إلى مدير الحزم Pip ليساعدنا في إدارة تطبيقات بايثون: conda install python=3.6 pip && \ تثبيت تطبيق بايثون بحسب طريقة استخدام التطبيق، يمكن تنفيذ أحد أمرين، الأول تثبيته باستخدام pip والذي بدوره يستدعي الملف setup.py ضمن مستودع التطبيق: git clone replace-me-with-repo-url cd repo-name pip install -e . الثاني تنفيذه مباشرة باستخدام الأمر python: git clone replace-me-with-repo-url cd repo-name python python-app.py لتجربة العملية بشكل كامل وفهم كيف يمكن تشغيل التطبيق مباشرةً عبر تشغيل حاوية أو داخل صدفة باش باستخدام دوكر، سنستخدم التطبيق المثال الذي ذكرناه سابقًا python-app.py، باتباع الطريقة الثانية ننشئ ملف التطبيق python-app.py: echo 'print("Hello World!")' > python-app.py الملف السابق سيطبع عبارة "Hello World!‎" عند تنفيذه باستخدام الأمرpython python-app.py. تحديث ملف ‎.bashrc للتطبيق مثل مينيكوندا كما ذكرنا سابقًا، يُحدث مثبت مينيكوندا ملف ‎.bashrc تلقائيًا بعد تنفيذ الأمر conda init bash، يمكننا تنفيذ أمر مشابه لتطبيقنا، بحيث كلما دخلنا إلى سطر أوامر باش داخل الحاوية سيتم تفعيل البيئة، ويمكن استخدام اسم التطبيق كأمر تشغيل له، سنستخدم الاسم python-app للدلالة على التطبيق: RUN echo 'conda activate python-app \n\ alias python-app="python python-app.py"' >> /root/.bashrc تحضير التطبيق للتنفيذ النهائي أخيرًا نٌنشئ المدخل للتطبيق ونعين له الأمر الذي سيسمح لنا بتشغيله في كل مرة نشغل حاوية مبنية من الصورة: ENTRYPOINT [ "/bin/bash", "-l", "-c" ] CMD ["python python-app.py"] ملف Dockerfile النهائي باستخدام برنامج محرر نصوص مثل Vim أو Nano أو باستخدام الأمر cat نضيف الأسطر التي شرحناها سابقًا إلى ملف Dockerfile: FROM python:slim RUN apt-get update && apt-get -y upgrade \ && apt-get install -y --no-install-recommends \ git \ wget \ g++ \ ca-certificates \ && rm -rf /var/lib/apt/lists/* ENV PATH="/root/miniconda3/bin:${PATH}" ARG PATH="/root/miniconda3/bin:${PATH}" RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \ && mkdir /root/.conda \ && bash Miniconda3-latest-Linux-x86_64.sh -b \ && rm -f Miniconda3-latest-Linux-x86_64.sh \ && echo "Running $(conda --version)" && \ conda init bash && \ . /root/.bashrc && \ conda update conda && \ conda create -n python-app && \ conda activate python-app && \ conda install python=3.6 pip && \ echo 'print("Hello World!")' > python-app.py RUN echo 'conda activate python-app \n\ alias python-app="python python-app.py"' >> /root/.bashrc ENTRYPOINT [ "/bin/bash", "-l", "-c" ] CMD ["python python-app.py"] لتشغيل تطبيق مختلف ضمن الحاوية، نبدل السطر التالي: echo 'print("Hello World!")' > python-app.py بإحدى الطريقتين المذكورتين سابقًا ضمن قسم "تثبيت تطبيق بايثون". الخطوة الثالثة: بناء صورة تطبيق بايثون اعتمادا على ملف Dockerfile أمر بناء صورة معدلة اعتمادًا على ملف Dockerfile هو كالتالي: docker build -t python-app PATH_to_Dockerfile الخيار ‎-t يُمكننا من وسم الصورة الناتجة باسم تطبيقنا، بحيث عينا له القيمة python-app في الأمر السابق، بفرض أن ملف Dockerfile موجود ضمن المجلد الحالي يمكننا إنشاء صورة دوكر جديدة لتطبيق بايثون كالتالي: docker build -t python-app . avimanyu@iborg-desktop:~/python-docker$ docker build -t python-app . Sending build context to Docker daemon 2.56kB Step 1/8 : FROM python:slim ---> 677f7ac99e48 Step 2/8 : RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends git wget g++ ca-certificates && rm -rf /var/lib/apt/lists/* ---> Using cache ---> 15ee9c47c83b Step 3/8 : ENV PATH="/root/miniconda3/bin:${PATH}" ---> Using cache ---> cfd5ed6b5ec9 Step 4/8 : ARG PATH="/root/miniconda3/bin:${PATH}" ---> Using cache ---> e70d06b5ff10 Step 5/8 : RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && mkdir /root/.conda && bash Miniconda3-latest-Linux-x86_64.sh -b && rm -f Miniconda3-latest-Linux-x86_64.sh && echo "Running $(conda --version)" && conda init bash && . /root/.bashrc && conda update conda && conda create -n python-app && conda activate python-app && conda install python=3.6 pip && echo 'print("Hello World!")' > python-app.py ---> Using cache ---> 8a7957a6abb2 Step 6/8 : RUN echo 'conda activate python-app \nalias python-app="python python-app.py"' >> /root/.bashrc ---> Running in e3193e93b631 Removing intermediate container e3193e93b631 ---> 948f45eb6024 Step 7/8 : ENTRYPOINT [ "/bin/bash", "-l", "-c" ] ---> Running in 621624951dcf Removing intermediate container 621624951dcf ---> 6e8824889502 Step 8/8 : CMD ["python python-app.py"] ---> Running in dc97f9d0d8fe Removing intermediate container dc97f9d0d8fe ---> 01bae0a9903c Successfully built 01bae0a9903c Successfully tagged python-app:latest يعتمد خرج الأمر السابق على البيانات الموجودة ضمن التخزين المؤقت، بحيث سيأخذ مدة أطول عند تنفيذه لأول مرة وسينتج خرج أطول، يمكننا التحقق فيما إذا كانت صورة دوكر الناتجة تحوي التطبيق التجريبي الذي ثبتناه عبر تشغيل حاوية من تلك الصورة: docker run python-app avimanyu@iborg-desktop:~/python-docker$ docker run python-app Hello World! باستخدام دوكر ومينيكوندا يمكننا الآن تشغيل التطبيق مباشرة بدون أي عملية تثبيت أو تهيئة مسبقة للبيئة، حيث كل ما نريده هو تلك الصورة الناتجة، ويمكننا الآن تسجيل الدخول إلى صدفة باش داخل الحاوية كالتالي: docker run -ti python-app bash avimanyu@iborg-desktop:~/python-docker$ docker run -ti python-app bash (python-app) root@2ceec4c9eaa4:/# وكما نلاحظ دخلنا إلى بيئة كوندا المفعلة التي أنشأناها سابقًا عبر ملف Dockerfile، استخدمنا الخيار ‎-ti لإنشاء طرفية تفاعلية لاستخدامها، يمكننا أيضًا استخدام الاسم بديل الذي عيناه للتطبيق سابقًا لتشغيله كالتالي: (python-app) root@2ceec4c9eaa4:/# python-app Hello World! لنتحقق أيضًا من استخدامنا لإصدار بايثون الخاص بمينيكوندا وليس الإصدار الافتراضي الخاص بنظام التشغيل: (python-app) root@2ceec4c9eaa4:/# python --version Python 3.6.12 :: Anaconda, Inc. كما ذكرنا سابقًا مينيكوندا هو نسخة مصغرة من أناكوندا، وبعد الانتهاء من التحضير بشكل كامل يمكننا دفع الصورة النهائية للتطبيق إلى Docker Hub في حال كان التطبيق نسخة مفتوحة المصدر ومُستضافة على أحد استضافات المستودعات مثل GitHub أو GitLab أو Gitea أو Bitbucket أو أي مستودع آخر. يمكن الخروج من الحاوية بتنفيذ الأمر exit ضمن الطرفية، لتوفير المساحة المستخدمة يمكننا إيقاف الحاوية وحذفها وحذف صور دوكر التي تم بناؤها خلال العملية. ختامًا تعلمنا في هذا المقال كيف يمكن إنشاء صورة دوكر مخصصة لتشغيل تطبيق بايثون، يزيد مينيكوندا من مرونة تطبيقات بايثون ويُسهل عملية تحديثها مستقبلًا، كما يسهل عمل المطور كثيرًا، حيث يمكن بنفس الطريقة إعداده مع PyCharm. قمنا بتثبيت مينيكوندا وتطبيق بايثون الذي نعمل عليه كما هي العادة، الفرق أن عملية بناء وحفظ الصورة يتم لمرة واحدة فقط، وبعدها يصبح الأمر عبارة عن عملية واحدة وهي تشغيل حاوية التطبيق فقط. ترجمة -وبتصرف- للمقال How to Dockerize Python Applications With Miniconda [A Hybrid Approach] لصاحبه Avimanyu Bandyopadhyay. اقرأ أيضًا مدخل إلى لغة بايثون البرمجية مدخل إلى دوكر Docker كيفية تثبيت دوكر واستخدامه على دبيان مقدّمة عن المُكوّنات المُشترَكة في Docker
  3. تحتاج معظم التطبيقات حاليًا إلى التواصل مع بعض الخوادم لجلب البيانات منها أو لإتمام بعض المهام، فمثلًا في تطبيق ويب لشراء الكتب سيحتاج للتواصل مع خادم إدارة طلبات الزبائن وخادم مستودع الكتب وخادم إتمام الدفع، حيث تتواصل تلك الخدمات مع بعضها عن طريق الويب عبر الواجهات البرمجية API وتتبادل البيانات برمجيًا. توفر نود دعمًا للتواصل عن طريق طلبات HTTP مع واجهات API عبر الويب، فتتيح الوحدة البرمجية ‎http‎ والوحدة ‎https‎، حيث تحتوي كل منهما على التوابع اللازمة لإنشاء خادم HTTP لمعالجة الطلبات الواردة إلى الخادم، وتوابع لإنشاء طلبات HTTP وإرسالها إلى الخوادم الأخرى، حيث تسمح هاتين الميزتين بتطوير تطبيقات ويب حديثة تعتمد على الواجهات البرمجية API للتواصل بينها، ولا حاجة لتثبيت أي وحدة برمجية خارجية حيث تأتي تلك الوحدات جاهزة مع نود افتراضيًا. سنتعلم في هذا المقال كيف يمكننا الاستفادة من الوحدة ‎https‎ لإرسال طلبات HTTP بمثال عن التعامل مع خادم JSON Placeholder وهو واجهة برمجية API وهمية تستخدم في عمليات التدريب والاختبار، حيث سنتعلم طريقة إرسال طلب HTTP لطلب البيانات من نوع ‎GET‎، ثم سنتعرف على طرق تخصيص الطلب المرسل كإضافة الترويسات، وسنتعرف أيضًا على الطلبات بمختلف أنواعها مثل ‎POST‎ و ‎PUT‎ و ‎DELETE‎ والتي تستخدم لتعديل البيانات على الخوادم الأخرى. المستلزمات نسخة نود مثبتة على جهازك، حيث استخدمنا في هذا المقال الإصدار رقم 10.19.0. تعتمد توابع إرسال طلبات HTTP على واجهة مجرى البيانات stream في نود وهي بدورها نسخة من كائن مرسل الأحداث event emitter، لذا يمكن التعامل مع البيانات الواردة منها بنفس طريقة تعاملنا مع الأحداث المرسلة، وللتعرف على مرسل الأحداث وطريقة التعامل معه يمكنك مراجعة المقال التاسع من هذه السلسلة. إرسال طلب من نوع ‎GET‎ إن أردنا طلب بيانات من خادم ويب ما عبر واجهته البرمجية API نرسل إليه عادة طلب HTTP من نوع ‎GET‎، ففي هذه الفقرة سنتعلم طريقة إرسال تلك الطلبات في نود وتحديدًا لجلب مصفوفة بيانات بصيغة JSON تحتوي على بيانات حسابات شخصية لمستخدمين وهميين من واجهة برمجية API متاحة للعموم، حيث تحوي الوحدة البرمجية ‎https‎ على تابعين يمكن استخدامهما لإرسال طلبات من نوع ‎GET‎ هما التابع ‎get()‎، والتابع ‎request()‎ الذي يمكن استخدامه لإرسال طلبات من أنواع متعددة أخرى كما سنتعلم لاحقًا، ولنبدأ أولًا بالتعرف على التابع ‎get()‎. إرسال الطلبات باستخدام التابع ‎get()‎ صيغة استخدام التابع ‎get()‎ تكون كالتالي: https.get(URL_String, Callback_Function) { Action } حيث نمرر له سلسلة نصية كمعامل أول تحوي المسار الذي سنرسل الطلب إليه، والمعامل الثاني يكون دالة رد النداء callback function لمعالجة نتيجة الطلب. سنبدأ بإنشاء مجلد جديد للمشروع سيحوي الأمثلة التي سنكتبها ضمن هذا الفصل كالتالي: $ mkdir requests وندخل إلى المجلد: $ cd requests ننشئ ملف جافاسكربت جديد ونفتحه باستخدام أي محرر نصوص حيث سنستخدم في أمثلتنا محرر نانو ‎nano‎ كالتالي: $ nano getRequestWithGet.js ونبدأ باستيراد الوحدة ‎https‎ كالتالي: const https = require('https'); ملاحظة: يوجد في نود وحدتين برمجيتين هما ‎http‎ و ‎https‎ تحويان على نفس التوابع التي تعمل بنفس الطريقة، والفرق بينها أن التوابع ضمن ‎https‎ ترسل الطلبات عبر طبقة أمان النقل Transport Layer Security أو TLS/SSL، وسنرسل الطلبات في أمثلتنا عبر HTTPS لذا سنستخدم تلك التوابع من الوحدة ‎https‎، بينما لو كنا سنرسل الطلبات عبر HTTP فيجب استخدام توابع الوحدة ‎http‎ بدلًا منها. نبدأ بكتابة شيفرة إرسال طلب ‎GET‎ إلى الواجهة البرمجية API لجلب بيانات المستخدمين، حيث سنرسل طلبًا إلى واجهة JSON Placeholder وهي واجهة برمجية متاحة للاستخدام العام لأغراض الاختبار، ولا تتأثر البيانات على ذلك الخادم بالطلبات المرسلة فمهمته فقط محاكاة عمل خادم حقيقي، حيث سيرجع لنا دومًا بيانات وهمية طالما أن الطلب المرسل إليه سليم، لنبدأ بكتابة الشيفرة التالية: const https = require('https'); let request = https.get('https://jsonplaceholder.typicode.com/users?_limit=2', (res) => { }); كما ذكرنا سابقًا يقبل التابع ‎get()‎ معاملين وهما المسار الوجهة للطلب URL كسلسلة نصية للواجهة البرمجية، ودالة رد نداء لمعالجة نتيجة طلب HTTP الواردة، حيث يمكن استخراج البيانات من الرد ضمن دالة رد النداء. يحمل طلب HTTP على رمز الحالة status code وهو عدد يشير إلى نجاح الطلب من عدمه، فمثلًا إذا كانت قيمة الرمز بين 200 و 299 فالطلب ناجح، أما إذا كان بين 400 و 599 فهناك خطأ ما، وفي مثالنا الرد من الواجهة البرمجية يجب أن يحتوي على رمز الحالة 200 إن نجح وهو أول ما سنتحقق منه ضمن تابع رد النداء لمعالجة الطلب كالتالي: const https = require('https'); let request = https.get('https://jsonplaceholder.typicode.com/users?_limit=2', (res) => { if (res.statusCode !== 200) { console.error(`Did not get an OK from the server. Code: ${res.statusCode}`); res.resume(); return; } }); يحوي كائن الرد res المُمرر لدالة رد النداء على الخاصية ‎statusCode‎ والتي تمثِّل قيمة رمز الحالة، وإذا لم تكن قيمته تساوي 200 سنطبع رسالة خطأ إلى الطرفية ونخرج مباشرةً. نلاحظ استدعاء التابع ‎res.resume()‎ من كائن الرد وهي طريقة لتحسين أداء البرنامج فعند إرسال طلبات HTTP في نود يتم عادة معالجة البيانات المرسلة ضمن الطلب كاملةً، أما عند استدعائنا للتابع ‎res.resume()‎ فإننا نخبر نود بتجاهل البيانات ضمن مجرى كائن الرد، وهي طريقة أسرع من لو تُركت تلك البيانات لالتقاطها في مرحلة كنس المهملات garbage collection التي تتم دوريًا لتفريغ الذاكرة المستخدمة من قبل التطبيق. والآن بعد أن تحققنا من رمز الحالة للرد سنبدأ بقراءة البيانات الواردة حيث تتوفر البيانات ضمن كائن مجرى الرد على دفعات، ويمكننا قراءتها بالاستماع إلى الحدث ‎data‎ من كائن الرد ثم تجميعها معًا ثم تحليلها بصيغة JSON لنتمكن من استخدامها ضمن التطبيق، لذلك سنضيف الشيفرة التالية ضمن تابع رد النداء: const https = require('https'); let request = https.get('https://jsonplaceholder.typicode.com/users?_limit=2', (res) => { if (res.statusCode !== 200) { console.error(`Did not get an OK from the server. Code: ${res.statusCode}`); res.resume(); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Retrieved all data'); console.log(JSON.parse(data)); }); }); عرفنا متغيرًا جديدًا بالاسم ‎data‎ والذي يحتوي على سلسلة نصية فارغة، حيث يمكننا تجميع البيانات الواردة إما على شكل مصفوفة من الأعداد تمثل البيانات للبايتات المكونة لها، أو على شكل سلسلة نصية وهو ما سنستخدمه في مثالنا لسهولة تحويل السلسلة النصية الناتجة عن عملية التجميع إلى كائن جافاسكربت. نضيف بعد ذلك تابع الاستماع للبيانات الواردة على دفعات من الحدث ‎data‎ ونجمع البيانات كلها ضمن المتغير السابق ‎data‎، ويمكننا التأكد من انتهاء دفعات البيانات الواردة عند إطلاق حدث الإغلاق ‎close‎ من كائن الرد، وبعدها يمكننا تحويل السلسلة النصية بصيغة JSON ضمن المتغير ‎data‎ وطباعة القيمة النهائية إلى الطرفية، وبذلك نكون قد أكملنا كتابة عملية إرسال طلب إلى واجهة برمجية ستُرسل بدورها مصفوفة من بيانات حسابات شخصية لثلاثة مستخدمين بصيغة JSON ونقرأ الرد الوارد. بقي لدينا إضافة معالجة لحالة رمي خطأ في حال لم نتمكن من إرسال الطلب لسبب ما، كحالة عدم وجود اتصال بالإنترنت كالتالي: ... res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Retrieved all data'); console.log(JSON.parse(data)); }); }); request.on('error', (err) => { console.error(`‎Encountered an error trying to make a request: ${err.message}‎`); }); عند حدوث خطأ في عملية الإرسال سنتلقى الحدث ‎error‎ من كائن الطلب، وإذا لم نستمع لهذا الحدث فسيرمى الخطأ الناتج ما يؤدي لإيقاف عمل البرنامج، لذلك نضيف دالة استماع للحدث ‎error‎ على كائن الطلب باستخدام التابع ‎on()‎ والذي سيطبع رسالة الخطأ الوارد إلى الطرفية، وبذلك نكون قد انتهينا من كتابة البرنامج. والآن نحفظ الملف ونخرج منه وننفذه باستخدام الأمر ‎node‎ كالتالي: $ node getRequestWithGet.js نحصل على الخرج التالي الذي يمثل الرد الوارد على الطلب المُرسل: Retrieved all data [ { id: 1, name: 'Leanne Graham', username: 'Bret', email: 'Sincere@april.biz', address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: [Object] }, phone: '1-770-736-8031 x56442', website: 'hildegard.org', company: { name: 'Romaguera-Crona', catchPhrase: 'Multi-layered client-server neural-net', bs: 'harness real-time e-markets' } }, { id: 2, name: 'Ervin Howell', username: 'Antonette', email: 'Shanna@melissa.tv', address: { street: 'Victor Plains', suite: 'Suite 879', city: 'Wisokyburgh', zipcode: '90566-7771', geo: [Object] }, phone: '010-692-6593 x09125', website: 'anastasia.net', company: { name: 'Deckow-Crist', catchPhrase: 'Proactive didactic contingency', bs: 'synergize scalable supply-chains' } } ] بذلك نكون قد أرسلنا طلبًا من نوع ‎GET‎ بنجاح باستخدام مكتبات نود فقط، حيث أن التابع الذي استخدمناه ‎get()‎ يوجد في نود بسبب كثرة الحاجة لإرسال الطلبات من نوع ‎GET‎، بينما الطريقة الأساسية لإرسال الطلبات هي باستخدام التابع ‎request()‎ والذي يمكنه إرسال أي نوع من الطلبات، وهو ما سنتعرف عليه في القسم التالي حيث سنستخدمه لإرسال طلب من نوع ‎GET‎. إرسال الطلبات باستخدام التابع ‎request()‎ يمكن استخدام التابع ‎request()‎ بعدة صيغ والصيغة التي سنستخدمها في أمثلتنا هي كالتالي: https.request(URL_String, Options_Object, Callback_Function) { Action } حيث نمرر له سلسلة نصية كمعامل أول تحتوي على مسار الواجهة البرمجية API الذي سنرسل الطلب إليه، والمعامل الثاني هو كائن جافاسكربت يحتوي على عدة خيارات للطلب المرسل، والمعامل الأخير المُمرر هو دالة رد النداء callback لمعالجة نتيجة الطلب. نبدأ بإنشاء ملف جافاسكربت جديد بالاسم ‎getRequestWithRequest.js‎: $ nano getRequestWithRequest.js سنكتب برنامجًا مشابهًا لما كتبناه في القسم السابق ضمن الملف ‎getRequestWithGet.js‎، حيث نبدأ باستيراد الوحدة ‎https‎ كالتالي: const https = require('https'); ثم نعرف كائن جافاسكربت يحتوي على الخاصية ‎method‎ كالتالي: const https = require('https'); const options = { method: 'GET' }; تعبر الخاصية ‎method‎ ضمن كائن خيارات التابع ‎request()‎ عن نوع الطلب الذي نريد إرساله، والآن نُرسل الطلب كما فعلنا سابقًا لكن مع بعض الاختلافات كالتالي: ... let request = https.request('https://jsonplaceholder.typicode.com/users?_limit=2', options, (res) => { if (res.statusCode !== 200) { console.error(`Did not get an OK from the server. Code: ${res.statusCode}`); res.resume(); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Retrieved all data'); console.log(JSON.parse(data)); }); }); request.end(); request.on('error', (err) => { console.error(`Encountered an error trying to make a request: ${err.message}`); }); مررنا مسار الوجهة للطلب كمعامل أول للتابع ‎request()‎ ثم كائن خيارات HTTP كمعامل ثاني، وبعدها دالة رد النداء لمعالجة الرد، حيث حددنا نوع الطلب المرسل كطلب ‎GET‎ ضمن كائن الخيارات ‎options‎ الذي عرفناه سابقًا، وبقي دالة رد النداء لمعالجة الطلب كما هو في المثال السابق، وأضفنا استدعاءً للتابع ‎end()‎ من كائن الطلب ‎request‎، حيث يجب استدعاء هذا التابع عند إرسال الطلبات باستخدام ‎request()‎ لإتمام الطلب وإرساله، وفي حال لم نستدعيه فلن يُرسل الطلب ويبقى نود ينتظر منا إضافة بيانات جديدة إلى الطلب. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج: $ node getRequestWithRequest.js ليظهر الخرج التالي كما المثال السابق تمامًا: Retrieved all data [ { id: 1, name: 'Leanne Graham', username: 'Bret', email: 'Sincere@april.biz', address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: [Object] }, phone: '1-770-736-8031 x56442', website: 'hildegard.org', company: { name: 'Romaguera-Crona', catchPhrase: 'Multi-layered client-server neural-net', bs: 'harness real-time e-markets' } }, { id: 2, name: 'Ervin Howell', username: 'Antonette', email: 'Shanna@melissa.tv', address: { street: 'Victor Plains', suite: 'Suite 879', city: 'Wisokyburgh', zipcode: '90566-7771', geo: [Object] }, phone: '010-692-6593 x09125', website: 'anastasia.net', company: { name: 'Deckow-Crist', catchPhrase: 'Proactive didactic contingency', bs: 'synergize scalable supply-chains' } } ] وبذلك نكون قد تعرفنا على طريقة استخدام التابع ‎request()‎ لإرسال الطلبات من نوع ‎GET‎ وهو تابع أقوى من التابع السابق ‎get()‎ حيث يسمح بتخصيصات عدة على الطلب المرسل كتحديد نوعه وخيارات أخرى سنتعرف عليها في الفقرة التالية. تخصيص خيارات HTTP للتابع ‎request()‎ يمكن استخدام التابع ‎request()‎ لإرسال طلبات HTTP دون تمرير عنوان مسار الوجهة للطلب كمعامل أول بل بتمريره ضمن كائن الخيارات ‎options‎ لتصبح صيغة استدعاء التابع كالتالي: https.request(Options_Object, Callback_Function) { Action } في هذه الفقرة سنستخدم هذه الصيغة للتركيز على إعداد وتخصيص خيارات التابع ‎request()‎، لذا نعود إلى ملف المثال السابق ‎getRequestWithRequest.js‎ ونعدله بأن نزيل المسار URL المُمرر للتابع ‎request()‎ لتصبح المعاملات المُمررة له هي كائن الخيارات ‎options‎ ودالة رد النداء فقط كالتالي: const https = require('https'); const options = { method: 'GET', }; let request = https.request(options, (res) => { ... لنضيف الخيارات الجديدة إلى الكائن ‎options‎ كالتالي: const https = require('https'); const options = { host: 'jsonplaceholder.typicode.com', path: '/users?_limit=2', method: 'GET' }; let request = https.request(options, (res) => { ... نلاحظ أنه وبدلًا من تمرير المسار كاملًا فإننا نمرره على قسمين ضمن الخاصيتين ‎host‎ و ‎path‎ حيث تُعبّر الخاصية ‎host‎ عن عنوان النطاق أو عنوان IP للخادم الوجهة، أما الخاصية ‎path‎ فهي كل ما يلي بعد ذلك ضمن المسار بما فيها معاملات الاستعلام query parameters التي تأتي بعد إشارة الاستفهام. ويمكن أن تحتوي الخيارات على بيانات مفيدة أخرى للطلب المرسل مثل الترويسات المرسلة وهي بيانات وصفية عن الطلب نفسه، فمثلًا عادة تتطلب الواجهة البرمجية API تحديد صيغة البيانات المرسلة من عدة صيغ مدعومة مثل JSON أو CSV أو XML، ولتحديد الصيغة التي يطلبها المستخدم يمكن للواجهة البرمجية معاينة قيمة الترويسة ‎Accept‎ ضمن الطلب الوارد إليها وتحدد على أساسه الصيغة المناسبة لإرسالها. تُعبّر الترويسة ‎Accept‎ عن نوع البيانات التي يمكن للمستخدم التعامل معها، وبما أننا نتعامل مع واجهة برمجية تدعم صيغة JSON فقط، فيمكننا إضافة الترويسة ‎Accept‎ وإضافة قيمة لها توضح أننا نريد البيانات بصيغة JSON كالتالي: const https = require('https'); const options = { host: 'jsonplaceholder.typicode.com', path: '/users?_limit=2', method: 'GET', headers: { 'Accept': 'application/json' } }; وبذلك نكون قد تعرفنا على أكثر أربعة خيارات استخدامًا ضمن طلبات HTTP وهي عنوان المضيف ‎host‎ و المسار ‎path‎ ونوع الطلب ‎method‎ والترويسات ‎headers‎، ويوجد العديد من الخيارات الأخرى المدعومة يمكنك الرجوع إلى التوثيق الرسمي لها ضمن نود للتعرف عليها. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج لنختبر طريقة إرسال الطلبات بتمرير كائن الخيارات فقط: $ node getRequestWithRequest.js ليظهر لنا بيانات الرد مطابقة للأمثلة السابقة: Retrieved all data [ { id: 1, name: 'Leanne Graham', username: 'Bret', email: 'Sincere@april.biz', address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: [Object] }, phone: '1-770-736-8031 x56442', website: 'hildegard.org', company: { name: 'Romaguera-Crona', catchPhrase: 'Multi-layered client-server neural-net', bs: 'harness real-time e-markets' } }, { id: 2, name: 'Ervin Howell', username: 'Antonette', email: 'Shanna@melissa.tv', address: { street: 'Victor Plains', suite: 'Suite 879', city: 'Wisokyburgh', zipcode: '90566-7771', geo: [Object] }, phone: '010-692-6593 x09125', website: 'anastasia.net', company: { name: 'Deckow-Crist', catchPhrase: 'Proactive didactic contingency', bs: 'synergize scalable supply-chains' } } ] تختلف متطلبات الواجهات البرمجية API بحسب الجهة المطورة لها، لذا من الضروري التعامل مع كائن الخيارات ‎options‎ لتخصيص الطلب بحسب حاجة التطبيق والخادم، من تحديد نوع البيانات المطلوبة وإضافة الترويسات المناسبة وبعض التخصيصات الأخرى. أرسلنا ضمن كل الأمثلة السابقة طلبات فقط من نوع ‎GET‎ لجلب البيانات، وفي الفقرة التالية سنتعلم طريقة إرسال الطلبات من نوع ‎POST‎ والتي تستخدم لرفع البيانات إلى الخادم. إرسال طلب من نوع ‎POST‎ نستخدم الطلبات من نوع ‎POST‎ لرفع البيانات إلى الخادم أو لطلب إنشاء بيانات جديدة من قبل الخادم، وفي هذه الفقرة سنتعرف على طريقة إرسال مثل هذه الطلبات في نود عبر إرسال طلب إنشاء مستخدم جديد إلى المسار ‎users‎ على الواجهة البرمجية API. يمكننا إعادة استخدام بعض الشيفرات من مثال إرسال طلب من نوع ‎GET‎ السابق لإرسال طلبات من نوع ‎POST‎ مع إجراء بعض التعديلات عليها: تعديل نوع الطلب ضمن كائن الخيارات ‎options‎ ليصبح ‎POST‎. تعيين ترويسة نوع المحتوى المُرسل وهو في حالتنا بصيغة JSON. التأكد من رمز الحالة للرد لتأكيد نجاح إنشاء مستخدم جديد. رفع بيانات المستخدم الجديد. نبدأ بإنشاء ملف جافاسكربت جديد بالاسم ‎postRequest.js‎ ونفتحه ضمن محرر النصوص: $ nano postRequest.js ونبدأ كما سابقًا باستيراد الوحدة ‎https‎ وتعريف كائن الخيارات ‎options‎ كالتالي: const https = require('https'); const options = { host: 'jsonplaceholder.typicode.com', path: '/users', method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=UTF-8' } }; ونعدل خيار مسار الطلب ‎path‎ ليتوافق مع مسار إرسال الطلبات من نوع ‎POST‎ على الخادم، ونعدل خيار نوع الطلب المرسل ‎method‎ إلى القيمة ‎POST‎، وأخيرًا نضيف الترويسة ‎Content-Type‎ ضمن الخيارات والتي تدل الخادم على نوع البيانات التي أرسلناها مع الطلب وهي في حالتنا بصيغة JSON وبترميز من نوع UTF-8، ثم نُرسل الطلب باستدعاء التابع ‎request()‎ كما فعلنا تمامًا عند إرسال طلب من نوع ‎GET‎ سابقًا ولكن هذه المرة سنتحقق من رمز الحالة للرد بقيمة تختلف عن 200 كالتالي: ... const request = https.request(options, (res) => { if (res.statusCode !== 201) { console.error(`‎Did not get a Created from the server. Code: ${res.statusCode}‎`); res.resume(); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Added new user'); console.log(JSON.parse(data)); }); }); تحققنا من صحة العملية بالتحقق من قيمة رمز الحالة بأن يساوي 201، وهو الرمز الذي يدل على إنشاء مورد جديد على الخادم بنجاح، ويُعبّر الطلب المرسل عن إنشاء مستخدم جديد لهذا سنحتاج لرفع بيانات هذا المستخدم وإرفاقها ضمن الطلب، لذا سننشئ تلك البيانات كالتالي: ... const requestData = { name: 'New User', username: 'hsoub', email: 'user@hsoub.com', address: { street: 'North Pole', city: 'Murmansk', zipcode: '12345-6789', }, phone: '555-1212', website: 'hsoub.com', company: { name: 'Hsoub', catchPhrase: 'Welcome to hsoub academy', bs: 'cloud scale security' } }; request.write(JSON.stringify(requestData)); عرفنا بيانات المستخدم الجديد ضمن المتغير ‎requestData‎ على شكل كائن جافاسكربت يحتوي على بيانات المستخدم، ونلاحظ أننا لم نرفق قيمة المعرف ‎id‎ للمستخدم حيث أن هذه القيمة يولدها الخادم تلقائيًا للبيانات الجديدة، ثم استدعينا التابع ‎request.write()‎ والذي يقبل سلسلة نصية أو كائن مخزن مؤقت buffer ليتم إرسالها ضمن الطلب، وبما أن البيانات لدينا ضمن المتغير ‎requestData‎ هي كائن جافاسكربت فيجب تحويله إلى سلسلة نصية باستخدام ‎JSON.stringify‎. ولإنهاء عملية الإرسال ننهي الطلب عبر استدعاء request.end()‎ ونتحقق من حدوث أي أخطاء في عملية الإرسال كالتالي: ... request.end(); request.on('error', (err) => { console.error(`‎Encountered an error trying to make a request: ${err.message}‎`); }); من الضروري استدعاء التابع ‎end()‎ لإنهاء الطلب وللإشارة إلى نود بأن كل البيانات التي نريد إرسالها ضمن الطلب قد أرفقت وأصبح بالإمكان إرساله. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج ونتأكد من عملية إنشاء المستخدم الجديد: $ node postRequest.js سنحصل على الخرج التالي: Added new user { name: 'New User', username: 'hsoub', email: 'user@hsoub.com', address: { street: 'North Pole', city: 'Murmansk', zipcode: '12345-6789' }, phone: '555-1212', website: 'hsoub.com', company: { name: 'Hsoub', catchPhrase: 'Welcome to the hsoub academy', bs: 'cloud scale security' }, id: 11 } ما يعني أن الطلب تم بنجاح، حيث أعاد الخادم بيانات المستخدم التي أرسلناها مضافًا إليها قيمة معرّف المستخدم ID التي تم توليدها له، وبذلك نكون قد تعلمنا طريقة إرسال الطلبات من نوع ‎POST‎ لرفع البيانات إلى الخادم باستخدام نود، وفي الفقرة التالية سنتعلم طريقة إرسال الطلبات من نوع ‎PUT‎ للتعديل على بيانات موجودة مسبقًا. إرسال طلب من نوع ‎PUT‎ تُستخدم الطلبات من نوع ‎PUT‎ لرفع البيانات إلى الخادم بشكل مشابه للطلبات من نوع ‎POST‎، ولكن الفرق أنه عند تنفيذ طلب من نوع ‎PUT‎ عدة مرات سنحصل على نفس النتيجة، بينما عند تكرار نفس طلب ‎POST‎ عدة مرات سنضيف بذلك البيانات المرسلة أكثر من مرة إلى الخادم، وطريقة إرسال هذا الطلب مشابهة للطلب من نوع ‎POST‎ حيث نعرف الخيارات ونُنشئ الطلب ونكتب البيانات التي نريد رفعها إلى الطلب ثم نتحقق من الرد الوارد في نتيجة الطلب. نختبر ذلك بإنشاء طلب من نوع ‎PUT‎ لتعديل اسم المستخدم لأول مستخدم، وبما أن طريقة إرسال الطلب مشابهة لطريقة إرسال الطلب من نوع ‎POST‎ يمكننا الاستفادة من المثال السابق ونسخ الملف ‎postRequest.js‎ إلى ملف جديد بالاسم ‎putRequest.js‎ كالتالي: $ cp postRequest.js putRequest.js نفتح الملف ‎putRequest.js‎ ضمن محرر النصوص: $ nano putRequest.js ونعدل نوع الطلب إلى ‎PUT‎ ومساره إلى ‎https://jsonplaceholder.typicode.com/users/1‎ والبيانات المرسلة كالتالي: const https = require('https'); const options = { host: 'jsonplaceholder.typicode.com', path: '/users/1', method: 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=UTF-8' } }; const request = https.request(options, (res) => { if (res.statusCode !== 200) { console.error(`‎Did not get an OK from the server. Code: ${res.statusCode}‎`); res.resume(); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Updated data'); console.log(JSON.parse(data)); }); }); const requestData = { username: 'hsoub' }; request.write(JSON.stringify(requestData)); request.end(); request.on('error', (err) => { console.error(`‎Encountered an error trying to make a request: ${err.message}‎`); }); نلاحظ تعديل قيم المسار ‎path‎ ونوع الطلب ‎method‎ ضمن كائن الخصائص ‎options‎ حيث يحوي المسار على معرّف المستخدم الذي نود تعديل بياناته، ثم نتحقق من من رمز الحالة للطلب بأن يكون بالقيمة 200 ما يدل على نجاح الطلب، ونلاحظ أن البيانات التي أرسلناها تحوي فقط على الخصائص التي نريد تحديثها من بيانات المستخدم. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج: $ node putRequest.js ليظهر الخرج التالي: Updated data { username: 'hsoub', id: 1 } أرسلنا بنجاح طلب من نوع ‎PUT‎ لتعديل بيانات مستخدم موجودة مسبقًا على الخادم، وبذلك نكون قد تعلمنا طرق طلب البيانات ورفعها وتحديثها، وسنتعلم في الفقرة التالية كيف يمكن حذف البيانات من الخادم بإرسال طلب من النوع ‎DELETE‎. إرسال طلب من نوع ‎DELETE‎ تستخدم الطلبات من نوع ‎DELETE‎ لحذف البيانات من الخادم، ويمكن أن يحتوي الطلب على بيانات مرفقة ضمنه ولكن معظم الواجهات البرمجية API لا تتطلب ذلك، حيث يستخدم هذا النوع من الطلبات لحذف بيانات كائن ما كليًا من الخادم. سنرسل في هذه الفقرة طلب من هذا النوع لحذف بيانات أحد المستخدمين، وطريقة إرسال هذا الطلب مشابهة لطريقة إرسال طلب من نوع ‎GET‎، لذا يمكننا نسخ ملف المثال السابق ‎getRequestWithRequest.js‎ إلى ملف جديد بالاسم ‎deleteRequest.js‎ كالتالي: $ cp getRequestWithRequest.js deleteRequest.js ونفتح الملف الجديد ضمن محرر النصوص: $ nano deleteRequest.js ونعدل الشيفرة لإرسال طلب حذف لأول مستخدم كالتالي: const https = require('https'); const options = { host: 'jsonplaceholder.typicode.com', path: '/users/1', method: 'DELETE', headers: { 'Accept': 'application/json', } }; const request = https.request(options, (res) => { if (res.statusCode !== 200) { console.error(`‎Did not get an OK from the server. Code: ${res.statusCode}‎`); res.resume(); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('close', () => { console.log('Deleted user'); console.log(JSON.parse(data)); }); }); request.end(); request.on('error', (err) => { console.error(`‎Encountered an error trying to make a request: ${err.message}‎`); }); عدلنا قيمة المسار ‎path‎ ضمن كائن خيارات الطلب ليحوي معرّف المستخدم الذي نريد حذفه، وعدلنا نوع الطلب إلى ‎DELETE‎، والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج كالتالي: $ node deleteRequest.js لنحصل على الخرج: Deleted user {} لا تعيد الواجهة البرمجية أي بيانات ضمن جسم الرد الوارد، ولكن رمز الحالة لهذا الرد يكون 200 أي تم حذف بيانات المستخدم بنجاح، وبذلك نكون قد تعرفنا على طريقة إرسال طلبات من نوع ‎DELETE‎ أيضًا في نود. خاتمة تعرفنا في هذا الفصل على طريقة إرسال الطلبات في نود بأنواعها مختلفة مثل ‎GET‎ و ‎POST‎ و ‎PUT‎ و ‎DELETE‎ دون استخدام أي مكتبات خارجية وفقط باستخدام الوحدة البرمجية ‎https‎ التي يوفرها نود، وتعرفنا على طريقة خاصة لإرسال الطلبات من نوع ‎GET‎ باستخدام التابع ‎get()‎ وكيف أن باقي الطلبات يمكن إرسالها باستخدام التابع ‎request()‎، وتعاملنا ضمن الأمثلة مع واجهة برمجية API عامة ويمكن بنفس الطريقة إرسال الطلبات إلى مختلف أنواع الواجهات البرمجية. ترجمة -وبتصرف- للمقال How To Create an HTTP Client with Core HTTP in Node.js. اقرأ أيضًا المقال السابق: التعامل مع الملفات باستخدام الوحدة fs في Node.js مدخل إلى خادم الويب مدخل إلى HTTP: شرح التخاطب بين العميل والخادم رموز الإجابة في HTTP
  4. يرجى إرفاق الجزء المتعلق بالاستفسار من الشيفرة مباشرة ضمن نص السؤال بدلًا من إرفاق ملفات المشروع، وذلك حتى تحصل على إجابة وحل لمشكلتك ممن لديه الخبرة
  5. كثيرًا ما نحتاج للتعامل مع نظام الملفات، فمثلًا لتخزين بعض الملفات بعد تنزيلها، أو لترتيب بعض البيانات ضمن مجلدات أو لقراءة الملفات للتعامل مع محتوياتها ضمن بعض التطبيقات، وحتى تطبيقات النظم الخلفية أو أدوات واجهة سطر الأوامر CLI قد تحتاج أحيانًا لحفظ بعض البيانات إلى ملفات، والتطبيقات التي تتعامل مع البيانات تحتاج أحيانًا لتصديرها بمختلف الصيغ مثل JSON أو CSV أو ملفات برنامج إكسل، فكل تلك المتطلبات تحتاج للتعامل مع نظام الملفات ضمن نظام التشغيل التي تعمل عليه. توفر نود طريقة برمجية للتعامل مع الملفات باستخدام الوحدة البرمجية ‎fs‎، وهي اختصار لجملة "نظام الملفات" أو "file system" حيث تحتوي على العديد من التوابع التي نحتاجها لقراءة الملفات أو الكتابة إليها أو حذفها، وهذه المزايا تجعل من لغة جافاسكربت لغة مفيدة لاستخدامها ضمن تطبيقات النظم الخلفية و أدوات سطر الأوامر. سنتعرف في هذا المقال على الوحدة البرمجية ‎fs‎ وسنستخدمها لقراءة الملفات وإنشاء ملفات جديدة والكتابة إليها وحذف الملفات وحتى نقل الملفات من مجلد إلى آخر، حيث توفر الوحدة البرمجية ‎fs‎ توابع للتعامل مع الملفات بالطريقتين المتزامنة synchronously واللامتزامنة asynchronously وباستخدام مجاري البيانات streams، حيث سنستخدم في هذا المقال الطريقة اللامتزامنة باستخدام الوعود Promises وهي الطريقة الأكثر استخدامًا. المستلزمات نسخة نود مثبتة على الجهاز، والتي ستوفر الوحدة البرمجية ‎fs‎ التي سنتعامل معها، حيث استخدمنا في هذا المقال الإصدار رقم 10.22.0. سنستخدم ضمن الأمثلة الوعود في جافاسكربت مع صيغة اللاتزامن والانتظار ‎async/await‎ للتعامل مع الملفات، وللتعرف أكثر على هذه الصيغة يمكنك مراجعة مقال طرق كتابة شيفرات غير متزامنة التنفيذ في Node.js من هذه السلسة. قراءة الملفات باستخدام ‎readFile()‎ سنطور في هذه الفقرة برنامجًا في نود لقراءة الملفات، وسنستعين بالتابع ‎readFile()‎ الذي توفره الوحدة البرمجية ‎fs‎ في نود لقراءة محتوى ملف معين وتخزينه ضمن متغير ثم طباعته إلى الطرفية. سنبدأ بإعداد المجلد الذي سيحوي على ملفات الأمثلة المستخدمة في هذا المقال ونُنشئ لذلك مجلدًا جديدًا بالاسم ‎node-files‎ كالتالي: $ mkdir node-files وندخل لذلك المجلد باستخدام الأمر ‎cd‎: $ cd node-files ننشئ داخل المجلد ملفين الأول هو الملف الذي سنحاول قراءته باستخدام البرنامج، والثاني هو ملف جافاسكربت للبرنامج الذي سنطوره، ونبدأ بإنشاء ملف جديد يحوي المحتوى النصي ‎greetings.txt‎، وهنا سنُنشئ الملف عن طريق سطر الأوامر كالتالي: $ echo "hello, hola, bonjour, hallo" > greetings.txt في الأمر السابق سيطبع الأمر ‎echo‎ النص المُمرر له إلى الطرفية، واستخدمنا المعامل ‎>‎ لإعادة توجيه خرج الأمر ‎echo‎ إلى الملف النصي الجديد ‎greetings.txt‎. والآن ننشئ ملف جافاسكربت جديد للبرنامج بالاسم ‎readFile.js‎ ونفتحه باستخدام أي محرر نصوص، حيث سنستخدم ضمن أمثلتنا محرر النصوص ‎nano‎ كالتالي: $ nano readFile.js يتكون البرنامج الذي سنكتبه من ثلاث أقسام رئيسية، حيث نبدأ أولًا باستيراد الوحدة البرمجية التي تحوي توابع التعامل مع الملفات كالتالي: const fs = require('fs').promises; تحوي الوحدة ‎fs‎ كافة التوابع المستخدمة في التعامل مع نظام الملفات، ونلاحظ كيف استوردنا منها الجزء ‎.promises‎ حيث كانت طريقة كتابة الشيفرة اللامتزامنة سابقًا ضمن الوحدة ‎fs‎ عبر استخدام دوال رد النداء callbacks، ولاحقًا وبعد أن انتشر استخدام الوعود كطريقة بديلة أضاف فريق التطوير في نود دعمًا لها ضمن الوحدة ‎fs‎، حيث وبدءًا من الإصدار رقم 10 من نود أضيفت الخاصية ‎promises‎ ضمن كائن الوحدة البرمجية ‎fs‎ والتي تحوي التوابع التي تدعم طريقة الوعود، بينما بقي عمل الوحدة البرمجية ‎fs‎ الأساسية كما هي سابقًا باستخدام توابع رد النداء لدعم البرامج التي تستخدم الطريقة القديمة، وفي أمثلتنا سنستخدم نسخة التوابع التي تعتمد على الوعود. في القسم الثاني من البرنامج سنضيف دالة لامتزامنة لقراءة محتوى الملف، حيث يمكن تعريف الدوال اللامتزامنة في جافاسكربت بإضافة الكلمة ‎async‎ في بدايتها، وبذلك نستطيع ضمن التابع انتظار نتيجة كائنات الوعود باستخدام الكلمة ‎await‎ مباشرةً بدلًا من ربط العمليات المتتالية باستخدام التابع ‎.then()‎. والآن نعرف الدالة ‎readFile()‎ التي تقبل سلسلة نصية ‎filePath‎ تمثِّل مسار الملف الذي نود قراءته، حيث سنستعين بتوابع الوحدة ‎fs‎ لقراءة محتوى الملف المطلوب وتخزينه ضمن متغير باستخدام صيفة ‎async/await‎ كالتالي: const fs = require('fs').promises; async function readFile(filePath) { try { const data = await fs.readFile(filePath); console.log(data.toString()); } catch (error) { console.error(`‎Got an error trying to read the file: ${error.message}‎`); } } يمكن التقاط الأخطاء التي قد يرميها استدعاء التابع ‎fs.readFile()‎ باستخدام ‎try...catch‎، حيث نستدعي التابع ‎fs.readFile()‎ ضمن جسم ‎try‎ ثم نخزن النتيجة ضمن المتغير ‎data‎، ويقبل ذلك التابع معاملًا وحيدًا إجباريًا وهو مسار الملف الذي نود قراءته، ويعيد كائن مخزن مؤقت ‎buffer‎ كنتيجة لعملية القراءة حيث يمكن لهذا الكائن أن يحوي أي نوع من الملفات، ولكي نطبع ذلك المحتوى إلى الطرفية يجب تحويله إلى سلسلة نصية باستخدام التابع ‎toString()‎ من كائن المخزن المؤقت. وفي حال رمي خطأ ما فالسبب يكون إما لعدم وجود الملف الذي نريد قراءته، أو لأن المستخدم لا يملك إذنًا لقراءته، ففي هذه الحالة سنطبع رسالة خطأ إلى الطرفية. أما القسم الثالث والأخير من البرنامج هو استدعاء دالة قراءة الملف مع تمرير اسم الملف ‎greetings.txt‎ ليصبح البرنامج كالتالي: const fs = require('fs').promises; async function readFile(filePath) { try { const data = await fs.readFile(filePath); console.log(data.toString()); } catch (error) { console.error(`Got an error trying to read the file: ${error.message}`); } } readFile('greetings.txt'); نحفظ الملف ونخرج منه وفي حال كنت تستخدم أيضًا محرر النصوص nano يمكنك الخروج بالضغط على الاختصار ‎CTRL+X‎، وعند تنفيذ البرنامج سيقرأ المحتوى النصي للملف ‎greetings.txt‎ ويطبع محتواه إلى الطرفية، والآن ننفذ البرنامج عبر الأمر ‎node‎ لنرى النتيجة: $ node readFile.js بعد تنفيذ الأمر سيظهر الخرج التالي: hello, hola, bonjour, hallo وبذلك نكون قد استخدمنا التابع ‎readFile()‎ من الوحدة ‎fs‎ لقراءة محتوى الملف باستخدام صيغة ‎async/await‎. ملاحظة: إذا كنت تستخدم إصدارًا قديمًا من نود وحاولت استخدام الوحدة ‎fs‎ بالطريقة السابقة سيظهر لك رسالة التحذير التالية: (node:13085) ExperimentalWarning: The fs.promises API is experimental حيث أن الخاصية ‎promises‎ من الوحدة ‎fs‎ تم اعتمادها رسميًا منذ الإصدار 10 من نود، وقبل ذلك كانت في المرحلة التجريبية وهذا سبب رسالة التحذير السابقة، ولاحقًا وتحديدًا ضمن إصدار نود رقم 12.6 أصبحت التوابع ضمن تلك الخاصية مستقرة وأزيلت رسالة التحذير تلك. الآن وبعد أن تعرفنا على طريقة قراءة الملفات باستخدام الوحدة ‎fs‎ سنتعلم في الفقرة التالية طريقة إنشاء الملفات الجديدة وكتابة المحتوى النصي إليها. كتابة الملفات باستخدام ‎writeFile()‎ سنتعلم في هذه الفقرة طريقة كتابة الملفات باستخدام التابع ‎writeFile()‎ من الوحدة البرمجية ‎fs‎، وذلك بكتابة ملف بصيغة CSV يحـوي على بيانات لفاتورة شراء، حيث سنبدأ بإنشاء ملف جديد وإضافة ترويسات عناوين الأعمدة له، ثم سنتعلم طريقة إضافة بيانات جديدة إلى نهاية الملف. نبدأ أولًا بإنشاء ملف جافاسكربت جديد للبرنامج ونفتحه باستخدام محرر النصوص كالتالي: $ nano writeFile.js ونستورد الوحدة ‎fs‎ كالتالي: const fs = require('fs').promises; وسنستخدم في هذا المثال أيضًا صيغة ‎async/await‎ لتعريف دالتين، الأولى لإنشاء ملف CSV جديد والثانية لكتابة بيانات جديدة إليه. نفتح الملف ضمن محرر النصوص ونضيف الدالة التالي: const fs = require('fs').promises; async function openFile() { try { const csvHeaders = 'name,quantity,price' await fs.writeFile('groceries.csv', csvHeaders); } catch (error) { console.error(`Got an error trying to write to a file: ${error.message}`); } } نُعرّف المتغير ‎csvHeaders‎ والذي يحتوي على عناوين رؤوس الأعمدة لملف CSV، ثم نستدعي التابع ‎writeFile()‎ من وحدة ‎fs‎ لإنشاء ملف جديد وكتابة البيانات إليه، حيث أن المعامل الأول المُمرر له هو مسار الملف الجديد، وإذا مررنا اسم الملف فقط فسيُنشَأ الملف الجديد ضمن المسار الحالي لتنفيذ البرنامج، وأما المعامل الثاني المُمرر هو البيانات التي نريد كتابتها ضمن الملف، وفي حالتنا هي عناوين الأعمدة الموجودة ضمن المتغير ‎csvHeaders‎. والآن نضيف الدالة الثانية ومهمتها إضافة بيانات جديدة ضمن ملف الفاتورة كالتالي: const fs = require('fs').promises; async function openFile() { try { const csvHeaders = 'name,quantity,price' await fs.writeFile('groceries.csv', csvHeaders); } catch (error) { console.error(`Got an error trying to write to a file: ${error.message}`); } } async function addGroceryItem(name, quantity, price) { try { const csvLine = `\n${name},${quantity},${price}` await fs.writeFile('groceries.csv', csvLine, { flag: 'a' }); } catch (error) { console.error(`Got an error trying to write to a file: ${error.message}`); } } عرفنا الدالة اللامتزامنة ‎addGroceryItem()‎ التي تقبل ثلاثة مُعاملات، وهي اسم المنتج والكمية والسعر للقطعة الواحدة منه، ويتم إنشاء السطر الجديد الذي نود كتابته إلى الملف باستخدام قالب نص template literal وتخزينه ضمن المتغير ‎csvLine‎، ثم نستدعي التابع ‎writeFile()‎ كما فعلنا سابقًا ضمن التابع الأول ‎openFile()‎، ولكن هذه المرة سنمرر كائن جافاسكربت كمعامل ثالث يحتوي على المفتاح ‎flag‎ بالقيمة ‎a‎، وتعبر تلك القيمة عن الرايات المستخدمة للتعامل مع نظام الملفات، والراية ‎a‎ هنا تخبر نود بأننا نريد إضافة ذلك المحتوى إلى الملف، وليس إعادة كتابة محتوى الملف كاملًا، وفي حال لم نمرر أي راية عند كتابة الملف كما فعلنا ضمن الدالة الأولى فإن القيمة الافتراضية هي الراية ‎w‎ والتي تعني إنشاء ملف جديد في حال لم يكن الملف موجودًا، وإذا كان موجودًا سيتم تبديله وإعادة كتابة محتواه كاملًا، ويمكنك الرجوع إلى التوثيق الرسمي لتلك الرايات من نود للتعرف عليها أكثر. والآن لننهي كتابة البرنامج باستدعاء الدوال التي عرّفناها كالتالي: ... async function addGroceryItem(name, quantity, price) { try { const csvLine = `\n${name},${quantity},${price}` await fs.writeFile('groceries.csv', csvLine, { flag: 'a' }); } catch (error) { console.error(`Got an error trying to write to a file: ${error.message}`); } } (async function () { await openFile(); await addGroceryItem('eggs', 12, 1.50); await addGroceryItem('nutella', 1, 4); })(); وبما أن الدوال التي سنستدعيها لامتزامنة، فيمكننا تغليفها بدالة لامتزامنة واستدعاءها مباشرة كي نستطيع استخدام ‎await‎ لانتظار إكمال تنفيذها، وذلك لأنه لا يمكن ضمن إصدار نود الذي نستخدمه حاليًا استخدام await مباشرة ضمن النطاق العام global scope، بل حصرًا ضمن دوال لامتزامنة تُستخدم ضمن تعريفها الكلمة async، ولا حاجة لتسمية تلك الدالة ويمكننا تعريفها كدالة مجهولة لأن الغرض منها فقط التغليف والتنفيذ المباشر ولن نشير إليها من أي مكان آخر. وبما أن كلا الدالتين ‎openFile()‎ و ‎addGroceryItem()‎ لا متزامنين فبدون انتظار نتيجة استدعاء الدالة الأولى ثم استدعاء الثانية لا يمكن ضمان ترتيب التنفيذ وبالتالي ترتيب المحتوى ضمن الملف الذي نريد إنشاءه، لذلك عرفنا دالة التغليف تلك الغير متزامنة بين قوسين وأضفنا قوسي الاستدعاء في النهاية قبل الفاصلة المنقوطة كي لاستدعائها مباشرةً، وتُدعى تلك الصيغة بصيغة التنفيذ المباشر لدالة Immediately-Invoked Function Expression أو IIFE، وباستخدام تلك الصيغة في مثالنا نضمن احتواء ملف CSV الجديد على الترويسات بدايةً ثم أول سطر للمنتج ‎eggs‎ وبعده المنتج الثاني ‎nutella‎. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج باستخدام الأمر ‎node‎: $ node writeFile.js لن نلاحظ أي خرج من التنفيذ ولكن سنلاحظ إنشاء ملف جديد ضمن المجلد الحالي ويمكن معاينة محتوى الملف ‎groceries.csv‎ باستخدام الأمر ‎cat‎ كالتالي: $ cat groceries.csv ليظهر الخرج التالي: name,quantity,price eggs,12,1.5 nutella,1,4 أنشأت الدالة ‎openFile()‎ ملف CSV وأضافت الترويسات له، ثم أضافت استدعاءات الدالة ‎addGroceryItem()‎ التي تليها سطرين من البيانات إلى ذلك الملف، وبذلك نكون قد تعلمنا طريقة استخدام التابع ‎writeFile()‎ لإنشاء الملفات الجديدة والتعديل على محتواها. سنتعلم في الفقرة التالية كيف يمكننا حذف الملفات في حال أردنا إنشاء ملفات مؤقتة مثلًا، أو لإزالة بعض الملفات لتوفير مساحة التخزين على الجهاز. حذف الملفات باستخدام ‎unlink()‎ سنتعلم في هذه الفقرة طريقة حذف الملفات باستخدام التابع ‎unlink()‎ من الوحدة البرمجية ‎fs‎، حيث سنكتب برنامجًا لحذف الملف ‎groceries.csv‎ الذي أنشأناه في الفقرة السابقة. نبدأ بإنشاء ملف جافاسكربت جديد بالاسم deleteFile.js نُعرف ضمنه الدالة اللامتزامنة ‎deleteFile()‎ التي تقبل مسار الملف المراد حذفه، وبدورها ستمرر ذلك المعامل إلى التابع ‎unlink()‎ والذي سيحذف ذلك الملف من نظام الملفات كالتالي: const fs = require('fs').promises; async function deleteFile(filePath) { try { await fs.unlink(filePath); console.log(`Deleted ${filePath}`); } catch (error) { console.error(`Got an error trying to delete the file: ${error.message}`); } } deleteFile('groceries.csv'); تحذير: لن تُنقل الملفات المحذوفة باستخدام التابع ‎unlink()‎ إلى سلة المحذوفات بل ستُحذف نهائيًا من نظام الملفات، لذا تلك العملية لا يمكن الرجوع عنها ويجب الحذر والتأكد من الملفات التي نحاول حذفها قبل تنفيذ البرنامج. والآن نخرج من الملف وننفذه كالتالي: $ node deleteFile.js ليظهر الخرج التالي: Deleted groceries.csv نستعرض الملفات الموجودة حاليًا بعد التنفيذ للتأكد من نجاح عملية الحذف باستخدام الأمر ‎ls‎ كالتالي: $ ls ليظهر لنا الملفات التالية: deleteFile.js greetings.txt readFile.js writeFile.js نلاحظ حذف الملف بنجاح باستخدام التابع ‎unlink()‎، وبذلك نكون قد تعلمنا طريقة قراءة وكتابة وحذف الملفات، وسنتعلم في الفقرة التالية كيف يمكن نقل الملفات من مجلد لآخر، لنكون بذلك قد تعلمنا كافة العمليات التي تسمح بإدارة الملفات عن طريق نود. نقل الملفات باستخدام ‎rename()‎ تُستخدم المجلدات لتنظيم وترتيب الملفات معًا، لذا من المفيد تعلم طريقة نقل تلك الملفات برمجيًا، حيث يتم ذلك في نود باستخدام التابع ‎rename()‎ من الوحدة ‎fs‎، وسنتعلم طريقة استخدامه بنقل الملف السابق ‎greetings.txt‎ إلى مجلد جديد مع إعادة تسميته. نبدأ بإنشاء ذلك مجلد جديد بالاسم ‎test-data‎ ضمن المجلد الحالي كالتالي: $ mkdir test-data ونُنشئ نسخة عن الملف ‎greetings.txt‎ بتنفيذ أمر النسخ ‎cp‎ كالتالي: $ cp greetings.txt greetings-2.txt ثم نُنشئ ملف جافاسكربت للبرنامج كالتالي: $ nano moveFile.js ونُعرف ضمنه الدالة ‎moveFile()‎ لنقل الملف، والتي ستستدعي بدورها التابع ‎rename()‎ الذي يقبل مسار الملف المراد نقله كمعامل أول، ثم المسار الجديد الوجهة كمعامل ثانِ، ففي حالتنا نريد استخدام الدالة ‎moveFile()‎ لنقل الملف الجديد ‎greetings-2.txt‎ إلى المجلد الذي أنشأناه ‎test-data‎ مع إعادة تسمية ذلك الملف إلى ‎salutations.txt‎، ولذلك نضيف الشيفرة التالية: const fs = require('fs').promises; async function moveFile(source, destination) { try { await fs.rename(source, destination); console.log(`Moved file from ${source} to ${destination}`); } catch (error) { console.error(`Got an error trying to move the file: ${error.message}`); } } moveFile('greetings-2.txt', 'test-data/salutations.txt'); كما ذكرنا سابقًا فالتابع ‎rename()‎ يقبل معاملين هما المسار المصدر والوجهة لنقل الملف، ويمكن استخدام هذا التابع إما لنقل الملفات من مجلد لآخر أو لإعادة تسمية الملفات، أو نقل وإعادة تسمية ملف ما معًا، وهو ما نريد تنفيذه في مثالنا. والآن نحفظ الملف ونخرج منه وننفذه باستخدام الأمر ‎node‎ كالتالي: $ node moveFile.js ليظهر الخرج التالي: Moved file from greetings-2.txt to test-data/salutations.txt نستعرض الملفات الموجودة حاليًا بعد التنفيذ للتأكد من نجاح عملية النقل باستخدام الأمر ‎ls‎ كالتالي: $ ls ليظهر لنا الملفات والمجلدات التالية: deleteFile.js greetings.txt moveFile.js readFile.js test-data writeFile.js ونستخدم الأمر ‎ls‎ مجددًا لعرض الملفات ضمن المجلد الوجهة ‎test-data‎: $ ls test-data ليظهر لنا الملف الذي نقلناه: salutations.txt وبذلك نكون قد تعلمنا كيف يمكن استخدام التابع ‎rename()‎ لنقل الملفات من مجلد لآخر مع إعادة تسمية الملف ضمن نفس العملية. خاتمة تعرفنا في هذا المقال على مختلف عمليات إدارة الملفات ضمن نود، بداية بقراءة محتوى الملفات باستخدام ‎readFile()‎ ثم إنشاء ملفات جديدة وكتابة البيانات إليها باستخدام ‎writeFile()‎ ثم طريقة حذف الملفات باستخدام ‎unlink()‎ ونقلها وإعادة تسميتها باستخدام ‎rename()‎. إنَّ التعامل مع الملفات من المهام الضرورية في نود فقد تحتاج البرامج أحيانًا إلى تصدير بعض الملفات للمستخدم أو تخزين البيانات الخاصة بها ضمن الملفات لاستعادتها لاحقًا، ولذلك توفر الوحدة البرمجية ‎fs‎ في نود كل التوابع الضرورية للتعامل مع الملفات في نود، ويمكنك الرجوع إلى التوثيق الرسمي للوحدة البرمجية ‎fs‎ من نود للتعرف عليها أكثر. ترجمة -وبتصرف- للمقال How To Work with Files using the fs Module in Node.js. اقرأ أيضًا المقال السابق: التعامل مع العمليات الأبناء Child Process في Node.js التعامل مع الملفات في Node.js إدارة الوحدات البرمجية في Node.js باستخدام npm وملف package.json تعرف على وحدات Node.js الأساسية
  6. قد تشعر كمبرمج بالضياع عند استكشافك لأول مرة شيفرة مصدرية تحتوي على الآلاف من الأسطر، في هذه الحالة سيفيدك استخدام الأداة grep كثيرًا لحل تلك المشكلة، لذا سنستعرض في هذا المقال عدة طرق مفيدة لاستخدام الأمر grep على لينكس مع أمثلة عليها. أمثلة واقعية ومفيدة للأمر grep في لينكس يصف الدليل man الأداة grep على أنها أداة لطباعة الأسطر المُطابقة لنمط ما، قد ينتقص ذلك الوصف من الإمكانيات والاستخدامات العديدة للأمر grep، والتي تعد من أفضل الأدوات ضمن حزمة أدوات نظام يونكس، حيث ستجد نفسك بحاجة إليها في الكثير من الحالات عند تعاملك مع الملفات النصية. تعد الأمثلة الواقعية أفضل مُعلم، سنوضح الاستخدامات المتعددة للأداة grep بتطبيق أمثلتنا في هذا المقال على الشيفرة المصدرية للمكتبة Asciidoctor.js، يمكن تنزيل الشيفرة المصدرية للمكتبة من Github واختيار نفس الإصدار الذي تم تطبيق الأمثلة في هذا المقال عليه، وذلك للحصول على نتائج مطابقة للأمثلة المذكورة خلال تطبيق الأمثلة، يمكنك تنفيذ الأوامر التالية لتحميل الشيفرة المصدرية محليًا وتحديد الإصدار المطلوب العمل ضمنه: git clone https://github.com/asciidoctor/asciidoctor.js cd asciidoctor.js git checkout v1.5.6-rc.1 1. الاستخدام الأساسي بالبحث عن نص توفر Asciidoctor.js دعمًا لمحرك جافاسكريبت Nashorn لمنصة جافا، يمكننا البحث عن أماكن ذكر ذلك المحرك ضمن الشيفرة المصدرية للتعرف ولاستكشاف طريقة استخدام المكتبة له، يمكننا مثلًا البحث ضمن ملف توصيف الاعتماديات package.json عن كل ذكر لاسم المحرك Nashorn كالتالي: $ grep nashorn package.json "test": "node npm/test/builder.js && node npm/test/unsupported-features.js && node npm/test/jasmine-browser.js && node npm/test/jasmine-browser-min.js && node npm/test/jasmine-node.js && node npm/test/jasmine-webpack.js && npm run test:karmaBrowserify && npm run test:karmaRequirejs && node npm/test/nashorn.js", كما نلاحظ يوجد عدة اختبارات متعلقة بـ Nashorn، في المثال التالي سنحاول البحث أكثر عن استخدامات ذلك المحرك. 2. بحث غير حساس لحالة الأحرف ضمن مجموعة من الملفات يمكننا البحث ضمن ملفات الاختبار التي وجدنا ذكر لها في المثال السابق، التي تقع ضمن المجلد ‎./npm/test/‎ والتي تحوي على ذكر مباشر لكلمة Nashorn، في هذه الحالة البحث الغير حساس لحالة الأحرف أفضل، بحيث ستظهر لنا جميع حالات ذكر اسم المكتبة مثل "nashorn" و"Nashorn" أو أي شكل آخر للكلمة مؤلف من حروف كبيرة وصغيرة، وذلك باستخدام الخيار ‎-i كالتالي: $ grep -i nashorn npm/test/*.js npm/test/nashorn.js:const nashornModule = require('../module/nashorn'); npm/test/nashorn.js:log.task('Nashorn'); npm/test/nashorn.js:nashornModule.nashornRun('jdk1.8.0'); نلاحظ من النتائج التي ظهرت فائدة البحث الغير حساس لحالة الأحرف في تلك الحالات، حيث لم يظهر في المثال الأول ضمن النتائج العبارة التالية ‎‎‎require('../module/nashorn')‎، سنعاين هذا الملف بالتفصيل لاحقًا. 3. البحث عن الملفات الغير مطابقة سنحاول معرفة ما إذا كان يوجد ملفات لا تتعلق بمحرك Nashorn ضمن المجلد npm/test/‎، يمكننا عكس نتيجة البحث والاستفادة من خيار طباعة الملفات الغير مطابقة لشرط البحث ‎-L كالتالي: sh$ grep -iL nashorn npm/test/* npm/test/builder.js npm/test/jasmine-browser-min.js npm/test/jasmine-browser.js npm/test/jasmine-node.js npm/test/jasmine-webpack.js npm/test/unsupported-features.js نلاحظ كيف أظهر لنا الأمر grep باستخدام الخيار ‎-L ضمن الخرج أسماء تلك الملفات فقط، لا تحوي كل تلك الملفات على أي ذكر للنص "nashorn" ضمنها (بغض النظر عن حالة الأحرف للكلمة)، لا يعني ذلك أن تلك الملفات لا تتعلق بالمحرك nashorn، بل هي فقط لا تحوي على أحرف تلك الكلمة متتالية معًا. 4. البحث عن الأنماط ضمن الملفات المخفية والمجلدات الفرعية تكراريًا استخدمنا في آخر مثالين نمط glob لتحديد الملفات التي نريد من الأمر grep البحث ضمنها، لكن استخدام رمز النجمة ("*") يمنعنا من البحث ضمن الملفات المخفية، وكذلك ضمن الملفات الموجودة داخل المجلدات الفرعية، يمكن حل تلك المشكلة بدمج الأمر grep مع أمر البحث عن الملفات find بدلًا من الاعتماد على تمرير نمط glob الخاص بالصَدفة بإحدى الطريقتين: # الطريقة التالية غير فعالة، ستولد إجرائية جديدة لكل ملف $ find npm/test/ -type f -exec grep -iL nashorn \{} \; # الطريقة التالية تسبب مشاكل للملفات التي يحوي اسمها على فراغ $ grep -iL nashorn $(find npm/test/ -type f) لكل من الطريقتين السابقتين مساوؤها كما هو مذكور ضمن التعليقات فوق الأوامر، فلحل مشكلة أسماء الملفات التي تحتوي على فراغ يمكنك البحث أكثر عن كيفية استخدام الخيار grep -z مع الخيار ‎-print0 للأمر find،لكن الحل الأمثل يكون باستخدام خيار البحث التكراري‎-r للأمر grep، وعندها يمكننا تحديد المجلد الجذر الذي نريد البحث ضمنه فقط، بدلًا من تمرير قائمة بأسماء الملفات المراد البحث فيها، سيبحث الأمر grep ضمن كل الملفات في المجلد المحدد، بما فيها المجلدات المخفية، ومن ثم سينزل تكراريًا إلى داخل المجلدات الفرعية ويعيد العملية: $ grep -irL nashorn npm/test/npm/ npm/test/builder.js npm/test/jasmine-browser-min.js npm/test/jasmine-browser.js npm/test/jasmine-node.js npm/test/jasmine-webpack.js npm/test/unsupported-features.js يمكن باستخدام هذا الخيار البحث أيضًا ضمن المجلد الأب npm لمعرفة الملفات المتعلقة بالمحرك Nashorn المغايرة لملفات الاختبار السابقة كالتالي: $ grep -irL nashorn npm/ يمكنك اختبار الأمر السابق بنفسك ومشاهدة النتيجة، بحيث سيظهر لك نتائج أكثر. 5. تصفية الملفات بأسمائها باستخدام التعابير النمطية كما لاحظنا سابقًا وجود ملفات اختبار متعلقة بالمحرك Nashorn ضمن ملفات المشروع، وبما أن محرك Nashorn مكتوب في لغة جافا، فيمكننا استكشاف ملفات جافا المصدرية ضمن المشروع التي تحوي ذكرًا صريحًا لاسم المحرك، وهناك طريقتين لتنفيذ ذلك بحسب إصدار grep المستخدَم، الطريقة الأولى باستخدام grep للعثور على الملفات التي تحوي النمط "nashorn" ثم تمرير نتيجة الأمر الأول إلى أمر grep آخر يُصفي ملفات جافا فقط كالتالي: $ grep -ir nashorn ./ | grep "^[^:]*\.java" ./spec/nashorn/AsciidoctorConvertWithNashorn.java:public class AsciidoctorConvertWithNashorn { ./spec/nashorn/AsciidoctorConvertWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn"); ./spec/nashorn/AsciidoctorConvertWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/asciidoctor-convert.js")); ./spec/nashorn/BasicJavascriptWithNashorn.java:public class BasicJavascriptWithNashorn { ./spec/nashorn/BasicJavascriptWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn"); ./spec/nashorn/BasicJavascriptWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/basic.js")); النصف الأول من الأمر تم شرحه مسبقًا، سنشرح القسم الثاني من الأمر وهو "‎^[\^:]*\.java"، حيث يعتبر الأمر grep افتراضيًا نمط البحث المٌمرر له تعبيرًا نمطيًا، إلا إذا استخدمنا الخيار ‎-F، مما يعني إضافةً لذكر نص صريح لمطابقته حرفيًا، يمكن استخدام محارف وصفية خاصة بالتعابير النمطية للتعبير عن أنماط بحث أعقد، فالنمط المستخدم في مثالنا تُعبر أقسامه عن التالي: ^ بداية سطر ‎[^:]*‎ متبوعة بسلسلة من المحارف لا تحوي على النقطتين (":"). ‎\.‎ متبوعة بنقطة، ولأن النقطة تملك معنى مميز في التعابير النمطية يجب حمايتها باستخدام خط مائل عكسي، مما يعني مطابقة النقطة حرفيًا. java متبوعة بكلمة java. بما أن grep يفصل بين اسم الملف وسياق نتيجة البحث بمحرف النقطتين (":")، يمكننا الاستفادة من ذلك وإبقاء أسماء الملفات التي تنتهي بالامتداد‎.java فقط في قسم اسم الملف، ويجب الانتباه إلى أن ذلك النمط سيُطابق أيضًا الملفات ذات الامتداد ‎.javascript، يمكنك محاولة حل تلك المشكلة بنفسك إن أردت. 6. تصفية الملفات بأسمائها باستخدام grep التعابير النمطية قوية جدًا، لكن مثالنا بسيط ولا يحتاج لكل هذا التعقيد، في المثال السابق بحثنا في جميع الملفات عن النمط "nashorn" وتجاهلنا العديد من النتائج بعد تمريرها للقسم الثاني من الأمر، يوجد حل لتلك المشكلة في إصدار جنو GNU من الأمر grep الذي يكون مثبتًا عادةً ضمن أنظمة لينكس، وهو باستخدام الخيار ‎--include الذي يوجّه الأمر grep للبحث ضمن الملفات التي تتطابق أسمائها مع نمط glob الممرر فقط: $ grep -ir nashorn ./ --include='*.java' ./spec/nashorn/AsciidoctorConvertWithNashorn.java:public class AsciidoctorConvertWithNashorn { ./spec/nashorn/AsciidoctorConvertWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn"); ./spec/nashorn/AsciidoctorConvertWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/asciidoctor-convert.js")); ./spec/nashorn/BasicJavascriptWithNashorn.java:public class BasicJavascriptWithNashorn { ./spec/nashorn/BasicJavascriptWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn"); ./spec/nashorn/BasicJavascriptWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/basic.js")); 7. البحث عن كلمات استُخدم في مشروع Asciidoctor.js عدة لغات برمجة أساسها لغة روبي، ولاستخدامه في جافاسكريبت يجب أن يُصرّف باستخدام Opal، وهو مُصرّف مصدر إلى مصدر من روبي إلى جافاسكريبت، يمكننا محاولة فهم الواجهة البرمجية API الخاصة بـ Opal، عبر البحث عن كل ذكر للكائن العام Opal ضمن ملفات جافاسكريبت في المشروع، قد يورد ذكر الكائن ضمن عبارة إسناد (‎Opal =‎) أو ضمن عبارة وصول للخواص الأعضاء ضمن الكائن (Opal‎.‎) أو ضمن سياق آخر، حيث يمكن استخدام التعابير النمطية للبحث عن ذلك، لكن الأمر grep يوفّر حلولًا أخف من ذلك لمثل تلك المشكلات الشائعة، فباستخدام الخيار ‎-w سيتم مطابقة الكلمات فقط التي تُعرّف تقنيًا بأنها أنماط مسبوقة ومتبوعة بغير محارف الكلمات، تلك المحارف تكون إما محرف بداية السطر أو نهاية السطر أو أي محرف آخر ليس بحرف أو رقم أو شرطة سفلية: $ grep -irw --include='*.js' Opal . ... 8. تلوين الخرج لم ننسخ خرج الأمر السابق لوجود الكثير من النتائج، لكن يمكن تحسين الخرج الكثيف مثل هذا بتلوينه لتسهيل فهمه، في حال لم تكن هذه الميزة مُفعلة افتراضيًا على نظام التشغيل، يمكننا تفعيلها يدويًا لخرج الأمر الحالي بتمرير خيار جنو ‎--color‎ كالتالي: $ grep -irw --color=auto --include='*.js' Opal . ... سيظهر نفس خرج الأمر السابق، لكن مع تلوين عبارة البحث ضمن النتيجة. 9. استخراج عدد الأسطر أو الملفات ضمن البحث يمكننا معرفة طول نتيجة الأمر السابق الذي لم نعرضه كالتالي: $ grep -irw --include='*.js' Opal . | wc -l 86 ما يعني أن لدينا 86 سطرًا يُطابق عبارة البحث ضمن كل الملفات التي جرى البحث ضمنها، يمكننا أيضًا معرفة عدد تلك الملفات التي استخرجت منها النتائج، يمكن باستخدام الخيار ‎-l تحديد خرج الأمر grep بحيث يعرض لنا أسماء الملفات التي احتوت على نتيجة فقط بدلًا من أسطر النتائج، يمكننا بذلك معرفة عدد تلك الملفات كالتالي: $ grep -irwl --include='*.js' Opal . | wc -l 20 قد يذكرنا هذا الخيار بالخيار ‎-L‎ بحرف كبير، حيث تدل عادة الأحرف الكبيرة والصغيرة للخيارات على خيارات متضادة، فيُظهر لنا الخيار ‎-l أسماء الملفات المطابقة للبحث، بينما يظهر الخيار ‎-L الملفات الغير مطابقة للبحث، لمثال آخر على ذلك يمكنك الرجوع إلى الدليل والتحقق من الفرق بين الخيارين ‎-h و ‎-H، وبالعودة لنتيجة بحثنا حصلنا على نتيجتين هما مطابقة 86 سطرًا، و 20 مِلفًا، يمكننا أيضًا معرفة طبيعة توزع تلك الأسطر ضمن تلك الملفات، فباستخدام الخيار ‎-c للأمر grep سيتم عد الأسطر المُطابقة ضمن كل ملف على حدى، بما فيها الملفات التي لا تحوي على نتائج: $ grep -irwc --include='*.js' Opal . ... يحتاج خرج الأمر السابق لمعالجة لاستخراج المعلومات المطلوبة، لأن ترتيب النتائج ضمنه تكون بحسب ترتيب معالجة الملفات، ويتضمن أيضًا الملفات التي لا تحوي على نتائج وهو ما لا يهمنا في هذه الحالة، يمكن حل المشكلة الأخيرة بسهولة عبر تصفية الملفات التي تحوي 0 نتائج كالتالي: $ grep -irwc --include='*.js' Opal . | grep -v ':0$' ولترتيب النتائج يمكننا تمرير النتيجة النهائية إلى أمر الترتيب sort كالتالي: $ grep -irwc --include='*.js' Opal . | grep -v ':0$' | sort -t: -k2n يمكنك الرجوع إلى دليل الأمر sort لمعرفة معاني الخيارات التي استخدمناها في المثال السابق. 10. إيجاد الفرق بين نتيجتي بحث استخدمنا في أحد الأمثلة السابقة الكلمة ".Opal"، في حال بحثنا في نفس تلك الملفات هذه المرة عن الكلمة "‎Opal,‎" سنحصل على عشرين نتيجة إضافية: $ grep -irw --include='*.js' Opal . | wc -l 86 $ grep -ir --include='*.js' Opal . | wc -l 105 يمكننا استخراج الفرق بين عمليتي البحث، وما هي الأسطر التي تحتوي على أحرف كلمة opal الأربعة متفرقة من دون أن تشكل كلمة واحدة؟، فمن الصعب الإجابة على هذا السؤال لأنه يمكن لنفس السطر أن يحوي كلًا من كلمة opal وكلمة أكبر منها تحوي حروفها الأربعة، يمكن محاولة الإجابة على هذا السؤال مبدأيًا بتمرير النتيجة إلى أمر آخر كما فعلنا في مثال سابق: $ grep -ir --include='*.js' Opal . | grep -ivw Opal ./npm/examples.js: const opalBuilder = OpalBuilder.create(); ./npm/examples.js: opalBuilder.appendPaths('build/asciidoctor/lib'); ./npm/examples.js: opalBuilder.appendPaths('lib'); ... يمكنك أيضًا محاولة البحث واستكشاف استخدامات الكائن opalBuilder بنفسك. ختامًا تنفيذك لعدد من أوامر grep ضمن مشروع جديد لن يمكنك من فهم ترتيب ومعمارية المشروع وبنية الشيفرات ضمنه، ولكن ستتمكن من إجراء تحليلات بسيطة مشابهة للأمثلة التي ذكرناها تفيدك في التعرف على المشاريع الجديدة، رأينا قوة الأمر grep واستخداماته المتعددة التي يمكنك إضافتها إلى مهاراتك. ترجمة -وبتصرف- للمقال ‎10 Practical Grep Command Examples for Developers لصاحبه Sylvain Leroux. اقرأ أيضًا دليل استخدام الأمر grep في لينكس ما الفروق بين الأوامر grep و egrep fgrep البحث ضمن جميع الملفات والمجلدات في لينكس باستخدام الأداة Grep تعلم استخدام الأمر rg بديل grep المحسن في لينكس
  7. تعد Grep الأداة الأفضل للبحث ضمن محتوى الملفات في نظام التشغيل لينكس، حيث لها استخدامات عديدة، ومن أشيع تلك الاستخدامات هو البحث ضمن ملف ما عن مصطلح، بالصيغة التالية: grep search_term filename وللبحث ضمن جميع الملفات ضمن مجلد ما يمكن تنفيذ الأمر كالتالي: grep search_term * المشكلة هنا أن عملية البحث ستتم على الملفات ضمن المجلد الحالي فقط، ولن يتم البحث ضمن المجلدات الفرعية له، يمكن حل تلك المشكلة بإجبار البحث ضمن كل الملفات وكل المجلدات الفرعية تكراريًا باستخدام الخيار r- كالتالي: grep -r search_term . يمكن ذكر اسم المجلد المراد البحث ضمنه بشكل صريح أيضًا في حال لم يكن هو المجلد الحالي كالتالي: grep -r search_term directory_path سنوضح استخدامات grep من خلال أمثلة عملية ضمن الفقرة التالية. البحث ضمن جميع ملفات مجلد ما يوضح خرج الأمر tree التالي بنية الملفات والمجلدات التي سنطبق عليها الأمثلة في هذا المقال، بحيث تحوي جميع الملفات الموضحة عبارة "simple"، فيما عدا الملف empty.txt فهو فارغ: abhishek@LHB:~/scripts$ tree . ├── dir1 │ └── new_script.sh ├── dir2 │ └── your_script.sh ├── dir3 │ ├── empty.txt │ └── linked.txt -> ../../sample.txt ├── my_script.sh └── your_script.sh 3 directories, 6 files يمكننا استخدام محرف البدل ("*") للبحث عن الكلمة "simple" ضمن جميع الملفات داخل المجلد الحالي، بحيث سيُبدّل محرف البدل بأسماء جميع الملفات والمجلدات داخل المجلد الحالي: grep simple * الأمر السابق سيُنفذ عملية البحث ضمن جميع الملفات ضمن المجلد الحالي، ولن يدخل إلى مجلداتها الفرعية، وبما أن البحث سيطبق فقط على الملفات، فسيظهر لنا رسالة بالصيغة التالية "XYZ is a directory" تدل على تجاهل المجلدات بنفسها. في حال لم يكن المجلد الحالي هو ما نريد البحث ضمنه، يمكن ذكر مسار المجلد المطلوب منتهيًا باللاحقة "*/" كالتالي: grep search_term directory_path/* هنا استخدمنا محرف البدل في النهاية لتوسيع العناصر المستهدفة سواء كانت ملفات أو مجلدات ضمن المجلد المحدد، وبالاعتماد على ما سبق سنرى في الفقرة القادمة كيف يمكن تنفيذ بحث تكراري يدخل إلى كافة الملفات ضمن المجلدات الفرعية. البحث التكراري ضمن جميع المجلدات الفرعية لمجلد يمكن استخدام الخيار r- لتفعيل البحث التكراري، بحيث سينظر grep داخل جميع الملفات ضمن المجلد المستهدف وداخل جميع الملفات ضمن المجلدات الفرعية داخله أيضًا، المثال التالي يشابه مثال البحث في الفقرة السابقة لكن مع إضافة البحث التكراري، سنلاحظ ظهور نتائج من ملفات داخل المجلدات الفرعية أيضًا: grep -r simple . خرج تنفيذ الأمر السابق: يوجد أيضًا الخيار R- لتنفيذ البحث التكراري، وهو يعمل بشكل مطابق للخيار r-: grep -R simple . الفرق الوحيد بين الخيارين هو أن الخيار R- خيار بحث بإلغاء المرجعية، يعني أنه يقوم باتباع الوصلات الرمزية إلى ملفها الأصلي الموجود في مكان آخر، ويوضح المثال التالي استخدام الخيار R-: نلاحظ الفرق في نتيجة البحث حيث ظهرت نتيجة ضمن ملف الوصلة الرمزية linked.txt، ولم يظهر لنا ضمن النتائج عند استخدام الخيار r- سابقًا، في حال لم نكن ضمن المجلد المراد البحث ضمنه يمكن ذكر المسار النسبي أو المطلق للمجلد المطلوب كالتالي: grep -r search_term path_to_directory استثناء مجلد من عملية البحث التكراري يمكن توجيه grep لاستثناء البحث ضمن مجلد فرعي ما عند تنفيذ عملية البحث التكراري باستخدام الخيار exclude-dir-- كالتالي: grep -r --exclude-dir=dir_name serach_term directory_path يمكننا أيضًا استثناء عدة مجلدات فرعية من البحث التكراري باستخدام الخيار السابق بالصيغة التالية: grep -r --exclude-dir={dir1,dir2} serach_term directory_path وبتطبيق ذلك على مثالنا سيظهر الخرج التالي: بحيث يعمل خيار الاستثناء مع كلًّا من خياري البحث التكراري r- و R-. الخلاصة الجدول التالي يتضمن خلاصة استخدامات grep للبحث ضمن كل الملفات والمجلدات، والتي شرحناها ضمن هذا المقال: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } أمر Grep الوصف grep string *‎ البحث ضمن كل الملفات ضمن المجلد الحالي grep string dir البحث ضمن كل الملفات ضمن المجلد dir grep -r string .‎ البحث التكراري ضمن كل الملفات داخل المجلدات الفرعية grep -r string dir البحث التكراري ضمن كل الملفات داخل المجلدات الفرعية ضمن المجلد dir grep -R string .‎ مثل الخيار r- لكن يتبع الوصلات الرمزية ترجمة -وبتصرف- للمقال How to Perform Grep Search on All Files and in All Directories لصاحبه Abhishek Prakash. اقرأ أيضًا دليل استخدام الأمر grep في لينكس ما الفروق بين الأوامر grep وegrep fgrep تعلم استخدام الأمر rg بديل grep المحسن في لينكس
  8. يوفر بعيد خدمة كتابة السيرة الذاتية لك حيث يتابع فريق مختص معك كافة نواحي كتابة سيرتك الذاتية مع فريق متخصص في ذلك، بدءا من محتواها وسلامة اللغة المكتوبة )العربية أو الإنجليزية( ، وصولًا لتصميم سيرتك الذاتية بشكل جذاب، وحتى تجربة عمل مقابلة تجريبية تدرب فيها نفسك على المقابلات الحقيقية مستقبلًا، يمكنك الاطلاع أكثر على تلك الخدمة ضمن موقع بعيد. أما ضمن الأكاديمية فعند إنهائك لأحد الدورات والنجاح بها فسيقدم لك فريق مختص النصائح اللازمة لتتمكن من عرض أعمالك التي نفذتها، ويقدم بعض النصائح الخاصة بحصولك على أول مشروع لك بصفتك عامل مستقل، يتم ذلك حصرًا بعد اجتيازك لأحد الدورات والنجاح بها.
  9. يمكنك تقسيم إرسال البيانات إلى عدة مسارات، يُخزن كل منها جزء بيانات الاستمارة في تلك المرحلة ضمن بيانات الجلسة session للمستخدم، وفي المرحلة الأخيرة يمكن استخراج تلك البيانات من الجلسة وإرسالها إلى قاعدة البيانات، حيث تُعرف لكل خطوة مسار لعرض استمارة form تلك الخطوة ومسار إرسال البيانات لها، بشكل مشابه للتالي: // الخطوة الأولى Route::get('step-one', ...)->name('step.one'); Route::post('step-one', ...); ... // خطوات إضافية // الخطوة الأخيرة Route::get('step-final', ...)->name('step.final'); Route::post('step-final', ...); وفي متحكم كل خطوة نستخرج البيانات الخاصة بها من الطلب ونخزنها ضمن الجلسة بشكل مشابه للتالي: public function index() { // عرض نموذج الخطوة return view('steps.one',...); } public function store(Request $request) { // استخراج البيانات الخطوة $data = $request->validate([...]); // تخزينها ضمن الجلسة $request->session()->put('data', $data) // إعادة التوجيه إلى الخطوة التالية return redirect()->route('step.two'); } وفي تابع تخزين بيانات آخر خطوة يمكنك استخراج بيانات الخطوات السابقة ثم تخزينها في قاعدة البيانات: public function store(Request $request) { // استخراج البيانات الخطوة الأخيرة $data = $request->validate([...]); // استخراج بيانات الخطوات السابقة $step_one_data = $request->session()->get('step-one-data'); $step_one_data = $request->session()->get('step-two-data'); ... // تخزين البيانات Model::create(...); // (اختياري) مسح البيانات المخزنة بعد نجاح العملية كاملةً $request->session()->forget('step-one-data'); $request->session()->forget('step-two-data'); // نجاح الخطوات كاملة .. }
  10. طوُّرت أداة ripgrep نتيجةً لجهود مطوري البرامج مفتوحة المصدر ضمن حملة لإعادة كتابة البرامج بلغة رست Rust كبديل أقوى عن الأداة المعروفة grep،صيغة استخدامها كالتالي: rg [files/folders] <pattern> مع استبدال files/folders بأسماء الملفات أو المجلدات المُراد البحث ضمنها، وهو معامل اختياري، واستبدال pattern بنمط البحث الذي نحاول العثور على نتائج تطابقه. نلاحظ وخلافًا للأمر grep، أن ذكر اسم ملف ليتم البحث ضمنه هو اختياري وغير ضروري، فعند عدم تمرير اسم ملف معين سيُبحث ضمن جميع الملفات، تفيد تلك الميزة عند نسيان أيّ من الملفات يحوي النمط الذي نبحث عنه، وهي طريقة أبسط بمقارنتها بتنفيذ نفس العملية باستخدام grep. ما هي أداة ripgrep؟ هي أداة مُطابقة أنماط قياسية متكررة تأخذ بالحسبان ملفات gitignore. في حال وجود ذكر لملفات معينة أو لواحق أو مجلدات ضمن ملف gitignore. ستتجاهلها الأداة ripgrep، مما يُسرّع زمن تنفيذ عملية البحث، ومن مزايا أداة ripgrep: البحث عن الأنماط تكراريًا ضمن المجلدات. تلوين المُطابقات ضمن الخرج. دعم للعديد من صيغ الترميز مثل UTF-8 و SHIFT_JIS. القدرة على البحث ضمن الملفات المضغوطة zip. تجاهل الملفات المخفية افتراضيًا، والملفات المطابقة للأنماط ضمن ملف gitignore لتسريع عملية البحث. فهي أداة مشابهة تمامًا لأداة grep، لكنها موجهة للبحث ضمن محتوى الملفات بدلًا من البحث ضمن مجرى من البايتات. تثبيت ripgrep لا تأتي أداة ripgrep مثبتة مسبقًا ضمن نظام التشغيل كما هو الحال مع grep، ولكنها متاحة ضمن كافة المستودعات ضمن توزيعات لينكس الشهيرة، بحيث يمكن تثبيتها باستخدام مدير الحزم، فضمن توزيعة لينكس Arch يمكن تثبيتها بتنفيذ الأمر التالي: pacman -S ripgrep وضمن جينتو Gentoo باستخدام الأمر: emerge sys-apps/ripgrep وعلى توزيعات فيدورا وريد هات بتنفيذ الأمر التالي: sudo dnf install ripgrep وضمن توزيعة أوبن سوزي إصدار 15.1 وما فوق بتنفيذ الأمر: sudo zypper install ripgrep وضمن توزيعة ديبيان Buster الإصدار 10 وما فوق باستخدام apt، ويمكن ضمن توزيعة أوبنتو Cosmic Cuttlefish إصدار 18.10 وما فوق تحميلها من المستودعات الرسمية للتوزيعة، باستخدام الأمر: sudo apt install ripgrep استخدام الأمر ripgrep ستجد استخدام الأمر سهلًا في حال لديك خبرة سابقة باستخدام الأمر grep، بحيث نمرر له نص البحث واسم ملف، ليبحث بعدها ضمنه ويعرض لنا أماكن تطابق نص البحث مع المحتوى داخل الملف، الأمثلة ضمن هذا المقال نُفذت ضمن نسخة من مجلد مستودع الأداة dust. عمليات البحث البسيطة المثال التالي يبحث عن النص description ضمن محتوى الملف Cargo.html: $ rg description Cargo.toml 3:description = "A more intuitive version of du" 53:extended-description = """\ فبعد تنفيذ الأمر بحثت لنا الأداة ضمن الملف المذكور وعرضت الأسطر مع أرقامها التي حَوت مُطابقة مع نص البحث: عند تمرير أسماء لعدة ملفات للبحث ضمنها، أو عند عدم تمرير أسماء ملفات ما يعني البحث ضمن جميع الملفات، سيظهر ضمن الخرج أسماء الملفات التي عُثر ضمنها على مطابقات: يمكن الاستفادة من استخدام الخيار file-- للإشارة إلى ملف يحوي أكثر من نمط ليتم البحث عنها، قد نحتاج ذلك في حال كنا نكرر البحث عن عدة أنماط دومًا: إظهار نتيجة البحث ضمن السياق من المفيد والضروري أحيانًا عرض السياق الذي عُثر ضمنه على النتيجة، خصوصًا عند بحثنا ضمن مستودع أو ملفات تحوي شيفرات، نستفيد من الخيار C- أو context-- لتمرير عدد يدل على عدد الأسطر التي نريد إظهارها قبل وبعد النتيجة: نحتاج أحيانًا لعرض سطر النتيجة وما فوقه، ونحتاج أحيانًا أخرى لعرض سطر النتيجة وما بعده فقط، يمكن الاستفادة من الخيار A- أو after-context-- وتمرير عدد الأسطر الواجب إظهارها أسفل كل نتيجة: ولإظهار الأسطر فوق النتيجة يمكن الاستفادة من الخيار B- أو before-context-- وتمرير عدد الأسطر الواجب إظهارها فوق كل نتيجة: الأعمدة يوفر ripgrep عدة خيارات تخص الأعمدة، منها الخيار column-- لطباعة إحداثيات مكان النتيجة بالصيغة "رقم السطر:رقم العمود" في بداية سطر كل نتيجة، ما يفيدنا في حال استخدامنا لمحرر نصوص مثل vim: يوجد أيضًا الخيار M- أو max-columns-- الذي يأخذ قيمة عددية تمثل عدد الأعمدة ضمن الخرج، وفي حال تجاوزت نتيجة ما في أعمدتها ذلك العدد، يتم تجاهل عرضها والاكتفاء بعرض رسالة توضيحية تعبر عن النتيجة: خيارات أخرى يوجد العديد من الخيارات الممكن استخدامها مع الأمر ripgrep، مثلًا يمكن استخدام الخيار s- أو case-sensitive-- لجعل عملية المطابقة حساسة لحالة الأحرف: وإذا أردنا إظهار كل النتائج وتجاهل حالة الأحرف يمكن استخدام الخيار i- أو ignore-case--: يمكن استخدام عدة مسالك Threads معًا لتنفيذ عملية البحث، حيث يفيد ذلك عند البحث ضمن كمية كبيرة من الملفات، كمستودع كبير للشيفرة المصدرية، يمكن تحديد عدد المسالك المستخدمة في عملية البحث بتمرير عدد للخيار j- أو threads--: $ rg -j 4 TODO لعكس نتيجة البحث أي استثناء نمط ما من نتائج البحث، يمكن استخدام الخيار v- أو invert-match--. يمكن للأداة ripgrep البحث ضمن الملفات النصية المٌحتواه داخل ملف أرشيف مضغوط باستخدام الخيار z- أو search-zip-- عادة يستخدم ذلك الخيار مع الخيار a- لمعاملة الملفات الثنائية على أنها ملفات نصية: في الختام يمكن الاستفادة من الأداة ripgrep بشكل كبير، وخاصة لو كنت مبرمجًا تعمل على أنظمة تشغيل ببيئات شبيهة بيونكس، مع أنها لا تعتبر بديلًا عن الأداة grep لأن وظيفتها مختلفة، حيث لكل منهما استخداماته وتطبيقاته المفيدة. ترجمة -وبتصرف- للمقال Using ripgrep (rg) Command in Linux لفريق الموقع. اقرأ أيضًا دليل استخدام الأمر grep في لينكس ما الفروق بين الأوامر grep وegrep fgrep الدليل النهائي لاختيار توزيعة لينكس
  11. يمكن لأي تطبيق من أي منصة كانت سواء تطبيق جوال أو تطبيق ويب أو حتى برنامج سطح مكتب التواصل مع أي واجهة برمجية للتطبيقات API ولا يوجد حد لذلك، وذلك لأن التواصل مع الواجهة API يتم عبر بروتوكول HTTP، أي يكفي لتلك التطبيقات )ما ندعوها عميل أو client( أن تتمكن من إرسال طلبات HTTP، يمكنك الاستفادة من الاطلاع على المقالات التالية للتعرف على ماهية API:
  12. توكلت العملية syslog إدارة السجلات لفترة طويلة من الزمن، بحيث كانت تُجمّع رسائل السجلات المُرسلة من قبل إجرائيات النظام والتطبيقات إلى الجهاز الزائف ‎/dev/log، ثم تعيد توجيهها إلى ملفات السجل النصية المناسبة ضمن المجلد /var/log/‏‏، ويستدل syslogd على الملف الوجهة للسجلات من خلال البيانات الوصفية ضمن ترويسات الرسائل القادمة نفسها، والتي تتضمن معلومات إضافية أيضًا كالعلامة الزمنية للرسالة ومصدرها وأولويتها. ومع أن أغلب السجلات كانت تُعالج باستخدام systemd، لكن مؤخرًا أصبح المدير الرئيسي لها هو journald، والذي يتم التحكم به عن طريق الأمر الخاص journalctl، ولكنه لم يستبدل سجلات syslogd كليًا ولازال بالإمكان رؤية ملفات السجلات تلك ضمن المجلد /var/log/، وسنركز في هذا المقال فقط على systemd. حفظ السجلات باستخدام syslogd تضاف كل السجلات المُولّدة من قبل الأحداث في نظام syslogd إلى الملف ‎/var/log/syslog، وقد تُرسل أيضًا إلى ملف واحد أو عدة ملفات ضمن نفس المجلد بحسب خصائصها، حيث يعتمد نظام syslogd على محتوى ملف الإعدادات ‎50-default.conf الموجود ضمن المجلد /etc/rsyslog.d/ لتحديد طريقة توزيع تلك الرسائل. يوضح المثال التالي محتوى ملف الإعداد ‎50-default.conf حيث ستُكتب رسائل السجل المتعلقة بالأعمال المجدولة cron ضمن الملف cron.log، وتخبر علامة النجمة ("*") نظام syslogd بأن يرسل كل السجلات بمختلف مستويات أولويتها إلى الملف المذكور، وذلك على عكس تحديد مستوى معين للرسائل الواجب إرسالها، كتحديد مستوى رسائل الخطأ err أو مستوى حالات انهيار النظام emerg: cron.* /var/log/cron.log على عكس journalctl، يمكن التعامل مع نظام ملفات سجلات syslogd مباشرةً ودون الحاجة إلى أدوات خاصة، ولاستخدامه بالشكل الأمثل يجب معرفة نوع المعلومات الموجودة ضمن ملفات السجل القياسية، الجدول التالي يوضح أشهر تلك الملفات syslogd وماذا يُخزّن ضمنها: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } اسم الملف المعلومات المخزنة auth.log أحداث الأمان والاستيثاق للنظام boot.log سجل أحداث الإقلاع dmesg أحداث التخزين المؤقت لحلقة النواة، الخاصة بتعريفات الأجهزة dpkg.log أحداث برنامج إدارة الحزم kern.log أحداث نواة لينكس syslog مجموع كل السجلات wtmp متابعة جلسات المستخدم (يستفيد منها الأمران who و last) تَكتب التطبيقات عادة ضمن ملفات سجلات خاصة بها، حيث ستلاحظ غالبًا وجود مجلدات خاصة مثل /var/log/apache2/ أو /var/log/mysql/ لاستقبال بيانات تلك التطبيقات، يمكن التحكم بإعادة توجيه السجلات من خلال مستويات الأولوية الثمانية لها، إضافةً إلى رمز النجمة الذي يشير إلى كافة المستويات كما رأينا سابقًا. المستوى الوصف debug يفيد في عملية اكتشاف الأخطاء info توفير المعلومات notice الوضع الطبيعي warn الأوضاع التحذيرية err حالات الخطأ crit الأوضاع الحرجة alert الحالات التي تتطلب إجراء فوري emerg حالات انهيار النظام إدارة ملفات السجل بواسطة syslogd يُدوّر syslogd السجلات دوريًا ويضغطها ويحذفها افتراضيًا دون الحاجة لإعداده مسبقًا، ومن المفيد مع ذلك معرفة كيف تتم تلك العمليات، خاصة عند التعامل مع سجلات بحاجة لمعاملة مختلفة عن المعاملة الافتراضية، ولتوضيح الحالات التي تحتاج فيها السجلات لمعاملة خاصة سنوضح ذلك بمثال، بفرض حاجة شركة ما لاتباع قوانين متعلقة بإنشاء تقارير عن العمليات التي تجري ضمنها، قد تكون تلك القوانين تنظيمية أو معايير خاصة بمجال عمل الشركة، كمعيار Sarbanes-Oxley أو PCI-DSS، في هذه الحال يجب أن نُبقي على السجلات المتعلقة بالبنية التحتية التقنية للشركة لفترات أطول، ويجب أن نعرف كيفية البحث والعثور على ما نريده من بين ملفات السجلات المخزنة. ولتوضيح كيف يتم تدوير سجلات النظام يمكننا عرض بعض الملفات ضمن المجلد /var/log/، حيث سنلاحظ وجود ملف السجل auth.log الخاص بعمليات الاستيثاق على سبيل المثال بثلاث صيغ مختلفة وهي: auth.log وهي نسخة ملف السجلات الفعال حالًيا الذي تُكتب ضمنه رسائل الاستيثاق الجديدة. auth.log.1 آخر ملف تم تدويره وأصبح خارج الخدمة، ويبقى دون ضغط لتسهيل عملية استخدامه عند الضرورة. auth.log.2.gz مجموعة سجلات قديمة، ومضغوطة لتوفير المساحة كما يُلاحظ من الامتداد ‎.gz في نهاية اسم الملف. عند قدوم موعد تدوير السجلات بعد 7 أيام، ستنفذ الخطوات التالية: يُعاد تسمية الملف auth.log.2.gz ليصبح auth.log.3.gz يُضغط الملف auth.log.1 ويعاد تسميته ليصبح auth.log.2.gz يُعاد تسمية الملف الفعال الحالي auth.log ليصبح auth.log.1 يُنشئ ملف جديد لاستقبال السجلات الجديدة بالاسم auth.log. يمكن التحكم بمدة التدوير الافتراضية للسجلات من خلال ملف الإعداد ‎/etc/logrotate.conf، يوضح المحتوى التالي للملف ضبط عملية التدوير لتكون أسبوعية، وحذف السجلات التي يمضي عليها مدة أربعة أسابيع: # تدوير السجلات أسبوعيًا weekly # إبقاء سجلات آخر 4 أسابيع rotate 4 # إنشاء ملف سجلات فارغ جديد بعد تدوير السجلات القديمة create # تضع الحزم معلومات تدوير السجلات ضمن المجلد التالي include /etc/logrotate. يحوي المجلد /etc/logrotate.d/ أيضًا على ملفات إعدادات مخصصة لإدارة عملية تدوير السجلات لخدمات وتطبيقات معينة، وبتنفيذ أمر عرض محتويات ذلك المجلد سنجد الملفات التالية: $ ls /etc/logrotate.d/ apache2 apt dpkg mysql-server rsyslog samba unattended-upgrade يمكنك عرض محتويات تلك الملفات للاطلاع على طريقة إدارتها لعملية تدوير ملفات السجل الخاصة بالخدمات والتطبيقات المتعلقة بها، ومن الشائع إعادة توجيه السجلات إلى خوادم مخصصة لاستقبال ومعالجة وتحليل تلك السجلات بدلًا من تخزينها محليًا، مما يريح خوادم التطبيقات من تلك المهمة، ويُجمّع كافة بيانات السجلات في مكان مركزي سهل الوصول. قراءة ملفات سجلات syslog البحث يدويًا ضمن ملفات سجلات غير مجدي، فقد تحوي تلك الملفات على الملايين من الأسطر، لذا يجب تجنب الاستعانة بالأمر cat نهائيًا، فهو سيطبع فقط الآلاف من تلك الأسطر على الشاشة ولن نتمكن من قراءتها، يمكن الاستعانة بالأمر grep بدلًا منه لتصفية النص ضمن تلك الملفات، ويمكن الاستفادة من الأمر tail -f لمعاينة الأسطر الأخيرة من الملف مع تغييراتها بالوقت الحقيقي، وبدمجه مع الأمر grep يمكن تصفية الأحداث التي تهمنا منها فقط. نضطر أحيانًا لمعاينة محتوى ملفات السجل القديمة المضغوطة، يمكن فك ضغط تلك الملفات واستخدام الأمر grepمع less أو أوامر أخرى مشابهة له للوصول للسجلات المطلوبة، لكن الطريقة الأفضل تكون باستخدام بعض الأوامر التي تبدأ بالحرف z كالأمر zcat و zless وغيرها، بحيث تسمح لنا تلك الأوامر بالتعامل مع الملفات المضغوطة مباشرةً دون الحاجة لفك ضغطها أولًا. مثال عملي على تحليل السجلات كمثال يمكننا البحث عن محاولات تسجيل الدخول الفاشلة ضمن ملف السجلات الخاص بعمليات الاستيثاق auth.log، فنبحث ضمنه عن الكلمة "failure" ليظهر لنا الأسطر التي تحتوي على جملة فشل الاستيثاق "authentication failure"، نستفيد من تلك العملية للتحقق من محاولات اختراق لحسابات عبر المحاولات المتكررة الفاشلة لتخمين كلمة السر لها، بحيث يدل تكرار المحاولات على وجود محاولة للاختراق، مع تجاهل المحاولات الفاشلة القليلة التي تظهر كمرة واحدة أو مرتين التي قد تحدث مع مستخدم حقيقي عند نسيانه لكلمة السر: $ cat /var/log/auth.log | grep 'Authentication failure' Sep 6 09:22:21 workstation su[21153]: pam_authenticate: Authentication failure في حال تجربة للأمر السابق ولم يظهر أي نتائج، السبب يكون في نجاح كل عمليات تسجيل الدخول الخاصة للمستخدمين على الجهاز ولا وجود لمحاولات فاشلة، لكن لا يزال بالإمكان التحقق من صحة تنفيذ الأمر السابق عبر توليد سجل وهمي يدويًا باستخدام برنامج يدعى logger عبر تنفيذ الأمر كالتالي: logger "Authentication failure" طريقة أخرى تكون بتوليد سجلات حقيقية عبر محاولة تسجيل الدخول بكلمة سر خاطئة عمدًا، لكن المعلومات التي استخرجناها تدل فقط على حدوث عملية تسجيل دخول فاشلة، ولا تخبرنا بأي معلومات مفيدة أخرى عن سياق ذلك الحدث، كالحساب الذي جرت عليه محاولة تسجيل الدخول الفاشلة، لكن يمكننا توسيع النتائج التي يٌظهرها الأمر grep لتشمل بعض الأسطر قبل وبعد سطر النتيجة، المثال التالي يطبع لنا نتائج البحث إضافةً إلى الأسطر حولها، نلاحظ ظهور معلومات مفيدة، كاسم الحساب david وهو الذي نفذ محاولة تسجيل دخول فاشل باستخدام الأمر su إلى الحساب ‎:studio $ cat /var/log/auth.log | grep -C1 failure Sep 6 09:22:19 workstation su[21153]: pam_unix(su:auth): authentication failure; logname= uid=1000 euid=0 tty=/dev/pts/4 ruser=david rhost= user=studio Sep 6 09:22:21 workstation su[21153]: pam_authenticate: Authentication failure Sep 6 09:22:21 workstation su[21153]: FAILED su for studio by david ختامًا يمكن تطبيق الأساسيات التي تعلّمتها في هذا المقال عمليًا لتزيد من خبرتك، فهم الأساسيات من أهم المراحل التي تفيد في العديد من المواقف، بحيث أصبح بإمكانك الآن التعامل بسهولة أكبر مع ملفات السجلات المخزنة على لينكس. ترجمة -وبتصرف- للمقال Beginner's Guide to Syslogs in Linux لفئة من المؤلفين. اقرأ أيضًا أهم 20 أمرا في نظام التشغيل لينكس لجميع المستخدمين مرجع إلى أشهر أوامر لينكس كيف تستعرض وتدير سجلات لينكس على أوبنتو وCentOS
  13. في بداية تعلم البرمجة ستبدو لك كل الأفكار جديدة وستواجه الكثير من المشاكل، الحل دومًا يكون بالبحث عن حل لمشكلتك والقراءة والسؤال، سواء في السؤال في مجتمعات البرمجة كأكاديمية حسوب أو مراجعة أحد التوثيقات لتلك اللغات كموسوعة حسوب، وأنصحك دومًا بالتطبيق العملي المتكرر فهو سيُعرضك لتلك المشاكل الجديدة مرارًا إلى أن يتقدم مستواك وتتقنها وستلاحظ التحسن تدريجيًا، يمكنك الاطلاع على المقالات التالية ففيها شرح وافِ عن مجال البرمجة بشكل عام وتطوير الويب:
  14. عند تشغيل أي برنامج في نود Node.js ستعمل نسخة منه افتراضيًا ضمن عملية process واحدة في نظام التشغيل، وسيُنفذ فيها البرنامج ضمن خيط معالجة thread وحيد، وكما تعلمنا في المقال الخامس طرق كتابة شيفرات غير متزامنة التنفيذ في Node.js من هذه السلسلة فإن تنفيذ البرنامج ضمن خيط وحيد ضمن العملية سيؤدي لأن تعيق العمليات التي تحتاج مدة طويلة لتنفيذها في جافاسكربت تنفيذ العمليات أو الشيفرات التي تليها ضمن خيط التنفيذ لحين انتهاءها، وهنا يأتي دور إنشاء عملية ابن child process منفصلة عن العملية الرئيسية، وهي عملية تُنشئها عملية أخرى وتُستخدم لتنفيذ المهام الطويلة، وبهذه الطريقة يمكن لنظام التشغيل تنفيذ كلا العمليتين الأب والابن معًا أو بنفس الوقت على التوازي دون أن يعيق أي منهما تنفيذ الآخر. توفر نود لذلك الغرض الوحدة البرمجية ‎child_process‎ التي تحتوي على توابع عدة تساعد في إنشاء عمليات جديدة، وحتى توابع للتعامل مع نظام التشغيل مباشرةً وتنفيذ الأوامر ضمن الصدفة shell، لذا يمكن لمسؤولي إدارة النظام الاستفادة من نود في تنفيذ أوامر الصدفة لإدارة نظام التشغيل وترتيب تلك الأوامر ضمن وحدات برمجية بدلًا من تنفيذ ملفات أوامر الصدفة مباشرةً. سنتعلم في هذا المقال طرق إنشاء عمليات أبناء بتطبيق عدة أمثلة حيث سننشئ تلك العمليات بالاستعانة بالوحدة البرمجية ‎child_process‎ ونعاين نتيجة تنفيذها على شكل مخزن مؤقت buffer أو سلسلة نصية باستخدام التابع ‎exec()‎، وسنتعلم كيف يمكن قراءة نتيجة تنفيذ تلك العملية من مجرى للبيانات data stream باستخدام التابع ‎spawn()‎، ثم سننفذ برنامج نود آخر ضمن عملية منفصلة باستخدام ‎fork()‎ ونتعلم طريقة التواصل معه أثناء تشغيله، وسنطبق هذه الأفكار على مثال لبرنامج مهمته عرض قائمة محتويات مجلد ما، وبرنامج آخر للبحث عن الملفات، وآخر لخادم ويب يدعم عدة مسارات فرعية. المستلزمات هذا المقال جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته: تثبيت نسخة نود على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.22.0. معرفة عن طريقة عمل خوادم الويب في نود، ويمكنك مراجعة مقال السابع إنشاء خادم ويب في Node.js من هذه السلسلة. إنشاء عملية ابن باستخدام ‎exec عادة ما ننشئ عملية ابن لتنفيذ بعض الأوامر ضمن نظام التشغيل، فمثلًا للتعديل على خرج برنامج ما في نود بعد تنفيذه ضمن الصدفة نمرر خرج ذلك البرنامج أو نعيد توجيهه إلى أمر آخر، وهنا يأتي دور التابع ‎exec()‎ الذي يمكننا من إنشاء عملية صدفة جديدة بنفس الطريقة وتنفيذ الأوامر ضمنها لكن من قبل برنامج نود، حيث يُخزَّن خرج ذلك الأمر ضمن مخزن مؤقت في الذاكرة ويمكننا بعدها الوصول إليه بتمرير دالة رد نداء callback function للتابع ‎exec()‎. لنبدأ بإنشاء عملية ابن جديدة في نود ولكن أولًا ننشئ مجلد جديد سيحتوي على البرامج التي سنعمل عليها في هذا المقال بالاسم ‎child-processes‎ كالتالي: $ mkdir child-processes وندخل إلى المجلد: $ cd child-processes ننشئ ملف جافاسكربت جديد بالاسم ‎listFiles.js‎ ونفتحه باستخدام أي محرر نصوص: $ nano listFiles.js سنستخدم في هذه الوحدة البرمجية التابع ‎exec()‎ لتنفيذ أمر عرض الملفات والمجلدات ضمن المجلد الحالي ‎ls‎، ومهمة برنامجنا هو قراءة خرج ذلك الأمر وعرضه للمستخدم، لذا نضيف الشيفرة التالية: const { exec } = require('child_process'); exec('ls -lh', (error, stdout, stderr) => { if (error) { console.error(`‎error: ${error.message}‎`); return; } if (stderr) { console.error(`‎stderr: ${stderr}‎`); return; } console.log(`‎stdout:\n${stdout}‎`); }); بدأنا باستيراد التابع ‎exec()‎ من الوحدة ‎child_process‎، ثم استدعيناه بتمرير الأمر الذي نريد تنفيذه كمعامل أول، وهو الأمر ‎ls -lh‎ الذي سيعرض كافة الملفات والمجلدات الموجودة ضمن المجلد الحالي بصيغة مفصلة، وسيعرض وحدة الحجم للملفات بصيغة مقروءة، وسيعرض أيضًا الحجم الكلي لها في أول سطر من الخرج. والمعامل الثاني المُمرر هو دالة رد النداء تقبل ثلاث معاملات، الأول كائن الخطأ ‎error‎ والثاني الخرج القياسي ‎stdout‎ والثالث خرج الخطأ ‎stderr‎، فإذا فشل تنفيذ الأمر سيحتوي المعامل ‎error‎ على كائن خطأ يشرح سبب حدوثه مثلًا عندما لا تعثر الصدفة على الأمر الذي نحاول تنفيذه، وإذا نُفذ الأمر بنجاح سيحتوي المعامل الثاني ‎stdout‎ على البيانات التي تكتب في مجرى الخرج القياسي، أما المعامل الثالث ‎stderr‎ سيمثل مجرى الخطأ القياسي ويحوي على أي بيانات يكتبها الأمر إلى ذلك المجرى. ملاحظة: يوجد فرق بين كائن الخطأ ‎error‎ ومجرى الخطأ ‎stderr‎، فإذا فشل تنفيذ الأمر كليًا سيمثل المعامل ‎error‎ ذلك الخطأ، بينما إذا نُفذ الأمر وكتب هو إلى مجرى الخطأ فيمكننا قراءة أي بيانات تكتب فيه من المعامل ‎stderr‎، ويفضل دومًا معالجة كل احتمالات الخرج الممكنة من كلا هذين المعاملين مع أي عملية ابن. نتحقق داخل دالة رد النداء الممررة من وجود أي خطأ أولًا، فإذا وُجد خطأ سنطبع رسالة الخطأ ‎message‎ وهي الخاصية ضمن كائن الخطأ ‎Error‎ باستدعاء أمر طباعة الخطأ ‎console.error()‎، ثم ننهي تنفيذ التابع مباشرةً باستخدام ‎return‎، وبعدها نتحقق من طباعة الأمر لأي أخطاء تُكتَب ضمن مجرى الخطأ القياسي وإذا وجد نطبع الرسالة وننهي تنفيذ التابع باستخدام ‎return‎ أيضًا، وإلا يكون الأمر قد نُفِّذ بنجاح، ونطبع حينها الخرج إلى الطرفية باستخدام ‎console.log()‎. والآن نخرج من الملف ثم ننفذ البرنامج ونعاين النتيجة، وفي حال كنت تستخدم محرر النصوص نانو ‎nano‎ كما في أمثلتنا يمكنك الخروج منه بالضغط على الاختصار ‎CTRL+X‎، ولتشغيل البرنامج ننفذ الأمر ‎node‎ كالتالي: $ node listFiles.js نحصل على الخرج: stdout: total 4.0K -rw-rw-r-- 1 hassan hassan 280 Jul 27 16:35 listFiles.js وهو محتوى المجلد ‎child-processes‎ مع تفاصيل عن الملفات الموجودة ضمنه، وحجم المجلد الكلي في السطر الأول، وهو ما يدل على تنفيذ البرنامج ‎listFiles.js‎ للأمر ‎ls -lh‎ ضمن الصدفة وقراءة نتيجته وطباعتها بنجاح. والآن سنتعرف على طريقة مختلفة لتنفيذ عملية ما على التوازي مع العملية الحالية، حيث توفر الوحدة ‎child_process‎ التابع ‎execFile()‎ الذي يُمكننا من تشغيل الملفات التنفيذية، والفرق بينه وبين الأمر ‎exec()‎ أن المعامل الأول المُمرر له سيكون مسار الملف التنفيذي الذي نريد تشغيله بدلًا من أمر يراد تنفيذه في الصدفة، وبطريقة مشابهة لعمل التابع ‎exec()‎ سيُخزن ناتج التنفيذ ضمن مخزن مؤقت يمكننا الوصول إليه ضمن دالة رد النداء الممررة، والتي تقبل المعاملات الثلاث نفسها ‎error‎ و ‎stdout‎ و ‎stderr‎. ملاحظة: يجب الانتباه أنه لا يمكن تشغيل الملفات التنفيذية ذات الصيغ ‎.bat‎ و ‎.cmd‎ على ويندوز، وذلك لأن التابع ‎execFile()‎ لا ينشئ الصدفة التي تحتاج إليها تلك الملفات لتشغيلها، بينما على الأنظمة مثل يونكس و لينكس و نظام ماك لا تحتاج الملفات التنفيذية إلى صدفة لتشغيلها، لذا لتنفيذ الملفات التنفيذية على ويندوز يمكن استخدام التابع ‎exec()‎ لأنه سيُنشئ لها صدفة عند التنفيذ، أو يمكن استدعاؤها باستخدام التابع ‎spawn()‎ وهو ما سنتعرف عليه لاحقًا، ولكن الملفات التنفيذية ذات اللاحقة ‎.exe‎ يمكن تشغيلها ضمن ويندوز باستخدام ‎execFile()‎ مباشرةً، حيث أنها لا تحتاج لصدفة لتشغيلها. والآن نبدأ بإنشاء الملف التنفيذي الذي سنحاول تنفيذه باستخدام ‎execFile()‎، حيث سنكتب نصًا برمجيًا ضمن صدفة باش bash مهمته تنزيل صورة شعار بيئة نود من الموقع الرئيسي لها، ثم يعيد ترميز صورة الشعار تلك بصيغة Base64 لنتعامل معها كسلسلة نصية بمحارف ASCII، ونبدأ بإنشاء ملف تنفيذي جديد بالاسم ‎processNodejsImage.sh‎: $ nano processNodejsImage.sh ونضيف إليه الشيفرة التالية لتحميل وتحويل صورة الشعار: #!/bin/bash curl -s https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg > nodejs-logo.svg base64 nodejs-logo.svg التعليمة في السطر الأول تسمى شِبانغ shebang، وتستخدم ضمن أنظمة يونكس ولينكس ونظام ماك لتحديد الصدفة التي نريد تشغيل النص البرمجي أو السكربت ضمنها، والتعليمة التالية هي الأمر ‎curl‎ وهي أداة سطر أوامر تمكننا من نقل البيانات من وإلى الخوادم، ويمكننا الاستفادة منها لتنزيل شعار نود من الموقع الرئيسي له، ثم نعيد توجيه الخرج لحفظ الصورة بعد تنزيلها إلى ملف بالاسم ‎nodejs-logo.svg‎، أما التعليمة الأخيرة تستخدم الأداة ‎base64‎ لإعادة ترميز محتوى ملف الشعار nodejs-logo.svg‎ الذي نزلناه سابقًا، ثم سيُطبَع نتيجة الترميز إلى الطرفية أي مجرى الخرج القياسي وهو خرج تنفيذ النص البرمجي هذا بالكامل. والآن نحفظ الملف ونخرج منه ونضيف إذن تنفيذ هذا النص البرمجي لكي نستطيع تنفيذه كالتالي: $ chmod u+x processNodejsImage.sh يمنح هذا الأمر المستخدم الحالي صلاحية التنفيذ لذلك الملف. يمكننا الآن البدء بكتابة برنامج نود الذي سيُنفذ ذلك النص البرمجي باستخدام التابع ‎execFile()‎ ضمن عملية ابن منفصلة ثم طباعة خرج التنفيذ، لذا نُنشئ ملف جافاسكربت جديد بالاسم ‎getNodejsImage.js‎: $ nano getNodejsImage.js ونكتب الشيفرة التالية: const { execFile } = require('child_process'); execFile(__dirname + '/processNodejsImage.sh', (error, stdout, stderr) => { if (error) { console.error(`‎error: ${error.message}‎`); return; } if (stderr) { console.error(`‎stderr: ${stderr}‎`); return; } console.log(`‎stdout:\n${stdout}‎`); }); استوردنا التابع ‎execFile()‎ من الوحدة ‎child_process‎ واستدعيناه بتمرير مسار ملف النص البرمجي، حيث استفدنا من قيمة الثابت ‎__dirname‎ الذي توفره نود للحصول على مسار المجلد الحالي الذي يحتوي على النص البرمجي، وبذلك يمكن للبرنامج الإشارة إلى النص البرمجي ‎processNodejsImage.sh‎ دومًا مهما كان نظام التشغيل الذي ينفذه أو مكان تنفيذ البرنامج ‎getNodejsImage.js‎ على نظام الملفات، وفي حالتنا يجب أن يكون مكان كل من الملفين ‎getNodejsImage.js‎ و ‎processNodejsImage.sh‎ في نفس المجلد. أما المعامل الثاني المُمرر هو رد نداء ويقبل ثلاثة معاملات، الأول كائن الخطأ ‎error‎ والثاني الخرج القياسي ‎stdout‎ والثالث خرج الخطأ ‎stderr‎، وكما فعلنا سابقًا عند استخدام ‎exec()‎ سنتحقق من حالة وخرج التنفيذ ونطبعها إلى الطرفية. والآن نحفظ الملف ونخرج من محرر النصوص ثم نشغله باستخدام الأمر ‎node‎ كالتالي: $ node getNodejsImage.js لنحصل على الخرج: stdout: PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDQyLjQgMjcwLjkiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE4MC43IiB5MT0iODAuNyIge ... تجاهلنا عرض الخرج كاملًا بسبب حجمه الكبير، ولكن النص البرمجي ‎processNodejsImage.sh‎ نزل الصورة أولًا بعدها أعاد ترميزها بصيغة base64، ويمكن التأكد من ذلك بمعاينة الصورة التي تم تنزيلها والموجودة ضمن المجلد الحالي، ولنتأكد يمكننا تنفيذ البرنامج السابق ‎listFiles.js‎ لمعاينة المحتوى الجديد للمجلد الحالي: $ node listFiles.js سنلاحظ ظهور الخرج التالي: stdout: total 20K -rw-rw-r-- 1 hassan hassan 316 Jul 27 17:56 getNodejsImage.js -rw-rw-r-- 1 hassan hassan 280 Jul 27 16:35 listFiles.js -rw-rw-r-- 1 hassan hassan 5.4K Jul 27 18:01 nodejs-logo.svg -rwxrw-r-- 1 hassan hassan 129 Jul 27 17:56 processNodejsImage.sh بذلك نكون قد نفذنا بنجاح النص البرمجي ‎processNodejsImage.sh‎ ضمن عملية ابن من برنامج نود باستخدام التابع ‎execFile()‎. تعلمنا في هذه الفقرة كيف يمكن للتابعين ‎exec()‎ و ‎execFile()‎ تنفيذ الأوامر ضمن صدفة نظام التشغيل داخل عملية ابن منفصلة في نود، وتوفر نود أيضًا التابع ‎spawn()‎ والذي يشبه في عمله هذين التابعين، ولكن الفرق في عمله أنه لا يقرأ خرج تنفيذ الأمر دفعة واحدة بل على عدة دفعات ضمن مجرى للبيانات stream، وهو ما سنتعرف عليه بالتفصيل في الفقرة التالية. إنشاء عملية ابن باستخدام ‎spawn يمكن استدعاء التابع ‎spawn()‎ لتنفيذ الأوامر ضمن عملية منفصلة والحصول على بيانات الخرج من ذلك الأمر عن طريق الواجهة البرمجية API لمجرى البيانات في نود، وذلك عبر الاستماع لبعض الأحداث المعينة على كائن المجرى لخرج ذلك الأمر. مجاري البيانات streams في نود هي نسخة من صنف مرسل الأحداث event emitter الذي تعرفنا عليه بالتفصيل في المقال التاسع من هذه السلسلة وعندما يكون خرج الأمر الذي سننفذه كبير نسبيًا فيفضل استخدام التابع ‎spawn()‎ بدلًا من التابعين‎exec()‎ و ‎execFile()‎، وذلك لأن التابعين ‎exec()‎ و ‎execFile()‎ سيخزنان خرج الأمر كاملًا ضمن مخزن مؤقت في الذاكرة، ما سيؤثر على أداء النظام، بينما باستعمال المجرى stream يمكننا قراءة البيانات من الخرج ومعالجتها على عدة دفعات، ما يؤدي لخفض استعمال الذاكرة والسماح لنا بمعالجة البيانات الكبيرة. سنتعرف الآن على طريقة استخدام التابع ‎spawn()‎ لإنشاء عملية ابن، لذلك نبدأ بكتابة برنامج في نود مهمته تنفيذ أمر البحث عن الملفات ‎find‎ ضمن عملية ابن لعرض كل الملفات الموجودة ضمن المجلد الحالي، ونبدأ بإنشاء ملف جافاسكربت جديد بالاسم ‎findFiles.js‎: $ nano findFiles.js ونستدعي التابع ‎spawn()‎ لتنفيذ أمر البحث: const { spawn } = require('child_process'); const child = spawn('find', ['.']); بدأنا باستيراد التابع ‎spawn()‎ من الوحدة ‎child_process‎، ثم استدعيناه لإنشاء عملية ابن جديدة يُنفذ ضمنها الأمر ‎find‎ وخزنّا نتيجة تنفيذ التابع ضمن المتغير ‎child‎ للاستماع لاحقًا إلى الأحداث الذي ستطلقها العملية الابن، ونلاحظ تمرير الأمر الذي نريد تنفيذه ‎find‎ كمعامل أول للتابع ‎spawn()‎، أما المعامل الثاني فهو مصفوفة من المعاملات التي نريد تمريرها لذلك الأمر، ويكون الأمر النهائي الذي سينفذ هو أمر البحث ‎find‎ مع تمرير المعامل ‎.‎ للدلالة على البحث عن كل الملفات الموجودة ضمن المجلد الحالي، أي شكل الأمر المنفذ النهائي هو ‎find .‎. وسابقًا عند استخدام التابعين ‎exec()‎ و ‎execFile()‎ مررنا لهما شكل الأمر الذي نريد تنفيذه بصيغته النهائية ضمن السلسلة النصية، أما عند استدعاء ‎spawn()‎ فيجب تمرير المعاملات للأمر المُنفذ ضمن مصفوفة، وذلك لأن هذا التابع لا يُنشئ صَدفة جديدة قبل إنشاء وتشغيل العملية، أما إذا أردنا تمرير المعاملات مع الأمر بنفس السلسلة النصية يجب إنشاء صَدفة جديدة لتفسر ذلك. ولنكمل معالجة تنفيذ الأمر بإضافة توابع استماع للخرج كالتالي: const { spawn } = require('child_process'); const child = spawn('find', ['.']); child.stdout.on('data', data => { console.log(`‎stdout:\n${data}‎`); }); child.stderr.on('data', data => { console.error(`‎stderr: ${data}‎`); }); كما ذكرنا سابقًا يمكن للأوامر كتابة الخرج على كل من مجرى الخرج القياسي ‎stdout‎ ومجرى خرج الأخطاء ‎stderr‎، لذا يجب إضافة من يستمع لهما على كل مجرى باستخدام التابع ‎on()‎ ونطبع البيانات التي تُرسل ضمن ذلك الحدث إلى الطرفية. نستمع بعدها للحدث ‎error‎ الذي سيُطلق في حال فشل تنفيذ الأمر، والحدث ‎close‎ الذي سيُطلق بعد انتهاء تنفيذ الأمر وإغلاق المجرى، ونكمل الآن كتابة البرنامج ليصبح كالتالي: const { spawn } = require('child_process'); const child = spawn('find', ['.']); child.stdout.on('data', (data) => { console.log(`‎stdout:\n${data}‎`); }); child.stderr.on('data', (data) => { console.error(`‎stderr: ${data}‎`); }); child.on('error', (error) => { console.error(`‎error: ${error.message}‎`); }); child.on('close', (code) => { console.log(`‎child process exited with code ${code}‎`); }); لاحظ أن الاستماع لكل من الحدثين ‎error‎ و ‎close‎ يكون على كائن العملية ‎child‎ مباشرةً، ولاحظ ضمن حدث الخطأ ‎error‎ أنه يوفر لنا كائن خطأ ‎Error‎ يعبر عن المشكلة، وفي تلك الحالة سنطبع رسالة الخطأ ‎message‎ إلى الطرفية، أما ضمن حدث الإغلاق ‎close‎ تمرر نود رمز الخروج للأمر بعد تنفيذه، ومنه يمكننا معرفة نجاح أو فشل تنفيذ الأمر، فعند نجاح التنفيذ سيعيد الأمر الرمز صفر ‎0‎ وإلا سيعيد رمز خروج أكبر من الصفر. والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج باستخدام الأمر ‎node‎: $ node findFiles.js ونحصل على الخرج: stdout: . ./findFiles.js ./listFiles.js ./nodejs-logo.svg ./processNodejsImage.sh ./getNodejsImage.js child process exited with code 0 يظهر لنا قائمة بكافة الملفات الموجودة ضمن المجلد الحالي، وفي آخر سطر يظهر رمز الخروج ‎0‎ ما يدل على نجاح التنفيذ، ومع أن الملفات ضمن المجلد الحالي قليلة لكن في حال نفذنا نفس الأمر ضمن مجلد آخر قد يظهر لنا قائمة طويلة جدًا من الملفات الموجودة ضمن كل المجلدات التي يمكن للمستخدم الوصول إليها، ولكن وبما أننا استخدمنا التابع ‎spawn()‎ فلا مشكلة في ذلك حيث سنعالج الخرج بأفضل طريقة ممكنة باستخدام مجاري البيانات بدلًا من تخزين الخرج كاملًا في الذاكرة ضمن مخزن مؤقت. وبذلك نكون قد تعلمنا طرق إنشاء عملية ابن في نود لتنفيذ الأوامر الخارجية ضمن نظام التشغيل، وتتيح نود أيضًا طريقة لإنشاء عملية ابن لتنفيذ برامج نود أخرى، وذلك باستعمال التابع ‎fork()‎ وهو ما سنتعرف عليه في الفقرة التالية. إنشاء عملية ابن باستخدام ‎fork تتيح نود التابع ‎fork()‎ المشابه للتابع ‎spawn()‎ لإنشاء عملية جديدة ابن لتنفيذ برنامج نود ما، وما يميز التابع ‎fork()‎ عن التوابع الأخرى مثل ‎spawn()‎ أو ‎exec()‎ هو إمكانية التواصل بين العملية الأب والابن، إذ إضافة لقراءة خرج الأمر الذي ننفذه باستخدام ‎fork()‎ يمكن للعملية الأب إرسال رسائل للعملية الابن والتواصل معها، ويمكن للعملية الابن أيضًا التواصل مع العملية الأب بنفس الطريقة. وسنتعرف في هذا المثال على طريقة إنشاء عملية ابن باستخدام ‎fork()‎ والاستفادة منها في تحسين أداء التطبيق الذي نطوره، حيث وبما أن البرامج في نود تعمل ضمن عملية واحدة فالمهام التي تحتاج لمعالجة طويلة من من قبل المعالج ستعيق عمل الشيفرات التالية في باقي البرنامج، فمثلًا المهام التي تتطلب تكرار تنفيذ حلقة برمجية ما لمرات عديدة طويلة، أو تفسير ملفات كبيرة من صيغة JSON، حيث ستشكل هذه العمليات عائقًا في بعض التطبيقات وتؤثر على الأداء، فمثلًا لا يمكن لخادم ويب أن تعيق عمله مثل تلك المهام الطويلة، حيث سيمنعه ذلك من استقبال الطلبات الجديدة ومعالجتها لحين الانتهاء من تنفيذ تلك المهام، لذا سنختبر ذلك بإنشاء خادم ويب يحتوي على مسارين الأول سيُنفذ عملية تحتاج لمعالجة طويلة وتعيق عمل عملية نود للخادم، والثاني سيعيد كائن بصيغة JSON يحوي على الرسالة hello‎. لنبدأ بإنشاء ملف جافاسكربت جديد لخادم HTTP بالاسم ‎httpServer.js‎ كالتالي: $ nano httpServer.js نبدأ بإعداد الخادم أولًا باستيراد الوحدة البرمجية ‎http‎ ثم إنشاء تابع استماع لمعالجة الطلبات الواردة، وكائن للخادم وربط تابع الاستماع معه، والآن نضيف الشيفرة التالية إلى الملف: const http = require('http'); const host = 'localhost'; const port = 8000; const requestListener = function (req, res) {}; const server = http.createServer(requestListener); server.listen(port, host, () => { console.log(`‎Server is running on http://${host}:${port}‎`); }); سيكون الخادم متاحًا للوصول على العنوان ‎http://localhost:8000‎، والآن سنكتب دالة مهمتها إعاقة عمل الخادم عبر حلقة ستُنفذ لعدد كبير من المرات، ونضيفها قبل التابع ‎requestListener()‎ كالتالي: ... const port = 8000; const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; } const requestListener = function (req, res) {}; ... وضمن تابع معالجة الطلب ‎requestListener()‎ سنستدعي تابع الإعاقة ‎slowFunction()‎ على المسار الفرعي، بينما سنعيد رسالة JSON على المسار الآخر كالتالي: ... const requestListener = function (req, res) { if (req.url === '/total') { let slowResult = slowFunction(); let message = `‎{"totalCount":${slowResult}}‎`; console.log('Returning /total results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(message); } else if (req.url === '/hello') { console.log('Returning /hello results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(`‎{"message":"hello"}‎`); } }; ... إذا تواصلنا مع الخادم على المسار الفرعي ‎/total‎ سيُنفذ تابع الإعاقة ‎slowFunction()‎، أما على المسار الفرعي ‎/hello‎ سنعيد الرسالة التالية بصيغة JSON ‎{"message":"hello"}‎، والآن نحفظ الملف ونخرج منه ثم نشغل الخادم باستخدام الأمر ‎node‎ كالتالي: $ node httpServer.js ليظهر لنا الرسالة التالية ضمن الخرج: Server is running on http://localhost:8000 يمكننا بدء الاختبار الآن ولهذا نحتاج لطرفيتين إضافيتين، ففي الأولى سنستخدم الأمر ‎curl‎ لإرسال طلب للخادم على المسار ‎/total‎ لإبطاء الخادم كالتالي: $ curl http://localhost:8000/total وضمن الطرفية الثانية نستخدم الأمر ‎curl‎ لإرسال طلب على المسار الآخر ‎/hello‎ كالتالي: $ curl http://localhost:8000/hello سيعيد الطلب الأول القيمة التالية: {"totalCount":5000000000} بينما سيعيد الطلب الثاني القيمة: {"message":"hello"} ونلاحظ أن الطلب الثاني للمسار ‎/hello‎ اكتمل بعد انتهاء معالجة الطلب على المسار ‎/total‎، حيث أعاق تنفيذ التابع ‎slowFunction()‎ معالجة أي طلبات وتنفيذ أي شيفرات على الخادم لحين انتهائه، ويمكننا التأكد من ذلك من خرج طرفية الخادم نفسه حيث نلاحظ ترتيب إرسال الرد على تلك الطلبات: Returning /total results Returning /hello results في مثل تلك الحالات يأتي دور التابع ‎fork()‎ لإنشاء عملية ابن جديدة يمكن توكيل معالجة المهام الطويلة إليها للسماح للخادم بالعمل على معالجة الطلبات الجديدة القادمة دون توقف، وسنطبق ذلك في مثالنا بنقل تابع المهمة الطويلة إلى وحدة برمجية منفصلة، حيث سيستدعيها خادم الويب لاحقًا ضمن عملية ابن منفصلة عند كل طلب إلى المسار الفرعي ‎/total‎ ويستمع إلى نتيجة التنفيذ. نبدأ بإنشاء ملف جافاسكربت بالاسم ‎getCount.js‎ سيحوي على التابع ‎slowFunction()‎: $ nano getCount.js ونضيف داخله ذلك التابع: const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; } وبما أننا ننوي استدعاء هذا التابع كعملية ابن باستخدام ‎fork()‎ يمكننا إضافة شيفرة للتواصل مع العملية الأب تعلمه عند انتهاء تنفيذ التابع ‎slowFunction()‎، لهذا نضيف الشيفرة التالية التي سترسل رسالة للعملية الأب تحوي على كائن JSON لنتيجة التنفيذ ولإرسالها إلى المستخدم: const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; } process.on('message', (message) => { if (message == 'START') { console.log('Child process received START message'); let slowResult = slowFunction(); let message = `‎{"totalCount":${slowResult}}‎`; process.send(message); } }); كما نلاحظ بإمكاننا الوصول للرسائل التي يُنشئها التابع ‎fork()‎ بين العملية الأب والابن عن طريق القيمة العامة للكائن ‎process‎ الذي يمثل العملية، حيث يمكننا إضافة مُستمع لحدث إرسال الرسائل ‎message‎ والتحقق ما إذا كانت الرسالة هي حدث بدء عملية المعالجة ‎START‎ الذي سيرسلها الخادم عند ورود طلب إلى المسار الفرعي ‎/total‎، ونستجيب لتلك الرسالة بتنفيذ تابع المعالجة ‎slowFunction()‎ ثم ننشئ السلسلة النصية للرد بصيغة JSON والتي تحوي على نتيجة التنفيذ، ثم نستدعي التابع ‎process.send()‎ لإرسال رسالة للعملية الأب تعلمه بالنتيجة. والآن نحفظ الملف ونخرج منه ونعود لملف الخادم ‎httpServer.js‎ للتعديل عليه وإضافة استدعاء للتابع ‎slowFunction()‎ بإنشاء عملية ابن لتنفيذ البرنامج ضمن الملف ‎getCount.js‎، فنبدأ باستيراد التابع ‎fork()‎ من الوحدة البرمجية ‎child_process‎ كالتالي: const http = require('http'); const { fork } = require('child_process'); ... ثم نزيل التابع ‎slowFunction()‎ من هذا الملف بما أننا نقلناه إلى وحدة برمجية منفصلة، ونعدل تابع معالجة الطلبات ‎requestListener()‎ ليُنشئ العملية الابن كالتالي: ... const port = 8000; const requestListener = function (req, res) { if (req.url === '/total') { const child = fork(__dirname + '/getCount'); child.on('message', (message) => { console.log('Returning /total results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(message); }); child.send('START'); } else if (req.url === '/hello') { console.log('Returning /hello results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(`‎{"message":"hello"}‎`); } }; ... ينتج الآن عن الطلبات الواردة إلى المسار ‎/total‎ إنشاء عملية ابن باستخدام ‎fork()‎، حيث مررنا لهذا التابع مسار وحدة نود البرمجية التي نريد تنفيذها، وهو الملف ‎getCount.js‎ في حالتنا ضمن المجلد الحالي، لهذا استفدنا هذه المرة أيضًا من قيمة المتغير ‎__dirname‎، وخزنا قيمة العملية الابن ضمن المتغير ‎child‎ للتعامل معها. أضفنا بعدها مستمعًا إلى الكائن ‎child‎ ليستقبل الرسائل الواردة من العملية الابن، وتحديدًا لاستقبال الرسالة التي سيرسلها تنفيذ الملف ‎getCount.js‎ الحاوية على سلسلة نصية بصيغة JSON لنتيجة تنفيذ حلقة ‎while‎، وعند وصول تلك الرسالة نرسلها مباشرة إلى المستخدم كما هي. ويمكننا التواصل مع العملية الابن باستدعاء التابع ‎send()‎ من الكائن ‎child‎ لإرسال رسالة لها، حيث نرسل الرسالة ‎START‎ التي سيستقبلها البرنامج ضمن العملية الابن لينفذ التابع ‎slowFunction()‎ داخله استجابة لها. والآن نحفظ الملف ونخرج منه ونختبر الميزة التي قدمها استخدام ‎fork()‎ لخادم HTTP بتشغيل الخادم من ملف ‎httpServer.js‎ باستخدام الأمر ‎node‎ كالتالي: $ node httpServer.js وسيظهر لنا الخرج التالي: Server is running on http://localhost:8000 وكما فعلنا سابقًا لاختبار عمل الخادم سنحتاج لطرفيتين، ففي الأولى سنستخدم الأمر ‎curl‎ لإرسال طلب للخادم على المسار ‎/total‎ والذي سيحتاج بعض الوقت للاكتمال: $ curl http://localhost:8000/total وضمن الطرفية الثانية نستخدم الأمر ‎curl‎ لإرسال طلب على المسار الآخر ‎/hello‎ والذي سيرسل لنا الرد هذه المرة بسرعة: $ curl http://localhost:8000/hello سيعيد الطلب الأول القيمة التالية: {"totalCount":5000000000} بينما سيعيد الطلب الثاني القيمة: {"message":"hello"} نلاحظ الفرق هذه المرة بأن الطلب للمسار ‎/hello‎ تم بسرعة، ويمكننا التأكد من ذلك أيضًا من الرسائل الظاهرة في طرفية الخادم: Child process received START message Returning /hello results Returning /total results حيث يظهر أن الطلب على المسار ‎/hello‎ تم استقباله بعد إنشاء العملية الابن ومعالجته قبل انتهاء عملها، وهذا بسبب نقل العملية التي تأخذ وقتا طويلًا إلى عملية ابن منفصلة واستدعائها باستخدام ‎fork()‎، حيث بقي الخادم متفرغًا لمعالجة الطلبات الجديدة الواردة وتنفيذ شيفرات جافاسكربت، وهذا بفضل الميزة الذي يوفرها التابع ‎fork()‎ من إرسال الرسائل إلى العملية الابن للتحكم بتنفيذ العمليات ضمنها، وإمكانية قراءة البيانات المُرسلة من قبل العملية لمعالجتها ضمن العملية الأب. ختامًا تعرفنا في هذا المقال على طرق مختلفة لإنشاء عملية ابن في نود، حيث تعلمنا كيف يمكن استخدام التابع ‎exec()‎ لإنشاء عملية ابن جديدة لتنفيذ أوامر الصدفة من قبل شيفرة برنامج نود، وبعدها تعرفنا على التابع ‎execFile()‎ الذي يُمكننا من تشغيل الملفات التنفيذية، ثم تعرفنا على التابع ‎spawn()‎ الذي يسمح بتنفيذ الأوامر وقراءة نتيجتها عبر مجرى للبيانات دون إنشاء صدفة لها كما يفعل التابعان ‎exec()‎ و ‎execFile()‎، وأخيرًا تعرفنا على التابع ‎fork()‎ الذي يسمح بالتواصل بين العملية الأب والابن. ويمكنك الرجوع إلى التوثيق الرسمي للوحدة البرمجية ‎child_process‎ من نود للتعرف عليها أكثر. ترجمة -وبتصرف- للمقال How To Launch Child Processes in Node.js لصاحبه Stack Abuse. اقرأ أيضًا المقال السابق: تنقيح أخطاء Node.js باستخدام المنقح debugger وأدوات المطور DevTools مفهوم الخيوط Threads في عملية المعالجة تسلسل العمليات الهرمي واستدعاءات النظام Fork و Exec في نظام تشغيل الحاسوب مدخل إلى صدفة باش Bash دليل ميسر لكتابة سكربتات shell
  15. عملية تتبع أخطاء البرامج لمعرفة مصدر المشكلة في نود Node.js خلال مرحلة التطوير توفر على المطور الكثير من وقت تطوير المشروع، وتزداد صعوبة تلك المهمة مع كبر حجم المشروع وزيادة تعقيده، وهنا يأتي دور مُنقِّح الأخطاء debugger ليساعد في ذلك، وهو برنامج يسمح للمطور بمعاينة البرنامج أثناء تشغيله عبر تنفيذ الشيفرة سطرًا تلو الآخر ومعاينة حالة التطبيق وتغيرها، مما يوفر للمبرمج نظرة أقرب على طريقة عمل البرنامج ما يسهل العثور على الأخطاء وإصلاحها. وعادة ما يضيف المطورون تعليمات الطباعة داخل شيفرة البرنامج لمعاينة بعض القيم أثناء تشغيله، حيث يضيف المطور في نود تعليمات طباعة مثل ‎console.log()‎ و ‎console.debug()‎، ومع أن هذه الطريقة سهلة وسريعة لكنها تبقى يدوية ولا تخدم دومًا في الحالات المعقدة أو عندما يكون التطبيق كبيرًا، فقد ينسى أحيانًا المطور بعض تعليمات الطباعة تلك ما قد يؤدي لطباعة معلومات خاصة وحساسة عن التطبيق يجعله عرضة للاختراق، وهنا يوفر لنا المنقح طريقة أفضل لمراقبة البرنامج أثناء التشغيل دون أن يُعرّض البرنامج لمثل تلك الأخطار. وأهم ميزتين في منقح الأخطاء الداخلي هما مراقبة الكائنات، وإضافة نقاط الوقوف breakpoints، حيث تتيح مراقبة الكائنات طريقة لمشاهدة التغير في حالة المتغيرات أثناء تنفيذ البرنامج خطوة بخطوة، أما نقاط الوقوف فهي أماكن ضمن الشيفرة يمكن للمبرمج تحديدها ليتوقف البرنامج عن التنفيذ مؤقتًا عند الوصول إليها، ليعطي فرصة للمبرمج لمعاينة حالة البرنامج في تلك اللحظة. سنتعلم في هذا المقال طريقة استخدام المنقح لاستكشاف الأخطاء ضمن بعض البرامج في نود، حيث سنستخدم بدايةً أداة تنقيح الأخطاء الداخلية في نود ونتعلم طريقة إعداد المراقبة للمتغيرات وإضافة نقاط التوقف لنتمكن من اكتشاف المشاكل وإصلاحها، ثم سنتعلم استخدام واجهة أداة المطور في متصفح جوجل كروم بدلًا من التعامل مع المنقح من سطر الأوامر. المستلزمات هذا المقال جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته: تثبيت بيئة Node.js على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.19.0. معرفة بأساسيات جافاسكربت والتعامل مع الدوال. تثبيت متصفح جوجل كروم أو متصفح كروميوم مفتوح المصدر. استخدام الراصدات Watchers مع المنقح Debugger الميزتين الأساسيتين لمنقح الأخطاء هما مراقبة المتغيرات وتغير قيمها أثناء التنفيذ، وميزة الإيقاف المؤقت لعمل البرنامج عند أماكن محددة من الشيفرة باستخدام نقاط الوقوف، وسنتعلم في هذه الفقرة طريقة مراقبة المتغيرات لتساعدنا في اكتشاف الأخطاء. تساعدنا عملية مراقبة المتغيرات ورصدها في فهم كيفية تغير قيم تلك المتغيرات أثناء تنفيذ البرنامج، وسنستفيد من هذه الميزة في اكتشاف الأخطاء في منطق عمل البرنامج وإصلاحها، وسنبدأ بإنشاء مجلد جديد بالاسم ‎debugging‎ سيحوي على البرامج التي سنتعامل معها: $ mkdir debugging وندخل إلى المجلد: $ cd debugging ننشئ داخله ملف جافاسكربت جديد بالاسم ‎badLoop.js‎ ونفتحه ضمن أي محرر نصوص، حيث سنستخدم في أمثلتنا محرر نانو ‎nano‎ كالتالي: $ nano badLoop.js سنكتب برنامجًا يمر على عناصر المصفوفة ويجمع قيمها لحساب المجموع الكلي لها، حيث تمثل تلك الأرقام عدد الطلبات اليومي لمتجر خلال فترة أسبوع، حيث سيطبع البرنامج المجموع الكلي للأرقام في تلك المصفوفة، ليكون البرنامج كالتالي: let orders = [341, 454, 198, 264, 307]; let totalOrders = 0; for (let i = 0; i <= orders.length; i++) { totalOrders += orders[i]; } console.log(totalOrders); أنشأنا بداية مصفوفة الطلبات ‎orders‎ والتي تحوي خمسة أعداد، ثم أنشأنا متغير المجموع الكلي للطلبات ‎totalOrders‎ وضبطنا قيمته الأولية إلى الصفر ‎0‎، حيث سنخزن ضمنه المجموع الكلي للأرقام السابقة، ومررنا ضمن حلقة ‎for‎ على عناصر المصفوفة ‎orders‎ وأضفنا كل قيمة منها إلى متغير المجموع الكلي ‎totalOrders‎، ثم أخيرًا طبعنا قيمة المجموع الكلي. والآن نحفظ الملف ونخرج منه وننفذ البرنامج ونعاين النتيجة: $ node badLoop.js يظهر لنا الخرج التالي: NaN القيمة ‎NaN‎ في جافاسكربت هي اختصار لجملة "ليس عددًا" أو "Not a Number"، ولكن كيف حصلنا على تلك القيمة مع أن المصفوفة لا تحوي سوى قيم عددية؟ الطريقة الأفضل لمعرفة سبب المشكلة هي استخدام منقح الأخطاء، وهنا سنبدأ بالتعرف على منقح نود ونستخدمه رصد قيمة كل من المتغيرين ‎totalOrders‎ و ‎i‎ ضمن حلقة ‎for‎، ولتشغيله نضيف خيار ‎inspect‎ قبل اسم الملف عند تشغيله بواسطة الأمر ‎node‎ كالتالي: $ node inspect badLoop.js سنلاحظ ظهور الخرج التالي: < Debugger listening on ws://127.0.0.1:9229/e1ebba25-04b8-410b-811e-8a0c0902717a < For help, see: https://nodejs.org/en/docs/inspector < Debugger attached. Break on start in badLoop.js:1 > 1 let orders = [341, 454, 198, 264, 307]; 2 3 let totalOrders = 0; يحتوي السطر الأول من الخرج على رابط خادم تنقيح الأخطاء، حيث تستفيد منه أدوات تنقيح الأخطاء الخارجية مثل متصفح الويب للتواصل مع خادم التنقيح الخاص بنود وهو ما سنتعامل معه لاحقًا، وافتراضيًا يكون هذا الخادم متاحًا على المنفذ ‎:9229‎ والعنوان المحلي ‎localhost‎ أو ‎127.0.0.1‎، ويفضل منع الوصول لهذا المنفذ من الشبكة الخارجية والوصول إليه من الجهاز محليًا فقط. وبعد ربط منقح الأخطاء ستظهر الرسالة ‎Break on start in badLoop.js:1‎ والتي تعني توقف التنفيذ عند أول سطر من الملف، حيث يمكن وضع نقاط الوقوف ضمن الشيفرة لتحديد مكان توقف التنفيذ وكما لاحظنا فمنقح الأخطاء يتوقف افتراضيًا عند أول سطر من الملف دومًا ويُظهر لنا مقطع من الشيفرة عند مكان التوقف وبعده سطر جديد يبدأ بالكلمة ‎debug‎ يمكننا كتابة الأوامر ضمنه: ... > 1 let orders = [341, 454, 198, 264, 307]; 2 3 let totalOrders = 0; debug> نلاحظ وجود الرمز ‎>‎ بجانب رقم السطر الأول ‎1‎ وهو دلالة على مكان توقف التنفيذ الحالي، ويُظهر السطر الأخير استعداد منقح الأخطاء لتلقي الأوامر، حيث يمكننا مثلًا تنفيذ أمر لتوجيهه لتقديم عملية التنفيذ خطوة إلى الأمام والذهاب إلى السطر التالي من التنفيذ، ويمكن إدخال أحد الأوامر التالية: ‎c‎ أو ‎cont‎: لإكمال عملية التنفيذ حتى الوصول إلى نقطة الوقوف التالية أو حتى الانتهاء من تنفيذ البرنامج. ‎n‎ أو ‎next‎: للتقدم خطوة إلى الأمام في التنفيذ إلى السطر التالي من الشيفرة. ‎s‎ أو ‎step‎: للدخول إلى دالة ما، حيث تكون عملية التقدم افتراضيًا ضمن النطاق scope الذي نصحح الأخطاء ضمنه فقط، وتمكننا هذه العملية من الدخول ضمن دالة استدعتها الشيفرة التي نفحصها لمعاينة عملها من الداخل ومراقبة تعاملها مع البيانات المُمررة لها. ‎o‎: للخروج من دالة حيث سيعود التنفيذ لخارجها إلى مكان استدعائها، وهو المكان الذي ستُرجع قيمة تنفيذ الدالة إليه، حيث يفيد هذا الأمر في العودة مباشرةً إلى خارج الدالة إلى المكان الذي كنا نعاينه قبل الدخول إليها. ‎pause‎: لإيقاف التنفيذ مباشرةً مؤقتًا. لنتقدم بتنفيذ البرنامج سطرًا تلو الآخر بتنفيذ الأمر ‎n‎ للانتقال إلى السطر التالي: debug> n نلاحظ تقدم التنفيذ إلى السطر الثالث: break in badLoop.js:3 1 let orders = [341, 454, 198, 264, 307]; 2 > 3 let totalOrders = 0; 4 5 for (let i = 0; i <= orders.length; i++) { يتم تجاوز الأسطر الفارغة، لذا إذا قدّمنا علمية التنفيذ سطرًا آخر الآن بتنفيذ الأمر ‎n‎ مجددًا سينتقل التنفيذ إلى السطر الخامس: break in badLoop.js:5 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } يوقف التنفيذ الآن في بداية الحلقة، وإذا كانت الطرفية تدعم إظهار الألوان في الخرج سنلاحظ تحديد القيمة ‎0‎ ضمن التعليمة ‎let i = 0‎، حيث يحدد المنقح أي قسم من الشيفرة على وشك التنفيذ، ففي الحلقة ‎for‎ أول ما ينفذ هو إسناد القيمة لعداد الحلقة، وسنبدأ هنا بمعاينة القيم للمتغيرات لنحدد سبب الحصول على القيمة ‎NaN‎ بدلًا من القيمة العددية لمتغير المجموع ‎totalOrders‎، حيث أن قيمتي المتغيرين ‎totalOrders‎ و ‎i‎ تتغيران عند كل دورة للحلقة، وسنستفيد من ميزة الرصد والمراقبة التي يوفرها المنقح في مراقبة قيم هذين المتغيرين. نبدأ بإعداد المراقبة لمتغير المجموع الكلي ‎totalOrders‎ بتنفيذ التعليمة التالية: debug> watch('totalOrders') لمراقبة أي متغير خلال تنقيح الأخطاء نستدعي الدالة ‎watch()‎ الذي يوفرها المنقح ونمرر لها سلسلة نصية تحوي على اسم المتغير الذي نريد مراقبته، وبعد الضغط على زر الإدخال ‎ENTER‎ وتنفيذ الدالة ‎watch()‎ سينتقل التنفيذ إلى سطر جديد دون ظهور أي خرج، وستظهر القيم التي نراقبها عند الانتقال للسطر التالي. لنراقب أيضًا المتغير الآخر ‎i‎ بنفس الطريقة: debug> watch('i') سنشاهد الآن عملية المراقبة للمتغيرات السابقة، ننفذ الأمر ‎n‎ للانتقال خطوة للأمام وسيظهر لنا التالي: break in badLoop.js:5 Watchers: 0: totalOrders = 0 1: i = 0 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } نلاحظ ظهور قيم المتغيرين اللذين نراقبهما ‎totalOrders‎ و ‎i‎ قبل الشيفرة حيث سيتم تحديث هذه القيم عند تغيرها، ونلاحظ أن المنقح يحدد حاليًا الخاصية ‎length‎ من التعليمة ‎orders.length‎، ما يعني أن الخطوة التالية هي التحقق من شرط إكمال التنفيذ للحلقة قبل إعادة تنفيذ التعليمات في جسم الحلقة، وبعدها ستُنفذ تعليمة زيادة قيمة عداد الحلقة ‎i++‎. والآن نتقدم خطوة للأمام بتنفيذ الأمر ‎n‎ مجددًا للدخول إلى جسم الحلقة: break in badLoop.js:6 Watchers: 0: totalOrders = 0 1: i = 0 4 5 for (let i = 0; i <= orders.length; i++) { > 6 totalOrders += orders[i]; 7 } 8 ستُعدِّل التعليمة الحالية من قيمة المتغير ‎totalOrders‎، وسنلاحظ ذلك من تغير تلك القيمة ضمن قسم المراقبة في الأعلى. والآن نتقدم خطوة إلى الأمام بتنفيذ ‎n‎ ليظهر لنا ما يلي: Watchers: 0: totalOrders = 341 1: i = 0 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } نلاحظ أن قيمة متغير المجموع الكلي ‎totalOrders‎ تساوي قيمة أول عنصر من المصفوفة ‎341‎، والخطوة التالية الآن هي التحقق من شرط إكمال تنفيذ الحلقة، لذا ننفذ الأمر ‎n‎ لتعديل قيمة عداد الحلقة ‎i‎: break in badLoop.js:5 Watchers: 0: totalOrders = 341 1: i = 1 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } إلى الآن قد تقدمنا عدة خطوات يدويًا ضمن الشيفرة لمراقبة التغير في قيم المتغيرات، لكن تلك الطريقة غير عملية حيث سنتعرف في الفقرة التالية على حل لهذه المشكلة باستخدام نقاط الوقوف، وسنُكمل حاليًا العمل بتقديم عملية التنفيذ يدويًا ومراقبة قيم المتغيرات للعثور على سبب المشكلة. والآن نتقدم في التنفيذ 12 خطوة للأمام لنلاحظ الخرج التالي: break in badLoop.js:5 Watchers: 0: totalOrders = 1564 1: i = 5 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } عدد القيم ضمن المصفوفة ‎orders‎ هو خمسة، ولكن قيمة عداد الحلقة ‎i‎ الحالية هي ‎5‎، وبما أننا نستخدم قيمة المتغير ‎i‎ للوصول إلى العنصر ضمن المصفوفة بالترتيب الحالي فالقيمة عند الترتيب ‎orders[5]‎ غير موجودة، وترتيب آخر قيمة ضمن المصفوفة ‎orders‎ هو ‎4‎، ما يعني أن محاولة الوصول للعنصر السادس باستخدام ‎orders[5]‎ سيعيد القيمة ‎undefined‎. والآن نتقدم بالتنفيذ خطوة للأمام بتنفيذ الأمر ‎n‎: break in badLoop.js:6 Watchers: 0: totalOrders = 1564 1: i = 5 4 5 for (let i = 0; i <= orders.length; i++) { > 6 totalOrders += orders[i]; 7 } 8 وبالتقدم خطوة إضافية بتنفيذ ‎n‎ نلاحظ القيمة الجديدة للمتغير ‎totalOrders‎: break in badLoop.js:5 Watchers: 0: totalOrders = NaN 1: i = 5 3 let totalOrders = 0; 4 > 5 for (let i = 0; i <= orders.length; i++) { 6 totalOrders += orders[i]; 7 } لاحظنا بالاستفادة من عملية تنقيح الشيفرة ومراقبة قيم المتغيرين ‎totalOrders‎ و ‎i‎ أن الحلقة تُنفَّذ ستة مرات بدلًا من خمسة، وعندما تكون قيمة عداد الحلقة ‎i‎ هي ‎5‎ فمحاولة الوصول للعنصر الحالي ‎orders[5]‎ وإضافته للمتغير ‎totalOrders‎ ستجعل من قيمة المجموع تساوي ‎NaN‎، لأن قيمة العنصر السادس ‎orders[5]‎ الغير موجود ستكون ‎undefined‎، فإذًا المشكلة هي في شرط الحلقة ‎for‎ فبدلًا من التحقق من أن قيمة العداد ‎i‎ هي أصغر أو تساوي طول المصفوفة ‎orders‎ يجب أن نتحقق من أنها أصغر من الطول فقط. وبعد أن حددنا المشكلة نخرج من المنقح ونصحح الخطأ ضمن الشيفرة ونعيد تنفيذ البرنامج ونتحقق من النتيجة، لكن أولًا ننفذ أمر الخروج ‎.exit‎ ثم نضغط زر الإدخال ‎ENTER‎: debug> .exit نخرج بذلك من وضع المنقح ونعود إلى الملف ‎badLoop.js‎ ونفتحه ضمن محرر النصوص ونعدل شرط حلقة ‎for‎ كالتالي: ... for (let i = 0; i < orders.length; i++) { ... نحفظ الملف ونخرج منه ونشغل البرنامج: $ node badLoop.js سنلاحظ ظهور قيمة المجموع الصحيحة ونكون بذلك حللنا المشكلة: 1564 نكون بذلك قد تعلمنا طريقة استخدام المنقح ودالة مراقبة المتغيرات ‎watch‎ الخاصة به لاستكشاف وتحديد الأخطاء أثناء التنفيذ! وسنتعلم الآن في الفقرة التالية كيف يمكننا الاستفادة من نقاط الوقوف لتنقيح الأخطاء ضمن البرنامج دون الحاجة لتقديم التنفيذ يدويًا سطرًا تلو الآخر. استخدام نقاط الوقوف Breakpoints تتألف البرامج في نود عادة من عدة وحدات برمجية يتشابك عملها مع بعضها بعضًا، لذا محاولة تنقيح الأخطاء سطرًا تلو الآخر كما فعلنا في الفقرة السابقة أمر صعب وغير مجدي في التطبيقات الكبيرة المعقدة، وهنا يأتي دور نقاط الوقوف breakpoints لحل تلك المشكلة. تسمح نقاط الوقوف بتخطي التنفيذ إلى السطر الذي نريده مباشرةً وإيقاف البرنامج لمعاينة حالته آنذاك، حيث لإضافة نقطة وقوف في نود نضيف الكلمة المحجوزة ‎debugger‎ ضمن الشيفرة مباشرةً، ويمكننا بعدها وخلال عملية التنقيح التنقل بين نقاط الوقوف ضمن الشيفرة بتنفيذ الأمر ‎c‎ في طرفية التنقيح بدلًا من الأمر ‎n‎ السابق، ويمكننا إضافة المراقبة للتعليمات التي نرغب بها عند نقاط الوقوف تلك. سنتعرف على طريقة استخدام نقاط الوقوف بمثال عن برنامج يقرأ قائمة من الجمل ويستخرج منها الكلمة الأكثر تكرارًا ويعيدها لنا، لذلك سنُنشئ لهذا المثال ثلاث ملفات، الأول هو ملف يحوي الجمل النصية ‎sentences.txt‎ التي سيعالجها البرنامج، حيث سنضيف داخله كمثال أول فقرة من مقال عن سمكة قرش الحوت من موسوعة بريتانيكا Britannica بعد إزالة علامات الترقيم منها، لذلك ننشئ الملف ونفتحه ضمن محرر النصوص: $ nano sentences.txt ونكتب داخله النص التالي: Whale shark Rhincodon typus gigantic but harmless shark family Rhincodontidae that is the largest living fish Whale sharks are found in marine environments worldwide but mainly in tropical oceans They make up the only species of the genus Rhincodon and are classified within the order Orectolobiformes a group containing the carpet sharks The whale shark is enormous and reportedly capable of reaching a maximum length of about 18 metres 59 feet Most specimens that have been studied however weighed about 15 tons about 14 metric tons and averaged about 12 metres 39 feet in length The body coloration is distinctive Light vertical and horizontal stripes form a checkerboard pattern on a dark background and light spots mark the fins and dark areas of the body نحفظ الملف ونخرج منه، ونضيف الشيفرة التالية إلى ملف جافاسكربت جديد بالاسم ‎textHelper.js‎، حيث سيحوي هذا الملف على بعض الدوال المساعدة في معالجة الملف النصي السابق خلال عملية تحديد الكلمة الأكثر تكرارًا من النص، ونبدأ بإنشاء الملف ‎textHelper.js‎ ونفتحه ضمن محرر النصوص: $ nano textHelper.js ونضيف ثلاث دوال لمعالجة النص ضمن الملف ‎sentences.txt‎ الأول لقراءة الملف: const fs = require('fs'); const readFile = () => { let data = fs.readFileSync('sentences.txt'); let sentences = data.toString(); return sentences; }; نستورد الوحدة البرمجية ‎fs‎ من نود لنتمكن من قراءة الملف، بعدها نضيف الدالة ‎readFile()‎ التي تستخدم التابع ‎readFileSync()‎ لتحميل محتوى الملف ‎sentences.txt‎ ككائن مخزن مؤقت ‎Buffer‎ ثم تستدعي منه التابع ‎toString()‎ لتحويل المحتوى إلى سلسلة نصية. نضيف بعدها دالة لتجزئة السلسلة نصية السابقة إلى مصفوفة من الكلمات كالتالي: ... const getWords = (text) => { let allSentences = text.split('\n'); let flatSentence = allSentences.join(' '); let words = flatSentence.split(' '); words = words.map((word) => word.trim().toLowerCase()); return words; }; استفدنا من التوابع ‎split()‎ و ‎join()‎ و ‎map()‎ لتحويل السلسلة النصية إلى مصفوفة من الكلمات الموجودة ضمنها، وحولنا حالة كل كلمة منها إلى أحرف صغيرة لتسهيل عملية المقارنة بينها وإحصائها. أما الدالة الثالثة والأخيرة فستحصي تكرار كل كلمة ضمن مصفوفة الكلمات السابقة ويعيد كل الكلمات مع تكراراتها ضمن كائن يعبر عن النتيجة كالتالي: ... const countWords = (words) => { let map = {}; words.forEach((word) => { if (word in map) { map[word] = 1; } else { map[word] += 1; } }); return map; }; أنشأنا كائنًا جديدًا بالاسم ‎map‎ يحوي الكلمات ضمن النص كمفاتيح وعدد مرات تكرارها كقيم لها، ثم مررنا على عناصر مصفوفة الكلمات وأضفناها إلى ذلك الكائن إن تكن موجودة أو زدنا قيمة تكرارها قيمة واحدة. وأخيرًا لنصدر تلك الدوال لنتمكن من استخدامها ضمن الوحدات البرمجية الأخرى: ... module.exports = { readFile, getWords, countWords }; نحفظ الملف ونخرج منه، والآن سننشئ الملف الثالث والأخير ضمن المثال هو الملف الأساسي الذي سيستعين بالدوال ضمن الوحدة البرمجية السابقة ‎textHelper.js‎ لاستخراج أكثر كلمة تكرارًا من النص. نبدأ بإنشاء الملف ‎index.js‎ ثم نفتحه ضمن محرر النصوص: $ nano index.js نستورد الوحدة البرمجية ‎textHelpers.js‎ كالتالي: const textHelper = require('./textHelper'); وننشئ مصفوفة جديدة تحتوي على بعض الكلمات المكررة الشائعة التي نرغب بتجاهلها مثل حروف العطف والجر والضمائر وبعض الصفات، تدعى الكلمات الشائعة أو stop words: ... const stopwords = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now', '']; بهذه الطريقة سنحصل على كلمات ذات معاني من ضمن النص الذي نعالجه بدلًا من الحصول على كلمات مثل أدوات التعريف التي تتكرر كثيرًا مثل the‎ و a‎. نبدأ باستخدام الدوال المساعدة من الوحدة ‎textHelper.js‎ لقراءة النص واستخراج الكلمات منه وإحصاء مرات التكرار لكل منها كالتالي: ... let sentences = textHelper.readFile(); let words = textHelper.getWords(sentences); let wordCounts = textHelper.countWords(words); بعد ذلك سنستخرج أكثر كلمة تكرارًا منها، وخوارزمية تحديد الكلمة الأكثر تكرارًا هي بالمرور أولًا على مفاتيح كائن الكلمات المحصاة ومقارنة التكرار مع آخر أعلى قيمة مررنا عليها سابقًا، وفي حال كانت قيمة التكرار للمفتاح الحالي أعلى من الكلمة السابقة سنحدد تكرار الكلمة الحالية على أنه التكرار الأعلى، لتصبح الشيفرة لهذه الخوارزمية كالتالي: ... let max = -Infinity; let mostPopular = ''; Object.entries(wordCounts).forEach(([word, count]) => { if (stopwords.indexOf(word) === -1) { if (count > max) { max = count; mostPopular = word; } } }); console.log(`The most popular word in the text is "${mostPopular}" with ${max} occurrences`); استخدمنا التابع ‎Object.entries()‎ لتحويل المفاتيح والقيم ضمن الكائن ‎wordCounts‎ إلى مصفوفة، ثم استخدمنا التابع ‎forEach()‎ وداخله عبارة شرطية لاختبار قيمة التكرار للكلمة الحالية مع أعلى قيمة تكرار شاهدناها سابقًا. والآن نحفظ الملف ونخرج منه وننفذه كالتالي: $ node index.js نلاحظ ظهور النتيجة التالية: The most popular word in the text is "whale" with 1 occurrences لكن الجواب الذي ظهر خاطئ فنلاحظ تكرار الكلمة ‎whale‎ أكثر من مرة ضمن النص في الملف ‎sentences.txt‎، وهذه المرة قد يكون السبب في أحد الدوال العديدة المستخدمة في البرنامج، فقد تكون المشكلة في عملية قراءة محتوى الملف كاملًا، أو خلال معالجته وتحويله لمصفوفة الكلمات، أو خلال عملية توليد كائن إحصاء مرات التكرار للكلمات، أو قد يكون الخطأ في خوارزمية تحديد الكلمة الأكثر تكرارًا. وأفضل أداة يمكن أن نستعين بها لتحديد الخطأ في مثل هذه الحالات هي أداة تنقيح الأخطاء، وحتى لو كانت شيفرة البرنامج الذي نعاينه قصيرة نسبيًا، فلا يفضل المرور سطرًا تلو الآخر خلال عملية التنفيذ وإضاعة الوقت، ويمكن بدلًا من ذلك الاستفادة من نقاط الوقوف للتوقف عند أماكن محددة مهمة لنا فقط، فمثلًا في نهاية جسم دالة لمعاينة القيمة التي ستعيدها. لنبدأ بإضافة نقاط وقوف ضمن كل من التوابع المساعدة في الملف ‎textHelper.js‎ بإضافة الكلمة المحجوزة ‎debugger‎ ضمن الشيفرة في تلك الأماكن، لذا نفتح الملف ‎textHelper.js‎ ضمن محرر النصوص ونضيف أول نقطة وقوف ضمن التابع ‎readFile()‎ كالتالي: ... const readFile = () => { let data = fs.readFileSync('sentences.txt'); let sentences = data.toString(); debugger; return sentences; }; ... بعدها نضيف نقطة وقوف أخرى ضمن الدالة ‎getWords()‎: ... const getWords = (text) => { let allSentences = text.split('\n'); let flatSentence = allSentences.join(' '); let words = flatSentence.split(' '); words = words.map((word) => word.trim().toLowerCase()); debugger; return words; }; ... وأخيرًا نضيف نقطة وقوف للدالة ‎countWords()‎ كالتالي: ... const countWords = (words) => { let map = {}; words.forEach((word) => { if (word in map) { map[word] = 1; } else { map[word] += 1; } }); debugger; return map; }; ... نحفظ الملف ونخرج منه، ونبدأ جلسة تنقيح الأخطاء ومع أن كل نقاط الوقوف التي أضفناها موجودة ضمن الملف ‎textHelpers.js‎ لكن عملية تنقيح الأخطاء ستبدأ من الملف الرئيسي للتطبيق ‎index.js‎، لذا ندخل لجلسة تنقيح الأخطاء من ذلك الملف كما تعلمنا سابقًا كالتالي: $ node inspect index.js ليظهر لنا التالي: < Debugger listening on ws://127.0.0.1:9229/b2d3ce0e-3a64-4836-bdbf-84b6083d6d30 < For help, see: https://nodejs.org/en/docs/inspector < Debugger attached. Break on start in index.js:1 > 1 const textHelper = require('./textHelper'); 2 3 const stopwords = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now', '']; هذه المرة سننفذ الأمر ‎c‎ وهو اختصار للكلمة continue وتعني إكمال التنفيذ لينتقل بذلك المنقح مباشرة إلى أول نقطة وقوف يصل إليها تنفيذ الشيفرة، وبعد الضغط على زر الإدخال ‎ENTER‎ لتنفيذ الأمر يظهر التالي: break in textHelper.js:6 4 let data = fs.readFileSync('sentences.txt'); 5 let sentences = data.toString(); > 6 debugger; 7 return sentences; 8 }; نلاحظ كم من الوقت قد وفرنا في هذه العملية حيث توجهنا مباشرة إلى أول نقطة وقوف، ولنتأكد من أن هذه الدالة تعمل بشكل سليم وتقرأ محتوى الملف النصي كاملًا وتعيده، سنراقب المتغير ‎sentences‎ لنعاين قيمته ونتأكد من صحة القيمة التي تعيدها الدالة: debug> watch('sentences') نتقدم بالتنفيذ خطوة للأمام فقط بتنفيذ الأمر ‎n‎ لنعاين قيمة المتغير ‎sentences‎: break in textHelper.js:7 Watchers: 0: sentences = 'Whale shark Rhincodon typus gigantic but harmless shark family Rhincodontidae that is the largest living fish\n' + 'Whale sharks are found in marine environments worldwide but mainly in tropical oceans\n' + 'They make up the only species of the genus Rhincodon and are classified within the order Orectolobiformes a group containing the carpet sharks\n' + 'The whale shark is enormous and reportedly capable of reaching a maximum length of about 18 metres 59 feet\n' + 'Most specimens that have been studied however weighed about 15 tons about 14 metric tons and averaged about 12 metres 39 feet in length\n' + 'The body coloration is distinctive\n' + 'Light vertical and horizontal stripes form a checkerboard pattern on a dark background and light spots mark the fins and dark areas of the body\n' 5 let sentences = data.toString(); 6 debugger; > 7 return sentences; 8 }; 9 تبدو القيمة صحيحة ولا مشاكل في عملية قراءة محتوى الملف إذًا فالمشكلة في مكان آخر. لننتقل إلى نقطة الوقوف التالية بتنفيذ الأمر ‎c‎ مجددًا ليظهر ما يلي: break in textHelper.js:15 Watchers: 0: sentences = ReferenceError: sentences is not defined at eval (eval at getWords (your_file_path/debugger/textHelper.js:15:3), <anonymous>:1:1) at Object.getWords (your_file_path/debugger/textHelper.js:15:3) at Object.<anonymous> (your_file_path/debugger/index.js:7:24) at Module._compile (internal/modules/cjs/loader.js:1125:14) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10) at Module.load (internal/modules/cjs/loader.js:983:32) at Function.Module._load (internal/modules/cjs/loader.js:891:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12) at internal/main/run_main_module.js:17:47 13 let words = flatSentence.split(' '); 14 words = words.map((word) => word.trim().toLowerCase()); >15 debugger; 16 return words; 17 }; رسالة الخطأ التي ظهرت سببها مراقبتنا سابقًا لقيمة المتغير ‎sentences‎ الذي لم يعد موجودًا الآن ضمن نطاق تنفيذ الدالة الحالية، حيث تبقى عملية المراقبة للمتغير طول مدة جلسة تنقيح الأخطاء، لذا سيتكرر ظهور رسالة الخطأ تلك ما دام المتغير لا يمكن الوصول إليه من مكان التنفيذ الحالي. ويمكننا حل تلك المشكلة بإيقاف مراقبة المتغير باستخدام الدالة ‎unwatch()‎ لإيقاف مراقبة المتغير ‎sentences‎ بتنفيذ التعليمة التالية: debug> unwatch('sentences') لن تظهر أي رسالة عند تنفيذ التعليمة السابقة، والآن لنعود إلى الدالة ‎getWords()‎ ونتأكد من صحة القيمة التي تعيدها وهي قائمة من كلمات النص السابق، لهذا نضيف مراقبة للمتغير ‎words‎ كالتالي: debug> watch('words') وننتقل لتنفيذ السطر التالي بتنفيذ التعليمة ‎n‎ ونعاين قيمة المتغير ‎words‎، ونلاحظ ظهور ما يلي: break in textHelper.js:16 Watchers: 0: words = [ 'whale', 'shark', 'rhincodon', 'typus', 'gigantic', 'but', 'harmless', ... 'metres', '39', 'feet', 'in', 'length', '', 'the', 'body', 'coloration', ... ] 14 words = words.map((word) => word.trim().toLowerCase()); 15 debugger; >16 return words; 17 }; 18 لم يُظهر منقح الأخطاء محتوى المصفوفة كاملةً بسبب طولها وصعوبة قراءتها كاملة، ولكن ما ظهر يكفي ليؤكد أن محتوى النص ضمن المتغير ‎sentences‎ تم تجزئته إلى كلمات بحالة أحرف صغيرة، أي أن الدالة ‎getWords()‎ تعمل بشكل سليم. والآن ننتقل لمعاينة الدالة الثالثة وهي ‎countWords()‎، ولكن أولًا سنزيل المراقبة للمصفوفة ‎words‎ كي لا يظهر لنا رسالة خطأ كما حدث سابقًا عند الانتقال إلى نقطة الوقوف التالية كالتالي: debug> unwatch('words') ثم ننفذ الأمر ‎c‎ لينتقل التنفيذ إلى نقطة الوقوف التالية ويظهر ما يلي: break in textHelper.js:29 27 }); 28 >29 debugger; 30 return map; 31 }; سنتأكد ضمن هذه الدالة من احتواء المتغير ‎map‎ على كل الكلمات السابقة مع قيم تكرارها، لذا نبدأ مراقبة المتغير ‎map‎ كالتالي: debug> watch('map') ثم ننتقل بالتنفيذ إلى السطر التالي بتنفيذ الأمر ‎n‎ ليظهر لنا ما يلي: break in textHelper.js:30 Watchers: 0: map = { 12: NaN, 14: NaN, 15: NaN, 18: NaN, 39: NaN, 59: NaN, whale: 1, shark: 1, rhincodon: 1, typus: NaN, gigantic: NaN, ... } 28 29 debugger; >30 return map; 31 }; 32 على ما يبدو أن هذه الدالة هي سبب المشكلة وعملية إحصاء تكرار الكلمات خاطئة، ولمعرفة سبب الخطأ يجب أن نعاين عمل هذه الدالة ضمن حلقة المرور على عناصر المصفوفة ‎words‎، لذا سنعدل أماكن نقاط الوقوف الحالية. نبدأ بالخروج من منقح الأخطاء بتنفيذ الأمر التالي: debug> .exit ثم نفتح الملف ‎textHelper.js‎ ضمن محرر النصوص لنعدل نقاط الوقوف ضمنه: $ nano textHelper.js بما أننا تأكدنا من صحة عمل الدالتين ‎readFile()‎ و ‎getWords()‎ سنزيل نقاط الوقوف من داخلهما، ونزيل نقطة الوقوف من نهاية الدالة ‎countWords()‎ ونضيف نقطتي وقوف جديدتين في بداية ونهاية الدالة ‎forEach()‎، ليصبح الملف ‎textHelper.js‎ كالتالي: ... const readFile = () => { let data = fs.readFileSync('sentences.txt'); let sentences = data.toString(); return sentences; }; const getWords = (text) => { let allSentences = text.split('\n'); let flatSentence = allSentences.join(' '); let words = flatSentence.split(' '); words = words.map((word) => word.trim().toLowerCase()); return words; }; const countWords = (words) => { let map = {}; words.forEach((word) => { debugger; if (word in map) { map[word] = 1; } else { map[word] += 1; } debugger; }); return map; }; ... نحفظ الملف ونخرج منه ثم نبدأ جلسة تنقيح أخطاء جديدة كالتالي: $ node inspect index.js كي نحدد سبب المشكلة يجب أن نراقب عدة قيم، أولها قيمة الكلمة الحالية ‎word‎ المُمررة كمعامل من قبل تابع حلقة التكرار ‎forEach()‎ كالتالي: debug> watch('word') لا تقتصر ميزة المراقبة ضمن جلسة تنقيح الأخطاء على المتغيرات فحسب، بل يمكن مراقبة قيم تعابير جافاسكربت البرمجية المستخدمة ضمن الشيفرة، كأن نراقب قيمة تنفيذ التعليمة الشرطية ‎word in map‎ والتي تحدد ما إذا كانت الكلمة الحالية موجودة مسبقًا، ويمكن مراقبتها بتنفيذ التالي: debug> watch('word in map') لنضيف مراقبة لقيمة تكرار الكلمة الحالية ضمن متغير النتيجة ‎map‎ كالتالي: debug> watch('map[word]') لا تقتصر ميزة المراقبة على التعابير البرمجية الموجودة ضمن الشيفرة فحسب، بل يمكن إضافة أي تعابير برمجية نريدها ليتم تنفيذها ومراقبة قيمتها، لذا سنستفيد من هذه الميزة ونضيف مراقبة لقيمة طول الكلمة الحالية ضمن المتغير ‎word‎: debug> watch('word.length') بعد أن انتهينا من إضافة القيم التي نريد مراقبتها أثناء التنفيذ سننفذ الأمر ‎c‎ ونراقب كيف تعالج الدالة أول كلمة من مصفوفة الكلمات ضمن الحلقة داخل الدالة ‎countWords()‎، ليظهر لنا ما يلي: break in textHelper.js:20 Watchers: 0: word = 'whale' 1: word in map = false 2: map[word] = undefined 3: word.length = 5 18 let map = {}; 19 words.forEach((word) => { >20 debugger; 21 if (word in map) { 22 map[word] = 1; الكلمة الأولى التي يتم معالجتها هي ‎whale‎ ولا يحوي الكائن ‎map‎ على مفتاح للكلمة ‎whale‎ لأنه فارغ، لذا قيمة المراقبة للكلمة الحالية ‎whale‎ ضمن الكائن ‎map‎ كما نلاحظ هي ‎undefined‎، وطول الكلمة الحالية ‎whale‎ هو ‎5‎، وهذه القيمة تحديدًا لا تفيدنا في البحث عن سبب الخطأ، ولكننا أضفناها لنتعلم كيف يمكن حساب ومراقبة أي تعبير برمجي خلال جلسة تنقيح الأخطاء. والآن ننفذ التعليمة ‎c‎ لنرى ماذا سيحدث في نهاية تنفيذ الدورة الحالية ليظهر لنا ما يلي: break in textHelper.js:26 Watchers: 0: word = 'whale' 1: word in map = true 2: map[word] = NaN 3: word.length = 5 24 map[word] += 1; 25 } >26 debugger; 27 }); 28 أصبحت قيمة العبارة ‎word in map‎ صحيحة ‎true‎ بسبب إضافة مفتاح للكلمة الحالية ‎whale‎ ضمن الكائن ‎map‎، ولكن قيمة المفتاح ‎whale‎ ضمن الكائن ‎map‎ هي ‎NaN‎ ما يدل على وجود مشكلة ما، وتحديدًا في العبارة الشرطية ‎if‎ ضمن الدالة ‎countWords()‎، فوظيفتها هي تحديد فيما إذا كنا سنضيف مفتاحًا جديدًا للكلمة الحالية إذا لم تكن موجودة سابقًا، أو إضافة واحد لقيمة المفتاح إن كان موجودًا مسبقًا، والصحيح هو تعيين القيمة ‎map[word]‎ إلى ‎1‎ إذا لم تكن الكلمة ‎word‎ موجودة كمفتاح ضمن ‎map‎، بينما حاليًا نحن نضيف قيمة واحد في حال العثور على ‎word‎ وهو عكس المطلوب. وكما لاحظنا في بداية الحلقة كانت قيمة التكرار للكلمة الحالية ‎map["whale"]‎ غير موجودة ‎undefined‎، وفي جافاسكربت إذا حاولنا إضافة واحد إلى تلك القيمة ‎undefined + 1‎ سينتج عن تلك العملية القيمة ‎NaN‎ وهو ما ظهر بالفعل، ولتصحيح هذه المشكلة يمكننا تعديل الشرط ضمن ‎if‎، فبدلًا من أن يكون ‎word in map‎ ننفي هذه العبارة لتصبح كالتالي ‎!(word in map)‎، حيث يُستخدم الرمز ‎!‎ لنفي العبارات المنطقية فيصبح الشرط صحيحًا إذا لم يحتوي الكائن ‎map‎ على مفتاح للقيمة ‎word‎. والآن لننفذ هذا التعديل ضمن الدالة ‎countWords()‎ ونختبرها مجددًا، لكن نخرج أولًا من جلسة تنقيح الأخطاء كالتالي: debug> .exit ونفتح الملف ‎textHelper.js‎ مجددًا ضمن محرر النصوص: $ nano textHelper.js نعدل الدالة ‎countWords()‎ بالشكل التالي: ... const countWords = (words) => { let map = {}; words.forEach((word) => { if (!(word in map)) { map[word] = 1; } else { map[word] += 1; } }); return map; }; ... نحفظ الملف ونخرج منه، وننفذ البرنامج ونراقب النتيجة: $ node index.js تظهر لنا النتيجة التالية هذه المرة: The most popular word in the text is "whale" with 3 occurrences وهي إجابة منطقية وأفضل من السابقة، ونلاحظ كيف ساعدنا منقح الأخطاء في تحديد الدالة التي كانت سبب المشكلة وتمييز الدوال التي تعمل بشكل سليم وساعدنا في اكتشاف سبب الخطأ، وبذلك نكون قد تعلمنا طريقة استخدام منقح الأخطاء الخاص بنود من سطر الأوامر. وتعلمنا أيضًا كيف يمكن إضافة نقاط الوقوف باستخدام الكلمة ‎debugger‎ وإعداد مراقبة لمختلف القيم والعبارات البرمجية لمراقبة حالة البرنامج أثناء التنفيذ وكل ذلك من سطر الأوامر، ولكن لتوفير تجربة استخدام أسهل يمكن إجراء العملية نفسها عبر واجهة مستخدم مرئية، وهذا ما سنتعرف عليه في الفقرة التالية. سنتعلم في الفقرة التالية طريقة استخدام منقح الأخطاء من أدوات المطور في متصفح جوجل كروم، حيث سنبدأ جلسة لتنقيح الأخطاء في نود كما فعلنا سابقًا، وسنستعمل صفحة مخصصة من واجهة متصفح كروم لتعيين نقاط الوقوف وعمليات المراقبة من واجهة مرئية بدلًا من سطر الأوامر. تنقيح الأخطاء في نود باستخدام أدوات المطور في كروم تعد أدوات المطور في متصفح كروم من أشهر أدوات نتقيح الأخطاء لشيفرة جافاسكربت عمومًا ونود خصوصًا ضمن متصفح الويب، وذلك لأن محرك جافاسكربت المستخدم من قبل نود V8 هو نفسه المستخدم في متصفح كروم، لذا فالتكامل بينهما يوفر تجربة مرنة لتنقيح الأخطاء. سنطبق في هذه الفقرة على مثال بسيط وهو خادم HTTP في نود مهمته إعادة قيمة بصيغة JSON كرد على الطلبات الواردة، وسنستخدم لاحقًا منقح الأخطاء لإعداد نقاط الوقوف ومراقبة عمل ذلك الخادم وتحديدًا كيف يتم توليد قيمة الرد على الطلبات الواردة، وللمزيد حول عملية إنشاء الخادم، راجع مقالة إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP. نبدأ بإنشاء ملف جافاسكربت جديد بالاسم ‎server.js‎ سيحوي على برنامج الخادم ونفتح الملف ضمن محرر النصوص كالتالي: $ nano server.js مهمة الخادم هي إعادة العبارة ‎Hello World‎ بصيغة JSON ضمن الرد، حيث سيحوي على مصفوفة لعدة ترجمات لتلك العبارة ليختار إحداها عشوائيًا ويعيدها ضمن جسم الرد بصيغة JSON، وسيستمع الخادم إلى الطلبات الواردة على العنوان المحلي ‎localhost‎ وعلى المنفذ رقم ‎:8000‎. والآن نبدأ بإضافة شيفرة البرنامج كما يلي: const http = require("http"); const host = 'localhost'; const port = 8000; const greetings = ["Hello world", "Hola mundo", "Bonjour le monde", "Hallo Welt", "Salve mundi"]; const getGreeting = function () { let greeting = greetings[Math.floor(Math.random() * greetings.length)]; return greeting } استوردنا الوحدة برمجية ‎http‎ والتي تساعد في إعداد خادم HTTP، ثم وضعنا قيم عنوان الخادم ورقم المنفذ ضمن المتغيرين ‎host‎ و ‎port‎ لاستخدامها لاحقًا لتشغيل الخادم، ثم عرفنا مصفوفة العبارات ‎greetings‎ والتي تحوي على جميع العبارات الممكن إرسالها من قبل الخادم لتختار الدالة ‎getGreeting()‎ إحداها عشوائيًا ويعيده. والآن سنضيف دالة معالجة طلبات HTTP القادمة للخادم وشيفرة بدء تشغيل الخادم كالتالي: ... const requestListener = function (req, res) { let message = getGreeting(); res.setHeader("Content-Type", "application/json"); res.writeHead(200); res.end(`‎{"message": "${message}"}‎`); }; const server = http.createServer(requestListener); server.listen(port, host, () => { console.log(`‎Server is running on http://${host}:${port}‎`); }); أصبح الخادم بذلك جاهزًا للخطوة التالية وهي إعداد منقح أخطاء كروم، لهذا نبدأ جلسة تنقيح الأخطاء بتنفيذ الأمر التالي: $ node --inspect server.js ملاحظة: نلاحظ الفرق بين أمر بدء منقح الأخطاء الخاص بنود من سطر الأوامر وبين أمر منقح الأخطاء الخاص بكروم، حيث ننفذ الأمر ‎inspect‎ للأول، أما للثاني نمرر الخيار ‎--inspect‎. وبعد تشغيل منقح الأخطاء سنلاحظ ظهور ما يلي: Debugger listening on ws://127.0.0.1:9229/996cfbaf-78ca-4ebd-9fd5-893888efe8b3 For help, see: https://nodejs.org/en/docs/inspector Server is running on http://localhost:8000 يمكننا الآن فتح متصفح جوجل كروم أو كروميوم Chromium والذهاب للعنوان ‎chrome://inspect‎ من شريط العنوان في الأعلى، ويمكن أيضًا استعمال منقح الأخطاء لمتصفح مايكروسوفت إيدج Microsoft Edge ولكن بالذهاب إلى العنوان ‎edge://inspect‎ بدلًا من العنوان السابق. وبعد الذهاب لذلك العنوان ستظهر لنا الصفحة التالية: نذهب لقسم الأجهزة Devices ونضغط على أمر فتح أداوت المطور الخاصة بنود "Open dedicated DevTools for Node" لتظهر لنا نافذة منفصلة كالتالي: يمكننا الآن تنقيح أخطاء برنامج نود السابق بواسطة كروم، لذلك نذهب إلى تبويب المصادر Sources ونوسع قسم شجرة الملفات الظاهر على اليسار ونختار منه ملف البرنامج الخاص بنا وهو ‎server.js‎: ونضيف نقطة وقوف ضمن الشيفرة التي تظهر، حيث نريد التوقف بعد أن يختار البرنامج عبارة الترحيب التي سيعيدها ضمن الرد لنعاينها، لذلك يمكننا الضغط مباشرة على رقم السطر 10 لتظهر نقطة حمراء بجانبه ما يدل على إضافة نقطة وقوف في هذا السطر، وهو ما نلاحظه من قائمة نقاط الوقوف في اللوحة على اليمين: لنراقب الآن عبارة برمجية، حيث يمكننا ذلك من اللوحة على اليمين وتحديدًا بجانب عنوان قسم المراقبة Watch بالضغط على علامة الزائد "+"، ونضيف اسم المتغير ‎greeting‎ لنراقب قيمته أثناء التنفيذ ثم نضغط على زر الإدخال ‎ENTER‎. والآن لنبدأ بتنقيح البرنامج، فنذهب ضمن نافذة المتصفح إلى عنوان الذي يستمع إليه الخادم ‎http://localhost:8000‎ وبعد الضغط على زر الإدخال ‎ENTER‎ للذهاب إلى ذلك العنوان سنلاحظ عدم ظهور أي رد مباشرة بل ستظهر لنا نافذة تنقيح الأخطاء في الواجهة مجددًا، وفي حال لم تظهر النافذة يمكن الذهاب إليها يدويًا لنلاحظ ظهور ما يلي: حيث توقف تنفيذ الخادم عند نقطة الوقوف التي عيّناها سابقًا، ونلاحظ تحديث قيم المتغيرات التي نراقبها في لوحة المراقبة على الجانب الأيمن، وكذلك تظهر تلك القيمة بجانب السطر الحالي ضمن الشيفرة. ولمتابعة تنفيذ الشيفرة نضغط على زر المتابعة الموجود في اللوحة على الجانب الأيمن فوق العبارة "Paused on breakpoint" والتي تعني توقف التنفيذ عند نقطة الوقوف، وبعد اكتمال التنفيذ ستلاحظ ظهور رد بصيغة JSON ضمن نافذة المتصفح التي تواصلنا منها مع الخادم: {"message": "Hello world"} نلاحظ أننا لم نضيف أي عبارات ضمن الشيفرة أو نعدل عليها لإضافة نقاط الوقوف، وهي الفائدة التي تقدمها أدوات تنقيح الأخطاء من الواجهة المرئية مثل كروم، وهو الخيار الأفضل لمن لا يرغب بالتعامل مع سطر الأوامر ويفضل التعامل مع الواجهات المرئية. ختامًا تعلمنا في هذا المقال طريقة التعامل مع منقح الأخطاء في تطبيقات نود وطريقة إعداد الراصدات لمراقبة حالة التطبيق، وتعلمنا طريقة استخدام نقاط الوقوف لمعاينة تنفيذ البرنامج في عدة أماكن ضمن البرنامج أثناء عمله، وتعاملنا مع كل من منقح أخطاء نود من سطر الأوامر ومن متصفح جوجل كروم من أدوات المطور الخاصة به، وذلك بدلًا من إضافة تعليمات الطباعة للقيم المختلفة داخل البرنامج. يمكن الاستعانة بمنقح الأخطاء ما يسهل عملية استكشاف أخطاء التنفيذ ضمن البرنامج ومعاينة حالته، ما يوفر من وقت التطوير وخصوصًا وقت حل المشكلات وإصلاح الأخطاء. ويمكن الرجوع إلى توثيق نود الرسمي عن أدوات تنقيح الأخطاء أو دليل أدوات المطور من كروم ودليل أدوات المطور لتنقيح شيفرة جافاسكربت. تُعَد الأخطاء البرمجية أمرًا شائعًا في المجال البرمجي، وللتعرف أكثر عليها وعلى كيفية التعامل معها عامةً ننصحك بالاطلاع على الفيديو الآتي: ترجمة -وبتصرف- للمقال How To Debug Node.js with the Built-In Debugger and Chrome DevTools لصاحبه Stack Abuse. اقرأ أيضًا المقال السابق: استخدام مرسل الأحداث Event emitter في Node.js مقدمة إلى Node.js تنقيح أخطاء شيفرة جافاسكربت في Chrome
  16. يمكنك البحث ضمن مصفوفة القيم التي تمرر للتابع onPress عن الكائن الذي يحوي على الخاصية selected بالقيمة true، ثم معالجة ذلك الكائن الذي هو أحد الكائنات ضمن المصفوفة radioButtons الممررة للمكون، لتصبح الشيفرة لديك كالتالي: function handlePress(buttons) { const selected = buttons.find(button => button.selected); // معالجة الاختيار } <RadioGroup layout="column" radioButtons={radioButtonsData} onPress={handlePress} />
  17. ذلك لأن قيمة المصفوفة تُخزن ضمن الذاكرة فقط، فأن تحديث على التطبيق أو تغيير في الشيفرة ستمحى الذاكرة القديمة ويبدأ التطبيق من جديد، إذا كنت تريد حفظ البيانات للاستخدام المتكرر يجب حفظها إما ضمن ملفات على الجهاز أو ضمن قاعدة بيانات للتطبيق نفسه،
  18. يرجى إرفاق نص الشيفرة الخاصة بالمشكلة بدلًا من الصورة، يمكنك تقسيم المشكلة إلى خطوتين، إضافة العنصر الجديد إلى المصفوفة، ثم حساب مجموع القيم بداخلها، ليصبح التابع لديك كالتالي: const yo = [] function Moneys(mo){ yo.push(mo) // إضافة للمصفوفة const total = yo.reduce((total, amount) => amount + total, 0) // حساب المجموع console.log(total) // طباعة المجموع }
  19. مرسل أو مطلق الأحداث event emitter هو كائن في نود Node.js مهمته إطلاق حدث ما عبر إرسال رسالة تخبر بوقوع حدث، حيث يمكن استخدامه لربط تنفيذ بعض التعليمات البرمجية في جافاسكربت بحدث ما، وذلك عبر الاستماع لذلك الحدث وتنفيذ تابع ما عند كل تنبيه بحدوثه، ويتم تمييز تلك الأحداث عن بعضها بسلسلة نصية تُعبّر عن اسم الحدث ويمكن إرفاق بيانات تصف ذلك الحدث إلى التوابع المُستمعة له. عادة ما نربط تنفيذ التعليمات البرمجية بعد اكتمال حدث ما باستخدام طرق البرمجة اللامتزامنة asynchronous programming، كتمرير توابع رد النداء أو ربط الوعود مع بعضها، ولكن من مساوئ تلك الطرق هو الربط بين أمر تنفيذ الحدث والتعليمات الواجب تنفيذها بعد انتهاءه، مما يزيد صعوبة التعديل على تلك التعليمات لاحقًا، وهنا يأتي دور مرسل الأحداث ليوفر طريقة بديلة للربط بين الحدث والمهام المرتبطة به، باتباع نمط ناشر-مشترك publish-subscribe، حيث يرسل فيه الناشر أو مرسل الأحداث رسالة تعبر عن حدث ما، ثم يستقبل بدوره المشترك هذه الإشارة وينفذ تعليمات برمجية استجابة لذلك الحدث، ومن ميزات هذا النمط هو المقال بين الناشر والمشترك، بحيث لا يعلم الناشر أي شيء عن المشتركين، فينشر الناشر الرسائل فقط ثم يتفاعل معها المشتركون كلٌّ بطريقته الخاصة، وبالتالي يصبح تعديل التطبيق أسهل عبر تعديل طريقة عمل المشتركين فقط دون أي تعديل على الناشر. سنتعلم في هذا المقال طريقة إنشاء واستخدام مرسل الأحداث عبر تطوير صنف مرسل أحداث خاص لإدارة شراء البطاقات بالاسم ‎TicketManager‎، وسنربط به بعض المشتركين الذين سيتفاعلون مع حدث الشراء ‎buy‎ الذي سيُنشر بعد كل عملية شراء لبطاقة ما، وسنتعلم أيضًا طرقًا لمعالجة أحداث الأخطاء التي قد يرسلها المرسل، وكيفية إدارة المشتركين بالأحداث. المستلزمات هذا المقال جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته: تثبيت بيئة Node.js على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.20.1. معرفة بأساسيات استخدام الأصناف في جافاسكربت، حيث سنستخدمها ضمن الأمثلة في هذا المقال وهي متوفرة في جافاسكربت منذ الإصدار ES2015 أو ES6. إرسال أحداث Emitting Events سنتعلم في هذه الفقرة طريقتين لإنشاء مرسل أحداث في نود، الأولى باستخدام صنف مرسل الأحداث مباشرةً EventEmitter، والثانية بإنشاء صنف خاص يرث من صنف مرسل الأحداث الأساسي، ويعتمد الاختيار بين هاتين الطريقتين على مدى الترابط بين الأحداث ضمن التطبيق وبين العمليات التي ستسبب إرسالها، فإذا كانت العمليات داخل الكائن هي ما ستسبب إرسال الأحداث، أي يوجد ترابط وثيق بين العمليات والأحداث فهنا يفضل استخدام طريقة الوراثة من صنف مرسل الأحداث الأساسي، أما إذا كان العمليات منفصلة أو متفرقة، مثلًا نتيجة عدة عمليات نُفذت ضمن أكثر من كائن، فيفضل استخدام كائن مرسل للأحداث منفصل نستخدمه ضمن التطبيق داخليًا. ولنبدأ بالتعرف على طريقة استخدام كائن مرسل أحداث منفصل، ونبدأ أولًا بإنشاء مجلد للمشروع بالاسم ‎event-emitters‎ كالتالي: $ mkdir event-emitters وندخل إلى المجلد: $ cd event-emitters نُنشئ ملف جافاسكربت جديد بالاسم ‎firstEventEmitter.js‎ ونفتحه ضمن أي محرر نصوص، حيث سنستخدم في أمثلتنا محرر ‎nano‎ كالتالي: $ nano firstEventEmitter.js يمكن استخدام الصنف ‎EventEmitter‎ الموجود ضمن الوحدة ‎events‎ في نود لإرسال الأحداث، ولنبدأ باستيراد ذلك الصنف من تلك الوحدة كالتالي: const { EventEmitter } = require("events"); ثم ننشئ كائنًا جديدًا من ذلك الصنف: const { EventEmitter } = require("events"); const firstEmitter = new EventEmitter(); ونختبر إرسال حدث ما من هذا الكائن كالتالي: const { EventEmitter } = require("events"); const firstEmitter = new EventEmitter(); firstEmitter.emit("My first event"); نلاحظ استدعاء التابع ‎emit()‎ لإرسال حدث جديد، حيث نمرر له اسم ذلك الحدث كسلسلة نصية وبعدها يمكن تمرير أي عدد من المعاملات الخاصة بذلك الحدث، حيث تفيد تلك المعاملات بإرسال بيانات إضافية مع الحدث تتلقاها التوابع المستمعة للحدث وتوفر بيانات إضافية توصف ذلك الحدث، وسنستخدم ذلك في مثالنا لاحقًا عندما نرسل حدث شراء لبطاقة جديدة بتمرير بعض البيانات المتعلقة بعملية الشراء تلك، ويجب أن نميز اسم الحدث لأننا سنستخدمه لاحقًا كما هو للاستماع إليه. ملاحظة: يعيد تنفيذ تابع الإرسال ‎emit()‎ قيمة منطقية تكون صحيحة ‎true‎ في حال كان هناك أي تابع يستمع لذلك الحدث، وفي حال لم يكن هناك أي مستمع سيعيد القيمة ‎false‎ رغم عدم توفر معلومات أخرى عن المستمعين. والآن نحفظ الملف وننفذه باستخدام الأمر ‎node‎ ونلاحظ النتيجة: $ node firstEventEmitter.js نلاحظ عدم ظهور أي خرج من عملية التنفيذ السابقة، وذلك لأننا لم نطبع أي رسالة إلى الطرفية ولا يوجد أي مشتركين يستمعون للحدث المرسل. والآن لنبدأ بتطبيق مثال مدير شراء البطاقات، حيث سيوفر هذا الصنف تابعًا لعملية الشراء وبعد أن إتمام هذه العملية بنجاح سيُرسل حدث يعبر عن ذلك مرفقًا ببيانات حول المشتري للبطاقة، ثم سنطور وحدة برمجية منفصلة لمحاكاة عملية إرسال بريد إلكتروني للمشتري استجابة لحدث الشراء لنعلمه بنجاح العملية. نبدأ بإنشاء مدير البطاقات حيث سيرث صنف مرسل الأحداث الأساسي ‎EventEmitter‎ مباشرة كي لا نضطر لإنشاء كائن مرسل للأحداث منفصل داخليًا واستخدامه، ونٌنشئ ملف جافاسكربت جديد بالاسم ‎ticketManager.js‎: $ nano ticketManager.js كما فعلنا سابقًا نستورد الصنف ‎EventEmitter‎ من الوحدة ‎events‎ لاستخدامه كالتالي: const EventEmitter = require("events"); ونعرف صنف مدير البطاقات ‎TicketManager‎ الذي سيوفر تابع الشراء لاحقًا: const EventEmitter = require("events"); class TicketManager extends EventEmitter {} نلاحظ أن صنف مدير البطاقات ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ ما يعني أنه سيرث كل التوابع والخواص التي يوفرها صنف مرسل الأحداث وبالتالي يمكننا استدعاء تابع إرسال الأحداث ‎emit()‎ من الصنف نفسه مباشرةً. ولنبدأ بتعريف التابع الباني للصنف لتمرير كمية البطاقات المتوفرة للبيع، والذي سيُستدعى عند إنشاء كائن جديد من هذا الصنف كالتالي: const EventEmitter = require("events"); class TicketManager extends EventEmitter { constructor(supply) { super(); this.supply = supply; } } يقبل التابع الباني معامل العدد ‎supply‎ والذي يعبر عن الكمية المتوفرة للبيع، وبما أن الصنف ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ فيجب استدعاء التابع الباني للصنف الأب عبر استدعاء ‎super()‎ وذلك لتهيئة توابع وخاصيات الصنف الأب بشكل صحيح. وبعد ذلك نعرف قيمة خاصية الكمية ‎supply‎ ضمن الصنف بواسطة ‎this.supply‎ ونسند القيمة المُمررة للتابع الباني لها، والآن سنضيف تابع شراء بطاقة جديدة ‎buy()‎ حيث سيُنقص هذا التابع كمية البطاقات المتوفرة ويرسل حدثًا يجوي تفاصيل عملية الشراء كالتالي: const EventEmitter = require("events"); class TicketManager extends EventEmitter { constructor(supply) { super(); this.supply = supply; } buy(email, price) { this.supply--; this.emit("buy", email, price, Date.now()); } } نلاحظ تمرير عنوان البريد الإلكتروني والعنوان الخاص بالمشتري والسعر المدفوع ثمنًا للبطاقة للتابع ‎buy()‎، حيث سينقص التابع كمية البطاقات المتوفرة بمقدار واحد، ثم سيرسل حدث الشراء ‎buy‎ مع تمرير بيانات إضافية هذه المرة وهي عنوان البريد الإلكتروني للمشتري وسعر البطاقة وتوقيت عملية الشراء تلك. والآن ولكي تستطيع باقي الوحدات البرمجية استخدام هذا الصنف يجب تصديره في نهاية الملف كالتالي: ... module.exports = TicketManager نحفظ الملف ونخرج منه، ونكون بذلك انتهينا من إعداد صنف مدير البطاقات المُرسل للأحداث ‎TicketManager‎، وأصبح جاهزًا لإرسال الأحداث المتعلقة بعملية شراء البطاقات الجديدة وبقي علينا الاشتراك والاستماع لذلك الحدث ومعالجته، وهذا ما سنتعرف عليه في الفقرة التالية حيث سنُنشئ توابع تستمع لذلك الحدث. الاستماع للأحداث يمكن تسجيل مستمع إلى حدث ما باستدعاء التابع ‎on()‎ من كائن مرسل الأحداث، حيث سيستمع لحدث معين وعند إرساله سيستدعي لنا تابع رد النداء المُمرر له، وصيغة استدعاءه كالتالي: eventEmitter.on(event_name, callback_function) { action } ملاحظة: التابع ‎on()‎ هو اسم بديل للتابع ‎addListener()‎ ضمن مرسل الأحداث ولا فرق في استخدام أي منهما، حيث سنستخدم في أمثلتنا التابع ‎on()‎ دومًا. والآن لنبدأ بالاستماع إلى الأحداث بإنشاء ملف جافاسكربت جديد بالاسم ‎firstListener.js‎: $ nano firstListener.js سنختبر عملية تسجيل المستمع بطباعة رسالة ضمنه إلى الطرفية عند تلقي الحدث، ونبدأ باستيراد الصنف ‎TicketManager‎ ضمن الملف الجديد كالتالي: const TicketManager = require("./ticketManager"); const ticketManager = new TicketManager(10); مررنا القيمة ‎10‎ للصنف ‎TicketManager‎ كقيمة لمخزون البطاقات المتاحة، والآن لنضيف مستمع جديد لحدث الشراء ‎buy‎ كالتالي: const TicketManager = require("./ticketManager"); const ticketManager = new TicketManager(10); ticketManager.on("buy", () => { console.log("Someone bought a ticket!"); }); لإضافة مستمع جديد نستدعي التابع ‎on()‎ من الكائن ‎ticketManager‎، والمتوفر ضمن كل كائنات صنف مرسل الأحداث، وبما أن الصنف ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ بالتالي فهذا التابع أصبح متوفرًا ضمن أي كائن من صنف مدير البطاقات ‎TicketManager‎. نمرر تابع رد نداء للتابع ‎on()‎ كمعامل ثاني حيث ستنفذ التعليمات ضمنه عند كل إطلاق للحدث، حيث يطبع هذا التابع الرسالة ‎"Someone bought a ticket!"‎ إلى الطرفية عند كل حدث لعملية الشراء ‎buy‎. وبعد أن سجلنا التابع كمستمع للحدث ننفذ عملية الشراء باستدعاء التابع ‎buy()‎ لينتج عنه إرسال لحدث الشراء كالتالي: ... ticketManager.buy("test@email.com", 20); استدعينا تابع الشراء ‎buy‎ بعنوان البريد الإلكتروني ‎test@email.com‎ وبسعر ‎20‎ لتلك للبطاقة، والآن نحفظ الملف ونخرج منه وننفذ البرنامج بتنفيذ الأمر ‎node‎ كالتالي: $ node firstListener.js نلاحظ ظهور الخرج: Someone bought a ticket! بذلك يكون مرسل الأحداث قد أرسل الحدث بنجاح وتم معالجته من قبل تابع الاستماع. والآن لنجرب أكثر من عملية شراء ونراقب ماذا سيحدث، نفتح الملف ‎firstListener.js‎ للتعديل مجددًا ونستدعي تابع الشراء ‎buy()‎ مرة أخرى: ... ticketManager.buy("test@email.com", 20); ticketManager.buy("test@email.com", 20); نحفظ الملف ونخرج منه وننفذ الملف مجددًا ونلاحظ النتيجة هذه المرة: Someone bought a ticket! Someone bought a ticket! بما أن تابع الشراء ‎buy()‎ قد استُدعي مرتين فقد نتج عنه إرسال لحدث الشراء ‎buy‎ مرتين أيضًا، ثم استقبل تابع الاستماع هذين الحدثين وطبع الرسالة مرتين. قد نحتاج في بعض الأحيان للاستماع لأول مرة يُرسَل فيها الحدث فقط وليس لكل مرة، ويمكن ذلك عبر استدعاء تابع مشابه للتابع ‎on()‎ وهو ‎once()‎ يعمل بنفس الطريقة، فهو سيسجل تابع الاستماع للحدث المحدد بالمعامل الأول، وسينفذ التابع المُمرر كمعامل ثاني له، ولكن الفرق هنا أن التابع ‎once()‎ وبعد استقبال الحدث لأول مرة سيُلغي اشتراك تابع الاستماع بالحدث ويزيله، وينفذه لمرة واحدة فقط عند أول استقبال للحدث بعد عملية التسجيل. ولنوضح ذلك باستخدامه ضمن الملف ‎firstListener.js‎ نفتحه مجددًا للتعديل ونضيف في نهايته الشيفرة التالية لتسجيل تابع للاستماع لحدث الشراء لمرة واحدة فقط باستخدام ‎once()‎ كالتالي: const TicketManager = require("./ticketManager"); const ticketManager = new TicketManager(10); ticketManager.on("buy", () => { console.log("Someone bought a ticket!"); }); ticketManager.buy("test@email.com", 20); ticketManager.buy("test@email.com", 20); ticketManager.once("buy", () => { console.log("This is only called once"); }); نحفظ الملف ونخرج منه وننفذ البرنامج ونلاحظ الخرج التالي: Someone bought a ticket! Someone bought a ticket! لا نلاحظ أي فرق هذه المرة عن الخرج السابق، والسبب أننا سجلنا مستمع للحدث بعد الانتهاء من إرسال حدث الشراء ‎buy‎ وليس قبله، لذا لم يُنفذ التابع لأنه لم يستقبل أي أحداث جديدة، أي لا يمكن الاستماع إلا للأحداث التي سترد لاحقًا بعد عملية تسجيل التابع، أما الأحداث السابقة فلا يمكن الاستماع لها، ولحل المشكلة يمكن استدعاء تابع الشراء ‎buy()‎ مرتين من جديد بعد تسجيل تابع الاستماع باستخدام ‎once()‎ لنتأكد أنه لن يُنفذ سوى لمعالجة أول حدث منها فقط: ... ticketManager.once("buy", () => { console.log("This is only called once"); }); ticketManager.buy("test@email.com", 20); ticketManager.buy("test@email.com", 20); نحفظ الملف ونخرج منه وننفذ البرنامج لنحصل على خرج كالتالي: Someone bought a ticket! Someone bought a ticket! Someone bought a ticket! This is only called once Someone bought a ticket! أول رسالتين ظهرتا نتيجة أول استدعاءين لتابع الشراء ‎buy()‎ وقبل تسجيل تابع الاستماع باستخدام ‎once()‎، ولكن إضافة تابع الاستماع الجديد لا يزيل وجود التوابع المسجلة سابقًا، وستبقى تستمع للأحداث اللاحقة وتطبع تلك الرسائل، وبوجود توابع استماع تم تسجيلها باستخدام ‎on()‎ قبل تابع الاستماع الجديد الذي سجلناه باستخدام ‎once()‎، فسنلاحظ ظهور الرسالة ‎Someone bought a ticket!‎ قبل الرسالة ‎This is only called once‎، وكلا السطرين هما استجابة لحدث الشراء ‎buy‎ الثاني وما بعده. وعند آخر استدعاء لتابع الشراء ‎buy()‎ لم يبقى ضمن مرسل الأحداث سوى التوابع التي تستمع لهذا الحدث والتي سُجلت باستخدام ‎on()‎، حيث أن التابع المستمع الذي سجلناه باستخدام ‎once()‎ أزيل تلقائيًا بعد تنفيذه لمرة واحدة فقط. وبذلك نكون قد تعلمنا الطرق المختلفة لتسجيل توابع الاستماع للأحداث، وسنتعلم في الفقرة التالية كيف يمكننا الوصول للبيانات المرسلة مع الأحداث لمعالجتها. استقبال بيانات الحدث تعلمنا في الفقرة السابقة طريقة الاستماع للأحداث والاستجابة لها، لكن عادة ما يُرسل مع هذه الأحداث بيانات إضافية توضح الحدث، وسنتعلم في هذه الفقرة كيف يمكننا استقبال البيانات والتعامل معها. سننشئ وحدتين برمجيتين الأولى لإرسال البريد الإلكتروني والثانية لتسجيل البيانات في قاعدة البيانات، ولن نتطرق لتفاصيل تلك الوحدات بل سنضع مثالًا يُعبّر عن تنفيذ العمليات الخاصة بها لأن تركيزنا هو على طريقة استقبالها لبيانات الأحداث، وبعد إنشاء تلك الوحدتين سنربطهما مع مرسل الأحداث ضمن ملف البرنامج الأساسي ‎index.js‎. والآن لنبدأ بإنشاء وحدة خدمة إرسال البريد الإلكتروني البرمجية نُنشئ لها ملف جافاسكربت جديد ونفتحه ضمن محرر النصوص: $ nano emailService.js ستحوي هذه الوحدة على صنف يوفر تابع الإرسال ‎send()‎ والذي سنمرر له عنوان البريد الإلكتروني المأخوذ من بيانات حدث الشراء ‎buy‎ كالتالي: class EmailService { send(email) { console.log(`‎Sending email to ${email}‎`); } } module.exports = EmailService عرفنا الصنف ‎EmailService‎ الحاوي على تابع الإرسال ‎send()‎ والذي بدلًا من إرسال بريد إلكتروني حقيقي سيطبع رسالة توضح تنفيذ هذه العملية مع توضيح عنوان البريد الإلكتروني المرسل إليه. والآن نحفظ الملف ونخرج منه ثم نُنشئ ملف جافاسكربت جديد بالاسم ‎databaseService.js‎ لوحدة خدمة قاعدة البيانات البرمجية ونفتحه ضمن محرر النصوص: $ nano databaseService.js سيُحاكي هذا الصنف حفظ بيانات عملية الشراء ضمن قاعدة البيانات عند استدعاء تابع الحفظ ‎save()‎ كالتالي: class DatabaseService { save(email, price, timestamp) { console.log(`Running query: INSERT INTO orders VALUES (email, price, created) VALUES (${email}, ${price}, ${timestamp})`); } } module.exports = DatabaseService عرفنا الصنف ‎DatabaseService‎ الحاوي على تابع الحفظ ‎save()‎ حيث سيحاكي عملية حفظ البيانات إلى قاعدة البيانات بطباعة البيانات الممررة له إلى الطرفية أيضًا، حيث سنمرر له البيانات المرفقة مع حدث الشراء ‎buy‎ وهي عنوان البريد الإلكتروني للمشتري وسعر البطاقة وتوقيت عملية الشراء. والآن نحفظ الملف ونخرج منه ونبدأ بربط مدير البطاقات ‎TicketManager‎ مع كل من خدمتي البريد الإلكتروني ‎EmailService‎ وخدمة قاعدة البيانات ‎DatabaseService‎، حيث سنسجل تابع استماع لحدث الشراء ‎buy‎ سيستدعي داخله تابع إرسال البريد الإلكتروني ‎send()‎ وتابع حفظ البيانات في قاعدة البيانات ‎save()‎، لذا ننشئ ملف جافاسكربت الرئيسي للبرنامج ‎index.js‎ ونفتحه ضمن محرر النصوص ونبدأ باستيراد الوحدات البرمجية اللازمة: const TicketManager = require("./ticketManager"); const EmailService = require("./emailService"); const DatabaseService = require("./databaseService"); ثم ننشئ كائنات جديدة من الأصناف السابقة، وفي هذه الخطوة سنحدد كمية قليلة للبطاقات المتاحة كالتالي: const TicketManager = require("./ticketManager"); const EmailService = require("./emailService"); const DatabaseService = require("./databaseService"); const ticketManager = new TicketManager(3); const emailService = new EmailService(); const databaseService = new DatabaseService(); بعدها نبدأ بتسجيل تابع الاستماع لحدث الشراء باستخدام الكائنات السابقة، حيث نريد بعد كل عملية شراء لبطاقة جديدة إرسال بريد إلكتروني للمشتري وحفظ بيانات تلك العملية في قاعدة البيانات، لذلك نضيف ما يلي: const TicketManager = require("./ticketManager"); const EmailService = require("./emailService"); const DatabaseService = require("./databaseService"); const ticketManager = new TicketManager(3); const emailService = new EmailService(); const databaseService = new DatabaseService(); ticketManager.on("buy", (email, price, timestamp) => { emailService.send(email); databaseService.save(email, price, timestamp); }); أضفنا كما تعلمنا سابقًا تابع استماع للحدث باستخدام التابع ‎on()‎، والفرق هذه المرة أننا نقبل ثلاث معاملات ضمن تابع رد النداء تمثل البيانات المرفقة مع الحدث، ولمعرفة البيانات التي سترسل نعاين طريقة إرسال حدث الشراء داخل التابع ‎buy()‎ من صنف مدير البطاقات: this.emit("buy", email, price, Date.now()); حيث سيقابل كل معامل نقبله ضمن تابع رد النداء معاملًا من البيانات التي نمررها لتابع إرسال الحدث السابق، فأول معامل هو البريد الإلكتروني ‎email‎ ثم السعر ‎price‎ ثم توقيت الشراء وهو التوقيت الحالي ‎Date.now()‎ والذي يقابل المعامل الأخير المسمى ‎timestamp‎ في تابع رد النداء. وفي تابع الاستماع للحدث وعند كل إرسال لحدث الشراء ‎buy‎ سيُستدعى تابع إرسال البريد الإلكتروني ‎send()‎ من كائن الخدمة ‎emailService‎، ثم تابع حفظ البيانات ضمن قاعدة البيانات من كائن الخدمة الخاصة به ‎databaseService‎. والآن لنختبر عملية الربط تلك كاملة باستدعاء تابع الشراء ‎buy()‎ في نهاية الملف: ... ticketManager.buy("test@email.com", 10); نحفظ الملف ونخرج منه، ننفذ البرنامج بتنفيذ الأمر ‎node‎ ونعاين النتيجة: $ node index.js نلاحظ ظهور النتيجة التالية: Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588720081832) نلاحظ استقبال تابع الاستماع للبيانات المرفقة بالحدث بنجاح، والآن بعد أن تعلمنا طرق إضافة توابع الاستماع لمختلف الأحداث بأسماء وبيانات مختلفة ماذا عن الأخطاء التي قد تحدث خلال عملية الشراء؟ وكيف يمكننا معالجتها والاستجابة لها؟ هذا ما سنتعرف عليه في الفقرة التالية، حيث سنتعرف أيضًا على المعايير الواجب اتباعها عند معالجة الأخطاء. معالجة أخطاء الأحداث عند فشل تنفيذ عملية ما يجب أن يُعلم مرسل الأحداث المشتركين بذلك، والطريقة المتبعة عادةً في نود تكون بإرسال حدث مخصص بالاسم‎error‎ يُعبّر عن حدوث خطأ ما أثناء التنفيذ، ويرفق به كائن الخطأ ‎Error‎ لتوضيح المشكلة. وحاليًا في صنف مدير البطاقات لدينا الكمية المتاحة تتناقص بمقدار واحد في كل مرة ننفذ تابع الشراء ‎buy()‎، ويمكن حاليًا تجاوز الكمية المتاحة وشراء عدد غير محدود من البطاقات، لنحل هذه المشكلة بتعديل تابع الشراء ‎buy()‎ ليرسل حدث يعبر عن خطأ في حال نفاذ الكمية المتاحة من البطاقات ومحاولة أحدهم شراء بطاقة جديدة، لذا نعود لملف مدير البطاقات ‎ticketManager.js‎ ونعدل تابع الشراء ‎buy()‎ ليصبح كالتالي: ... buy(email, price) { if (this.supply > 0) { this.supply—; this.emit("buy", email, price, Date.now()); return; } this.emit("error", new Error("There are no more tickets left to purchase")); } ... أضفنا العبارة الشرطية ‎if‎ لحصر عملية شراء البطاقات فقط في حال توفر كمية منها، عبر التحقق من أن الكمية الحالية أكبر من الصفر، أما في حال نفاذ الكمية سنرسل حدث الخطأ ‎error‎ ونرفق به كائن خطأ ‎Error‎ جديد يحوي وصفًا حول سبب الخطأ. والآن نحفظ الملف ونخرج منه ونحاول الوصول لتلك الحالة من الملف الرئيسي ‎index.js‎، فحاليًا نشتري بطاقة واحدة فقط والكمية المتاحة ضمن كائن مدير البطاقات ‎ticketManager‎ هي ثلاث بطاقات فقط، لذا للوصول لحالة الخطأ يجب أن نشتري أربعة بطاقات لتجاوز الكمية المتاحة، لهذا نعود للملف ‎index.js‎ لنعدل عليه ونضيف الأسطر التالية في نهاية الملف لشراء أربعة بطاقات: ... ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); نحفظ الملف ونخرج منه وننفذ البرنامج: $ node index.js نحصل على خرج التالي: Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932796) Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932812) Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932812) events.js:196 throw er; // Unhandled 'error' event ^ Error: There are no more tickets left to purchase at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:28) at Object.<anonymous> (/home/hassan/event-emitters/index.js:17:15) at Module._compile (internal/modules/cjs/loader.js:1128:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10) at Module.load (internal/modules/cjs/loader.js:983:32) at Function.Module._load (internal/modules/cjs/loader.js:891:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12) at internal/main/run_main_module.js:17:47 Emitted 'error' event on TicketManager instance at: at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:14) at Object.<anonymous> (/home/hassan/event-emitters/index.js:17:15) [... lines matching original stack trace ...] at internal/main/run_main_module.js:17:47 جرى معالجة أول ثلاث أحداث شراء ‎buy‎ بنجاح بينما سبب الحدث الرابع بعد نفاذ الكمية ذلك الخطأ، لنعاين رسالة الخطأ: ... events.js:196 throw er; // Unhandled 'error' event ^ Error: There are no more tickets left to purchase at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:28) ... توضح رسالة الخطأ نتيجة التنفيذ ونلاحظ تحديدًا رسالة الخطأ التالية: ‎"Unhandled 'error' event"‎ والتي تعني أن خطأ الحدث لم يتم معالجته، ما يعني أنه في حال أرسل مرسل الأحداث حدث الخطأ ولم نسجل أي مستمع لمعالجة هذا الحدث سيتم رمي الخطأ كما بالشكل السابق، ما يؤدي كما رأينا لتوقف تنفيذ البرنامج، لذا يفضل دومًا الاستماع لحدث الخطأ ‎error‎ من مرسل الأحداث لحل هذه المشكلة ومعالجة هذا الحدث لمنع توقف عمل البرنامج. والآن لنطبق ذلك ونضيف تابع لمعالجة حدث الخطأ ضمن الملف ‎index.js‎ حيث نضيف تابعًا مستمعًا لحدث الخطأ قبل تنفيذ عملية شراء البطاقات، وذلك لأنه وكما ذكرنا سابقًا لا يمكن سوى معالجة الأحداث التي سترد منذ لحظة تسجيل تابع الاستماع، لذا نضيف تابع معالجة الخطأ كالتالي: ... ticketManager.on("error", (error) => { console.error(`‎Gracefully handling our error: ${error}‎`); }); ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); ticketManager.buy("test@email.com", 10); نطبع داخل ذلك التابع رسالة إلى الطرفية تدل على معالجة الخطأ المُرسل باستخدام ‎console.error()‎، والآن نحفظ الملف ونخرج منه ثم نعيد تنفيذ البرنامج لنرى ما إذا كانت معالجة الخطأ ستتم بنجاح: $ node index.js لنحصل على الخرج التالي هذه المرة: Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293332) Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293348) Sending email to test@email.com Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293348) Gracefully handling our error: Error: There are no more tickets left to purchase نلاحظ في آخر سطر ظهور رسالة معالجة الخطأ من قبل تابع الاستماع الذي سجلناه ولم يفشل تنفيذ البرنامج كما حدث سابقًا. والآن وبعد أن تعلمنا طرق إرسال والاستماع للأحداث بمختلف أنواعها سنتعرف في الفقرة التالية على طرق مفيدة لإدارة توابع الاستماع للأحداث. إدارة توابع الاستماع للأحداث يوفر صنف مرسل الأحداث طرقًا لمراقبة عدد توابع الاستماع المشتركة بحدث ما والتحكم بها، حيث يمكن مثلًا الاستفادة من التابع ‎listenerCount()‎ لمعرفة عدد توابع الاستماع المسجلة لحدث معين ضمن الكائن، حيث يقبل ذلك التابع معامل يدل على الحدث الذي نريد معرفة عدد المستمعين له. والآن لنعود للملف الأساسي ‎index.js‎ ونطبق ذلك حيث نزيل بدايةً استدعاءات تابع الشراء ‎buy()‎ الأربعة السابقة ثم نضيف السطرين التاليين لتصبح الشيفرة كالتالي: const TicketManager = require("./ticketManager"); const EmailService = require("./emailService"); const DatabaseService = require("./databaseService"); const ticketManager = new TicketManager(3); const emailService = new EmailService(); const databaseService = new DatabaseService(); ticketManager.on("buy", (email, price, timestamp) => { emailService.send(email); databaseService.save(email, price, timestamp); }); ticketManager.on("error", (error) => { console.error(`Gracefully handling our error: ${error}`); }); console.log(`We have ${ticketManager.listenerCount("buy")} listener(s) for the buy event`); console.log(`We have ${ticketManager.listenerCount("error")} listener(s) for the error event`); بدلًا من استدعاء تابع الشراء ‎buy()‎ نطبع إلى الطرفية سطران الأول لطباعة عدد التوابع المُستمعة لحدث الشراء ‎buy‎ باستخدام التابع ‎listenerCount()‎، والثاني لطباعة عدد التوابع المستمعة لتابع الخطأ ‎error‎، والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج مجددًا باستخدام الأمر ‎node‎ لنحصل على الخرج التالي: We have 1 listener(s) for the buy event We have 1 listener(s) for the error event بما أننا استدعينا تابع التسجيل ‎on()‎ مرة واحدة لحدث الشراء ‎buy‎ ومرة واحدة أيضًا لحدث الخطأ ‎error‎ فالخرج السابق صحيح. سنستفيد من التابع ‎listenerCount()‎ عندما نتعلم طريقة إزالة توابع الاستماع من مرسل الأحداث لنتأكد من عدم وجود مشتركين بحدث ما، فقد نحتاج أحيانًا لتسجيل تابع استماع لفترة مؤقتة فقط ثم نزيله بعد ذلك. يمكن الاستفادة من التابع ‎off()‎ لإزالة تابع استماع من كائن مرسل الأحداث، ويقبل معاملين هما اسم الحدث وتابع الاستماع الذي نرغب بإزالته. ملاحظة: التابع ‎off()‎ هو اسم بديل عن التابع ‎removeListener()‎ وكلاهما ينفذ نفس العملية ويقبل نفس المعاملات، وسنستخدم في أمثلتنا التابع ‎off()‎ دومًا. وبما أنه يجب تمرير تابع الاستماع الذي نرغب بإزالته كمعامل ثانِ للتابع ‎off()‎ فيجب حفظ ذلك التابع أولًا ضمن متغير أو ثابت كي نشير إليه لاحقًا ونمرره للإزالة، فلا تصلح طريقة استخدام التوابع التي سجلناها سابقًا للأحداث ‎buy‎ و ‎error‎ للإزالة باستخدام ‎off()‎. ولنتعرف على طريقة عمل تابع الإزالة ‎off()‎ سنضيف تابع استماع جديد ونختبر إزالته، ونبدأ بتعريف تابع رد النداء وحفظه ضمن متغير سنمرره لاحقًا لتابع الإزالة ‎off()‎، والآن نعود للملف الأساسي ‎index.js‎ ونفتحه ضمن محرر النصوص ونضيف التالي: ... const onBuy = () => { console.log("I will be removed soon"); }; بعدها نسجل هذا التابع للاستماع إلى الحدث ‎buy‎ كالتالي: ... ticketManager.on("buy", onBuy); وللتأكد من تسجيل التابع بشكل سليم سنطبع عدد التوابع المستمعة للحدث ‎buy‎ ثم نستدعي تابع الشراء ‎buy()‎: ... console.log(`We added a new event listener bringing our total count for the buy event to: ${ticketManager.listenerCount("buy")}`); ticketManager.buy("test@email", 20); نحفظ الملف ونخرج منه ونشغل البرنامج: $ node index.js سيظر لنا الخرج التالي: We have 1 listener(s) for the buy event We have 1 listener(s) for the error event We added a new event listener bringing our total count for the buy event to: 2 Sending email to test@email Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588814306693) I will be removed soon نلاحظ ظهور الرسالة التي توضح عدد التوابع المستمعة لذلك الحدث، ثم استدعينا بعدها التابع ‎buy()‎ ونلاحظ تنفيذ تابعي الاستماع لذلك الحدث، حيث نفذ المستمع الأول عمليتي إرسال البريد الإلكتروني وحفظ البيانات ضمن قاعدة البيانات، ثم طبع المستمع الثاني الرسالة ‎I will be removed soon‎. والآن لنختبر إزالة تابع الاستماع الثاني باستخدام ‎off()‎، لذا نعود للملف مجددًا ونضيف عملية الإزالة بواسطة ‎off()‎ في نهاية الملف وبعدها نطبع عدد توابع الاستماع الحالية المسجلة لنتأكد من الإزالة، ثم نختبر استدعاء تابع الشراء ‎buy()‎ مجددًا: ... ticketManager.off("buy", onBuy); console.log(`We now have: ${ticketManager.listenerCount("buy")} listener(s) for the buy event`); ticketManager.buy("test@email", 20); نلاحظ كيف مررنا المتغير ‎onBuy‎ كمعامل ثاني لتابع الإزالة ‎off()‎ لنحدد تابع الاستماع الذي نرغب بإزالته، نحفظ الملف ونخرج منه وننفذ البرنامج لمعاينة النتيجة: $ node index.js نلاحظ أن الخرج بقي كما كان سابقًا، وظهر سطر جديد يؤكد عملية الإزالة ويوضح عدد التوابع المسجلة، ثم بعد استدعاء تابع الشراء ‎buy()‎ نلاحظ ظهور خرج تابع الاستماع الأول فقط، بينما أُزيل تابع الاستماع الثاني: We have 1 listener(s) for the buy event We have 1 listener(s) for the error event We added a new event listener bringing our total count for the buy event to: 2 Sending email to test@email Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588816352178) I will be removed soon We now have: 1 listener(s) for the buy event Sending email to test@email Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588816352178) يمكن أيضًا إزالة كل توابع الاستماع لحدث ما دفعة واحدة باستدعاء التابع ‎removeAllListeners()‎، ونمرر له اسم الحدث الذي نرغب بإزالة التوابع التي تستمع إليه، وسنستفيد من هذا التابع لإزالة تابع الاستماع الأول للحدث ‎buy‎ الذي لم نتمكن من إزالته سابقًا بسبب طريقة تعريفه الآن نعود للملف ‎index.js‎ ونزيل كافة توابع الاستماع باستخدام ‎removeAllListeners()‎ ثم نطبع عدد التوابع المسجلة باستخدام ‎listenerCount()‎ للتأكد من نجاح العملية، ونتحقق من ذلك أيضًا بتنفيذ عملية شراء جديدة بعد الإزالة، ونلاحظ أن لا شيء سيحدث بعد إرسال ذلك الحدث، لذا نضيف الشيفرة التالية في نهاية الملف: ... ticketManager.removeAllListeners("buy"); console.log(`We have ${ticketManager.listenerCount("buy")} listeners for the buy event`); ticketManager.buy("test@email", 20); console.log("The last ticket was bought"); نحفظ الملف ونخرج منه ونشغل البرنامج: $ node index.js نحصل على الخرج: ... ticketManager.removeAllListeners("buy"); console.log(`We have ${ticketManager.listenerCount("buy")} listeners for the buy event`); ticketManager.buy("test@email", 20); console.log("The last ticket was bought"); نلاحظ بعد إزالة كل توابع الاستماع لم يُرسل أي بريد إلكتروني ولم تُحفظ أي بيانات في قاعدة البيانات. ختامًا تعلمنا في هذا المقال وظيفة مرسل الأحداث وطريقة إرسال الأحداث منه باستخدام التابع ‎emit()‎ الذي يوفره الصنف ‎EventEmitter‎، ثم تعلمنا طرق الاستماع لتلك الأحداث باستخدام التابعين ‎on()‎ و ‎once()‎ لتنفيذ التعليمات البرمجية استجابة لإرسال حدث ما، وتعلمنا كيف يمكن معالجة أحداث الأخطاء، وكيفية مراقبة توابع الاستماع المسجلة باستخدام ‎listenerCount()‎، وإدارتها باستخدام التابعين ‎off()‎ و ‎removeAllListeners()‎. لو استخدمنا توابع رد النداء callbacks والوعود promises للاستجابة للأحداث ضمن نظام مدير البطاقات لكنا سنحتاج لربطه مع الوحدات البرمجية للخدمات الأخرى كخدمة البريد الإلكتروني وخدمة قاعدة البيانات، لكن بالاستفادة من مرسل الأحداث تمكنا من فصل تلك الوحدات البرمجية عن بعضها، ويمكن لأي وحدة برمجية جديدة قد نضيفها لاحقًا وتستطيع الوصول لمدير البطاقات أن تُربط معه وتستجيب للأحداث التي سيرسلها، وهي الفائدة التي يوفرها التعامل مع مرسل الأحداث فعند تطوير وحدة برمجية نرغب بربطها لاحقًا مع عدة وحدات برمجية أخرى أو مراقبتها يمكن أن نجعلها ترث صنف مرسل الأحداث الأساسي لتسهيل عملية الربط تلك. ويمكنك الرجوع إلى توثيق نود الرسمي العربي لمرسل الأحداث للتعرف عليه أكثر. ترجمة -وبتصرف- للمقال Using Event Emitters in Node.js لصاحبه Stack Abuse. اقرأ أيضًا المقال السابق: استخدام المخازن المؤقتة Buffers في Node.js تعرف على وحدات Node.js الأساسية مقدمة إلى Node.js
  20. عند كل استدعاء لذلك التابع سيتم تعريف مصفوفة جديدة وإضافة عنصر واحد إليها ثم طباعتها، بعد انتهاء تنفيذ التابع سيمسح المفسّر قيمة المصفوفة من الذاكرة وذلك لأن المصفوفة لا يمكن الوصول إليها سوى من داخل التابع ولم يعد هناك حاجة لها، لذا للمحافظة على قيمتها يمكن تعريفها خارج التابع كالتالي: const yo =[]; // تعريف خارج التابع function Moneys(mo){ .. // إضافة للمصفوفة وطباعتها }
  21. يمكنك إضافة الخاصية title لعنصر القائمة ضمن HTML، وتمرير قيمة النص التي تريد أن تظهر عند وضع مؤشر الفأرة فوق العنصر كالتالي: <li title="الرئيسية"> ... </li>
  22. لا داعٍ لاستخدام مكتبة redux لإدارة حالة التطبيق ما دامت الحالة بسيطة، في مشروعك والسيناريو الذي ذكرته لا يوجد حالة للتطبيق يجب إدارتها، كل ما عليك هو تعريف التوجيه بشكل صحيح باستخدام مكتبة react-router ليتم توجيه الروابط إلى صفحاتها المطلوبة، ثم ذكر تلك الروابط ضمن المكونات المسؤولة عن عرض نوع المنتج والمنتج، الصعوبة في التطبيق قد يكون سببها اختيار طريقة معقدة لحل المشكلة، مع الوقت والتنفيذ العملي المتكرر والقراءة عن الحلول والإمكانيات وميزات المكتبات المستخدمة ستزيد مهارتك في حل المشكلة بأبسط الطرق، أنصحك بالتركيز فقط على مكتبة react-router لحل مشكلة التنقل بين الصفحات وتجنب تعيين متغيرات أو خطافات hooks مسؤولة عن تغيير الصفحة المعروضة بل فقط استخدم روابط عادية مثل href للمكون a، ومكتبة react-router ستتولى عرض الصفحة المطلوبة
  23. يمكنك التواصل مع مركز المساعدة وشرح مشكلتك لهم بالتفصيل مع إرفاق تلك الصورة لتوضيح المشكلة، وسيتابعون معك سبب المشكلة ويرشدونك لحلها، حاول إرسال شرح مفصل للخطوات التي قمت بها وما حدث وانتظر الرد منهم في أقرب وقت
  24. يتم تحديث الموقع دوريًا من قبل الفريق المختص، التحديث الأخير يركز على تحسين المظهر العام للموقع من كافة النواحي، كذلك أُضيف عدة أنواع من التفاعل والتعبير مع المشاركات، بالنسبة للدورات يتم دوريًا إضافة الدروس وتحديثها لاستخدام آخر المكتبات وأطر العمل المناسبة لمجال الدورة، يمكنك إذا أردت التواصل مع مركز المساعدة إذا كان لديك استفسارات اضافية
  25. يمكنك لذلك الاستفادة من رمز الخطأ عن طريق التابع getCode والذي يعيد رمز الخطأ الذي حدث، ثم التحقق من ذلك الرمز، فمثلًا في حال كان الرمز يبدأ بالرقم 08 فيعني أن الخطأ سببه مشكلة في الاتصال، في هذه الحالة يمكنك تنفيذ الأوامر الأخرى التي تريد كالتالي: catch(PDOException $e) { if(str_starts_with((string)$e->getCode(),'08')){ // خطأ في الاتصال ... } else { // خطأ آخر echo "Connection failed: " . $e->getMessage(); } }
×
×
  • أضف...