كيفية كتابة كود غير متزامن في Node.js

اختار المؤلف صندوق الإنترنت المفتوح / صندوق الحرية في التعبير لتلقي تبرع كجزء من برنامج الكتابة من أجل التبرعات.

المقدمة

بالنسبة للعديد من البرامج في جافا سكريبت، يتم تنفيذ الشفرة كما يكتبها المطور – سطرا بعد الآخر. ويُطلق على هذا النوع من التنفيذ اسم التنفيذ المتزامن، لأن الأسطر تُنفذ واحدة تلو الأخرى، في النظام الذي كُتبت به. ومع ذلك، ليس كل تعليمة تُعطى للكمبيوتر بحاجة للانتظار على الفور. على سبيل المثال، إذا قمت بإرسال طلب شبكة، فإن العملية التي تنفذ الشفرة الخاصة بك ستضطر إلى الانتظار حتى يعود البيانات قبل أن تعمل عليها. في هذه الحالة، سيتم إضاعة الوقت إذا لم تُنفذ شفرة أخرى بينما تنتظر العملية لإكمال طلب الشبكة. لحل هذه المشكلة، يستخدم المطورون البرمجة الغير متزامنة، حيث يتم تنفيذ أسطر الشفرة بترتيب مختلف عن الذي كُتبت به. باستخدام البرمجة الغير متزامنة، يمكننا تنفيذ شفرة أخرى بينما ننتظر الأنشطة الطويلة مثل طلبات الشبكة لإنهاء عملها.

تُنفَّذُ رمز JavaScript على خيط واحد داخل عملية الكمبيوتر. يُعالجُ رمزه بشكل متزامن على هذا الخيط، حيثُ يُشغَّلُ تعليمةً واحدةً في كل مرةٍ. لذلك، إذا قمنا بمهمةٍ طويلة الأمد على هذا الخيط، فإن كل الرمز المتبقي يُحجَبُ حتى اكتمال المهمة. بتسخير ميزات البرمجة غير المتزامنة في JavaScript، يُمكننا تفريغ المهام الطويلة الأمد إلى خيط خلفي لتجنب هذه المشكلة. عند اكتمال المهمة، يتم وضع الرمز الذي نحتاج إلى معالجة بيانات المهمة مرة أخرى على الخيط الرئيسي الواحد.

في هذا البرنامج التعليمي، ستتعلم كيفية إدارة المهام غير المتزامنة في JavaScript بمساعدة حلقة الأحداث، وهي بنية JavaScript تكمِّل مهمة جديدة أثناء انتظار أخرى. بعد ذلك، ستقوم بإنشاء برنامج يستخدم البرمجة غير المتزامنة لطلب قائمة بالأفلام من واجهة برمجة تطبيقات Studio Ghibli وحفظ البيانات في ملف CSV. سيتم كتابة الرمز غير المتزامن بثلاث طرق: استدعاء العودة، الوعود، وباستخدام الكلمات الرئيسية async/await.

ملاحظة: حتى تاريخ كتابة هذا المقال، لم يعد استخدام البرمجة غير المتزامنة يعتمد فقط على استدعاءات العودة، ولكن تعلم هذه الطريقة القديمة يمكن أن يوفر سياقًا رائعًا لماذا تستخدم مجتمع JavaScript الوعود الآن. تمكِّننا كلمات الرئيسية async/await من استخدام الوعود بطريقة أقل حجمًا، وبالتالي فهي الطريقة القياسية للقيام بالبرمجة غير المتزامنة في JavaScript حتى تاريخ كتابة هذا المقال.

المتطلبات

دورة الحدث

لنبدأ بدراسة آلية عمل تنفيذ وظائف JavaScript الداخلية. فهم كيف يتصرف هذا سيسمح لك بكتابة كود متزامن بشكل عنيد أكثر، وسيساعدك في إصلاح الكود في المستقبل.

عندما يقوم مترجم JavaScript بتنفيذ التعليمات البرمجية، يتم إضافة كل وظيفة مكالمة إلى كومة المكالمات في JavaScript. تعتبر كومة المكالمات كومة – بنية بيانات شبيهة بالقائمة حيث يمكن إضافة العناصر فقط إلى القمة، وإزالتها من القمة. تتبع المكدسات مبدأ “آخر دخول، أول سحب” أو LIFO. إذا قمت بإضافة عنصرين إلى المكدس، فإن العنصر الأخير المضاف يتم إزالته أولاً.

دعونا نوضح بمثال باستخدام كومة المكالمات. إذا قابل JavaScript وظيفة functionA()، يتم إضافتها إلى كومة المكالمات. إذا كانت تلك الوظيفة functionA() تطلق وظيفة أخرى functionB()، فإن functionB() يتم إضافتها إلى قمة كومة المكالمات. عندما يكمل JavaScript تنفيذ وظيفة، يتم إزالتها من كومة المكالمات. لذلك، سيقوم JavaScript بتنفيذ functionB() أولاً، وإزالتها من المكدس عند الانتهاء، ثم الانتهاء من تنفيذ functionA() وإزالتها من كومة المكالمات. لهذا السبب يتم تنفيذ الوظائف الداخلية دائمًا قبل وظائفها الخارجية.

عندما يواجه JavaScript عملية غير متزامنة، مثل الكتابة إلى ملف، يُضيفها إلى جدول في ذاكرته. يخزن هذا الجدول العملية، والشرط لاستكمالها، والدالة التي يجب استدعاؤها عند اكتمالها. كلما اكتملت العملية، يضيف JavaScript الدالة المرتبطة بها إلى طابور الرسائل. الطابور هو هيكل بيانات مشابه للقائمة حيث لا يمكن إضافة العناصر إلا في الأسفل وإزالتها من الأعلى. في طابور الرسائل، إذا كانت عمليتان أو أكثر غير متزامنة جاهزة لتنفيذ دوالها، ستكون العملية غير المتزامنة التي اكتملت أولاً لها دالتها محددة للتنفيذ أولاً.

الدوال في طابور الرسائل في انتظار إضافتها إلى كومة الاستدعاء. الحلقة الحدثية هي عملية دائمة تتحقق مما إذا كانت كومة الاستدعاء فارغة. إذا كانت كذلك، يتم نقل أول عنصر في طابور الرسائل إلى كومة الاستدعاء. يعطي JavaScript الأولوية للدوال في طابور الرسائل على الدوال التي يفسرها في الشيفرة. الآثار المجتمعة لكومة الاستدعاء، وطابور الرسائل، والحلقة الحدثية تسمح لشيفرة JavaScript بالمعالجة بينما تدير الأنشطة غير المتزامنة.

الآن بعد أن لديك فهم عالي المستوى للحلقة الحدثية، تعرف كيف سيتم تنفيذ الشيفرة غير المتزامنة التي تكتبها. باستخدام هذا المعرفة، يمكنك الآن إنشاء شيفرة غير متزامنة باستخدام ثلاثة نهج مختلفة: التعامل مع الاستدعاءات، الوعود، و async/await.

البرمجة الغير متزامنة باستخدام التعامل مع الاستدعاءات

A callback function is one that is passed as an argument to another function, and then executed when the other function is finished. We use callbacks to ensure that code is executed only after an asynchronous operation is completed.

لفترة طويلة، كانت الاستدعاءات هي الآلية الأكثر شيوعًا لكتابة الشفرة غير المتزامنة، ولكن الآن أصبحت في الغالب قديمة لأنها يمكن أن تجعل الشفرة مربكة للقراءة. في هذه الخطوة، ستقوم بكتابة مثال على الشفرة غير المتزامنة باستخدام الاستدعاءات حتى تتمكن من استخدامه كقاعدة لرؤية الكفاءة المتزايدة لاستراتيجيات أخرى.

هناك العديد من الطرق لاستخدام وظائف الاستدعاء في دالة أخرى. بشكل عام، تأخذ هذه الهيكل:

function asynchronousFunction([ Function Arguments ], [ Callback Function ]) {
    [ Action ]
}

على الرغم من أنه ليس من الضروري بناءً على الصياغة في جافا سكريبت أو نود.جي.إس أن يكون دالة الاستدعاء كآخر وسيط للدالة الخارجية، إلا أنه من الممارسات الشائعة التي تجعل الاستدعاءات أسهل في التعرف عليها. كما أنه من الشائع أيضًا على مطوري جافا سكريبت استخدام دالة مجهولة الاسم كاستدعاء. الدوال المجهولة الاسم هي تلك التي يتم إنشاؤها بدون اسم. عادةً ما يكون أكثر قراءة عندما تكون الدالة محددة في نهاية قائمة الوسائط.

لتوضيح الاستدعاءات، دعونا ننشئ وحدة Node.js تكتب قائمة بأفلام استوديو غيبلي إلى ملف. أولاً، قم بإنشاء مجلد سيحتوي على ملف JavaScript لدينا والمخرجات الخاصة به:

  1. mkdir ghibliMovies

ثم ادخل إلى هذا المجلد:

  1. cd ghibliMovies

سنبدأ بعمل طلب HTTP إلى واجهة برمجة تطبيقات استوديو غيبلي، الذي ستسجله دالتنا المستدعاة نتائجه. للقيام بذلك، سنقوم بتثبيت مكتبة تتيح لنا الوصول إلى بيانات استجابة HTTP في دالة استدعاء.

في الطرفية الخاصة بك، قم بتهيئة npm حتى نتمكن من الرجوع إلى حزمنا لاحقًا: ثم، قم بتثبيت مكتبة request:

  1. npm init -y

ثم، قم بتثبيت مكتبة request:

  1. npm i request --save

الآن افتح ملفًا جديدًا يسمى callbackMovies.js في محرر نص مثل nano:

  1. nano callbackMovies.js

في محرر النص الخاص بك، أدخل التعليمات البرمجية التالية. لنبدأ بإرسال طلب HTTP باستخدام وحدة request:

callbackMovies.js
const request = require('request');

request('https://ghibliapi.herokuapp.com/films');

في السطر الأول، نحمل وحدة request التي تم تثبيتها عبر npm. تُرجع الوحدة وظيفة يمكنها إرسال طلبات HTTP؛ ثم نحفظ تلك الوظيفة في الثابت request.

ثم نقوم بإرسال طلب HTTP باستخدام الدالة request(). دعونا الآن نطبع بيانات الطلب HTTP على وحدة التحكم عن طريق إضافة التغييرات المظللة:

callbackMovies.js
const request = require('request');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    movies.forEach(movie => {
        console.log(`${movie['title']}, ${movie['release_date']}`);
    });
});

عندما نستخدم الدالة request()، نعطيها معلمتين:

  • عنوان URL الموقع الذي نحاول طلبه
  • A callback function that handles any errors or successful responses after the request is complete

دالة المكالمة المتصلة (callback) الخاصة بنا لديها ثلاثة أعضاء: error، response، و body. عندما يكتمل طلب HTTP، يتم تعيين القيم تلقائيًا بناءً على النتيجة. إذا فشل الطلب في الإرسال، فإن error سيحتوي على كائن، لكن response و body سيكونا null. إذا تم إرسال الطلب بنجاح، فإن الاستجابة التي حصل عليها HTTP تُخزّن في response. إذا أرجع طلب HTTP الخاص بنا بيانات (في هذا المثال نحصل على JSON) ، فإن البيانات تُضاف في body.

تقوم وظيفة الرد الخاصة بنا أولاً بالتحقق مما إذا كنا قد تلقينا خطأ. من الممارسات الجيدة التحقق من الأخطاء في الرد أولاً حتى لا يستمر تنفيذ الرد بدون بيانات مفقودة. في هذه الحالة، نقوم بتسجيل الخطأ وتنفيذ الوظيفة. ثم نتحقق من كود الحالة في الاستجابة. قد لا يكون خادمنا متاحًا دائمًا، ويمكن أن تتغير واجهات برمجة التطبيقات مما يؤدي إلى طلبات منطقية مرة واحدة لتصبح غير صحيحة. من خلال التحقق من أن كود الحالة هو 200، مما يعني أن الطلب كان “موافق”، يمكننا أن نكون واثقين من أن الاستجابة هي ما نتوقعه.

أخيرًا، نقوم بتحليل جسم الاستجابة إلى Array ونكرر كل فيلم لتسجيل اسمه وسنة الإصدار.

بعد حفظ الملف وإغلاقه، قم بتشغيل هذا النص بالأمر التالي:

  1. node callbackMovies.js

ستحصل على الناتج التالي:

Output
Castle in the Sky, 1986 Grave of the Fireflies, 1988 My Neighbor Totoro, 1988 Kiki's Delivery Service, 1989 Only Yesterday, 1991 Porco Rosso, 1992 Pom Poko, 1994 Whisper of the Heart, 1995 Princess Mononoke, 1997 My Neighbors the Yamadas, 1999 Spirited Away, 2001 The Cat Returns, 2002 Howl's Moving Castle, 2004 Tales from Earthsea, 2006 Ponyo, 2008 Arrietty, 2010 From Up on Poppy Hill, 2011 The Wind Rises, 2013 The Tale of the Princess Kaguya, 2013 When Marnie Was There, 2014

لقد تلقينا بنجاح قائمة بأفلام ستوديو غيبلي مع السنة التي تم إصدارها فيها. الآن دعنا نكمل هذا البرنامج بكتابة قائمة الأفلام التي نقوم حاليًا بتسجيلها في ملف.

قم بتحديث ملف callbackMovies.js في محرر النص الخاص بك لتضمين الكود المميز التالي، الذي ينشئ ملف CSV باستخدام بيانات الفيلم الخاصة بنا:

callbackMovies.js
const request = require('request');
const fs = require('fs');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    let movieList = '';
    movies.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });

    fs.writeFile('callbackMovies.csv', movieList, (error) => {
        if (error) {
            console.error(`Could not save the Ghibli movies to a file: ${error}`);
            return;
        }

        console.log('Saved our list of movies to callbackMovies.csv');;
    });
});

ملاحظة التغييرات المميزة، نرى أننا نقوم بأستيراد وحدة fs. تعد هذه الوحدة قياسية في جميع تثبيتات Node.js، وتحتوي على طريقة writeFile() التي يمكنها كتابة الملف بشكل غير متزامن.

بدلاً من تسجيل البيانات في وحدة التحكم، نقوم الآن بإضافتها إلى متغير سلسلة movieList. ثم نستخدم writeFile() لحفظ محتويات movieList في ملف جديد – callbackMovies.csv. وأخيرًا، نقدم وظيفة رد الاتصال إلى دالة writeFile()، التي تحتوي على وسيط واحد: error. يتيح لنا هذا التعامل مع الحالات التي لا يمكننا فيها كتابة الملف، على سبيل المثال عندما لا يكون للمستخدم الذي نقوم بتشغيل عملية node عليه تلك الأذونات.

احفظ الملف وقم بتشغيل برنامج Node.js هذا مرة أخرى باستخدام:

  1. node callbackMovies.js

في مجلد ghibliMovies الخاص بك، سترى callbackMovies.csv، الذي يحتوي على المحتوى التالي:

callbackMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

من المهم أن نلاحظ أننا نكتب إلى ملف CSV الخاص بنا في رد الاتصال لطلب HTTP. عندما يكون الكود في دالة الرد، فإنه سيقوم بكتابة الملف فقط بعد اكتمال طلب HTTP. إذا أردنا التواصل مع قاعدة بيانات بعد كتابة ملف CSV الخاص بنا، فسنقوم بإنشاء وظيفة غير متزامنة أخرى ستُستدعى في رد الاتصال لـ writeFile(). كلما زاد الكود غير المتزامن الذي لدينا، زاد عدد وظائف الرد.

لنتخيل أننا نريد تنفيذ خمس عمليات غير متزامنة، كل منها قادر فقط على التشغيل عندما يكتمل آخر. إذا كنا سنبرمج هذا، سنكون لدينا شيء مثل هذا:

doSomething1(() => {
    doSomething2(() => {
        doSomething3(() => {
            doSomething4(() => {
                doSomething5(() => {
                    // إجراء نهائي
                });
            });
        }); 
    });
});

عندما يحتوي التداخل المتداخل على العديد من الأسطر للتنفيذ، يصبح أكثر تعقيدًا وعدم قراءة. مع نمو مشروعك في جافا سكريبت من حيث الحجم والتعقيد، سيصبح هذا التأثير أكثر وضوحًا، حتى يصبح غير قابل للإدارة في نهاية المطاف. وبسبب ذلك، لم يعد المطورون يستخدمون التداعيات للتعامل مع العمليات الغير متزامنة. لتحسين بناء جملة شفرتنا الغير متزامنة، يمكننا استخدام الوعود بدلاً من ذلك.

A promise is a JavaScript object that will return a value at some point in the future. Asynchronous functions can return promise objects instead of concrete values. If we get a value in the future, we say that the promise was fulfilled. If we get an error in the future, we say that the promise was rejected. Otherwise, the promise is still being worked on in a pending state.

تستخدم الوعود بشكل عام الشكل التالي:

promiseFunction()
    .then([ Callback Function for Fulfilled Promise ])
    .catch([ Callback Function for Rejected Promise ])

كما هو موضح في هذا القالب، تستخدم الوعود أيضًا وظائف التداعيات. لدينا وظيفة تداعيات لطريقة then()، التي يتم تنفيذها عندما يتم تحقيق الوعد. لدينا أيضًا وظيفة تداعيات لطريقة catch() للتعامل مع أي أخطاء تحدث أثناء تنفيذ الوعد.

دعنا نحصل على تجربة مباشرة مع الوعود عن طريق إعادة كتابة برنامج ستوديو جيبلي الخاص بنا لاستخدام الوعود بدلاً من ذلك.

Axios هو عميل HTTP مبني على الوعود لجافا سكريبت، لذا دعنا نقوم بتثبيته:

  1. npm i axios --save

الآن، باستخدام محرر النص الخاص بك المفضل، قم بإنشاء ملف جديد promiseMovies.js:

  1. nano promiseMovies.js

سيقوم برنامجنا بإجراء طلب HTTP مع axios ثم استخدام نسخة وعودية خاصة من fs للحفظ في ملف CSV جديد.

قم بكتابة هذا الكود في promiseMovies.js حتى نتمكن من تحميل Axios وإرسال طلب HTTP إلى واجهة برمجة التطبيقات الخاصة بالأفلام:

promiseMovies.js
const axios = require('axios');

axios.get('https://ghibliapi.herokuapp.com/films');

في السطر الأول نقوم بتحميل وحدة الـ axios، مخزنين الدالة المُرجعة في ثابت يُسمى axios. ثم نستخدم طريقة axios.get() لإرسال طلب HTTP إلى واجهة برمجة التطبيقات.

تُعيد طريقة axios.get() وعدًا. لنقم بربط تلك الوعدة حتى نتمكن من طباعة قائمة أفلام Ghibli إلى وحدة التحكم:

promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        response.data.forEach(movie => {
            console.log(`${movie['title']}, ${movie['release_date']}`);
        });
    })

لنقم بتحليل ما يحدث. بعد إجراء طلب GET HTTP بواسطة axios.get()، نستخدم الدالة then()، التي لا تُنفَذ إلا عندما يتم تحقيق الوعد. في هذه الحالة، نقوم بطباعة الأفلام إلى الشاشة كما فعلنا في مثال التعريفات الخاصة بالإستدعاءات الرجوعية.

لتحسين هذا البرنامج، أضف الشيفرة المحددة لكتابة البيانات HTTP إلى ملف:

promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })

نقوم بإستيراد وحدة fs إضافية مرة أخرى. لاحظ كيف أنه بعد إستيراد fs لدينا .promises. يتضمن Node.js نسخة معدلة للوعود من مكتبة الـ fs المعتمدة على الإستدعاءات الرجوعية، لذا لا يتم كسر التوافق الخلفي في المشاريع القديمة.

الدالة then() الأولى التي تُعالج طلب الـ HTTP الآن تستدعي fs.writeFile() بدلاً من الطباعة إلى وحدة التحكم. نظرًا لأننا قمنا بإستيراد النسخة المعتمدة على الوعود من fs، فإن دالتنا writeFile() تُعيد وعدًا آخر. لذا، نضيف دالة then() أخرى للتعامل مع وعد writeFile() عند تحقيقه.

A promise can return a new promise, allowing us to execute promises one after the other. This paves the way for us to perform multiple asynchronous operations. This is called promise chaining, and it is analogous to nesting callbacks. The second then() is only called after we successfully write to the file.

ملاحظة: في هذا المثال، لم نتحقق من رمز حالة HTTP كما فعلنا في مثال الاستدعاء. بشكل افتراضي، axios لا يفي بوعده إذا حصل على رمز حالة يشير إلى خطأ. وبالتالي، لم نعد بحاجة إلى التحقق منه.

لإكمال هذا البرنامج، قم بربط الوعد بوظيفة catch() كما هو موضح فيما يلي:

promiseMovies.js
const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })
    .catch((error) => {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    });

إذا لم يتم تحقيق أي وعد في سلسلة الوعود، ينتقل JavaScript تلقائيًا إلى وظيفة catch() إذا تم تعريفها. لهذا السبب، لدينا فقط تعليمة واحدة catch() على الرغم من وجود عمليتين غير متزامنتين.

دعنا نتأكد من أن برنامجنا ينتج نفس الناتج من خلال تشغيل:

  1. node promiseMovies.js

في مجلد ghibliMovies الخاص بك، سترى ملف promiseMovies.csv الذي يحتوي على:

promiseMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

مع الوعود، يمكننا كتابة رمز أكثر انتظامًا بكثير من استخدام المداخل فقط. سلسلة الوعود بالمداخل هي خيار أنظف من تضمين المداخل. ومع ذلك، كلما قمنا بإجراء المزيد من الاستدعاءات غير المتزامنة، تصبح سلسلة الوعود أطول وأصعب في الصيانة.

الطول المفرط للمداخل والوعود يأتي من الحاجة إلى إنشاء وظائف عندما نحصل على نتيجة مهمة غير متزامنة. تجربة أفضل ستكون بانتظار نتيجة غير متزامنة ووضعها في متغير خارج الوظيفة. بهذه الطريقة، يمكننا استخدام النتائج في المتغيرات دون الحاجة إلى إنشاء وظيفة. يمكننا تحقيق هذا باستخدام الكلمات المفتاحية async و await.

كتابة JavaScript مع async/await

الكلمات المفتاحية async/await توفر بنية بديلة عند العمل مع الوعود. بدلاً من وجود نتيجة الوعد متاحة في طريقة then()، يتم إرجاع النتيجة كقيمة مثل أي دالة أخرى. نقوم بتعريف دالة باستخدام الكلمة الرئيسية async لإخبار JavaScript بأنها دالة غير متزامنة تُرجع وعدًا. نستخدم كلمة await لإخبار JavaScript بإرجاع نتائج الوعد بدلاً من إرجاع الوعد نفسه عندما يتم تحقيقه.

بشكل عام، يبدو استخدام async/await مثل هذا:

async function() {
    await [Asynchronous Action]
}

لنرى كيف يمكن استخدام async/await لتحسين برنامج Studio Ghibli الخاص بنا. استخدم محرر النصوص لإنشاء ملف جديد وافتحه باسم asyncAwaitMovies.js:

  1. nano asyncAwaitMovies.js

في ملف JavaScript الجديد الذي تم فتحه، دعنا نبدأ بالاستيراد نفس الوحدات التي استخدمناها في مثال الوعد:

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

الاستيرادات هي نفسها كما في promiseMovies.js لأن async/await تستخدم الوعود.

الآن نستخدم الكلمة الرئيسية async لإنشاء دالة مع الكود غير المتزامن الخاص بنا:

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {}

نقوم بإنشاء وظيفة جديدة تسمى saveMovies() ولكننا نضمن وجود async في بداية تعريفها. هذا مهم لأننا يمكن أن نستخدم الكلمة الرئيسية await فقط في وظيفة غير متزامنة.

نستخدم الكلمة الرئيسية await لإجراء طلب HTTP يحصل على قائمة الأفلام من API جيبلي:

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
}

في وظيفتنا saveMovies()، نقوم بعمل طلب HTTP باستخدام axios.get() كما فعلنا من قبل. هذه المرة، لا نقوم بربطها بوظيفة then(). بدلاً من ذلك، نضيف await قبل استدعائها. عندما يرى JavaScript await، سينفذ الشيفرة المتبقية من الوظيفة فقط بعد انتهاء تنفيذ axios.get() وتعيين المتغير response. الشيفرة الأخرى تقوم بحفظ بيانات الفيلم حتى نتمكن من الكتابة إلى ملف.

لنكتب بيانات الفيلم إلى ملف:

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
    await fs.writeFile('asyncAwaitMovies.csv', movieList);
}

نستخدم أيضًا الكلمة الرئيسية await عندما نكتب إلى الملف باستخدام fs.writeFile().

لإكمال هذه الوظيفة، نحتاج إلى التقاط الأخطاء التي يمكن أن تقذفها الوعود لدينا. دعونا نفعل ذلك عن طريق تضمين شيفرتنا في كتلة try/catch:

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

نظرًا لأن الوعود قد تفشل، نقوم بتغليف شيفرتنا الغير متزامنة بعبارة try/catch. سيتم ذلك لالتقاط أي أخطاء يتم رميها عند فشل عمليات الطلب HTTP أو كتابة الملف.

أخيرًا، دعونا نستدعي وظيفتنا الغير متزامنة saveMovies() حتى يتم تنفيذها عند تشغيل البرنامج بواسطة node.

asyncAwaitMovies.js
const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

saveMovies();

عند النظرة الأولى، يبدو هذا مثل كود JavaScript متزامن نموذجي. يحتوي على أقل عدد من الدوال التي يتم تمريرها، مما يبدو أكثر نظافة. هذه التعديلات الصغيرة تجعل كود اللا مزامن مع async/await أسهل للصيانة.

اختبر هذا الإصدار من برنامجنا من خلال إدخال هذا في الطرفية الخاصة بك:

  1. node asyncAwaitMovies.js

سيتم إنشاء ملف جديد بعنوان asyncAwaitMovies.csv في مجلد ghibliMovies بالمحتويات التالية:

asyncAwaitMovies.csv
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

لقد استخدمت الآن ميزات JavaScript async/await لإدارة الكود غير المتزامن.

الختام

في هذا البرنامج التعليمي، تعلمت كيفية تنفيذ الدوال وإدارة العمليات غير المتزامنة في JavaScript باستخدام حلقة الأحداث. ثم كتبت برامج أنشأت ملف CSV بعد عمل طلب HTTP لبيانات الأفلام باستخدام تقنيات برمجة غير متزامنة مختلفة. أولاً، استخدمت الطريقة المبنية على الاستدعاءات المتجاوبة القديمة. ثم استخدمت الوعود، وأخيرًا async/await لجعل بنية الوعد أكثر إيجازًا.

مع فهمك للكود الغير متزامن بـ Node.js، يمكنك الآن تطوير برامج تستفيد من البرمجة غير المتزامنة، مثل تلك التي تعتمد على استدعاءات واجهة برمجة التطبيقات (APIs). انظر إلى هذه القائمة من الـ APIs العامة. لاستخدامها، ستحتاج إلى إجراء طلبات HTTP غير متزامنة مثلما فعلنا في هذا الدرس. للدراسة العميقة، جرب بناء تطبيق يستخدم هذه الـ APIs لممارسة التقنيات التي تعلمتها هنا.

Source:
https://www.digitalocean.com/community/tutorials/how-to-write-asynchronous-code-in-node-js