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 تعيد الكائن المخزّن نفسه
ينفّذ Node.js شيفرة الوحدة مرة واحدة، أول مرة تُستدعى فيها، ويخزّن module.exports. كل require لاحق للملف نفسه يعيد ذلك الكائن ذاته، فتكون الحالة داخل الوحدة مشتركة بين كل الملفات التي تحمّلها:
ظهر counter.js is running مرة واحدة، وأعاد b.next() القيمة 3، وa === b قيمته true، لأن مفتاح الذاكرة المؤقتة هو المسار المحلول للملف، و./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 أن تستورد ملف CommonJS عبر import: تصل module.exports الخاصة به على أنها الاستيراد الافتراضي. ومنذ 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 = ...، أو لأن وحدتين تستدعي كل منهما الأخرى. في الاستدعاء الدائري، تستقبل إحداهما module.exports الخاص بالأخرى قبل أن ينتهي ذلك الملف من التنفيذ.
هل أحتاج إلى الامتداد .js في require؟
لا. require('./math') يجرّب ./math، ثم ./math.js و./math.json و./math.node، ثم مجلدًا ./math فيه index.js. أما في import داخل ES module فالامتداد إلزامي.
كيف أعيد تحميل وحدة خزّنتها require مسبقًا؟
احذف مدخلها بـ delete require.cache[require.resolve('./math')] ثم استدعها من جديد. هذا مفيد أساسًا في الاختبارات وأدوات التطوير؛ ولا ينبغي أن تعتمد عليه شيفرة التطبيق.