اگر با زبان جاوا اسکریپت کار می کنید، احتمالاً موقع جستجو در اینترنت یا خواندن مقالات این حوزه، به واژه هایی مثل CommonJS ،requireJS ،AMD ،ESM ،UMD و ... زیاد برخورده اید. واژه هایی که شباهت های بسیاری با هم دارند و در مقالات، ابزار ها و متن های مرتبط زیادی دیده می شوند، ولی در عین حال کمی گنگ و مرموز به نظر می رسند. اما این ها چه هستند و چرا پای آنها به دنیای جاوااسکریپت باز شده است؟
برای ورود به این بحث، باید کمی درباره ی مفهوم module - ماژول در جاوا اسکریپت بیشتر بدانیم. اما قبل از آن، بگذارید ببینیم قبل از ظهور ماژول ها، دنیای کدنویسی در جاوااسکریپت چه شکلی داشت و module ها آمدند تا چه مشکلی را رفع کنند.
چرا به module ها نیاز داریم؟
اگر در حوزه ی برنامه نویسی تجربه دارید، حتماً درباره ی مفاهیم «کپسوله سازی» (encapsulation) و «وابستگی» (dependency) شنیده اید. قطعه کد های مختلف که هر یک کار خاصی انجام می دهند، به صورت جداگانه و در انزوا توسعه داده می شوند و آماده ی استفاده می گردند. حال اگر یک قطعه کد (مثلاً کد یک پروژه) به عملکردِ قطعه کد آماده ی دیگری نیاز داشت، آن را به عنوان یک «وابستگی» به کد خود اضافه می کند و از قابلیت هایی که ارائه کرده استفاده می کند.
در سوی دیگر، از آنجا که قطعه کد آماده ای را به پروژه اضافه کرده اید، باید مراقب تداخل های احتمالی کد های پروژه با قطعه کد آماده ای که اضافه کرده اید باشید. و اینجاست که پای مفهوم کپسوله سازی به میان می آید. این تداخل ها در هر زبانی به نحوی مدیریت می شوند.
اما این چالش در جاوا اسکریپت، در گذشته ای نه چندان دور، خیلی ابتدایی و سخت حل می شد. برنامه نویسان جاوااسکریپت برای جلوگیری از چنین چالشی، اولاً باید دقت می کردند که قطعه کد های مختلف مورد استفاده در برنامه با یکدیگر تداخل نداشته باشند (مثلاً متغیر با نام یکسان نداشته باشند) و همچنین باید توجه می داشتند که ترتیب اضافه کردن کد ها را رعایت کنند.
توجه شما را به یک مثال جلب می کنیم. فرض کنید یک فایل test.html داریم که می خواهیم در آن از اسکریپت script1 استفاده کنیم. ساختار فایل ما به این شکل است:
فایل script1.js چنین محتوایی دارد:
var testVar = 'value1';
(script1.js)
حال اگر در فایل test.html بنویسیم:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Script loading order</title>
</head>
<body>
hello world from Sokan Academy...
<script src="script1.js"></script>
<script>
console.log('the value of the variable is: ', testVar);
</script>
</body>
</html>
یعنی ابتدا تگ مربوط به فایل script1.js را قرار دهیم و بعد اسکریپت مربوط به صفحه ی خود را بگذاریم، با باز کردن صفحه ی test.html در کنسول مرورگر، مقدار متغیر testVar چاپ خواهد شد:
حال اگر محل تعریف script1 را با اسکریپت اصلی صفحه عوض کنیم، داریم:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Script loading order</title>
</head>
<body>
hello world from Sokan Academy...
<script>
console.log('the value of the variable is: ', testVar);
</script>
<script src="script1.js"></script>
</body>
</html>
یعنی قبل از تعریف متغیر testVar سعی کردیم آن را چاپ کنیم، که در این صورت به خطا برخورد می کنیم:
پس باید خودمان دقت کنیم که ترتیب تعریف اسکریپت ها چگونه باشد.
حال ترتیب تعریف اسکریپت ها را به حالت اولیه برمی گردانیم، اما این بار سعی می کنیم در اسکریپت اصلی برنامه یک متغیر همنام با متغیری که در فایل script1.js وجود داشت تعریف کنیم:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Script loading order</title>
</head>
<body>
hello world from Sokan Academy...
<script src="script1.js"></script>
<script>
var testVar = 'value2'
console.log('the value of the variable is: ', testVar);
</script>
</body>
</html>
در کنسول مرورگر مقداری که در اسکریپت اصلی برنامه به testVar داده ایم اعمال می شود، و در نتیجه مقدار متغیر testVar که در اسکریپت script1 تعیین شده بود عوض می شود:
یعنی دیگر به این مقدار دسترسی نداریم و بدون اینکه بخواهیم آن را تغییر دادیم. حال تصور کنید منطق اسکریپتِ script1 به گونه ای بود که به مقدار testVar نیاز داشت، آن موقع چه اتفاقی می افتاد؟ قطعاً این منطق دچار اختلال می شد، چرا که ما از بیرون اسکریپت اقدام به تغییر متغیرِ آن کردیم.
می دانیم این قبیل چالش ها در زبان های مختلف وجود دارد و برنامه نویس هر یک از این زبان ها باید با روشی ترتیب اضافه کردن قطعه کد ها و کتابخانه های مختلف را به درستی تنظیم کند و مفهوم encapsulation را به گونه ای پیاده کند که از رخ دادن چنین اتفاقاتی جلوگیری نماید.
طبیعی ست که اگر در یک زبان راه حل مشخصی برای رفع این مشکل وجود نداشته باشد، برنامه نویس باید همه ی این موارد را خودش و به صورت دستی مدیریت کند. و ناگفته پیداست که با این روش، به محض اینکه مقیاس پروژه و تعداد وابستگی ها زیاد شود، مدیریت آنها به کاری بسیار مشکل تبدیل خواهد شد.
جالب است بدانید که در گذشته ی نه چندان دور، جاوااسکریپت نویسانی که کدهای سمت کاربر می نوشتند، دقیقاً این کار را خود به صورت دستی انجام می دادند!
اما کم کم راه حل ها و استاندارد های مختلف در برنامه نویسی جاوااسکریپت پا به عرصه گذاشتند.
در ابتدا این مشکل با یک راه حل نسبتاً ساده مدیریت می شد، راه حلی به نام «الگوی Revealing module».
این راه حل بر مبنای این ویژگی جاوا اسکریپت بود: «متغیر هایی که با کلیدواژه ی var تعریف می شوند، به بازه (scope) تابع (function) ای که به آن تعلق دارند محدود می شوند. و در نتیجه بیرون از آن قابل دسترسی نیستند.»
ایده ی الگوی Revealing module این بود که هر بخشی را که می خواهیم به عنوان یک عضو مستقل (یا همان ماژول) تعریف کنیم، به تابعی تبدیل کنیم که به محض تعریف شدن اجرا می شود (IIFE یا Immediately Invoked Function Expression). در تعریف این تابع نیز دقت می کردیم مقادیری که قرار است از بیرون دیده شوند را به عنوان خروجی تابع برگردانیم (return کنیم) و مقادیری که مختص خود ماژول هست را درون آن نگه داریم. به این ترتیب مقادیر عمومی (public) از مقادیر خصوصی (private) مربوط به یک اسکریپت جدا می شد و عملاً کپسوله سازی اتفاق می افتاد. به مثال زیر دقت کنید:
var revealingModule = (function () {
var privateVar = "Ben Thomas";
function setNameFn( strName ) {
privateVar = strName;
}
return {
setName: setNameFn,
};
})();
کد بالا مربوط به یک ماژول است. با وارد کردن کد این ماژول به کدی دیگر، با توجه به اینکه متغیر privateVar محدود به بازه ی تابع خودش می شود، اگر متغیری با همین نام در اسکریپت بیرونی تعریف کنیم، هیچ مشکلی به وجود نخواهد آمد، یعنی این متغیر را به صورت private در نظر گرفته ایم و کپسوله سازی کرده ایم. از طرفی با توجه به نیازمندی مان، تابع setNameFn را به صورت public در اختیار کد های دیگر قرار داده ایم.
با استفاده از الگوی revealing module، می توانستیم در یک فایل چندین ماژول تعریف کنیم. از طرفی این روش مسئله ی کپسوله سازی را به خوبی حل می کرد. دیگر ویژگی خوب این روش سادگی اش بود، به طوری که بدون استفاده از هیچ کتابخانه یا پشتیبانی خاصی امکان پیاده سازی آن ممکن بود. اما همچنان مشکلاتی وجود داشت.
- مشکل اول همان مشکل وابستگی ها و تقدم و تأخر وارد کردن آن ها به یک کد بود.
- مشکل دیگر این بود که نمی توانستیم ماژول ها را به صورت غیرهمزمان (asynchronous) به کد وارد کنیم.
- همچنین این امکان که ماژول ها را با منطق برنامه (programmatically) و در صورت نیاز وارد کنیم وجود نداشت، مگر با استفاده از eval که آن هم چالش های خاص خود را داشت،( در مورد eval می توانید در سایت MDN بیشتر بخوانید)
- مشکل بعد این بود که اگر دو ماژول نسبت به هم وابستگی چرخه ای داشتند، یعنی ماژول اول از ماژول دوم استفاده می کرد، و همزمان ماژول دوم نیز از ماژول اول استفاده می کرد (موضوعی که با عنوانcircular dependency یا cyclic dependency شناخته می شود)، امکان اضافه کردن آن ها به یک کد با روش revealing module pattern وجود نداشت.
- و در آخر نیز تحلیل ایستای کد (static code analysis) بر روی کد هایی که با این الگو نوشته می شدند دشوار می شد. (اگر می خواهید در مورد مفهوم static code analysis بیشتر بدانید، مطالعه ی این مقاله از سایت perforce را به شما پیشنهاد می کنیم.)
پس برایمان روشن شد که باید راه حل بهتری برای این مشکل در نظر گرفته می شد.
این راه حل، تعریف دقیق تر مفهوم module و وضع قواعد مشخصی برای کار با آن ها در برنامه نویسی جاوااسکریپت بود.
حال بگذارید کمی بیشتر درباره ی مفهوم module صحبت کنیم ...
ماژول یا module چیست؟
مفهوم ماژول شباهت بسیار زیادی با مفهوم کتابخانه در سایر زبان های برنامه نویسی دارد. اما در دنیای جاوااسکریپت، کمتر از واژهی کتابخانه استفاده می شود. در واقع منظور از ماژول، کوچک ترین واحد functionality است. اگر بخواهیم دقیق تر بیان کنیم:
ماژول یک فایل جاوا اسکریپت است که حداقل یکی از نماد (symbol) هایی را که تعریف می کند، به عنوان خروجی ارائه می کند. (export می کند) خواه این نماد متغیر، تابع (function) یا شئ (object) باشد.
ماژول های کوچک و بزرگ که هر یک کار خاصی انجام می دهند، در کنار یکدیگر قرار می گیرند و پروژه های مختلف را ایجاد می کنند. حتماً برای شما هم پیش آمده که بخواهید از قطعه کدی در کد برنامه ی خود استفاده کنید. اما از کجا بدانیم که باید آن قطعه کد را import کنیم یا require کنیم؟
بسیاری از برنامه نویسان بر حسب تجربه این کار را انجام می دهند، اما از قوانینی که بر این حوزه حاکم است اطلاع چندانی ندارند. Module system ها دقیقاً با این موضوع سر و کار دارند.
این که نحوه ی export کردن داده ها در یک ماژول به چه ترتیبی باشد، و نحوه ی اضافه کردن کد یک ماژول در ماژولی دیگر چه قواعد و نوشتاری (syntax) داشته باشد و مسائلی از این دست، نیازمند قواعدیست که کار توسعه ی برنامه ها را ممکن کند. این قواعد را با عنوان module system می شناسند.
ماژول سیستم یا Module system چیست؟
همان طور که گفتیم، برای کار با ماژول ها نیازمند تنظیم قواعدی بودیم.
خوب است بدانید که در طول زمان و در حوزه های مختلف برنامه نویسی به زبان جاوا اسکریپت، برای نحوهی تعریف ماژول ها و رفتار آن ها قواعد مختلفی وضع شد. قواعدی که امروزه آنها را با عنوان module system میشناسیم.
به عنوان مثال commonJS ،AMD ،UMD ،ESM هر یک نوعی module system در دنیای جاوا اسکریپت محسوب می شوند.
این module system ها از نظر نوشتاری و همچنین در برخی قابلیت ها با یکدیگر تفاوت هایی دارند که توضیح مفصل تر در مورد آنها را به مقاله ای جداگانه موکول می کنیم.
با ما همراه باشید.