نست فریمورکی برای ساختن اپلیکشن های بک اند در NodeJs هست که با TypeScript ساخته شده و بطور کامل ازش پشتیبانی میکنه و همچنین قابلیت های کار با وب سوکت، GraphQl ،Rest و ... رو برامون فراهم میکنه.
در لایه های زیرین خودش از فریمورک ExpressJs بطور پیشفرض استفاده میکنه و همچنین میتونید بجای اکسپرس از Fastify هم استفاده کنید. در واقع یک لایه ابسترکشن روی فریمورکهای express یا fastify هست و در عین حال امکان این رو بهمون میده که مستقیما با api های این فریمورک ها هم کار کنیم و خوبیش اینه که میتونیم از لایبری ها و ماژول هایی که برای این فریمورک ها هست به راحتی استفاده کنیم.
چرا بجای اکسپرس از نست استفاده کنیم؟
یکی از دلایل میشه گفت وجود نداشتن ساختار و استانداردی در پروژه های nodejs هست. هر موقع که وارد یک پروژه جدید نود میشیم احتمال زیاد با یک ساختار متفاوت رو به رو میشیم مثلا بعضی ها ممکنه فقط از یک فایل استفاده کرده باشند ! یا بعضیا از معماری MVC و خیلی چیزای دیگه. این حالت بیشتر واسه کسایی که تازه کار هستن و زیاد با معماری ها و ساختارها آشنا نیستن میتونه گیج کننده باشه و در کل احتمال تولید کد کثیف و غیرقابل نگهداری با گذشت زمان بیشتر میشه که در نهایت کار توسعه رو سخت تر و زمانبر میکنه.
نست فقط یک ساختار و معماری رو بهمون معرفی میکنه و میگه که فقط توی این قالب و استایل کد بزنین.
کامپوننت ها معماری یک اپلیکشن نست به این صورته:
- کنترلر ها (Controllers)
- ماژول ها (Modules)
- پروایدر Provider ها (services, repositories, factories, helpers , ...)
- کلاس های DTO
- کلاس های مدل (Entities)
دلیل اصلی استفاده از این معماری و ساختار این هست که اپلیکشن رو به بخش های کوچیکی بشکنیم. یادگیری و کارکردن به این شیوه برای کسایی که انگولار یا spring boot کار کردن میتونه ساده باشه.
در ادامه به معرفی این کامپوننت ها بصورت اجمالی میپردازیم.
کنترلر ها (Controllers):
هدف اصلی دریافت درخواست از کاربر یا هرچیزی دیگه ای هست و فرستادنش به کامپوننت سرویس ها (business logic) برای انجام کار مورد نظر و سپس برگرداندن پاسخ.
ماژول ها (Modules):
ماژول مثل یک کانتینر هست که بقیه بخش هایی که بهم مرتبط هستن رو در خودش نگه میداره .
مثلا یک ماژول بنام user فقط شامل سرویس ها و کنترل ها و ... مخصوص و مرتبط کار با یوزر هاست و به این صورت بخش های مختلف اپ رو جداسازی میکنه.
پروایدر ها Providers:
این بخش مقداری عمومی هستش و میتونه شامل ریپازیتوری مدل های دیتابیس، سرویس، کلاس های helper و ... باشه. ایده کلی پرواید ها اینه که میتونه در جاهای مختلف به صورت وابستگی تزریق بشه، مثل یک کانکشن دیتابیس که میتونه در بخش ها و کلاس های مختلفی تزریق بشه و امکان کار کردن با دیتابیس رو بده. در واقع سرویس ها شامل business logic و منطق برنامه هستن که میتونن در هرجایی استفاده بشن.
کلاس های DTO:
این بخش که مخفف Data Transfer Object هست یک ساختار و فرمت برای داده هایی که در یک اندپوینت (route) از کاربر دریافت میشه تعیین میکنه. مثلا میخواید که کاربر سن رو بصورت عدد براتون بفرسته یا نام بصورت یک رشته باشه. که با ترکیب اینها با لایبری هایی مثل class-validator میتونیم بعنوان ولیدشن هایی قوی ازشون استفاده کنیم و داده های ورودی کاربر رو کنترل کنیم.
مدل ها (Entities):
مدل ها جایی هستن که اطلاعات مربوط به دیتابیس رو توی اون ها میسازیم مثلا با استفاده از typeorm که یک ORM کاملا سازگار با تایپ اسکریپت هست میتونیم یک کلاس مدل بسازیم و جدول دیتابیس رو بدون یک خط کد SQL و فقط با کد تایپ اسکریپت درست کنیم.
سخن پایانی:
این فریمورک استاداردها و استایلی رو جلوی پامون میزاره که باعث افزایش کارایی کدنویسی میشه و ممکنه که در نگاه اول سخت بنظر بیاد ولی وقتی که یادش گرفتید درکنار تایپ اسکریپت میتونه به سرعت کدنویسی، خوانایی، ساختار منظم، نگهداری کد و ... کمک زیادی کنه.
میتونید به سایت nestjs مراجعه کنید که داکیومنتش خیلی خوب توضیح داده و به راحتی میتونید از اونجا یاد بگیرید.
خواندن فایل ها با استفاده از ماژول fs در Node.js
شما می توانید با استفاده از متود fs.readFile()
یک فایل را به صورت ناهمگام بخوانید.
سینتکس زیر نحوه تعریف یک متد خواندن فایل را با استفاده از ماژول fs در Node.js نشان می دهد:
fs.readFile(fileName [,options], callback)
توضیح پارامترهای سینتکس بالا:
filename
: آدرس کامل یا همان full path فایلی که قرار است خوانده شود. (در قالب رشته)
options
: این پارامتر میتواند یک رشته و یا Object باشد که حاوی encoding و flag است. encoding پیشفرض برابر utf8 و flag برابر r است. (بعدا بیشتر توضیح می دهیم)
callback
: یک فانکشن است که دو پارامتر err
و fd
را دریافت می کند. پارامتر err
حاوی پیغام های خطا و پارامتر fd
حاوی اطلاعات درون فایل خواهد بود.
مثال زیر نشان می دهد که چگونه می توانیم اطلاعات فایلی به نام TestFile.txt
که در کنار اسکریپت ما قرار دارد را به صورت ناهمگام بخوانیم:
var fs = require('fs');
fs.readFile('TestFile.txt', function (err, data) {
if (err) throw err;
console.log(data);
});
فصل ۱۱ – مثال ۱
هنگامی که از ناهمگام بودن نحوه خواندن اطلاعات از فایل ها صحبت می کنیم منظورمان این است که ابتدا نرم افزار شروع به خواندن اطلاعات فایلها می کند و پس از اتمام فعالیت خود فانکشن callback را صدا می زند.
در مثال بالا پارامتر err
در صورتی که فایل موجود نباشد حاوی مقداری خواهد بود. در غیر این صورت پارامتر data
اطلاعات فایل را دارا می باشد.
به عنوان مثال در کنار فایل js مثال بالا، فایلی ایجاد کنید به نام TextFile.txt
که حاوی متن Hello word باشد.
سپس در محیط ترمینال یا CMD با زدن دستور node server.js
، فایل اصلی را اجرا کنید.
در صورتی که مشکلی وجود نداشته باشد باید متن Hello word را در محیط کنسول مشاهده کنید.
همچنین می توانید با استفاده از متود fs.readFileSync()
به صورت همگام اطلاعات را بخوانید.
مثال زیر نشان می دهد که چگونه می توانید با استفاده از متود fs.readFileSync()
یک فایل را به صورت همگام یا synchronous بخوانید:
var fs = require('fs');
var data = fs.readFileSync('TestFile.txt', 'utf8');
console.log(data);
فصل ۱۱ – مثال ۲
نوشتن بر روی فایل با استفاده از ماژول fs در Node.js
نوشتن بر روی یک فایل به همان سادگی خواندن فایل ها در Node.js است. با استفاده از متود fs.writeFile()
قادر هستید تا بر روی یک فایلی که در حال حاضر موجود است اطلاعاتی را بنویسید.
سینتکس نگارش متود fs.writeFile()
به شرح زیر است:
fs.writeFile(filename, data[, options], callback)
توضیح پارامترهای سینتکس بالا:
filename
: آدرس کامل یا همان full path فایلی که قرار است بر روی آن متنی شود. (در قالب رشته)
data
: اطلاعاتی که باید درون فایل نوشته شود.
options
: این پارامتر میتواند یک رشته و یا Object باشد که حاوی encoding و flag است. encoding پیشفرض برابر utf8 و flag برابر r است. (بعدا بیشتر توضیح می دهیم)
callback
: یک فانکشن است که دو پارامتر err
و fd
را دریافت می کند. پارامتر err
حاوی پیغام های خطا و پارامتر fd
حاوی اطلاعات درون فایل خواهد بود.
مثال زیر یک فایل به نام test.txt
ایجاد خواهد کرد و به صورت ناهمگام متن “Hello World” را درون آن خواهد نوشت:
var fs = require('fs');
fs.writeFile('test.txt', 'Hello World!', function (err) {
if (err)
console.log(err);
else
console.log('Write operation complete.');
});
فصل ۱۱ – مثال ۳
توجه داشته باشید که مثال بالا، اگر فایل موجود باشد، هر چیزی که درون این فایل از قبل موجود باشد را پاک خواهد کرد و سپس متن جدید را جایگزین آن خواهد کرد.
اگر می خواهید که اطلاعات درون یک فایل پاک نشود و اطلاعات جدید بعد از اطلاعات حال حاضر درون این فایل قرار بگیرد می توانید از متود fs.appendFile()
استفاده کنید.
مثال زیر نشان می دهد که چگونه می توانید جمله “Hello Word!” را بعد از هر چیزی که درون فایل test.txt
وجود دارد بنویسید:
var fs = require('fs');
fs.appendFile('test.txt', 'Hello World!', function (err) {
if (err)
console.log(err);
else
console.log('Append operation complete.');
});
فصل ۱۱ – مثال ۴
باز کردن یک فایل با ماژول fs در Node.js
به صورت آلترناتیو نسبت به متود های بالا شما میتوانید از متود fs.open()
در باز کردن یک فایل برای خواندن یا نوشتن بر روی آن استفاده کنید.
سینتکس متود fs.open()
به شرح زیر است:
fs.open(path, flags[, mode], callback)
توضیح پارامترهای سینتکس بالا:
path
: آدرس کامل یا همان full path فایلی که قرار است باز شود. (در قالب رشته)
flag
: پرچم عملیاتی که قرار است بر روی فایل صورت پذیرد. (در ادامه آموزش جدول کل پرچم های ممکن را می توانید مشاهده کنید)
mode
: انتخاب حالت باز کردن فایل برای خواندن، نوشتن یا خواندن-نوشتن فایل.
callback
: یک فانکشن است که دو پارامتر err
و fd
را دریافت می کند. پارامتر err
حاوی پیغام های خطا و پارامتر fd
حاوی اطلاعات درون فایل خواهد بود.
پرچم های قابل استفاده درون فانکشن های مرتبط با فایل در Node.js:
پرچم | توضیحات |
---|---|
r |
یک فایل را برای خواندن باز می کند، اگر فایل موجود نباشد ارور ایجاد می شود. |
r+ |
یک فایل را برای خواندن و نوشتن باز می کند، اگر فایل موجود نباشد ارور ایجاد می شود. |
rs |
فایل را برای خواندن همگام باز می کند. |
rs+ |
یک فایل را برای خواندن و نوشتن باز می کند، این فلگ به سیستم اعلام می کند که این کار را به صورت همگام انجام دهد. |
w |
یک فایل را برای نوشتن باز می کند. اگر فایل موجود نباشد آن را می سازد، اگر فایل موجود باشد آن را ابتدا خالی می کند. |
wx |
مثل w عمل می کند، اما اگر path موجود باشد ارور ایجاد خواهد شد. |
w+ |
یک فایل را برای نوشتن و نوشتن باز می کند. اگر فایل موجود نباشد آن را می سازد، اگر فایل موجود باشد آن را ابتدا خالی می کند. |
wx+ |
مثل w+ عمل می کند، اما اگر path موجود باشد ارور ایجاد خواهد شد. |
a |
یک فایل را برای اضافه کردن چیزی به انتهای آن باز می کند. |
ax |
مثل a عمل می کند، اما اگر path موجود باشد ارور ایجاد خواهد شد. |
a+ |
یک فایل را برای اضافه کردن چیزی به انتهای آن و خواندن آن باز می کند. اگر فایل موجود نباشد ایجاد خواهد شد. |
ax+ |
مثل a+ عمل می کند، اما اگر path موجود باشد ارور ایجاد خواهد شد. |
مثال زیر یک فایل از قبل موجود را باز می کند و اطلاعات درون آن را می خواند:
var fs = require('fs');
fs.open('TestFile.txt', 'r', function (err, fd) {
if (err) {
return console.error(err);
}
var buffr = Buffer.alloc(1024);
fs.read(fd, buffr, 0, buffr.length, 0, function (err, bytes) {
if (err) throw err;
// Print only read bytes to avoid junk.
if (bytes > 0) {
console.log(buffr.slice(0, bytes).toString());
}
// Close the opened file.
fs.close(fd, function (err) {
if (err) throw err;
});
});
});
فصل ۱۱ – مثال ۵
حذف فایل با ماژول fs در Node.js
حذف فایل ها در Node.js بسیار ساده انجام می شود اما در مورد حذف حتما مراقب باشید.
شما می توانید با استفاده از متود fs.unlink()
یک فایل را از سیستم حذف کنیم.
سینتکس استفاده از متود fs.unlink()
برای حذف فایلها در Node.js به شرح زیر است:
fs.unlink(path, callback);
مثال زیر نشان می دهد که چگونه می توانید یک فایل را در نود حذف کنید:
var fs = require('fs');
fs.unlink('test.txt', function () {
console.log('deleting operation complete.');
});
فصل ۱۱ – مثال ۶
متود های قابل استفاده در ارتباط با فایل ها در Node.js
fs.readFile(fileName [,options], callback)
اطلاعات درون یک فایل که از قبل موجود است را می خواند.
fs.writeFile(filename, data[, options], callback)
اطلاعاتی را در یک فایل می نویسد. اگر فایل موجود باشد اطلاعات اولیه را پاک کرده و سپس متن را درون آن می نویسد و اگر فاییل موجود نباشد، ابتدا آن را ایجاد می کند و سپس اطلاعات را درون آن قرار می دهد.
fs.open(path, flags[, mode], callback)
فایل را برای خواندن و نوشتن باز می کند.
fs.rename(oldPath, newPath, callback)
یک فایل که از قبل وجود دارد را تغییر نام می دهد.
fs.chown(path, uid, gid, callback)
عملیات chown را به صورت نا همگام انجام می دهد.
fs.stat(path, callback)
آبجکت fs.stat
را باز می گرداند که حاوی اطلاعات مهمی از فایل درخواستی است.
fs.link(srcpath, dstpath, callback)
به صورت ناهمگام فایل ها را لینک می کند.
fs.symlink(destination, path[, type], callback)
به صوت ناهمگام Symlink برای فایل ایجاد می کند.
fs.rmdir(path, callback)
یک دایکتوری که از قبل وجود دارد را تغییر نام می دهد.
fs.mkdir(path[, mode], callback)
یک دایرکتوری جدید را ایجاد می کند.
fs.readdir(path, callback)
محتوای یک دایرکتوری درخواستی را می خواند.
fs.utimes(path, atime, mtime, callback)
timestamp یک فایل را تغییر می دهد.
fs.exists(path, callback)
مشخص می کند آیا فایل درخواستی وجود دارد یا نه.
fs.access(path[, mode], callback)
قابلیت دسترسی کاربر به فایلی را مشخص می کند.
fs.appendFile(file, data[, options], callback)
مقداری متن جدید به انتهای فایلی که از قبل وجود دارد اضافه می کند.
برنامه نویسی Node.js با ماژول ها، برنامه نوشته شده را بسیار خوانا تر می کند. معمولا ماژول فقط یک کار خاص را انجام می دهند و بار ها در طول اپلیکیشن مورد قابل استفاده قرار می گیرند. یک ماژول معمولا حاوی یک عملیات خاص می باشد که به صورت مستقل قادر به انجام هدف خود است.
در فصل قبلی ما دیدیم که چگونه می توانید با استفاده از module.exports
یک ماژول لوکال را اکسپورت کنید (برای استفاده در تمامی اپلیکیشن، ماژول را معرفی کنید). اکنون می خواهیم نشان دهیم که چگونه می توانید ماژول های مختلف از انواع مختلف را با استفاده از module.exports
اکسپورت کنید.
module.exports
یا exports
دو شی خاص هستند که در تمامی فایلهای جاوا اسکریپت Node.js به طور پیش فرض وارد می شوند.
در
module.exports
درواقع کلمهmodule
یک متغیر است که اشاره به ماژولی که درون آن هستیم دارد و کلمهexports
یک شی است که این ماژول را برای کل اپلیکیشن به عنوان یک ماژول در دسترس معرفی خواهد کرد.
هر چیزی که شما به module.exports
اختصاص دهید به عنوان یک ماژول به اپلیکیشن معرفی خواهد شد.
معرفی مقدار Literal به عنوان ماژول (خروجی گرفتن از Literal)
همانطور که در بالا اشاره کردیم exports
یک شی یا همان object است. پس این شی هر چیزی که به آن اختصاص دهید را در اختیار کل اپلیکیشن قرار خواهد داد. پس در نتیجه اگر شما یک مقدار رشته را به این شیء اختصاص دهید آن رشته به عنوان یک ماژول به اپلیکیشن معرفی خواهد شد.
مثال زیر نشان میدهد که شما چگونه می توانید یک مقدار رشته را به عنوان یک ماژول به سیستم معرفی کنید. بهتر است فایلی به نام Message.js
بسازید و فقط کد زیر را درون آن قرار دهید:
module.exports = 'Hello world';
//or
exports = 'Hello world';
فصل ۸ – مثال ۱ (فایل Message.js)
اکنون ماژولی که ساختید را درون فایل app.js
با فانکشن require()
فراخوانی کنید. مانند مثال زیر:
var msg = require('./Messages.js');
console.log(msg);
فصل ۸ – مثال ۱ (فایل app.js)
اگر دقت کنید قبل از اسم فایل نقطه و اسلش قرار گرفته است. ./
درواقع به همان فولدر که درون آن هستیم اشاره دارد. البته برای استفاده از ماژول های هسته ای نیاز به مشخص کردن path یا همان مسیر فایل ها ندارید.
برای اجرای مثال بالا می توانید در محیط ترمینال یا CMD دستور node app.js
را تایپ کرده و اینترنت بزنید. (دقت کنید که در محیط ترمینال ابتدا باید با دستور cd به مکانی که فایل را ساخته اید نقل مکان کنید.)
معرفی Object به عنوان ماژول (خروجی گرفتن از Object)
exports
یک object است پس می توانید به آن صفت یا متود اختصاص دهید.
مثال زیر نشان می دهد که شما چگونه می توانید یک رشته را به عنوان صفت به یک آبجکت اختصاص دهید و object را اکسپورت کنید. بهتر است فایلی به نام Message.js
ساخته و کد زیر را درون آن کپی کنید:
exports.SimpleMessage = 'Hello world';
//or
module.exports.SimpleMessage = 'Hello world';
فصل ۸ – مثال ۲ (فایل Message.js)
در مثال بالا ما صفت SimpleMessage
را به شی export
اختصاص داده ایم.
شما اکنون می توانید ماژول ساخته شده را در فایل app.js
به شکل زیر مورد استفاده قرار دهید:
var msg = require('./Messages.js');
console.log(msg.SimpleMessage);
فصل ۸ – مثال ۲ (فایل app.js)
در مثال بالا فانکشن require()
آبجکت { SimpleMessage : 'Hello World'}
را باز می گرداند. پس شما می توانید با استفاده از نام متغیر اختصاص داده شده به این ماژول، صفت های درون آن را صدا بزنید. مانند: msg.SimpleMessage
مثال بالا را با استفاده از node app.js
در ترمینال اجرا کنید.
به همین سبک بالا می توانید یک فانکشن را به همراه آبجکت اکسپورت کنید.
مثال زیر نشان می دهد که شما چگونه می توانید فانکشن لاگ را به همراه یک شی به عنوان یک ماژول اکسپورت کنید، این کد ها را در فایل Log.js
ذخیره کنید:
module.exports.log = function (msg) {
console.log(msg);
};
فصل ۸ – مثال ۳ (فایل Log.js)
مثال بالا در واقع آبجکت { log : function(msg){ console.log(msg); } }
را در هنگام استفاده از require()
باز خواهد گرداند. پس شما می توانید با استفاده از نام متغیر اختصاص داده شده به متود های درونی آن دسترسی پیدا کنید.
به مثال زیر توجه کنید، این کد ها محتویات فایل app.js
است:
var msg = require('./Log.js');
msg.log('Hello World');
فصل ۸ – مثال ۳ (فایل app.js)
مثال بالا را با استفاده از node app.js
در ترمینال اجرا کنید.
شما می توانید به روش زیر نیز یک object را به عنوان ماژول اکسپورت کنید. بهتر است فایلی به نام data.js
ساخته و متن زیر را درون آن کپی کنید:
module.exports = {
firstName: 'James',
lastName: 'Bond'
}
فصل ۸ – مثال ۴ (فایل data.js)
برای فراخوانی این ماژول می توانید در فایل app.js
کد زیر را استفاده کنید:
var person = require('./data.js');
console.log(person.firstName + ' ' + person.lastName);
فصل ۸ – مثال ۴ (فایل app.js)
مثال بالا را با استفاده از node app.js
در ترمینال اجرا کنید.
معرفی فانکشن به عنوان ماژول (خروجی گرفتن از Function)
به روش زیر شما قادر هستید تا یک فانکشن را به عنوان یک ماژول export کنید. پیشنهاد می شود فایلی به نام Log.js
ساخته و کد زیر را درون آن کپی کنید:
module.exports = function (msg) {
console.log(msg);
};
فصل ۸ – مثال ۵ (فایل Log.js)
اکنون می توانید با کد زیر ماژول را درون فایل app.js
فراخوانی کنید:
var msg = require('./Log.js');
msg('Hello World');
فصل ۸ – مثال ۵ (فایل app.js)
در مثال بالا متغیر msg
به یک عبارت عملیاتی یا همان Function Expression تبدیل خواهد شد. پس شما می توانید با استفاده از پرانتز بعد از نام متغیر، فانکشن را اجرا کنید.
مثال بالا را با استفاده از node app.js
در ترمینال اجرا کنید.
معرفی فانکشن به عنوان یک کلاس
در زبان جاوا اسکریپت می توان با یک فانکشن به عنوان یک کلاس برخورد کرد.
مثال زیر نشان می دهد که شما چگونه می توانید یک فانکشن را مانند یک کلاس export کنید. این محتویات را درون فایل Person.js
کپی کنید:
module.exports = function (firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = function () {
return this.firstName + ' ' + this.lastName;
}
}
فصل ۸ – مثال ۶ (فایل Person.js)
ماژول ساخته شده بالا را می توان به روش زیر در فایل app.js
مورد استفاده قرار داد:
var person = require('./Person.js');
var person1 = new person('James', 'Bond');
console.log(person1.fullName());
فصل ۸ – مثال ۶ (فایل app.js)
مثال بالا را با استفاده از node app.js
در ترمینال اجرا کنید تا نتیجه James Bond را همان جا مشاهده کنید.
بارگذاری ماژول ها از پوشه های دیگر
برای بارگذاری ماژول ها، در هر پوشه ای که باشند، در فانکشن require()
فقط کافی است تا از full path یا همان آدرس کامل فایل استفاده کنید.
به عنوان مثال اگر فایلی که در آن از فانکشن require()
استفاده کرده اید ماژولی که در فولدر utility قرار داده شده باشد را فراخوانی می کند، به این صورت عمل کنید:
var log = require('./utility/Log.js');
فصل ۸ – مثال ۷ (فایل app.js)
در مثال بالا نقطه .
مشخص می کند که فولدر utility کنار همین فایلی که در حال اجرای require هست، وجود دارد.
بارگذاری ماژول ها فقط با نام پوشه ای که درون آن قرار دارند
Node.js به شما اجازه می دهد تا فقط با اشاره به نام فولدری که ماژول درون آن قرار دارد، ماژول را فراخوانی کنید. البته مراحل کمی متفاوت است.
مانند مثال زیر که شبیه مورد بالا عمل می کند:
var log = require('./utility');
فصل ۸ – مثال ۸ (فایل app.js)
این مرحله اول کار بود. هنگامی که از نام پوشه استفاده می کنید، Node.js بنا را بر این می گذارد که درون این پوشه یک پکیج وجود دارد. برای اینکه شما Node.js را برای درک بهتر این پکیج راهنمایی کنید، باید فایلی به نام package.json
را درون پوشه utility قرار دهید. با محتویات زیر:
{
"name" : "log",
"main" : "./Log.js"
}
فصل ۸ – مثال ۸ (فایل package.json)
درواقع این کلمه کلیدی “main
” مشخص می کند که فایل اصلی که پکیج ما به آن اشاره دارد کدام فایل است و همچنین نام پکیج را کلمه کلیدی “name
” مشخص می کند.
اگر فایل
package.json
وجود نداشته باشد، Node.js به صورت اتوماتیک فایلindex.js
را به عنوان فایل ماژول در نظر می گیرد.
منبع: نتران
به دلیل اینکه با مباحث مهمی روبرو هستیم، سعی کردم یه منبع پیدا کنم و راجع این مباحث یه مرجع کامل تر داشته باشیم. توجه کنید که ترتیب مباحث از ابتدای صفحه تا انتهای صفحه ست. اول callback رو بفمید و سپس promise و در آخر async و await .
جاوا اسکریپت به صورت پیشفرض یک زبان برنامهنویسی همگام و تک نخی است. این بدان معنی است که کد نمیتواند نخهای جدیدی ایجاد کرده و به صورت موازی اجرا شود. در این مقاله به بررسی روشهای مختلف برنامه نویسی ناهمگام در محیط Node.js میپردازیم.
ناهمگامی در زبانهای برنامهنویسی
رایانهها اساساً دارای طراحی ناهمگام هستند. منظور از ناهمگامی این است که کارهای مختلف میتوانند در گردش برنامه اصلی مستقل از هم اتفاق بیفتند. در رایانههای امروز هر برنامهای برای مدت زمان معینی اجرا میشود و سپس اجرای آن متوقف میشود تا برنامه دیگری بتواند به اجرای خود ادامه بدهد. این کار در چنان چرخه سریعی اتفاق میافتد که کاربر هرگز متوجه نمیشود و ما فکر میکنیم رایانه ما میتواند به طور همزمان برنامههای زیادی را اجرا کند، اما این یک توهم است (البته قضیه در پردازندههای چندهستهای متفاوت است).
برنامهها به صورت داخلی از «وقفه» (interrupt) استفاده میکنند. وقفه یک سیگنال است که به سمت پردازنده ارسال میشود تا توجه سیستم را کسب کند. پرداختن به جزییات این موضوع خارج از موضوع این مقاله است؛ اما باید به خاطر داشته باشید که ناهمگامی در برنامهها امری معمول است و به طور نرمال برنامهها، اجرای خود را متوقف میکنند تا زمانی که پردازنده به آنها وقتی اختصاص بدهد. زمانی که یک برنامه منتظر پاسخی از سوی شبکه است، نمیتواند پردازنده را معطل خود نگه دارد تا درخواست پایان یابد.
به طور معمول زبانهای برنامهنویسی همگام هستند. برخی از آنها نیز در حد زبان یا کتابخانههای جانبی، روشهایی برای مدیریت ناهمگامی ارائه میکنند. زبانهای C ،Java ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیشفرض همگام هستند. برخی از این زبانها با استفاده از نخها به صورت ناهمگام عمل میکنند و میتوانند یک پردازش جدید ایجاد کنند.
جاوا اسکریپت
زبان برنامهنویسی جاوا اسکریپت نیز به صورت پیشفرض همگام و تک نخی است. این بدان معنی است که کد نمیتواند نخهای جدید ایجاد کرده و به صورت موازی اجرا شود. در این حالت، خطوط مختلف کد یکی پس از دیگری و به ترتیب اجرا میشوند. برای نمونه:
const a = 1const b = 2const c = a * bconsole.log(c)doSomething()
اما میدانیم که جاوا اسکریپت درون مرورگر تولد یافته است و وظیفه اصلی آن در ابتدا پاسخ دادن به واکنشهای کاربر مانند onClick ،onMouseOver ،onChange ،onSubmit و غیره بوده است. چنین فرایندی در یک مدل برنامهنویسی همگام چگونه ممکن است؟
پاسخ در محیط برنامهنویسی این زبان است. مرورگر روشی برای ارائه یک مجموعه از API-ها دارد که میتوانند این نوع کارکرد را مدیریت کنند. در سالهای اخیر Node.js یک محیط غیر مسدودکننده I/O معرفی کرده است که این مفهوم را به دسترسی فایلها، فراخوانیهای شبکه و موارد مشابه گسترش داده است.
Callback-ها
ما هرگز نمیدانیم که یک کاربر چه زمانی روی یک دکمه کلیک خواهد کرد، بنابراین باید یک «دستگیره رویداد» (event handler) برای رویداد کلیک تعریف کنیم.
این دستگیره رویداد تابعی قبول میکند که هنگام اتفاق افتادن رویداد فراخوانی میشود:
document.getElementById('button').addEventListener('click'، () => {//item clicked})
این همان callback مشهور است. در واقع callback تابع سادهای است که به صورت یک مقدار به تابع دیگر ارسال میشوند و تنها زمانی اجرا خواهد شد که رویدادی اتفاق بیفتد. ما این کار را به این دلیل میتوانیم اجرا کنیم که جاوا اسکریپت تابعهای درجه نخست دارد که میتوانند به متغیرها انتساب یافته و به تابعهای دیگر (که تابعهای درجه بالا نامیده میشوند) ارسال شوند.
رویه معمول این است که کد کلاینت در یک شنونده رویداد load در شیء window قرار میگیرد و تابع callback را در زمان آماده شدن صفحه اجرا میکند:
window.addEventListener('load'، () => {//window loaded //do what you want}
Callback-ها در همه جا استفاده میشوند و اختصاص به رویدادهای DOM ندارند. یک مثال عمومی استفاده از تایمر است:
setTimeout(() => {// runs after 2 seconds}، 2000
درخواستهای XHR نیز یک callback میپذیرند. در مثال زیر یک تابع به مشخصهای که در زمان رخداد اتفاق خاص فراخوانی میشود، انتساب خواهد یافت:
const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send()
مدیریت خطا در Callback-ها
یکی از رایجترین راهبردها برای مدیریت خطا در callback-ها کاری است که Node.js انجام میدهد. پارامتر اول در هر تابع callback شیء خطا است که به نام callback-های error-first شناخته میشوند.
اگر خطایی وجود نداشته باشد، این شیء null خواهد بود. اگر خطایی رخ داده باشد این پارامتر نوعی توضیح در مورد خطا و اطلاعات مرتبط ارائه میکند.
fs.readFile('/file.json'، (err، data) => { if (err!== null) {//handle error console.log(err) return }
مشکل Callback-ها
به جای استفاده از کال بک میتوانیم کد خود را با پرامیس بنویسیم و یا کدهایی که به صورت کال بک نوشته شده تبدیل به پرامیس کنیم تا بتوانیم در آینده از مزایای async/await هم استفاده کنیم
Callback-ها در استفادههای ساده بسیار عالی هستند. اما هر Callback یک سطح به سلسلهمراتب تودرتو اضافه میکند. زمانی که Callback-های زیادی وجود داشته باشند، کد به سرعت پیچیده میشود:
window.addEventListener('load'،() => { document.getElementById('button').addEventListener('click'،() => { setTimeout(() => { items.forEach(item => { //your code here }) }، 2000) }) })
این کد صرفاً 4 سطح ساده دارد، اما در موارد زیادی سطوح بسیار بیشتری از کد تودرتو را مشاهده میکنیم که جالب نیست. برای حل این مشکل چه باید کرد:
جایگزینهای Callback-ها
از ES6 به بعد جاوا اسکریپت چند قابلیت معرفی کرده است که به کدنویسی ناهمگام بدون استفاده از Callback-ها کمک میکند:
- Promises (ES6)
- Async/Await (ES8)
Promise-ها
Promise-ها یک روش برای اجرای کد ناهمگام در جاوا اسکریپت محسوب میشوند و با استفاده از آنها دیگر لازم نیست نگران وجود Callback-های زیاد در کد باشیم.
مقدمهای بر Promise-ها
یک Promise عموماً به صورت واسطی برای یک مقدار تعریف میشود که در زمانی در آینده موجود خواهد شد. با این که سالهاست Promise-ها معرفی شدهاند اما صرفاً در ES2015 استاندارد و معرفی شدند و هم اکنون در ES2017 با معرفی تابعهای async منسوخ گشتهاند. تابعهای async از API مربوط به Promise ها به عنوان بلوکهای سازنده خود استفاده میکند و از این رو درک آنها حتی در صورتی که بخواهید در کد خود از تابعهای async به جای promise استفاده کنید ضروری خواهد بود.
طرز کار Promise-ها به طور خلاصه
زمانی که یک Promise فراخوانی میشود در «حالت انتظار» (pending state) کار خود را آغاز میکند. این بدان معنی است که تابع فراخواننده به اجرای خود ادامه میدهد در حالی که منتظر Promise است تا پردازشهای خودش را اجرا کند و بازخوردی به تابع فراخواننده بدهد.
در این زمان تابع فراخواننده منتظر آن میماند تا Promise را در «حالت حل شده» (resolved state) یا در «حالت رد شده» (rejected state) بازگشت دهد، اما چنان که میدانید جاوا اسکریپت همگام است و از این رو در زمانی که Promise کار نمیکند، تابع همچنان به اجرای خود ادامه میدهد.
کدام API جاوا اسکریپت از Promise-ها استفاده میکند؟
علاوه بر کد کاربر و کد کتابخانههای مختلف، Promise-ها از سوی API-های مدرن استاندارد وب مانند لیست زیر نیز مورد استفاده قرار میگیرند:
- Battery API
- Fetch API
- Service Workers
این که در جاوا اسکریپت مدرن از Promise-ها استفاده نکنید امر بسیار نامحتملی است، بنابراین در ادامه به بررسی دقیقتر آنها میپردازیم.
ایجاد یک Promise
API مربوط به Promise یک سازنده Promise عرضه میکند که با استفاده از ()new Promise مقداردهی اولیه میشود:
let done = true ---------------------------------------- const isItDoneYet = new Promise((resolve، reject) => { if (done) { const workDone = 'Here is the thing I built' resolve(workDone) } else { const why = 'Still working on something else' reject(why) } })
چنان که میبینید Promise ثابت سراسری done را بررسی میکند و اگر درست باشد، یک Promise حل شده و در غیر این صورت Promise رد شده بازگشت مییابد. ما با استفاده از resolve و reject میتوانیم یک مقدار را بازگشت دهیم. در حالت فوق ما صرفاً یک رشته بازگشت میدهیم اما میتوان یک شیء را نیز بازگشت داد.
مصرف کردن یک Promise
در بخش قبلی، با روش ایجاد شدن یک Promise آشنا شدیم. اکنون به بررسی شیوه مصرف یا استفاده شدن یک Promise میپردازیم:
const isItDoneYet = new Promise(//...) ----------------------------------------------------------------- const checkIfItsDone = () => { isItDoneYet.then((ok) => { console.log(ok) }).catch((err) => { console.error(err) }) }
جاوا اسکریپت به صورت پیشفرض یک زبان برنامهنویسی همگام و تک نخی است. این بدان معنی است که کد نمیتواند نخهای جدیدی ایجاد کرده و به صورت موازی اجرا شود. در این مقاله به بررسی روشهای مختلف برنامه نویسی ناهمگام در محیط Node.js میپردازیم.
ناهمگامی در زبانهای برنامهنویسی
رایانهها اساساً دارای طراحی ناهمگام هستند. منظور از ناهمگامی این است که کارهای مختلف میتوانند در گردش برنامه اصلی مستقل از هم اتفاق بیفتند. در رایانههای امروز هر برنامهای برای مدت زمان معینی اجرا میشود و سپس اجرای آن متوقف میشود تا برنامه دیگری بتواند به اجرای خود ادامه بدهد. این کار در چنان چرخه سریعی اتفاق میافتد که کاربر هرگز متوجه نمیشود و ما فکر میکنیم رایانه ما میتواند به طور همزمان برنامههای زیادی را اجرا کند، اما این یک توهم است (البته قضیه در پردازندههای چندهستهای متفاوت است).
برنامهها به صورت داخلی از «وقفه» (interrupt) استفاده میکنند. وقفه یک سیگنال است که به سمت پردازنده ارسال میشود تا توجه سیستم را کسب کند. پرداختن به جزییات این موضوع خارج از موضوع این مقاله است؛ اما باید به خاطر داشته باشید که ناهمگامی در برنامهها امری معمول است و به طور نرمال برنامهها، اجرای خود را متوقف میکنند تا زمانی که پردازنده به آنها وقتی اختصاص بدهد. زمانی که یک برنامه منتظر پاسخی از سوی شبکه است، نمیتواند پردازنده را معطل خود نگه دارد تا درخواست پایان یابد.
به طور معمول زبانهای برنامهنویسی همگام هستند. برخی از آنها نیز در حد زبان یا کتابخانههای جانبی، روشهایی برای مدیریت ناهمگامی ارائه میکنند. زبانهای C ،Java ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیشفرض همگام هستند. برخی از این زبانها با استفاده از نخها به صورت ناهمگام عمل میکنند و میتوانند یک پردازش جدید ایجاد کنند.
جاوا اسکریپت
زبان برنامهنویسی جاوا اسکریپت نیز به صورت پیشفرض همگام و تک نخی است. این بدان معنی است که کد نمیتواند نخهای جدید ایجاد کرده و به صورت موازی اجرا شود. در این حالت، خطوط مختلف کد یکی پس از دیگری و به ترتیب اجرا میشوند. برای نمونه:
const a = 1const b = 2const c = a * bconsole.log(c)doSomething()
اما میدانیم که جاوا اسکریپت درون مرورگر تولد یافته است و وظیفه اصلی آن در ابتدا پاسخ دادن به واکنشهای کاربر مانند onClick ،onMouseOver ،onChange ،onSubmit و غیره بوده است. چنین فرایندی در یک مدل برنامهنویسی همگام چگونه ممکن است؟
پاسخ در محیط برنامهنویسی این زبان است. مرورگر روشی برای ارائه یک مجموعه از API-ها دارد که میتوانند این نوع کارکرد را مدیریت کنند. در سالهای اخیر Node.js یک محیط غیر مسدودکننده I/O معرفی کرده است که این مفهوم را به دسترسی فایلها، فراخوانیهای شبکه و موارد مشابه گسترش داده است.
Callback-ها
ما هرگز نمیدانیم که یک کاربر چه زمانی روی یک دکمه کلیک خواهد کرد، بنابراین باید یک «دستگیره رویداد» (event handler) برای رویداد کلیک تعریف کنیم.
این دستگیره رویداد تابعی قبول میکند که هنگام اتفاق افتادن رویداد فراخوانی میشود:
document.getElementById('button').addEventListener('click'، () => {//item clicked})
این همان callback مشهور است. در واقع callback تابع سادهای است که به صورت یک مقدار به تابع دیگر ارسال میشوند و تنها زمانی اجرا خواهد شد که رویدادی اتفاق بیفتد. ما این کار را به این دلیل میتوانیم اجرا کنیم که جاوا اسکریپت تابعهای درجه نخست دارد که میتوانند به متغیرها انتساب یافته و به تابعهای دیگر (که تابعهای درجه بالا نامیده میشوند) ارسال شوند.
رویه معمول این است که کد کلاینت در یک شنونده رویداد load در شیء window قرار میگیرد و تابع callback را در زمان آماده شدن صفحه اجرا میکند:
window.addEventListener('load'، () => {//window loaded //do what you want}
Callback-ها در همه جا استفاده میشوند و اختصاص به رویدادهای DOM ندارند. یک مثال عمومی استفاده از تایمر است:
setTimeout(() => {// runs after 2 seconds}، 2000
درخواستهای XHR نیز یک callback میپذیرند. در مثال زیر یک تابع به مشخصهای که در زمان رخداد اتفاق خاص فراخوانی میشود، انتساب خواهد یافت:
const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send()
مدیریت خطا در Callback-ها
یکی از رایجترین راهبردها برای مدیریت خطا در callback-ها کاری است که Node.js انجام میدهد. پارامتر اول در هر تابع callback شیء خطا است که به نام callback-های error-first شناخته میشوند.
اگر خطایی وجود نداشته باشد، این شیء null خواهد بود. اگر خطایی رخ داده باشد این پارامتر نوعی توضیح در مورد خطا و اطلاعات مرتبط ارائه میکند.
fs.readFile('/file.json'، (err، data) => { if (err!== null) {//handle error console.log(err) return }
مشکل Callback-ها
Callback-ها در استفادههای ساده بسیار عالی هستند. اما هر Callback یک سطح به سلسلهمراتب تودرتو اضافه میکند. زمانی که Callback-های زیادی وجود داشته باشند، کد به سرعت پیچیده میشود:
window.addEventListener('load'،() => { document.getElementById('button').addEventListener('click'،() => { setTimeout(() => { items.forEach(item => { //your code here }) }، 2000) }) })
این کد صرفاً 4 سطح ساده دارد، اما در موارد زیادی سطوح بسیار بیشتری از کد تودرتو را مشاهده میکنیم که جالب نیست. برای حل این مشکل چه باید کرد؟
جایگزینهای Callback-ها
از ES6 به بعد جاوا اسکریپت چند قابلیت معرفی کرده است که به کدنویسی ناهمگام بدون استفاده از Callback-ها کمک میکند:
- Promises (ES6)
- Async/Await (ES8)
Promise-ها
Promise-ها یک روش برای اجرای کد ناهمگام در جاوا اسکریپت محسوب میشوند و با استفاده از آنها دیگر لازم نیست نگران وجود Callback-های زیاد در کد باشیم.
مقدمهای بر Promise-ها
یک Promise عموماً به صورت واسطی برای یک مقدار تعریف میشود که در زمانی در آینده موجود خواهد شد. با این که سالهاست Promise-ها معرفی شدهاند اما صرفاً در ES2015 استاندارد و معرفی شدند و هم اکنون در ES2017 با معرفی تابعهای async منسوخ گشتهاند. تابعهای async از API مربوط به Promise ها به عنوان بلوکهای سازنده خود استفاده میکند و از این رو درک آنها حتی در صورتی که بخواهید در کد خود از تابعهای async به جای promise استفاده کنید ضروری خواهد بود.
طرز کار Promise-ها به طور خلاصه
زمانی که یک Promise فراخوانی میشود در «حالت انتظار» (pending state) کار خود را آغاز میکند. این بدان معنی است که تابع فراخواننده به اجرای خود ادامه میدهد در حالی که منتظر Promise است تا پردازشهای خودش را اجرا کند و بازخوردی به تابع فراخواننده بدهد.
در این زمان تابع فراخواننده منتظر آن میماند تا Promise را در «حالت حل شده» (resolved state) یا در «حالت رد شده» (rejected state) بازگشت دهد، اما چنان که میدانید جاوا اسکریپت همگام است و از این رو در زمانی که Promise کار نمیکند، تابع همچنان به اجرای خود ادامه میدهد.
کدام API جاوا اسکریپت از Promise-ها استفاده میکند؟
علاوه بر کد کاربر و کد کتابخانههای مختلف، Promise-ها از سوی API-های مدرن استاندارد وب مانند لیست زیر نیز مورد استفاده قرار میگیرند:
- Battery API
- Fetch API
- Service Workers
این که در جاوا اسکریپت مدرن از Promise-ها استفاده نکنید امر بسیار نامحتملی است، بنابراین در ادامه به بررسی دقیقتر آنها میپردازیم.
ایجاد یک Promise
API مربوط به Promise یک سازنده Promise عرضه میکند که با استفاده از ()new Promise مقداردهی اولیه میشود:
let done = true ---------------------------------------- const isItDoneYet = new Promise((resolve، reject) => { if (done) { const workDone = 'Here is the thing I built' resolve(workDone) } else { const why = 'Still working on something else' reject(why) } })
چنان که میبینید Promise ثابت سراسری done را بررسی میکند و اگر درست باشد، یک Promise حل شده و در غیر این صورت Promise رد شده بازگشت مییابد. ما با استفاده از resolve و reject میتوانیم یک مقدار را بازگشت دهیم. در حالت فوق ما صرفاً یک رشته بازگشت میدهیم اما میتوان یک شیء را نیز بازگشت داد.
مصرف کردن یک Promise
در بخش قبلی، با روش ایجاد شدن یک Promise آشنا شدیم. اکنون به بررسی شیوه مصرف یا استفاده شدن یک Promise میپردازیم:
const isItDoneYet = new Promise(//...) ----------------------------------------------------------------- const checkIfItsDone = () => { isItDoneYet.then((ok) => { console.log(ok) }).catch((err) => { console.error(err) }) }
اجرای ()checkIfItsDone موجب اجرا شدن یک Promise به نام ()isItDoneYet میشود که منتظر حل شدن با استفاده از Callback-ی به نام then باقی میماند و اگر خطایی رخ دهد آن را در callback با نام catch مدیریت میکند.
زنجیرهسازی Promise-ها
هر Promise میتواند به Promise دیگری بازگشت یابد و یک زنجیره از Promise-ها تشکیل دهد. مثالی عالی از زنجیرهسازی Promise-ها در API با نام Fetch مشاهده میشود که یک لایه فوقانی روی API مربوط به XMLHttpRequest است و میتوان از آن برای ایجاد یک منبع و صفبندی یک زنجیره از Promise-ها برای اجرا در زمان واکشی شدن منبع استفاده کرد.
API با نام Fetch یک سازوکار مبتنی بر Promise است به طوری که فراخوانی ()fetch معادل تعریف کردن یک Promise با استفاده از ()new Promise است.
نمونهای از زنجیرهسازی Promise-ها
const status = (response) => { if (response.status >= 200 && response.status < 300) { return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) }
const json = (response) => response.json()
fetch('/todos.json').then(status).then(json).then((data) => { console.log('Request succeeded with JSON response'، data) }).catch((error) => { console.log('Request failed'، error) })
در این مثال، ()fetch را فراخوانی میکنیم تا فهرستی از آیتمهای TODO را از فایل todos.json که در ریشه دامنه قرار دارد به دست آوریم و بنابراین یک زنجیره از Promise-ها ایجاد کردهایم.
اجرای ()fetch موجب بازگشت یک response میشود که مشخصههای زیادی دارد، اما دو مورد از آنها برای ما مهم هستند:
- Status – یک مقدار عددی است که کد وضعیت HTTP را نشان میدهد.
- statusText – یک پیام وضعیت است که در صورت موفق بودن درخواست، مقدار آن OK خواهد بود.
response یک متد ()json نیز دارد که یک Promise بازگشت میدهد و در آن به بررسی محتوای متنی پردازش شده و تبدیل آن به JSON میپردازد. با توجه به Promise-های فوق روند رویدادها به این صورت است که Promise اول در زنجیره، تابعی است که ما تعریف کردهایم و آن را ()status مینامیم. این Promise وضعیت پاسخ را بررسی میکند و در صورتی که پاسخ موفق (کد بین 200 تا 299) نباشد، Promise را رد میکند.
این عملیات موجب میشود که زنجیره Promise همه Promise-های بعدی لیست شده را رد کند و مستقیماً به گزاره ()catch در انتهای کد برسد و متن Request failed را به همراه پیام خطا لاگ کند. اما اگر Promise اول موفق باشد، تابع ()json را که تعریف کردهایم، فراخوانی میکند. از آنجا که Promise قبلی در صورت موفقیت، شیء response را بازگشت داده است، میتوانیم آن را به عنوان ورودی به Promise دوم بدهیم.
در این حالت، دادههای JSON پردازش شده را بازگشت میدهیم تا Promise سوم مستقیماً JSON را دریافت کند:
.then((data) => { console.log('Request succeeded with JSON response'، data)})
در نهایت آن را در کنسول لاگ میکنیم.
مدیریت خطاها
در مثال بخش قبلی، یک catch داشتیم که به زنجیره Promise-ها الصاق میشد. هر زمان که جزئی از زنجیره Promise-ها ناموفق باشد و خطایی رخ دهد یا یک Promise رد شود، کنترل به گزاره ()catch در انتهای زنجیره میرسد.
new Promise((resolve، reject) => { throw new Error('Error') }).catch((err) => { console.error(err) })
یا
new Promise((resolve، reject) => { reject('Error') }).catch((err) => { console.error(err) })
آبشار سازی خطاها
اگر درون ()catch خطایی رخ دهد، میتوان یک ()catch دیگر برای مدیریت آن الصاق کرد و این روند همین طور تا انتها ادامه مییابد:
new Promise((resolve، reject) => { throw new Error('Error') }).catch((err) => { throw new Error('Error') }).catch((err) => { console.error(err) })
هماهنگسازی Promise-ها
در این بخش به روش هماهنگ کردن چند Promise میپردازیم.
()Promise.all
اگر لازم است که Promise-های مختلف را با یکدیگر هماهنگ کنید، میتوانید از متد ()Promise.all برای تعریف کردن لیستی از Promise-ها استفاده کنید و زمانی که همه آنها حل شدند، تابع دیگری را اجرا کنید.
مثالی از آن به صورت زیر است:
const f1 = fetch('/something.json') const f2 = fetch('/something2.json') Promise.all([f1، f2]).then((res) => { console.log('Array of results'، res) }).catch((err) => { console.error(err) })
ساختار destructuring assignment در ES2015 نیز اجازه اجرای چنین کاری را میدهد:
Promise.all([f1، f2]).then(([res1، res2]) => { console.log('Results'، res1، res2) })
البته شما محدود به استفاده از fetch نیستید و میتوانید از هر Promise دیگری نیز استفاده کنید.
()Promise.race
این متد زمانی اجرا میشود که اولین Promise که به آن ارسال کردهاید حل شود و callback الصاق شده به آن را یک بار به همراه نتیجه نخستین Promise حل شده اجرا میکند. مثالی از آن به صورت زیر است:
const first = new Promise((resolve، reject) => { setTimeout(resolve، 500، 'first') }) const second = new Promise((resolve، reject) => { setTimeout(resolve، 100، 'second') }) Promise.race([first، second]).then((result) => { console.log(result) // second}
یک خطای رایج
اگر با خطای زیر در کنسول مواجه شدید:
Uncaught TypeError: undefined is not a promise
ابتدا باید اطمینان حاصل کنید که به جای ()Promise از ()new Promise استفاده کردهاید.
Async و Await
جدیدترین رویکرد به تابعهای ناهمگام در جاوا اسکریپت Async و Await است. جاوا اسکریپت در طی مدت بسیار کوتاهی (ES2015) استفاده از Callback را کنار گذاشت و به بهرهگیری از Promise-ها روی آورد. از نسخه ES2017 نیز کدهای ناهمگام در جاوا اسکریپت با استفاده از ساختار async/await سادهتر شدهاند.
تابعهای async ترکیبی از Promise-ها و generatos-ها هستند و اساساً لایه بالاتری از تجرید نسبت به Promise محسوب میشوند. در اینجا باید یک بار دیگر تکرار کنیم که async/await بر مبنای Promise-ها ساخته شدهاند.
Async/Await چرا معرفی شد؟
تابعهای Async/Await باعث کاهش حجم کد مورد نیاز برای نوشتن Promise-ها میشوند و محدودیت «عدم قطع زنجیره» Promise-های زنجیرهسازی را نیز مرتفع میسازند. زمانی که Promise-ها در نسخه ES2015 معرفی شدند، هدف از ارائه آنها حل مشکلی در کدنویسی ناهمگام بود و در این کار نیز موفق بودند، اما در طی دو سال که بین عرضه نسخههای ES2015 و ES2017 وجود داشت مشخص شد که Promise-ها نمیتوانند راهحل نهایی باشند.
Promise-ها برای حل مشکل مشهور جهنم callback-ها عرضه شدند، اما آنها نیز پیچیدگی خاص خود را داشتند و از نظر کاربران دارای ساختار پیچیدهای بودند. Promise-ها با این کارکرد که به ما نشان دادند میتوان از ساختار مناسبتری به این منظور استفاده کرد مفید بودند و لذا اینک زمانی فرا رسیده است که از تابعهای async استفاده کنیم. تابعهای async/await موجب میشوند کد همگام به نظر برسد، اما اساساً ناهمگام است و در پشت صحنه به روشی غیر مسدودکننده عمل میکند.
طرز کار Async/Await چگونه است؟
تابع async یک Promise بازگشت میدهد. به مثال زیر توجه کنید:
const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something')، 3000) }) }
زمانی که بخواهیم این تابع را فراخوانی کنیم یک await آماده میکنیم و کد فراخوانی کننده متوقف میشود تا این که Promise حل یا رد شود. تنها الزام این است که تابع کلاینت باید به صورت async تعریف شود. به مثال زیر توجه کنید:
const doSomething = async () => { console.log(await doSomethingAsync()) }
یک مثال ساده
در مثال ساده زیر از async/await برای اجرای ناهمگام یک تابع استفاده شده است:
const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something')، 3000) }) } const doSomething = async () => { console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After')
کد فوق عبارت زیر را در کنسول مرورگر نمایش میدهد:
BeforeAfterI did something //after 3s
Promise کردن همه چیز
الصاق کردن کلیدواژه async به هر تابعی به این معنی است که تابع یک Promise بازگشت خواهد داد. حتی اگر این کار به طور صریح انجام نشده باشد، این کار موجب میشود که تابع به صورت درونی یک Promise بازگشت دهد. به همین دلیل است که کد زیر معتبر است:
const aFunction = async () => { return 'test'}
aFunction().then(alert) // This will alert 'test'
این کد نیز مانند کد فوق است:
const aFunction = async () => { return Promise.resolve('test')}
aFunction().then(alert) // This will alert 'test
خواندن این کد کاملاً آسانتر است
چنان که در مثال فوق میبینیم، کد ما بسیار ساده به نظر میرسد. آن را با کدی که از Promise-های ساده استفاده میکرد و از زنجیرهسازی و تابعهای callback بهره میگرفت مقایسه کنید. این یک مثال بسیار ساده است، مزیت اصلی زمانی بروز مییابد که کد بسیار پیچیدهتر باشد. برای نمونه با استفاده از Promise-ها به صورت زیر میتوان یک منبع JSON را دریافت کرده و آن را تحلیل کرد:
const getFirstUserData = () => { return fetch('/users.json') get users list.then(response => response.json()) parse JSON.then(users => users[0]) pick first user.then(user => fetch(`/users/${user.name}`)) get user data.then(userResponse => response.json()) parse JSON } getFirstUserData()
کد زیر همان کارکرد فوق را با استفاده از await/async اجرا میکند:
const getFirstUserData = async () => { const response = await fetch('/users.json') get users list const users = await response.json() parse JSON const user = users[0] pick first user const userResponse = await fetch(`/users/${user.name}`) get user data const userData = await user.json() parse JSON return userData } getFirstUserData()
سری کردن چند تابع Async
تابعهای Async را میتوان بسیار ساده به هم زنجیر کرد و ساختار آن نسبت به Promise-های ساده بسیار خواناتر است:
const promiseToDoSomething = () => { return new Promise(resolve => { setTimeout(() => resolve('I did something'), 10000) }) } const watchOverSomeoneDoingSomething = async () => { const something = await promiseToDoSomething() return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => { const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { console.log(res)})
کد فوق عبارت زیر را نمایش میدهد:
I did something and I watched and I watched as well
دیباگ آسانتر
دیباگ کردن Promise-ها دشوار است زیرا دیباگر روی کد ناهمگام نمیایستد. اما async/await کار دیباگ را بسیار آسانتر ساختهاند، زیرا از نظر کامپایلر آنها کد همگام هستند.
Event Emitter در Node.js
با استفاده از Event Emitter در Node.js میتوان رویدادهای سفارشی ایجاد کرد. اگر با جاوا اسکریپت در مرورگر کار کرده باشید، میدانید که بخش عمده تعامل با کاربر از طریق رویدادهایی مانند کلیکهای ماوس، فشردن کلیدهای کیبورد، واکنش به حرکت ماوس و غیره مدیریت میشود. در سمت بکاند، Node.js کلاس EventEmitter را ارائه کرده است که برای مدیریت رویدادها استفاده میشود. این کلاس را میتوان با استفاده از کد زیر مقداردهی کرد:
const eventEmitter = require('events').EventEmitter()
این شیء متدهای on و emit برخی متدهای دیگر را عرضه میکند.
- emit – برای تحریک کردن یک رویداد استفاده میشود.
- on – برای افزودن تابع callback استفاده میشود که قرار است هنگام تحریک شدن رویداد اجرا شود.
برای نمونه، یک رویداد start ایجاد میکنیم و از آنجا که میخواهیم مثال سادهای باشد، واکنش ما به آن لاگ کردن پیامی در کنسول است:
eventEmitter.on('start'، () => { console.log('started')})
زمانی که رویداد را اجرا کنیم:
eventEmitter.emit('start')
تابع دستگیره رویداد تحریک میشود و لاگ کنسول ایجاد میشود. با استفاده از آرگومانهای اضافی ()emit میتوان آرگومانهایی به دستگیره رویداد ارسال کرد:
eventEmitter.emit('start') eventEmitter.emit('start'، 23)
آرگومانهای چندگانه
eventEmitter.on('start'، (start، end) => { console.log(`started from ${start} to ${end}`)}) eventEmitter.emit('start'، 1، 100)
شیء EventEmitter چند متد دیگر نیز برای تعامل با رویدادها در اختیار ما قرار میدهد که از آن جملهاند:
- ()once – یک شنونده یکبارمصرف اضافه میکند.
- ()removeListener() / off – یک شنونده رویداد را از رویداد حذف میکند.
- ()removeAllListeners – همه شنوندهها را از یک رویداد حذف میکند.
منبع: وبلاگ فرادرس
پروفایل
نکات، تجربیات، محتوای آموزشی و مطالب گردآوری شده در حوزه برنامه نویسی وب