همانطور که برنامه ما بزرگتر میشود، سعی میکنیم آن را به فایل های متفاوتی به نام ماژول (modules) تقسیم بندی کنیم. یک ماژول ممکن است شامل یک کلاس یا کتابخانه ای از توابع برای یک هدف خاص باشد.
تا مدت ها، جاوااسکریپت ساختاری برای نوشتن ماژول ها در سطح زبان نداشت.
اما با گذر زمان اسکریپت ها بیشتر و بیشتر پیچیپده شدند، در نتیجه برنامه نویسان روش های متفاوتی برای سازماندهی و ماژول بندی کردن کد خود و کتابخانه های خاص برای بارگذاری ماژول ها در زمان نیاز ابداع کردند.
این ها نمونه هایی از این کتابخانه ها است (این بخش صرفا جنبه تاریخی دارد):
- AMD – یکی از قدیمی ترین سیستم های ماژول بندی، که اولین بار توسط کتابخانه require.js پیاده سازی شد.
- CommonJS – سیستم ماژول بندی که برای سرورها Node.js درست شد.
- UMD – یک سیستم ماژول بندی که به عنوان یک سیستم جامع شناخته شده و با هردو سیستم AMD و CommonJS همخوانی دارد.
همه این سیستم ها کم کم تبدیل به بخشی از تاریخ شدند اما هنوز هم میتوان آن ها را در اسکریپت های قدیمی مشاهده کرد.
سیستم ماژول بندی در سطح زبان در استاندار سال 2015 مشاهده شد، از آن زمان کمکم پیشرفت کرده و در حال حاضر توسط تمامی مرورگرهای اصلی و Nodejs پشتیبانی میشود. در نتیجه از این به بعد درباره سیستم ماژول بندی مدرن در جاوااسکریپت صحبت میکنیم.
ماژول چیست؟
ماژول در اصل یک فایل است. هر اسکریپت یک ماژول است. به همین سادگی.
ماژول ها میتوانند یکدیگر را لود کرده و به وسیله توابع خاصی مانند export
و import
بین هم عملکردی را رد و بدل کنند، به وسیله صدا زدن تابعی از یک ماژول در یک ماژول دیگر:
- کلمه کلیدی
export
متغیرها و توابعی را مشخص میکند که باید بیرون از این ماژول قابل دسترسی باشند. - کلمه کلیدی
import
اجازه ایمپورت کردن و استفاده از توانایی های ماژول های دیگر را میدهد.
برای مثال، ما یک فایل با نام sayHi.js
داریم که یک تابع را اکسپورت میکند:
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
…در این حالت یک فایل دیگر میتواند این ماژول را ایمپورت کرده و از این تابع استفاده کند:
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // تابع...
sayHi('John'); // Hello, John!
تابع import
ماژول را بر اساس مسیر ./sayHi.js
که بر مبنای فایل فعلی است، بارگذاری کرده، سپس تابع اکسپورت شده sayHi
را به متغیر مناسب اختصاص میدهد.
بگذارید تا این مثال را در مرورگر امتحان کنیم.
از آن جایی که ماژول ها از کلیدواژه ها و امکانات خاصی استفاده میکنند، ما باید به وسیله مشخصه <script type="module">
به مرورگر بگوییم که این اسکریپت یک ماژول است.
مانند مثال زیر:
export function sayHi(user) {
return `Hello, ${user}!`;
}
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
مرورگر به صورت خودکار ماژول ایمپورت شده (و ماژول هایی که این ماژول به آن وابسته است) را دریافت و بررسی میکند، سپس اسکریپت را اجرا میکند.
اگر سعی کنید که یک صفحه وب را به صورت محلی، از طریق پروتکل file://
باز کنید، توابع import/export
کار نمیکنند. برای این کار از یک وب سرور لوکال استفاده کنید، مانند static-server یا از قابلیت “سرور زنده” ویرایشگر متن خود استفاده کنید، مانند VS Code Live Server Extension برای تست ماژول خود.
امکانات اصلی ماژول ها
چه مواردی در ماژول ها با اسکریپت های “معمولی” متفاوت است؟
بعضی از این امکانات، در هر دو محیط مرورگر و سرور معتبر هستند.
حالت “use strict” به صورت پیش فرض فعال است.
در ماژول ها حالت use strict
به صورت پیش فرض فعال است، برای مثال اختصاص دادن مقدار به یک متغیر که از قبل تعریف نشده است باعث به وجود آمدن خطا میشود.
<script type="module">
a = 5; // خطا
</script>
محدوده/اسکوپ سطح ماژول
هر ماژول اسکوپ سطح بالای خود را دارد. به عبارت دیگر، توابع و متغیر های سطح بالا در یک ماژول قابل دسترسی توسط اسکریپت های دیگر نیستند.
در مثال پایین، دو اسکریپت ایمپورت شده اند، و hello.js
سعی در استفاده از متغیر user
که در فایل user.js
تعریف شده است، کرده و شکست خورده است.
alert(user); // no such variable (each module has independent variables)
let user = "John";
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
در موقع کار با ماژول ها انتظار میرود که هر چیزی که قرار است از بیرون قابل دسترسی باشد export
و هر چیزی که آن ها در اسکریپت خود نیاز دارند import
شود.
- ماژول
user.js
باید متغیرuser
را اکسپورت کند. - ماژول
hello.js
باید آن را از ماژولuser.js
ایمپورت کند.
به عبارتی دیگر، در ماژولها ما از import/export به جای متغیرهای سراسری (global) استفاده میکنیم.
این نمونه درست این کد است:
import {user} from './user.js';
document.body.innerHTML = user; // John
export let user = "John";
<!doctype html>
<script type="module" src="hello.js"></script>
در مرورگر، یک اسکوپ سطح بالا ی مستقل هم برای هر تگ <script type="module">
وجود دارد:
اینجا دو اسکریپت در یک صفحه وجود دارد، هر دو از نوع type="module"
هستند. آنها متغیرهای سطح بالا(top-level) هم را نمیبینند:
<script type="module">
// متغیر تنها در اسکریپت ماژول قابل دسترسی است.
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
در مرورگر، ما میتوانیم یک متغیر window-level گلوبال بسازیم با اختصاص دادن آن صریحاً به یک مقدار window
، برای مثال: windows.user = "John"
.
پس همه اسکریپتها آن را خواهند دید، هم با type="module"
و هم بدون آن.
گرچه به این روش اشاره شد، ولی استفاده از چنین متغیرهای گلوبالی توصیهشده نیست. لطفاً تلاش کنید که از آن استفاده نکنید.
کد یک ماژول تنها اولین بار که به اسکریپت ما ایمپورت شده، ارزیابی میشود.
اگر یک ماژول مشابه در چندین مکان مختلف ایمپورت شود، کد آن تنها در مرتبه اول اجرا میشود، بعد از آن نتیجه به تمامی مکان های دیگر اکسپورت میشود.
این رفتار عواقب مهمی دارد، که باید از آنها آگاه باشیم.
بگذارید تا آن ها را در مثال بررسی کنیم:
اول از همه، اگر اجرای کد ما باعث اتفاق افتادن یک سری اتفاقات شود، مانند نشان دادن یک پیغام، در این صورت چندین بار ایمپورت کردن کد تنها باعث یک بار اجرا شدن این پیغام میشود – تنها بار اول:
// 📁 alert.js
alert("Module is evaluated!");
// ایمپورت کردن یک ماژول مشابه در دو فایل متفاوت
// 📁 1.js
import `./alert.js`; // ماژول ارزیابی و اجرا میشود.
// 📁 2.js
import `./alert.js`; // (پیغامی نمایش داده نمیشود.)
ایمپورت دوم چیزی را نشان نمیدهد، چون ماژول پیش از این ارزیابی شده است.
یک قانون وجود دارد: ماژولهای top-level باید برای مقداردهی اولیه استفاده شوند، ساختن ساختارهای دادهای داخلی. اگر ما نیاز به ساخت چیزی داریم که چندین بار آن را فراخوانی کنیم – باید آن را به عنوان یک تابع اکسپورت کنیم، مانند کاری که با sayHi
در بالا کردیم.
خب، حال با هم یک مثال پیشرفته تر را میبینیم.
فرض میکنیم که یک ماژول یک آبجکت را اکسپورت میکند:
// 📁 admin.js
export let admin = {
name: "John"
};
اگر این ماژول چند مرتبه در چند فایل ایمپورت شود، ماژول تنها در مرتبه اول ارزیابی میشود، این به این معناست که آبجکت admin
یک بار درست شده، و سپس به جاهای دیگر ایمپورت میشود.
همه مکان هایی که ماژول ایمپورت شده است، دقیقا یک و تنها یک آبجکت admin
دریافت میکنند:
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// یک آبجکت مشابه را ایمپورت میکنند ( 1.js , 2.js ) هردو فایل
// هم قابل مشاهده است (2.js) ایجاد شود در فایل دوم (1.js) هر تغیری که در فایل اول
همانطور که میبینید، وقتی 1.js
مقدار name
را در admin
ایمپورت شده تغییر میدهد، سپس 2.js
میتواند مقدار جدید admin.name
را ببیند.
این دقیقاً به خاطر این است که ماژول فقط یک بار اجرا شده است. اکسپورتها تولیدشدهاند، و سپس بین ایمپورتها به اشتراک گذاشته شدهاند، پس اگر چیزی شیء admin
را تغییر دهد، بقیه ایمپورتها هم آن را میبینند.
چنین رفتاری در واقع خیلی مفید است، چون به ما اجازه میدهد تا ماژولها را کانفیگ کنیم.
به عبارتی دیگر، یک ماژول میتواند عملکردی عمومی ارائهدهد که نیاز به راهاندازی دارد. برای مثال احراز هویت نیازمند مدارک است. پس میتواند یک شی configuration اکسپورت کند و انتظار داشته باشد تا کد بیرونی، آن را مقداردهی کند.
اینجا الگوی کلاسیک را میبینید:
- یک ماژول مقادیر قابل کانفیگشدن را اکسپورت میکند، مثلاً: یک شی قابل کانفیگ
- در ایمپورت اول، آن را مقداردهی میکنیم، در مقادیر آن مینویسیم. برنامه top-level ما آن را انجام میدهد.
- ایمپورتهای آتی از ماژول استفاده میکنند.
برای مثال، ماژول admin.js
ممکن است یک سری قابلیت ها به ما بدهد، اما از ما انتظار دارد که یک سری متغیر ها از بیرون آبجکت admin
به آن پاس دهیم:
// 📁 admin.js
export let config = { };
export function sayHi() {
alert(`Ready to serve, ${config.user}!`);
}
در اینجا، admin.js
شی config
را اسکپورت میکند (مقدار اولیه خالی، ولی ممکن مقادیر پیشفرض نیز داشته باشد).
سپس در init.js
، که اولین اسکریپت برنامه ما میباشد، ما config
را از آن ایمپورت میکنیم و config.user
را مقداردهی میکنیم.
// 📁 init.js
import {config} from './admin.js';
config.user = "Pete";
…حالا ماژول admin.js
کانفیگشده است.
ایمپورتهای آتی میتوانند آن را فراخوانی کنند، و آن به درستی شی user فعلی را نمایش میدهد:
// 📁 another.js
import {sayHi} from './admin.js';
sayHi(); // Ready to serve, Pete!
شئ import.meta
آبجکت import.meta
دارای یک سری اطلاعات درباره ماژول فعلی است.
اطلاعات آن بستگی به محیطی که در آن اجرا میشود، دارد. در مروگر، شامل آدرس اسکریپت است، و یا آدرس صفحه فعلی اگر داخل فایل HTML باشد:
<script type="module">
alert(import.meta.url); // آدرس اسکریپت
// برای یک اسکریپت اینلاین - آدرس صفحه فعلی HTML
</script>
در یک ماژول، “this” تعریف نشده است
این یک قابلیت جزئی است، اما برای کامل بودن آموزش به آن اشاره میکنیم.
در یک ماژول، this
سطح بالا undefined است.
مقایسه آن با اسکریپت های غیر ماژول، که در آن ها this
آبجکت جهانی است:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
قابلیت های مخصوص محیط مرورگر
اسکریپت ها از نوع ماژول نسبت به انواع معمولی آن تفاوت هایی مخصوص محیط مرورگر هم دارند.
اگر این اولین بار است که این آموزش را میخوانید، یا از جاوااسکریپت در مرورگر استفاده نمیکنید، میتوانید این بخش را رد کنید.
اسکریپت های ماژولی به تعویق افتاده اند. (deferred)
اسکریپت ها از نوع ماژول همیشه به تعویق افتاده اند، دقیقا مانند خصوصیت defer
(در فصل Scripts: async, defer توضیح داده شده است.)، برای هر دو نوع اکسترنال و اینلاین.
به عبارت دیگر:
- دانلود اسکریپت های از نوع ماژول خارجی (external)
<script type="module" src="...">
جلوی پردازش HTML را نمیگیردند، این اسکریپت ها موازی با دیگر منابع بارگیری میشوند. - اسکریپت های ماژولی تا بارگیری کامل اسناد HTML صبر میکنند (حتی با وجود اینکه این اسکریپت ها کوچک بوده و سریع تر از HTML بارگیری میشوند)، و سپس اجرا میشوند.
- اسکریپت ها به همان ترتیبی که نوشته میشوند، اجرا میشوند: اسکریپتی که در فایل ها اول آمده است، اول اجرا میشود.
به عنوان یک عارضه جانبی، اسکریپت های ماژولی همیشه صفحه HTML کامل بارگیری شده را “میبینند”، از جمله عناصری که در متن جلوتر از آن ها قرار دارند.
برای مثال:
<script type="module">
alert(typeof button); // آبجکت: اسکریپت میتواند دکمه ای که زیر آن هست را "ببیند".
// به دلیل اینکه ماژول ها به تعویق افتاده اند، اسکریپت بعد از بارگیری کل صفحه اجرا میشود.
</script>
در مقایسه با اسکریپت معمولی زیر:
<script>
alert(typeof button); // دکمه undefined است، به دلیل اینکه اسکریپت نمیتواند عناصر زیر را ببیند.
// اسکریپت های معمولی بالافاصله قبل از اینکه بقیه صفحه پردازش شود، اجرا میشوند.
</script>
<button id="button">Button</button>
توجه کنید که: اسکریپت دوم در حقیقت قبل از اولی اجرا میشود! در نتیجه ما ابتدا undefined
و سپس object
را میبینیم.
این پدیده به این خاطر است که ماژول ها به تعویق افتاده هستند، در نتیجه ابتدا صبر میکند تا تمام سند بارگیری شود. اسکریپت های معمولی بالافاصله اجرا میشوند. در نتیجه ما خروجی آن را ابتدا مشاهده میکنیم.
وقتی که از ماژول ها استفاده میکنیم، باید به این نکته توجه کنیم که صفحات HTML همان طور که بارگیری میشوند، به کاربر نشان داده میشوند و ماژول های جاوااسکریپت بعد از آن اجرا میشوند، پس کاربر صفحه را قبل از اینکه برنامه جاوااسکریپت اجرا شود، میبیند. بعضی از قابلیت ها ممکن است که کار نکنند. ما باید از یک “مشخص کننده مقدار بارگیری شده” استفاده کنیم، یا مطمئن شویم که این پدیده باعث سردرگم شدن کاربر نمیشود.
Async در اسکریپت های اینلاین معتبر است.
در اسکریپت های غیر ماژولی، مشخصه async
تنها در اسکریپت های اکسترنال کار میکنند. اسکریپت های غیر ترتیبی به محض آماده شدن، اجرا میشوند، بدون توجه به اسکریپت های دیگر یا کد های HTML.
برای اسکریپت های ماژولی، در حالت اینلاین هم معتبر است.
برای مثال، اسکریپت اینلاین زیر async
دارد، در نتیجه برای هیچ چیزی صبر نمیکند.
اسکریپت، ایمپورت (fetche ./analytics.js
) را انجام میدهد و وقتی که آماده شد، اجرا میشود. حتی اگر سند HTML یا دیگر اسکریپت ها آماده نباشند.
این رفتار برای قابلیت هایی که به هیچ چیز دیگری وابسته نیستند، خوب هست، مانند شمارنده ها، تبلیغات، event listener های در سطح سند.
<!-- همه وابستگی ها دریافت شده(analytics.js)، و اسکریپت اجرا میشود. -->
<!-- برای دیگر سندها یا تگ های <script> منتظر نمیشود. -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
اسکریپت های اکسترنال
اسکریپت های اکسترنال که از نوع module type="module"
هستند، دو خصوصیت متفاوت دارند:
-
اسکریپت های اکسترنال با
src
مشابه تنها یک مرتبه اجرا میشوند:<!-- اسکریپت my.js تنها یکبار دریافت و اجرا میشود --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script>
-
اسکریپت های اکسترنالی که از یک منبع دیگر (مانند یک سایت دیگر) دریافت شده اند. به CORS header نیاز دارند، همانگونه که در فصل Fetch: Cross-Origin Requests توضیح داده شد. به عبارت دیگر، اگر یک اسکریپت ماژولی از یک منبع دیگر دریافت شده باشد، سرور دیگر باید هدر
Access-Control-Allow-Origin
را ست کرده باشد تا دریافت امکان پذیر باشد.<!-- یک سایت دیگر مانند another-site.com باید Access-Control-Allow-Origin را فراهم کرده باشد. --> <!-- در غیر این صورت، اسکریپت اجرا نخواهد شد. --> <script type="module" src="http://another-site.com/their.js"></script>
این قابلیت به صورت پیش فرض باعث افزایش امنیت میشود.
ماژول های “bare” غیر مجاز هستند.
در مرورگر، import
باید یک لینک رلتیو یا ابسولوت دریافت کند. ماژول هایی که هیچونه آدرسی یا مسیری ندارند را “bare” یا برهنه مینامیم. چنین ماژول هایی در import
مجاز نیستند.
برای مثال، import
زیر مجاز نیست:
import {sayHi} from 'sayHi'; // Error, "bare" module
// ماژول باید یک مسیر داشته باشد، برای مثال './sayHi.js' یا هر ماژولی که هست.
بعضی محیط ها مانند Node.js یا ابزارهای bundle اجازه استفاده از ماژول های برهنه را میدهند، بدون هیچ مسیری، به این دلیل که این محیط ها روش های دیگری برای پیدا کردن ماژول ها و هوک ها و تنظیم آن ها دارند. اما مرورگر ها در حال حاضر از ماژول های برهنه پشتیبانی نمیکنند.
سازگاری، “nomodule”
مرورگرهای قدیمی منظور را از type="module"
نمی فهمند. اسکریپت هایی از نوع ناشناخته نادیده گرفته میشوند. برای این موارد این امکان وجود دارد که یک حالت استثنا به وسیله nomodule
تعریف کنید:
<script type="module">
alert("اجرا در مرورگرهای مدرن");
</script>
<script nomodule>
alert("مرورگرهای مدرن هر دو مورد type=modeule و nomodule را میفهمند، پس از این مورد در میشوند.")
alert("مرورگرهای قدیمی اسکریپت از نوع ناشناخته را نادیده میگیرند type=module اما این مورد را اجرا میکنند.");
</script>
ابزارهای ساخت
در زندگی واقعی، ماژول های مرورگر به ندرت در حالت “خام” خود استفاده میشوند. معمولا، ما این اسکریپت ها را با ابزارهایی مانند Webpack با هم استفاده میکنیم و در سرور نهایی اعمال میکنیم.
یکی از مزایای استفاده از باندلرها – اینها به ما کنترل بیشتر بر روی اینکه ماژول ها چگونه اجرا میشوند، میدهد، اجازه اجرا شدن ماژول های برهنه و بسیار کارهای دیگر، مانند ماژول های CSS/HTML.
ابزارهای ساخت کارهای زیر را انجام میدهند:
- ماژول “اصلی”، همان ماژولی که قرار است توی
<script type="module">
در HTML قرار بگیرد را بردار. - وابستگی های آن را بررسی کن: importهای آن و سپس importهای importهای آن و تا به آخر.
- یک فایل با تمام ماژول ها بساز(یا چند فایل، این مورد قابل تنظیم است)، جایگزینی
import
های صدا زده شده با توابع باندلر، تا این کار شدنی باشد. ماژول های “خاص” مانند ماژول های HTML/CSS هم پشتیبانی میشوند. - در حین عملیات، تبدیل ها و ارتقاهای دیگری هم ممکن است انجام شود:
- کدهایی که هیچ وقت اجرا نمیشوند، حذف میشوند.
- exportهایی که استفاده نمیشوند، پاک میشوند.(“tree-shaking”).
- عباراتی که مخصوص زمان توسعه نرم افزار هستند مانند
console
وdebugger
حذف میشوند. - سینتکس و املای مدرن جاوااسکریپت ممکن است به نمونه های قدیمی با عملکرد مشابه توسط Babel تبدیل شوند.
- فایل نهایی فشرده میشود. (فاصله های پاک میشوند، متغیرها با نام های کوتاه تر جایگزین میشوند و غیره)
اگر ما از ابزارهای باندل استفاده کنیم، در این صورت تمام اسکریپت ها با هم در یک فایل ( یا تعداد کمی فایل ) جمع میشوند، عبارات import/export
داخل اسکریپت ها با توابع خاص باندلر ها جایگزین میشوند. در نتیجه اسکریپت باندل نهایی هیچ عبارت import/export
ندارد، این اسکریپت نیازی به type="module"
ندارد، و ما میتوانیم آن را در یک اسکریپت معمولی بگذاریم.
<!-- با فرض اینکه ما bundle.js را از یک ابزار مانند Webpack گرفته ایم -->
<script src="bundle.js"></script>
با این حساب، ماژول ها بومی و نیتیو هم قابل استفاده هستند. در نتیجه ما از Webpack در اینجا استفاده نمیکنیم: شما میتوانید آن را در آینده تنظیم کنید.
خلاصه
بطور خلاصه، مفاهیم اصلی عبارتند از:
- هر ماژول یک فایل است. برای اینکه عبارات
import/export
کار بکنند، مرورگرها نیاز به<script type="module">
دارند. ماژول های چندین تفاوت با اسکریپت های معمولی دارند:- به صورت پیش فرض به تعویق افتاده (Deferred) هستند.
- در اسکریپت های inline Async جواب میدهد.
- برای بارگزاری اسکریپت های خارجی (external) از منابع دیگر (دامنه/پروتکل/پورت)، هدر های CORS نیاز هستند.
- اسکریپت های مشابه external نادیده گرفته میشوند.
- ماژول های اسکوپ سطح بالای خود را دارند و از طریق عبارات
import/export
کارایی های خود را با دیگر اسکریپت های به اشتراک میگذارند. - ماژول ها همیشه در حالت
use strict
هستند. - کد ماژول ها تنها یک مرتبه اجرا میشوند. Exportها تنها یک مرتبه ساخته شده و سپس بین تمام importer ها به اشتراک گذاشته میشوند.
وقتی که ما از یک ماژول استفاده میکنیم، هر ماژول یک کارایی را بوجود آورده و ان را اکسپورت میکند. سپس ما از عبارت import
برای مستقیما ایمپورت کردن ماژول به جایی که به آن نیاز داریم، استفاده میکنیم. مرورگر به صورت خودکار اسکریپت را بارگذاری و ارزیابی میکند.
در زمان انتشار، برنامه نویسان معمولا از باندل هایی مانند Webpack برای جمع کردن ماژول ها در کنار هم و بالا بردن کارایی و چند دلیل دیگر استفاده میکنند.
در فصل بعد ما مثال های بیشتری از ماژولها میبینیم، و اینکه چگونه آن ها اکسپورت/ایمپورت میشوند.