לדלג לתוכן

4.4 מודולים הרצאה

מודולים - modules

בתחילת הדרך, ב-JS לא היה מנגנון מודולים מובנה. כל הקוד רץ ב-scope גלובלי אחד, וספריות שונות היו מתנגשות זו בזו. היום יש לנו ES Modules - מערכת מודולים סטנדרטית שמובנית בשפה.


למה צריך מודולים?

בלי מודולים:
- כל המשתנים בסקופ גלובלי - התנגשויות שמות
- קשה להבין תלויות בין קבצים
- אי אפשר לטעון רק את מה שצריך
- קשה לבדוק ולתחזק קוד גדול

עם מודולים:
- כל קובץ הוא מודול עם scope משלו
- ייצוא וייבוא מפורשים - ברור מה תלוי במה
- אפשר לייבא רק מה שצריך (tree shaking)
- אנקפסולציה - מה שלא מיוצא נשאר פרטי


ES Modules - התחביר הסטנדרטי

ייצוא בשם - named exports

קובץ יכול לייצא מספר דברים, כל אחד בשם:

// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export const PI = 3.14159;

אפשר גם לייצא בסוף הקובץ:

// utils.js
function formatDate(date) {
    return date.toISOString().split("T")[0];
}

function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

const VERSION = "1.0.0";

export { formatDate, capitalize, VERSION };

ייבוא בשם - named imports

// main.js
import { add, subtract, PI } from "./math.js";

console.log(add(2, 3)); // 5
console.log(PI);         // 3.14159

אפשר לייבא עם שם חדש באמצעות as:

import { add as sum, subtract as minus } from "./math.js";

console.log(sum(2, 3));   // 5
console.log(minus(5, 2)); // 3

אפשר לייבא הכל כאובייקט:

import * as math from "./math.js";

console.log(math.add(2, 3)); // 5
console.log(math.PI);        // 3.14159

ייצוא ברירת מחדל - default export

כל מודול יכול לייצא דבר אחד כ-default:

// User.js
export default class User {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
}

ייבוא default - בלי סוגריים מסולסלים, ואפשר לתת כל שם:

import User from "./User.js";
// or: import MyUser from "./User.js"; - any name works

const user = new User("Alice");

אפשר לשלב default עם named:

// api.js
export default function fetchData(url) {
    return fetch(url).then(r => r.json());
}

export function buildUrl(base, params) {
    const query = new URLSearchParams(params);
    return `${base}?${query}`;
}

export const BASE_URL = "https://api.example.com";
import fetchData, { buildUrl, BASE_URL } from "./api.js";

מתי להשתמש ב-default?

  • כשלמודול יש "דבר עיקרי" אחד (class, פונקציה ראשית)
  • קומפוננטות React בדרך כלל מיוצאות כ-default

כשיש כמה דברים שווי חשיבות - השתמשו ב-named exports.


ייצוא מחדש - re-exports

אפשר לייצא דברים ממודול אחר בלי לייבא אותם תחילה:

// shapes/circle.js
export class Circle { /* ... */ }

// shapes/rectangle.js
export class Rectangle { /* ... */ }

// shapes/index.js - barrel file
export { Circle } from "./circle.js";
export { Rectangle } from "./rectangle.js";

עכשיו אפשר לייבא הכל ממקום אחד:

// instead of:
import { Circle } from "./shapes/circle.js";
import { Rectangle } from "./shapes/rectangle.js";

// you can do:
import { Circle, Rectangle } from "./shapes/index.js";

אפשר גם לייצא מחדש default:

export { default as Circle } from "./circle.js";
export { default as Rectangle } from "./rectangle.js";

קבצי חבית - barrel files

הדוגמה שראינו למעלה נקראת barrel file - קובץ index.js שמרכז את כל הייצואים של תיקיה:

src/
  utils/
    index.js          <-- barrel file
    string.js
    array.js
    date.js
  components/
    index.js          <-- barrel file
    Button.js
    Modal.js
    Input.js
// utils/string.js
export function capitalize(str) { /* ... */ }
export function truncate(str, len) { /* ... */ }

// utils/array.js
export function chunk(arr, size) { /* ... */ }
export function unique(arr) { /* ... */ }

// utils/date.js
export function formatDate(date) { /* ... */ }

// utils/index.js (barrel)
export { capitalize, truncate } from "./string.js";
export { chunk, unique } from "./array.js";
export { formatDate } from "./date.js";

עכשיו הייבוא נקי:

import { capitalize, chunk, formatDate } from "./utils/index.js";
// or simply:
import { capitalize, chunk, formatDate } from "./utils";

יתרונות:
- ייבוא נקי ומרוכז
- אפשר לשנות מבנה תיקיות בלי לשנות ייבואים
- ברור מה ה-API הציבורי של כל תיקיה

חסרונות:
- עלול לפגוע ב-tree shaking אם לא מוגדר נכון
- ייבוא של דבר אחד טוען את כל הקובץ


ייבוא דינמי - dynamic import

import() כפונקציה מחזיר Promise ומאפשר לטעון מודול בזמן ריצה:

// load a module only when needed
async function loadChart() {
    const { Chart } = await import("./chart.js");
    const chart = new Chart("#container");
    chart.render();
}

// conditional loading
if (user.isAdmin) {
    const { AdminPanel } = await import("./admin.js");
    new AdminPanel().show();
}

// loading based on variable
const lang = navigator.language;
const messages = await import(`./i18n/${lang}.js`);

מתי להשתמש:
- קוד שלא תמיד נדרש (admin panel, עורך טקסט)
- טעינה לפי תנאי (שפה, הרשאות)
- שיפור ביצועים - טעינת קוד רק כשצריך (lazy loading)


CommonJS - המערכת הישנה

CommonJS הוא מנגנון המודולים של Node.js מהתחלה. עדיין נפוץ בספריות ישנות:

// math.js (CommonJS)
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = { add, subtract };
// or: exports.add = add;
// main.js (CommonJS)
const { add, subtract } = require("./math");

console.log(add(2, 3)); // 5

ההבדלים העיקריים בין ESM ל-CommonJS:

נושא ES Modules CommonJS
תחביר import / export require / module.exports
טעינה סטטית (compile time) דינמית (runtime)
סינכרוני/אסינכרוני אסינכרוני סינכרוני
tree shaking נתמך לא נתמך
שימוש דפדפן + Node.js Node.js בלבד

היום ההמלצה היא להשתמש ב-ES Modules. Node.js תומך בהם מגרסה 12, ורוב הספריות החדשות מספקות ESM.

כדי להשתמש ב-ESM ב-Node.js:
- שנו את הסיומת ל-.mjs
- או הוסיפו "type": "module" ל-package.json


מודולים ב-HTML

בדפדפן, משתמשים ב-type="module" בתג <script>:

<script type="module">
    import { greet } from "./utils.js";
    greet("World");
</script>

<!-- or from external file -->
<script type="module" src="./main.js"></script>

הבדלים בין script רגיל ל-module:
- מודולים מריצים ב-strict mode אוטומטית
- למודולים יש scope משלהם (לא גלובלי)
- מודולים נטענים כ-deferred כברירת מחדל (אחרי שה-HTML מוכן)
- מודולים נטענים פעם אחת בלבד גם אם מיובאים כמה פעמים


השוואה לפייתון

מערכת המודולים של פייתון דומה למדי:

# Python
# from math_utils import add, subtract
# import math_utils
# from math_utils import add as sum_func
// JavaScript ESM
import { add, subtract } from "./math-utils.js";
import * as mathUtils from "./math-utils.js";
import { add as sumFunc } from "./math-utils.js";
נושא פייתון ג׳אווהסקריפט ESM
ייבוא בשם from module import func import { func } from "./module.js"
ייבוא הכל import module import * as module from "./module.js"
שינוי שם import func as alias import { func as alias } from "..."
ברירת מחדל אין מקבילה ישירה export default / import X from "..."
ייבוא דינמי importlib.import_module() await import("...")
barrel file __init__.py index.js

ההבדל העיקרי: בפייתון אין default export - כל ייבוא הוא בשם. ב-JS, default export נפוץ מאוד.


סיכום

  • כל קובץ JS הוא מודול עם scope משלו
  • export ו-import - התחביר הסטנדרטי (ES Modules)
  • named exports - לכמה ייצואים ממודול אחד
  • default export - לדבר העיקרי של המודול
  • barrel files (index.js) - מרכזים ייצואים מתיקיה
  • import() דינמי - טעינה בזמן ריצה לפי צורך
  • CommonJS (require/module.exports) - המערכת הישנה של Node.js
  • בדפדפן: <script type="module">
  • העדיפו ES Modules בכל פרויקט חדש