📦 Node.js — מודולים (Modules) הסבר מקיף + דוגמאות

📘 מה זה מודול?

מודול הוא קובץ JavaScript עם מרחב שמות מבודד. כברירת מחדל, משתנים/פונקציות שנכתבו בקובץ אינם נגישים מחוצה לו—אלא אם כן מייצאים אותם. זה מאפשר בנייה של אפליקציה מחלקים קטנים, קריאים וניתנים לשימוש חוזר.

  • בידוד (Encapsulation) מונע התנגשויות שמות.
  • שימוש חוזר (Reusability) ותחזוקה קלה יותר.
  • חלוקה לוגית של אחריות (Separation of Concerns).

⚡️ דוגמאות מהירות (CommonJS) – require / module.exports

הנה סט הדוגמאות שביקשת ממש בתחילת העמוד, מיד אחרי ההסבר על מטרת מודולים ופיצול הקוד. אלה דוגמאות בסיסיות ל-CommonJS: איך מייצאים ומייבאים משתנים/פונקציות, איך עובדים עם כמה קבצים, ואיך קובץ יכול לבצע פעולה צדדית בעת הטעינה.

// index.js
// CommonJS, every file is module (by default)
// Modules - Encapsulated Code (only share minimum)
const names = require('./04-names');
const sayHi = require('./05-utils');
const data = require('./06-alternative-flavor');
require('./07-mind-grenade'); // מפעיל קובץ שמבצע פעולה בזמן הטעינה

sayHi('chen');
sayHi(names.eylon);
sayHi(names.maayan);

// אפשר לגשת גם ל-data שהגיע מ-06-alternative-flavor
// console.log(data.items, data.singlePerson);
// 04-names.js
// local
const secret = 'SUPER SECRET';
// share
const eylon = 'eylon';
const maayan = 'maayan';

module.exports = { eylon, maayan };
// 05-utils.js
const sayHi = (name) => {
  console.log(`Hello there ${name}`);
};
// export default (ב-CommonJS עושים דרך module.exports)
module.exports = sayHi;
// 06-alternative-flavor.js
module.exports.items = ['item1', 'item2'];
const person = { name: 'bob' };
module.exports.singlePerson = person;
// 07-mind-grenade.js
const num1 = 2;
const num2 = 3;
console.log(`The sum is: ${num1 + num2}`);
// עצם ה-require בקובץ הראשי גורם לשורה הזו לרוץ (side effect)

🧰 מודולים מובנים (Built-in)

Node.js כולל ספריית ליבה: fs, path, http, os ועוד. אין צורך בהתקנה—פשוט מייבאים.

// ES Modules
import fs from "fs";
import path from "path";

// CommonJS
const http = require("http");
const os = require("os");

🔄 CommonJS – הסבר על require ו־module.exports

CommonJS הוא פורמט המודולים הוותיק והבררת מחדל ב-Node.js. כל קובץ הוא מודול.require(path) טוען את המודול ומחזיר את מה שיוצא מהקובץ דרך module.exports (או exports כקיצור).

  • ייבוא: const X = require("./file")
  • ייצוא: module.exports = value או module.exports.obj = ...
  • טעינה חד־פעמית: קובץ נטען ומבוצע פעם אחת ונשמר ב־Cache.
  • סנכרוני: require הוא קריאה סנכרונית.

דוגמה בסיסית (מורחב):

// 04-names.js
const eylon = "Eylon";
const maayan = "Maayan";
module.exports = { eylon, maayan };
// 05-utils.js
const sayHi = (name) => {
  console.log(`Hello there, ${name}`);
};
module.exports = sayHi;
// 06-alternative-flavor.js
module.exports.items = ["item1", "item2"];
module.exports.singlePerson = { name: "Bob" };
// 07-mind-grenade.js
const num1 = 2;
const num2 = 3;
// קובץ זה "מבצע פעולה צדדית" מיד בעת הטעינה
console.log(`The sum is: ${num1 + num2}`);
// index.js
const names = require("./04-names");
const sayHi = require("./05-utils");
const data = require("./06-alternative-flavor");
require("./07-mind-grenade"); // טעינה לצורך הפעלה בלבד

sayHi("Susan");
sayHi(names.eylon);
sayHi(names.maayan);
// data = { items: [...], singlePerson: {...} }

⚠️ הערות חשובות על require:

  • Cache: אם תקרא require("./07-mind-grenade") שוב—הקובץ לא ירוץ פעם שנייה, כי תוצאת הטעינה נשמרת ב־require.cache.
  • מעגליות (Circular Dependencies): אם שני מודולים דורשים זה את זה, בזמן הטעינה תיתכן חשיפה של ייצוא חלקי. הימנעו ממעגליות או פרקו לוגיקה.
  • סיומות: Node מוסיף אוטומטית .js/.json/.node אם לא ציינת. טעינת JSON עם require אפשרית (מחזיר אובייקט).

✨ ES Modules (ESM) – import / export

מודרני ומבוסס תקן. כדי להשתמש: הוסף "type": "module" ל־package.json או השתמש בסיומת .mjs. ב-ESM קיימים import / export, top-level await, ו־import() דינמי.

// package.json
{
  "name": "esm-demo",
  "type": "module",
  "version": "1.0.0"
}
// math.mjs (או math.js אם "type":"module")
export const multiply = (a, b) => a * b;
export default function square(x) { return x * x; }
// index.mjs
import square, { multiply } from "./math.mjs";
console.log(multiply(4, 5)); // 20
console.log(square(9));      // 81

📥 Dynamic Import + Top-Level Await

// index.mjs
const { default: dayjs } = await import("dayjs"); // top-level await מותר ב-ESM
console.log(dayjs().format());

📂 __dirname / __filename ב-ESM

ב-ESM אין __dirname/__filename מובנים. משתמשים ב-import.meta.url ו־fileURLToPath.

// path-helpers.mjs
import { fileURLToPath } from "url";
import path from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export { __filename, __dirname };

📤 ייצוא ברירת מחדל (Default Export)

ייצוא יחיד כברירת מחדל נוח כשמודול מספק פונקציה/מחלקה מרכזית אחת.

// logger.js (ESM)
export default function log(message) {
  console.log("[LOG]", message);
}
// app.js (ESM)
import log from "./logger.js";
log("Hello");

📦 package.json – שדות main/exports

בעת פרסום ספרייה, קובעים נקודות כניסה שונות ל־CommonJS ול־ESM, וגם טיפוסים.

{
  "name": "my-lib",
  "version": "1.0.0",
  "main": "dist/index.cjs",   // כניסה ל-CJS
  "module": "dist/index.mjs", // כניסה היסטורית ל-ESM (בעיקר bundlers)
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.cjs",
      "import": "./dist/index.mjs"
    }
  }
}

הערה: בשנים האחרונות ממליצים להסתמך על exports כמקור האמת, כאשר main/module נשארים לתאימות לאחור.

🌍 מודולים מצד שלישי (npm)

מתקינים עם pnpm add/npm install ומייבאים.

pnpm add lodash
// ESM
import _ from "lodash";
console.log(_.shuffle([1, 2, 3, 4]));
// CJS
const _ = require("lodash");
console.log(_.random(1, 10));

🔁 תאימות בין CJS ↔ ESM (Interop)

  • מ-CJS ל-ESM: אפשר require("esm-package")—Node יבצע גשר, אך לפעמים יידרשו התאמות (למשל שימוש ב־default).
  • מ-ESM ל-CJS: ב-ESM אין require; משתמשים ב-import() דינמי או בייבוא סטטי. ב-CJS לעיתים ניגשים ל-module.exports.default.
// CJS -> ייבוא מודול ESM (ייתכן צורך ב-default)
const esmPkg = require("some-esm-pkg");
const real = esmPkg.default ?? esmPkg;
// ESM -> טעינת CJS
import cjsHelper from "./helper.cjs";
cjsHelper.run();

🗃️ Cache של מודולים

Node טוען כל מודול פעם אחת ושומר ב־Cache. טעינה חוזרת מחזירה את אותו מופע.

// counter.js (CJS)
let count = 0;
module.exports = {
  inc() { count++; },
  get() { return count; }
};
// a.js
const counter = require("./counter");
counter.inc();
console.log("A sees:", counter.get()); // 1
// b.js
const counter = require("./counter");
console.log("B sees:", counter.get()); // עדיין 1 (אותו מופע) 
counter.inc();
// index.js
require("./a");
require("./b");
// A sees: 1
// B sees: 1

🧭 Module Resolution + טעינת JSON

  • נתיב יחסי: ./ או ../; נתיב חבילה: שם החבילה.
  • ב-CJS: ניתן require("./data.json") ומקבלים אובייקט.
  • ב-ESM: טעינת JSON דורשת דגל/Loader מתאים או שימוש ב-fetch/קריאה לקובץ.
// CJS
const pkg = require("./package.json");
console.log(pkg.name);

📊 השוואה קצרה: CommonJS מול ES Modules

היבטCommonJS (CJS)ES Modules (ESM)
ייבוא/ייצואrequire / module.exportsimport / export
סנכרוניותסנכרוניסטטי + import() דינמי, top-level await
ברירת מחדל ב-Nodeכן (ללא הגדרות מיוחדות)נדרש "type":"module" או .mjs
טעינת JSONמובנהתלוי Loader/גרסה
שונותCache לפי require.cacheimport.meta, אין __dirname מובנה

📝 סיכום

  • כל קובץ הוא מודול; ייצוא מפורש נדרש לחשיפה.
  • CommonJS: פשוט ומהיר לשילוב; ES Modules: התקן המודרני עם יכולות מתקדמות.
  • שימוש ב־exports ב־package.json מומלץ לחשיפת נקודות כניסה.
  • הקפידו על הימנעות ממעגליות, והבינו את התנהגות ה־Cache.