آموزش جاوا اسکریپت – جلسه ۲۷ – ماژول [module]

درود بر شما عزیزان همراه راهکارینو. در جلسه قبل (بیست و ششم) از آموزش جامع javascript با بررسی تست نویسی در جاوا اسکریپت و معرفی ابزارهای آن در خدمتتان بودیم. در این جلسه می خواهیم درباره ماژول (Module) در جاوا اسکریپت و انواع import و export ماژول بطور مفصل صحبت کنیم.
وقتی مقیاس پروژه جاوا اسکریپت وسیع می شود احتمالا می خواهیم چندین فایل مجزای js داشته باشیم. به هر یک از این فایل ها ماژول (Module) گفته می شود.
هر ماژول می تواند شامل کلاس (class)، کتابخانه (library) یا تابع (function) باشد که یک هدف خاصی را دنبال می کنند. زبان جاوا اسکریپت چندین سال بدون قابلیت ماژول کار می کرد و برای زمان خودش مشکلی نداشت و نیازی احساس نمیشد. زیرا مقیاس پروژه ها و پیچیدگی اسکریپت ها به حدی نبود که به ماژول نیازی باشد. اما به مرور زمان کدهای جاوااسکریپت پیچیده تر شدند و پروژه های پیاده سازی شده با js نیاز به ماژولار شدن پیدا کردند.
از سال 2015 میلادی که اکما اسکریپت 6 (ES6) معرفی شد، قابلیت ماژولار کردن اسکریپت های javascript نیز فراهم شد و اکنون توسط تمام مرورگرهای مدرن پشتیبانی می شود.
ماژول (module) چیست؟
یک ماژول یک فایل js است. به همین سادگی!
ماژول ها می توانند توسط دستورات import و export یکدیگر را صدا بزنند، توابع یکدیگر را فراخوانی و استفاده کنند و…
- Export: با استفاده از کلمه کلیدی export برای توابع یا متغیرها، می توان آنها را در سایر ماژول ها (فایل ها) مورد استفاده قرار داد و فراخوانی کرد.
- Import: امکان دریافت و استفاده از توابع یا فایل های export شده را فراهم می کند.
در مثال زیر، می خواهیم تابع sayHi را در فایل بنام sayHi.js اکسپورت کنیم:
// 📁 sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); }
سپس فرضا می خواهیم در فایل main.js آن را ایمپورت کرده و استفاده کنیم:
// 📁 main.js import {sayHi} from './sayHi.js'; alert(sayHi); // function... sayHi('John'); // Hello, John!
دستور import در خط اول، ماژول sayHi را از مسیر جاری (./sayHi.js) ایمپورت می کند و در متغیر sayHi ذخیره می کند. سپس در دستورات بعدی، از مقدار برگشتی تابع sayHi استفاده می کنیم.
از آنجا که ماژول ها امکانات و قابلیت های ویژه ای دارند، باید بصورت زیر، نوع تگ script را برابر module قرار دهیم:
محتوای فایل index.html
<!doctype html> <script type="module"> import {sayHi} from './say.js'; document.body.innerHTML = sayHi('John'); </script>
محتوای فایل say.js
export function sayHi(user) { return `Hello, ${user}!`; }
نکته: ماژول ها در جاوا اسکریپت تنها از طریق HTTP(s) کار می کنند و روی فایل های لوکال کار نخواهند کرد. بعنوان مثال اگر فایل ماژول را در ویندوز سیستم خود بصورت file:// کنید، دستورات import و export کار نمی کنند. بلکه باید یک وب سرور لوکال اجرا کنید (مثلا توسط live server در نرم افزار vs-code) سپس فایل ماژول js را در آن اجرا کنید.
ویژگی های ماژول چیست؟
اما چه فرقی بین اسکریپت عادی و ماژولار می باشد؟ در واقع یک سری از ویژگی ها و خاصیت ها بین اسکریپت ماژول سمت سرور (server-side) و سمت کلاینت (client-side) مشترک می باشد. عبارتند از:
استفاده از use strict بصورت پیش فرض:
در اسکریپتی که به صورت module تعریف شده است، اگر قبل از اینکه یک متغیر را تعریف کنیم، آن را مقدار دهی کنیم، با خطا روبرو خواهیم شد:
<script type="module"> a = 5; // error </script>
اسکوپ در سطح ماژول:
هر ماژول محدوده یا scope تعریف خودش را دارد و نمی توان به متغیری در ماژول دیگر دسترسی داشت. در مثال زیر می خواهیم از ماژول hello.js به متغیری در ماژول user دسترسی مستقیم داشته باشیم مه با خطا مواجه می شویم:
محتوای index.html
<!doctype html> <script type="module" src="user.js"></script> <script type="module" src="hello.js"></script>
محتوای فایل user.js
let user = "John";
محتوای فایل hello.js
alert(user); // no such variable (each module has independent variables)
ماژول ها انتظار دارند چیزی که قرار است در خارج از ماژول جاری فراخوانی شود با کلمه کلیدی export تعریف شود و همچنین چیزی که قرار است در فایل مقصد دریافت شود توسط کلمه import اعلان شود.
بنابراین ما باید ماژول user.js را در فایل hello.js ایمپورت کنیم. کد آپدیت شده ماژول hello.js بصورت زیر خواهد بود:
import {user} from './user.js'; document.body.innerHTML = user; // John
و در فایل مبدا یعنی user.js باید export کنیم:
export let user = "John";
این قضیه فقط در مورد ماژول بصورت فایل های مجزا صادق نیست. بلکه در زمانی که اسکریپت را با تایپ module بصورت زیر تعریف می کنیم نیز همینطور است. یعنی باید برای فراخوانی یک ماژول، ابتدا آن را در ماژول مبدا export کرد و در ماژول مقصد import کرد. پس کد زیر خطا دارد:
<script type="module"> // The variable is only visible in this module script let user = "John"; </script> <script type="module"> alert(user); // Error: user is not defined </script>
اسکریپت ماژول تنها یکبار توسط دستور import اجرا می شود:
دقت داشته باشید که اگر یک ماژول در چندین فایل دیگر import شود و فراخوانی شود، کد ماژول تنها یکبار اجرا می شود و سپس به بقیه ماژول ها export می شود. اگر به این نکته توجه نکنیم خروجی مورد انتظار را از اسکریپت نخواهیم داشت. برای روشن شدن قضیه به مثال های زیر توجه کنید:
مثال اول: نمایش یک پیغام
اگر خروجی یک ماژول نمایش یک پیغام باشد، و آن را در چندین ماژول مختلف import کنیم، پیغام مورد نظر ما فقط یکبار نمایش داده می شود.
کد alert.js
// 📁 alert.js alert("Module is evaluated!");
ایمپورت alert در فایل های 1.js و 2.js
// 📁 1.js import `./alert.js`; // Module is evaluated! // 📁 2.js import `./alert.js`; // (shows nothing)
مثال دوم: ارسال آبجکت بعنوان خروجی
در این مثال ماژولی که می خواهیم export کنیم و در چند فایل دیگر آن را import کنیم دارای خروجی object است.
کد admin.js
// 📁 admin.js export let admin = { name: "John" };
همانطور که گفتیم اسکریپت ماژول تنها بار اول تفسیر و اجرا می شود (آبجکت admin ایجاد می شود) و برای دفعات بعد، آبجکت ایجاد شده به ماژول های بعدی export می شود.
کد سایر ماژول ها:
// 📁 1.js import {admin} from './admin.js'; admin.name = "Pete"; // 📁 2.js import {admin} from './admin.js'; alert(admin.name); // Pete // Both 1.js and 2.js imported the same object // Changes made in 1.js are visible in 2.js
هر دو ماژول 1.js و 2.js یک آبجکت یکسان را import می کنند و تغییراتی که در فایل 1.js روی آبجکت صورت می گیرد در فایل 2.js نیز قابل مشاهده است.
مثال سوم: مقداردهی اولیه (initial value)
با این تفاسیر، می توان از اولین import بعنوان تنظیمات اولیه ماژول یا مقداردهی اولیه استفاده کرد. زیرا هر تغییری که در اولین import انجام شود در سایر import ها نیز دیده می شود.
در این مثال می خواهیم در import اول، یک فیلد بنام name به آبجکت admin اضافه کنیم.
کد admin.js
// 📁 admin.js export let admin = { }; export function sayHi() { alert(`Ready to serve, ${admin.name}!`); }
و در ماژول init.js نام را برای ادمین تعریف می کنیم:
// 📁 init.js import {admin} from './admin.js'; admin.name = "Pete";
در این حالت admin.name برای سایر ماژول ها (شامل خود ماژول admin) قابل شناسایی خواهد بود. مانند ماژول other.js
// 📁 other.js import {admin, sayHi} from './admin.js'; alert(admin.name); // Pete sayHi(); // Ready to serve, Pete!
در ماژول ها، کلمه this تعریف نشده است:
یکی از خاصیت های module در جاوا اسکریپت اینست که کلمه کلیدی this تعریف نشده یا undefined است. در حالیکه در اسکریپت های عادی js کلمه this بصورت گلوبال به window بر می گردد:
<script> alert(this); // window </script> <script type="module"> alert(this); // undefined </script>
Export و Import در ماژول ها:
دستورات export و import ماژول ها در جاوا اسکریپت را به چندین روش مختلف می توان اجرا کرد. تا اینجای مقاله از دستورات فوق به یک روش استفاده کردیم اما در این بخش می خواهیم مدل های مختلف import و export را بررسی کنیم.
درج کلمه کلیدی export قبل از تعریف:
یکی از روش های اجرای دستور export درج آن قبل از تعریف متغیر، کلاس یا تابع است. بعنوان مثال تمام دستورات زیر صحیح هستند و بدرستی اجرا خواهند شد:
// export an array export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // export a constant export const MODULES_BECAME_STANDARD_YEAR = 2015; // export a class export class User { constructor(name) { this.name = name; } }
سمی کالن بعد از دستور export:
دقت کنید که نباید بعد از دستور export class/function از سمی کالن (;) استفاده شود. کلمه export در قبل از توابع، آنها را به function expression تبدیل نخواهد کرد. توابع همچنان function declaration باقی می مانند. البته بصورت export شده.
توصیه می شود پس از تعریف تابع چه بصورت expression و چه declaration، از سمی کالن در انتهای بلاک تابع استفاده نشود. به همین خاطر است که می گوییم نیازی به استفاده از سمی کالن در انتهای توابع یا کلاس های export شده نیست:
export function sayHi(user) { alert(`Hello, ${user}!`); } // no ; at the end
Export کردن جداگانه:
می توانیم یک تابع یا کلاس را بصورت مجزا از تعریف آن export کنیم. یعنی مانند بالا، کلمه export را در هنگام تعریف کلاس یا تابع درج نکنیم. در واقع ابتدا بدنه تابع یا کلاس را می نویسیم و سپس آن را export می کنیم.
// 📁 say.js function sayHi(user) { alert(`Hello, ${user}!`); } function sayBye(user) { alert(`Bye, ${user}!`); } export {sayHi, sayBye}; // a list of exported variables
* import
اغلب نام چیزهایی که می خواهیم ایمپورت کنیم را در مقابل کلمه import بصورت {…} قرار می دهیم. مانند کد زیر:
// 📁 main.js import {sayHi, sayBye} from './say.js'; sayHi('John'); // Hello, John! sayBye('John'); // Bye, John!
اما اگر تعداد المان هایی که می خواهیم import کنیم زیاد باشد می توانیم از علامت * بصورت import * as obj استفاده کنیم:
// 📁 main.js import * as say from './say.js'; say.sayHi('John'); say.sayBye('John');
دستور import * یعنی همه المان های export شده از یک ماژول را ایمپورت کن.
در نگاه اول شاید با خودتان بگویید استفاده از import * عقلانی تر است و بجای اینکه بطور مشخص بگوییم چه چیزی را import کن، می گوییم همه چیز را ایمپورت کن. پس چه نیازی است که عینا تعریف کنیم چه چیزی را می خواهیم ایمپورت کنیم؟
چندین دلیل برای اینکار وجود دارد:
دلیل اول:
بسیاری از ابزارهای مدرن bundle مانند وب پک ماژول ها را بهینه سازی می کنند و باندل می کنند و برای افزایش سرعت نرم افزار، کدهای بلا استفاده را پاک می کنند. برای روشن شدن مطلب به یک مثال توجه کنید. فرض کنید ما می خواهیم یک کتابخانه جانبی بنام say.js به پروژه خود اضافه کنیم که شامل چندین تابع export شده است:
// 📁 say.js export function sayHi() { ... } export function sayBye() { ... } export function becomeSilent() { ... }
و ما در پروژه خود می خواهیم فقط از تابع sayHi در برنامه خود استفاده کنیم. اگر دستور import * را بنویسیم، تمام توابع export شده از say.js را به برنامه ما import می کند و باعث کند شدن سرعت برنامه می شود اما اگر بطور مشخص بگوییم چه چیزی را می خواهیم در برنامه خود import کنیم، مانند کد زیر، فقط کد مربوط به آن تابع یا کلاس را در برنامه لود می کند:
// 📁 main.js import {sayHi} from './say.js';
بنابراین فایل باندل شده خروجی کوچکتر خواهد شد. به این کار “tree-shaking” یا “تکان دادن درخت” گفته می شود.
دلیل دوم:
تعریف دستور import بصورت explicit باعث کوتاه تر شدن نام ها هنگام فراخوانی می شود. مانند sayHi() بجای sayHi()
دلیل سوم:
تعریف صریح دستور import (explicit) باعث خوانایی بیشتر کد و راحت تر شدن توسعه و پشتیبانی آن می شود.
Import as
توسط import * as می توانیم به مقادیر import شده یک متغیر محلی نسبت دهیم. فرض کنید می خواهیم تابع sayHi را که از کتابخانه say.js اکسپورت شده را با نام hi ایمپورت کنیم:
// 📁 main.js import {sayHi as hi, sayBye as bye} from './say.js'; hi('John'); // Hello, John! bye('John'); // Bye, John!
بنابراین در کد فوق، sayHi را در متغیر محلی (local variable) hi و تابع sayBye را در bye ذخیره کرده ایم.
Export as
قابلیت as در export هم وجود دارد. می خواهیم توابع sayHi و sayBye را (با نام های خلاصه) از ماژول say.js اکسپورت کنیم:
// 📁 say.js ... export {sayHi as hi, sayBye as bye};
اکنون برای import کردن باید از نام های مستعار توابع یعنی hi و bye استفاده کنیم:
// 📁 main.js import * as say from './say.js'; say.hi('John'); // Hello, John! say.bye('John'); // Bye, John!
Export default
در عمل ما دو نوع ماژول داریم:
- ماژول هایی که شامل چندین تابع export شده هستند مانند js در مثال این مقاله
- ماژول هایی که فقط یک موجودیت را پیاده سازی و export می کنند مانند js در مثال زیر که فقط class User را اکسپورت می کند.
در اغلب موارد، ماژول های نوع دوم توصیه می شود. زیرا هر ماژول یک کار مشخص و منحصربفرد انجام می دهد.
مزیت این روش اینست که بازبینی کدهای پروژه و توسعه آن ساده تر و سریع تر می شود. البته در صورتی که ساختار فایل ها و فولدرهای پروژه بخوبی رعایت شده باشد و نام مناسبی برای ماژول ها در نظر گرفته شده باشد.
در ماژول ها یک کلمه کلیدی بنام default داریم که اعلام می کند فقط یک موجودیت از این ماژول export می شود:
// 📁 user.js export default class User { // just add "default" constructor(name) { this.name = name; } }
کافیست کلمه کلیدی default را بعد از کلمه export درج کنیم. و برای import کردن این ماژول نیز نام آن را بدون {} می نویسیم:
// 📁 main.js import User from './user.js'; // not {User}, just User new User('John');
دستورات import بدون {} زیباتر بنظر می رسند. یک اشتباه رایج در بین برنامه نویسان جاوا اسکریپت در کار با ماژول ها اینست که نمی دانند هنگام import کردن یک ماژول، چه زمانی باید از }{ استفاده کنند و چه زمانی بدون }{
به یاد داشته باشید که اگر دستور export بصورت نام گذاری شده باشد در دستور import متناظر آن باید از }{ استفاده شود. اما اگر از default در هنگام export استفاده شده است، هنگام import نباید }{ درج شود:
Named export | Default export |
export class User {…} | export default class User {…} |
import {User} from … | import User from … |
بصورت تئوری ممکن است یک ماژول شامل هر دو نوع export باشد (با default و با نام). اما در عمل، توسعه دهندگان معمولا این دو نوع export را با هم ترکیب نمی کنند.
روش دیگر تعریف default
ممکن است در بعضی مواقع از کلمه کلیدی default بصورت زیر استفاده شود:
function sayHi(user) { alert(`Hello, ${user}!`); } // same as if we added "export default" before the function export {sayHi as default};
یا در مثالی دیگر ممکن است در ماژول user.js یک export default اصلی داریم و چند export با نام بصورت زیر:
// 📁 user.js export default class User { constructor(name) { this.name = name; } } export function sayHi(user) { alert(`Hello, ${user}!`); }
نحوه import کردن ماژول های فوق بصورت زیر خواهد بود:
// 📁 main.js import {default as User, sayHi} from './user.js'; new User('John');
بعنوان مثال آخر اگر همه چیز را توسط * بعنوان آبجکت import کنیم، default property دقیقا همان default export خواهد بود:
// 📁 main.js import * as user from './user.js'; let User = user.default; // the default export new User('John');
ایمپورت داینامیک (Dynamic Import):
به روش های export و import که تا اینجای مقاله آموزش دادیم “static” گفته می شود. سینتکس آن بسیار ساده و سرراست است. ایمپورت و اکسپورت استاتیک ویژگی هایی دارد:
اول: ما نمی توانیم هیچیک از پارامترهای import را بصورت داینامیک تولید کنیم. مسیر ماژول باید یک رشته (string) باشد و نمی تواند یک تابع باشد. کد زیر خطا دارد:
import ... from getModuleName(); // Error, only from "string" is allowed
دوم: امکان import شرطی وجود ندارد. یعنی نمی توان دستور import را در بلاک شرطی if و else تعریف کرد:
if(...) { import ...; // Error, not allowed! } { import ...; // Error, we can't put import in any block }
البته مواردی که بیان کردیم ایراد نیست. زیرا ذات import و export سادگی و ثبات است. به همین دلیل است که ابزارهای module bundler مانند وب پک می توانند ماژول ها را بهینه سازی و ادغام کنند و export های بلا استفاده را از خروجی حذف کنند (tree shaken).
عبارت import():
دستور import(module) یک ماژول را لود می کند و یک promise برمی گرداند که درون آن آبجکتی است که شامل تمام export ها می باشد. می توانیم آن را بصورت داینامیک در هر جای کد استفاده کنیم. بعنوان مثال:
let modulePath = prompt("Which module to load?"); import(modulePath) .then(obj => <module object>) .catch(err => <loading error, e.g. if no such module>)
یا می توانیم در داخل توابع async از دستور زیر استفاده کنیم:
let module = await import(modulePath)
بعنوان مثال اگر ماژول زیر را با نام say.js داشته باشیم:
// 📁 say.js export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); }
سپس dynamic import بصورت زیر خواهد بود:
let {hi, bye} = await import('./say.js'); hi(); bye();
یا اگر say.js یک default export داشته باشد، بصورت زیر:
// 📁 say.js export default function() { alert("Module loaded (export default)!"); }
سپس به منظور دسترسی به آن، می توان از default در آبجکت ماژول استفاده کرد. بصورت زیر:
let obj = await import('./say.js'); let say = obj.default; // or, in one line: let {default: say} = await import('./say.js'); say();
سورس کد کامل این مثال در بخش زیر آمده است:
کد صفحه index.html
<!doctype html> <script> async function load() { let say = await import('./say.js'); say.hi(); // Hello! say.bye(); // Bye! say.default(); // Module loaded (export default)! } </script> <button onclick="load()">Click me</button>
کد فایل say.js
export function hi() { alert(`Hello`); } export function bye() { alert(`Bye`); } export default function() { alert("Module loaded (export default)!"); }
نکته: ایمپورت داینامیک در اسکریپت های عادی کار می کند و نیازی به تگ اسکریپت از نوع ماژول (کد زیر) ندارند:
<script type="module">
نکته: با اینکه import() شبیه فراخوانی تابع است، اما در واقع تابع نیست و نمی توان import را داخل یک متغیر ریخت یا از متدهای call/apply برای آن استفاده کرد.
جمع بندی:
در این مقاله، سعی شد تمام مباحث کاربردی ماژول ها در زبان برنامه نویسی جاوااسکریپت بررسی شود. سرفصلهای اصلی مقاله عبارت بودند از: ویژگی های ماژول ها و تفاوت آنها با اسکریپت های عادی، انواع روش های import و export کردن ماژول در اکما اسکریپت، ایمپورت پویا یا Dynamic Import.
خواص ماژول ها در جاوا اسکریپت:
- هر ماژول یک فایل است که برای اینکه از دستورات import و export استفاده کنید، باید تگ اسکریپت را با نوع ماژول تعریف کنید. یعنی
<script type="module">
- ماژول ها اسکوپ مختص خود را دارند و متغیری که در ماژول A تعریف شده است در ماژول B غیرقابل دسترسی می باشد. فقط توسط import و export قابلیت استفاده از توابع یا کلاس های یکدیگر را دارند.
- ماژول ها بصورت پیش فرض use strict هستند.
- کد ماژول فقط یکبار اجرا می شود. ماژول های اکسپورت شده فقط در بار اول اجرا می شوند و در دفعات بعدی بین importer ها به اشتراک گذاشته می شوند.
روش های export کردن ماژول:
- قبل از تعریف کلاس یا تابع بصورت زیر:
export [default] class/function/variable ...
- Export مجزا:
export {x [as y], ...}
روش های import کردن ماژول:
- ایمپورت کردن export های نامدار:
import {x [as y], ...} from "module"
- Default export:
import x from "module" import {default as x} from "module"
- ایمپورت کردن همه چیز:
import * as obj from "module"
نکات import/export کردن ماژول:
- امکان تعریف دستورات import و export در ابتدا و انتهای اسکریپت وجود دارد و در اجرا تفاوتی ندارد. اما در عمل بهتر است دستورات import-export در ابتدای فایل نوشته شوند.
sayHi(); // ... import {sayHi} from './say.js'; // import at the end of the file
- دقت کنید که دستورات import و export در داخل بلاک شرطی if-else اجرا نمی شوند:
if (something) { import {sayHi} from "./say.js"; // Error: import must be at top level }
در بخش 28 از سری آموزشی جاوا اسکریپت، با مبحث “بدست آوردن سایز عناصر DOM و نحوه اسکرول صفحه” توسط javascript در خدمتتان هستیم.
دیدگاهتان را بنویسید