module.exports הוא הערך שקובץ Node.js נותן לכל קובץ אחר שטוען אותו עם require(). הוא מתחיל כאובייקט ריק: מוסיפים לו מאפיינים כדי לייצא כמה דברים, או מציבים לו ערך חדש (פונקציה, מחלקה) כדי לייצא דבר אחד. exports הוא משתנה מקוצר שמצביע על אותו אובייקט, לכן exports.add = ... עובד, אבל exports = ... שובר את הקישור ולא מייצא כלום.
הוא הדפיס 5 20 3.14159, ואז [ 'add', 'multiply', 'PI' ]. כל שלוש השורות ב-math.js הוסיפו מאפיינים לאותו אובייקט, בין אם דרך module.exports ובין אם דרך exports, ו-require('./math') החזיר את האובייקט הזה.
ייצוא של פונקציה, מחלקה או אובייקט
כשלמודול יש דבר מרכזי אחד, הציבו אותו ישירות ב-module.exports. מי שקורא למודול מקבל את הפונקציה עצמה, ולא אובייקט שעוטף אותה:
הוא הדפיס Hello, Ada ו-Grace (user). אם אתם רואים TypeError: greet is not a function, הקוד הקורא השתמש בצורה הלא נכונה: כתבו const { greet } = require(...) עבור מאפיין של אובייקט, ו-const greet = require(...) עבור ערך שהוצב.
module.exports מול exports: מלכודת ההצבה
לפני שהקובץ שלכם רץ, Node.js מגדיר exports = module.exports. שני השמות מצביעים על אותו אובייקט, ו-require מחזיר את module.exports. הצבת ערך חדש ל-exports משנה רק את המשתנה המקומי:
broken.js ייצא {}, כי exports מצביע עכשיו על אובייקט חדש ששום דבר לא מחזיר. working.js ייצא { version: 2 }.
| מה כתבתם | מיוצא? |
|---|---|
module.exports.x = 1 | כן |
exports.x = 1 | כן, אותו אובייקט |
module.exports = { x: 1 } | כן, מחליף את האובייקט |
exports = { x: 1 } | לא, הערך הולך לאיבוד |
הכלל הפשוט ביותר: תמיד כתבו module.exports. זה עובד בכל מקרה, והקוד אומר מה הוא עושה.
require מחזיר את אותו אובייקט מה-cache
Node.js מריץ את הקוד של מודול פעם אחת, בפעם הראשונה שעושים לו require, ושומר את module.exports ב-cache. כל require מאוחר יותר של אותו קובץ מחזיר את אותו אובייקט, כך שמצב שנמצא בתוך מודול משותף לכל קובץ שטוען אותו:
counter.js is running הופיע פעם אחת, b.next() החזיר 3, ו-a === b הוא true, כי המפתח ב-cache הוא הנתיב המלא של הקובץ, ו-./counter ו-./counter.js מצביעים על אותו קובץ. בגלל זה חיבור למסד נתונים או אובייקט הגדרות שנוצרו במודול אחד משותפים לכל האפליקציה.
module.exports ו-ES modules
ב-ES modules משתמשים ב-export וב-import. הצורות מקבילות זו לזו כך:
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
ES module יכול לעשות import לקובץ CommonJS: ה-module.exports שלו מגיע כ-default import. מאז Node.js 22.12, גם require() יכול לטעון ES module שאין בו await ברמה העליונה. מתי לבחור במה, ראו CommonJS מול ES modules.
טעויות נפוצות
ערבוב של exports.x עם module.exports = ... באותו קובץ. ההצבה מחליפה את האובייקט, וכל exports.x שנכתב לפניה נעלם. בחרו סגנון אחד לכל קובץ.
ייצוא של ערך שמשתנה בהמשך. module.exports = { count } מעתיק את המספר ברגע הטעינה; כשהמודול מעדכן את count, הקוראים עדיין רואים את הערך הישן. ייצאו פונקציה שמחזירה אותו, כמו ש-counter.js עושה למעלה.
השמטה של ./. require('math') מחפש חבילה ב-node_modules, לא את math.js שנמצא ליד הקובץ שלכם. קבצים מקומיים תמיד מתחילים ב-./ או ב-../.
שאלות נפוצות
אפשר להשתמש ב-module.exports וב-export באותו קובץ?
לא. קובץ הוא או CommonJS (require, module.exports) או ES module (import, export). Node.js מחליט לפי הסיומת (.cjs או .mjs) או לפי "type" ב-package.json.
למה require מחזיר אובייקט ריק?
בדרך כלל המודול כתב exports = ... במקום module.exports = ..., או ששני מודולים טוענים זה את זה. ב-require מעגלי, אחד מהם מקבל את ה-module.exports של השני לפני שהקובץ ההוא סיים לרוץ.
צריך את הסיומת .js ב-require?
לא. require('./math') מנסה את ./math, אחר כך את ./math.js, ./math.json ו-./math.node, ואז תיקייה ./math עם index.js. ב-import של ES module הסיומת חובה.
איך טוענים מחדש מודול ש-require כבר שמר ב-cache?
מוחקים את הרשומה שלו עם delete require.cache[require.resolve('./math')] ועושים לו require שוב. זה שימושי בעיקר בבדיקות ובכלי פיתוח; קוד של האפליקציה לא צריך להסתמך על זה.