Node.js: نظام الوحدات
آخر تحديث: 2026-08-26
تضخم مشروع تشارلي من 3 ملفات إلى 30 ملفًا. كانت جميع الوظائف والتكوينات وفئات الأدوات مكدسة في ملف ضخم واحد app.js، وكان تغيير وظيفة واحدة يتطلب نصف ساعة من البحث في 2,000 سطر من الكود. قرر تقسيم الكود إلى وحدات منفصلة، ليكتشف أن require وimport تبدوان مختلفتين، وأن module.exports وexports كانتا تتشابكان دائمًا، وأن التبعيات الدائرية كانت تتسبب في قيام البرنامج بإخراج مجموعة من indefinido. في هذا الدرس، سنرافق تشارلي وهو يكشف النقاب عن نظام الوحدات النمطية في Node.js، ويحول كوده من فوضى متشابكة إلى مجموعة منظمة جيدًا من اللبنات الأساسية.
ستتعلم:
- تنظيم الكود باستخدام CommonJS
require/module.exports/exports - تكوينات
importوexportو"type":"module"باستخدام وحدات ES - فهم آلية البحث عن المكونات الإضافية في
require(المدمجة → node_modules → المسار) - فهم آلية التخزين المؤقت للوحدات النمطية ودور
require.cache - تحديد مشكلات التبعية الدائرية وفهم كيفية تعامل Node.js معها
1. وحدات CommonJS
(1) التصدير باستخدام module.exports
يستخدم Node.js مواصفات الوحدات النمطية CommonJS بشكل افتراضي. كل ملف يمثل وحدة نمطية تقوم بتصدير القيم باستخدام module.exports، بينما تقوم الملفات الأخرى بتحميلها باستخدام require().
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
(2) تحميل الوحدات النمطية باستخدام require
require() تقبل معرّف الوحدة النمطية وتُرجع القيمة module.exports لتلك الوحدة النمطية.
// app.js
const math = require('./math');
console.log(math.add(10, 3)); // 13
console.log(math.subtract(10, 3)); // 7
(3) الاختصار exports
exports هو مرجع إلى module.exports وهو مناسب لإضافة الخصائص واحدة تلو الأخرى.
// logger.js
exports.info = function (msg) {
console.log(`[INFO] ${msg}`);
};
exports.error = function (msg) {
console.log(`[ERROR] ${msg}`);
};
▶ مثال: تصدير دالة واحدة مقابل تصدير كائن
// greet.js — Export a Single Function
module.exports = function (name) {
return `Hello, ${name}!`;
};
// config.js — Export Objects
module.exports = {
port: 3000,
host: 'localhost',
debug: true,
};
// app.js
const greet = require('./greet');
const config = require('./config');
console.log(greet('Charlie')); // Hello, Charlie!
console.log(`Server: ${config.host}:${config.port}`); // Server: localhost:3000
(4) الفروق بين module.exports و exports
| الميزة | module.exports | exports |
|---|---|---|
| الجوهر | الكائن الفعلي الذي تم تصديره من الوحدة النمطية | إشارة إلى module.exports |
| تصدير المهمة | ✅ module.exports = fn |
❌ exports = fn إزالة المرجع |
| أضف واحدًا تلو الآخر | ✅ module.exports.foo = fn |
✅ exports.foo = fn |
| تصدير قيمة واحدة | ✅ موصى به | ❌ غير متاح |
| الأمان | صالحة دائمًا | تنتهي صلاحيتها بعد إعادة التخصيص |
المبدأ الأساسي: إذا كنت بحاجة إلى تصدير دالة واحدة أو فئة أو كائن جديد تمامًا، فيجب عليك استخدام
module.exports؛ أماexportsفيمكن استخدامه فقط لإضافة خصائص.
2. وحدات ES
(1) قواعد النحو الأساسية
تُعد وحدات ES (ESM) المعيار الرسمي لوحدات جافا سكريبت، وتستخدم صيغة export وimport.
// utils.mjs
export function square(n) {
return n * n;
}
export const VERSION = '2.0.0';
export default function greet(name) {
return `Hello, ${name}!`;
}
// app.mjs
import greet, { square, VERSION } from './utils.mjs';
console.log(greet('Charlie')); // Hello, Charlie!
console.log(square(5)); // 25
console.log(VERSION); // 2.0.0
(2) ثلاث طرق لتفعيل ESM
| الطريقة | الوصف |
|---|---|
امتداد الملف .mjs |
تقوم Node.js بمعالجته تلقائيًا على أنه ESM |
"type": "module" في ملف package.json |
الملفات التي تحمل اسم .js داخل المشروع تُعامل افتراضيًّا على أنها ESM |
--input-type=module |
معلمة سطر الأوامر المستخدمة لإدخال stdin |
// package.json
{
"type": "module"
}
▶ مثال:(3) الصادرات المحددة والصادرات الافتراضية
// shapes.mjs
export const PI = 3.14159;
export function circleArea(radius) {
return PI * radius * radius;
}
export default class Shape {
constructor(name) {
this.name = name;
}
describe() {
return `This is a ${this.name}`;
}
}
▶ مثال: التصدير وإعادة التصدير الموحدان
// api.mjs — Batch Export
export { addUser, removeUser } from './users.mjs';
export { logError } from './logger.mjs';
// You can also rename it
export { add as addUser } from './math.mjs';
3. مقارنة بين CommonJS و ESM
(1) الاختلافات الرئيسية
| البعد | CommonJS | وحدات ES |
|---|---|---|
| الصيغة | require() / module.exports |
import / export |
| طريقة التحميل | متزامن، التحميل أثناء وقت التشغيل | غير متزامن، التحليل الثابت في وقت التحويل البرمجي |
| نوع القيمة | نسخ القيمة (الأنواع الأولية) | ربط القيمة (مرجع نشط) |
| وضع هذا في المستوى الأعلى | module.exports |
indefinido |
| التبعيات الدائرية | إرجاع الصادرات غير المحلولة | ربط مرجعي، لكنه قد يكون ضمن منطقة TDZ |
| حالة الاستخدام | مشروع Node.js (افتراضي) | مشروع جديد، مشاركة الكود في المتصفح |
| امتداد الملف | .js / .cjs |
.mjs / .js (النوع: وحدة) |
▶ مثال:(2) نسخ القيمة مقابل الربط
// counter.cjs — CommonJS
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// counter.mjs — ESM
export let count = 0;
export function increment() {
count++;
}
// CJS: count is a copy, it won't change
const c = require('./counter.cjs');
c.increment();
console.log(c.count); // 0 (still the initial value)
// ESM: count is bound, real-time updates
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 (updated)
▶ مثال: استيراد وحدة CJS في ESM
// legacy.cjs
module.exports = { legacyMethod() { return 'old school'; } };
// app.mjs
import cjs from './legacy.cjs';
console.log(cjs.legacyMethod()); // old school
عند
importوحدة CJS في ESM، تُستخدم قيمةmodule.exportsكصادرات افتراضية.
4. آلية البحث عن الوحدة النمطية require
(1) عملية البحث
عندما تكتب require('express')، يقوم Node.js بالبحث وفقًا للترتيب التالي:
flowchart TD
A["require('express')"] --> B{Does it have a built-in module??}
B -- Yes --> C[Return built-in module]
B -- No --> D{Path starting with ./ or / ?}
D -- Yes --> E[Find file by path]
E --> E1[Try .js / .json / .node]
E1 --> E2[Try index.js]
D -- No --> F[Search node_modules]
F --> F1[Current Directory/node_modules/express]
F1 --> F2[Parent directory/node_modules/express]
F2 --> F3[Move up one level at a time until root directory]
F3 --> F4{Found?}
F4 -- No --> G[Throw MODULE_NOT_FOUND]
F4 -- Yes --> H[Load and cache module]
E2 --> H
C --> H
(2) قواعد تحديد المسار
| المعلمة المطلوبة | طريقة التحليل | مثال |
|---|---|---|
./math |
المسار بالنسبة للملف الحالي | ./math → /project/src/math.js |
../utils |
بالنسبة إلى الدليل الأصلي | ../utils → /project/utils.js |
/abs/path |
المسار المطلق | /lib/helper.js |
express |
الوحدات المدمجة → node_modules | البحث حسب المستوى |
| حزمة «Scope» |
▶ مثال: عرض مسار تحديد وحدة النمط
// show-paths.js
console.log(module.paths);
[
'/project/src/node_modules',
'/project/node_modules',
'/node_modules',
'C:\\Users\\Charlie\\.node_modules',
'C:\\Users\\Charlie\\.node_libraries',
'C:\\Program Files\\nodejs\\lib\\node'
]
5. آلية التخزين المؤقت للوحدات النمطية
(1) كيف يعمل التخزين المؤقت
require عند تحميل الوحدة النمطية لأول مرة، يتم تنفيذ كودها وتخزين النتيجة في ذاكرة التخزين المؤقت. وبعد ذلك، require تعرض الوحدة النمطية نفسها النتيجة المخزنة في ذاكرة التخزين المؤقت مباشرةً دون إعادة تنفيذ الكود.
// counter.js
console.log('counter.js executed!');
let count = 0;
module.exports = {
increment() { return ++count; },
getCount() { return count; },
};
// app.js
const c1 = require('./counter'); // counter.js executed!
const c2 = require('./counter'); // (no output, using cache)
console.log(c1 === c2); // true
console.log(c1.increment()); // 1
console.log(c2.getCount()); // 1 (shared state)
(2) require.cache
يتم تخزين جميع الوحدات النمطية التي تم تحميلها مؤقتًا في الكائن require.cache، حيث يُستخدم المسار المطلق للوحدة النمطية كمفتاح.
// inspect-cache.js
const path = require('path');
const math = require('./math');
const cacheKey = path.resolve(__dirname, 'math.js');
console.log(require.cache[cacheKey] !== undefined); // true
console.log(require.cache[cacheKey].exports === math); // true
▶ مثال: مسح ذاكرة التخزين المؤقت لتنفيذ إعادة التحميل السريع
// hot-reload.js
function loadConfig() {
const path = require('path');
const cacheKey = path.resolve(__dirname, 'config.js');
delete require.cache[cacheKey];
return require('./config');
}
const cfg1 = loadConfig();
// ... config.js Modified ...
const cfg2 = loadConfig(); // Re-execute, loading the latest content
إذا قمت بحذف الإدخال الموجود في
require.cacheثم قمت بتشغيلrequireمرة أخرى، فسيقوم Node.js بإعادة تنفيذ تلك الوحدة النمطية. وهذا مفيد لإعادة التحميل السريع في بيئة التطوير، ولكن يجب توخي الحذر عند استخدامه في بيئة الإنتاج.
6. نظرة عامة على الوحدات المدمجة
(1) مرجع سريع للوحدات المدمجة الشائعة
يأتي Node.js مزودًا بعدد كبير من الوحدات المدمجة الجاهزة للاستخدام دون الحاجة إلى التثبيت.
| الوحدة | الغرض | الطرق/الخصائص الشائعة |
|---|---|---|
fs |
عمليات نظام الملفات | readFile، writeFile، readdir، stat |
path معالجة المسار join، resolve، parse، extname، basename |
||
http |
خادم/عميل HTTP | createServer، get، request |
https |
خادم/عميل HTTPS | createServer، get، request |
url تحليل عناوين URL وتكوينها URL، fileURLToPath، pathToFileURL |
||
os |
معلومات نظام التشغيل | cpus، freemem، hostname، platform |
events |
مُصدر الأحداث | EventEmitter، on، emit، off |
stream |
معالجة التدفق | Readable، Writable، Transform، pipe |
crypto |
التشفير والتجزئة | createHash، createHmac، randomBytes |
util |
المرافق | promisify، callbackify، format، inspect |
child_process |
إدارة العمليات الفرعية | exec، spawn، fork |
buffer |
معالجة البيانات الثنائية | Buffer.alloc، Buffer.from، concat |
▶ مثال:(2) لا تتطلب الوحدات المدمجة أي تثبيت
const fs = require('fs');
const path = require('path');
const os = require('os');
console.log(os.platform()); // win32 / darwin / linux
console.log(path.join('/project', 'src', 'app.js')); // /project/src/app.js
▶ مثال: البدء السريع باستخدام path وos
const path = require('path');
const os = require('os');
const filePath = '/project/src/utils/helper.js';
console.log(path.extname(filePath)); // .js
console.log(path.dirname(filePath)); // /project/src/utils
console.log(path.basename(filePath)); // helper.js
console.log(`CPU cores: ${os.cpus().length}`);
console.log(`Free memory: ${(os.freemem() / 1024 / 1024).toFixed(0)} MB`);
7. التبعيات الدائرية
(1) ما المقصود بالتبعية الدائرية؟
تتطلب الوحدة النمطية «أ» الوحدة النمطية «ب»، وتطلب الوحدة النمطية «ب» الوحدة النمطية «أ»، مما يؤدي إلى حدوث مرجع دائري. ولا تدخل Node.js في حلقة لا نهائية؛ بل تقوم بدلاً من ذلك بإرجاع الصادرات من الجزء الذي تم تنفيذه بالفعل.
▶ مثال:(2) كيف يتعامل Node.js مع ذلك
// a.js
exports.loaded = false;
const b = require('./b');
exports.loaded = true;
console.log('a.js - b.loaded =', b.loaded);
// b.js
exports.loaded = false;
const a = require('./a'); // Received a Unfinished exports { loaded: false }
exports.loaded = true;
console.log('b.js - a.loaded =', a.loaded);
node a.js
b.js - a.loaded = false
a.js - b.loaded = true
عندما يقوم b.js بتنفيذ require('./a')، لا يكون a.js قد انتهى من التنفيذ بعد، لذا تقوم Node.js بإرجاع الجزء من a.js الذي تم تعيين قيمة له في تلك اللحظة ({ loaded: false }).
(3) استراتيجيات لتجنب التبعيات الدائرية
| الاستراتيجية | الوصف |
|---|---|
| استخراج المنطق المشترك | نقل الأجزاء المشتركة إلى وحدة ثالثة |
| متطلب مؤجل | نقل require إلى داخل الدالة بحيث يتم تحميله فقط عند استدعائه |
| فصل الأحداث | استخدم EventEmitter بدلاً من الاستدعاءات المباشرة |
| حقن التبعيات | تمرير التبعيات عبر المعلمات بدلاً من تضمينها بشكل ثابت في الكود |
▶ مثال: تأجيل require لحل التبعيات الدائرية
// user.js
exports.getName = function () {
return 'Charlie';
};
exports.getProfile = function () {
const format = require('./format'); // Delay until the time of the call require
return format.upper(exports.getName());
};
// format.js
exports.upper = function (str) {
return str.toUpperCase();
};
exports.getUserDisplay = function () {
const user = require('./user'); // Deferred require
return `User: ${user.getName()}`;
};
يضمن تأجيل
requireأن تقوم الوحدة النمطية بتحميل تبعياتها فقط عند استدعاء إحدى الطرق للمرة الأولى، حيث تكون الوحدتان النمطيتان قد تم تهيئتهما بالكامل بحلول ذلك الوقت، مما يمنع استردادexportsغير المكتملة.
8. مثال شامل: مشروع معياري
فيما يلي، سنقوم بإنشاء مشروع معياري يتضمن وحدة مساعدة، ووحدة تسجيل، ونقطة دخول رئيسية:
// math.js — Tools Module
const PI = 3.14159;
function circleArea(radius) {
return PI * radius * radius;
}
function rectangleArea(width, height) {
return width * height;
}
function round(value, decimals = 2) {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
module.exports = { circleArea, rectangleArea, round };
// logger.js — Log Module
const LEVELS = { INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR' };
function formatMessage(level, msg) {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] ${msg}`;
}
function info(msg) {
console.log(formatMessage(LEVELS.INFO, msg));
}
function warn(msg) {
console.warn(formatMessage(LEVELS.WARN, msg));
}
function error(msg) {
console.error(formatMessage(LEVELS.ERROR, msg));
}
module.exports = { info, warn, error, LEVELS };
// app.js — Main Entrance
const { circleArea, rectangleArea, round } = require('./math');
const { info, error } = require('./logger');
const radius = 5;
const area = round(circleArea(radius));
info(`Circle area (r=${radius}): ${area}`);
const roomArea = rectangleArea(4.5, 6.2);
info(`Room area: ${round(roomArea)} sqm`);
if (radius < 0) {
error('Radius cannot be negative');
} else {
info('Calculation complete');
}
node app.js
[2026-07-03T10:30:00.000Z] [INFO] Circle area (r=5): 78.54
[2026-07-03T10:30:00.001Z] [INFO] Room area: 27.9 sqm
[2026-07-03T10:30:00.001Z] [INFO] Calculation complete
❓ أسئلة شائعة
module.exports كصادرات افتراضية)، ولكن في CJS، لا يمكنك استخدام require لتحميل وحدات ESM؛ بل يجب عليك استخدام الدالة الديناميكية import() بدلاً من ذلك. يُنصح بأن تلتزم المشاريع بمواصفات وحدة واحدة فقط.require متزامن أم غير متزامن؟require يوقف تنفيذ الكود حتى تنتهي الوحدة النمطية من التحميل. ولهذا السبب توصي Node.js بوضع require في أعلى الملف وتجنب الاستدعاءات المتكررة لـ require للوحدات النمطية الجديدة في المسار الساخن أثناء وقت التشغيل.module.exports وexports؟exports هي إشارة مختصرة إلى module.exports. يمكنك إضافة خصائص باستخدام exports.xxx = ...، لكن exports = xxx يقطع الإشارة، مما يتسبب في فشل التصدير. عندما تحتاج إلى تصدير دالة واحدة أو كائن جديد تمامًا، يجب عليك استخدام module.exports = xxx.require.cache. المفاتيح هي المسارات المطلقة للمكون، والقيم هي كائنات المكون. إذا حذفت مفتاحًا (delete require.cache[key])، فسيتم إعادة تنفيذ المكون في المرة التالية التي تستخدم فيها require.exports التي لم يتم تنفيذها بالكامل بعد عند نقطة بدء الحلقة (والتي قد تكون كائنات غير مكتملة)، مما قد يؤدي إلى خصائص غير محددة. وتشمل الحلول استخراج وحدة نمطية مشتركة، وتأجيل استدعاءات require، وفصل الأحداث.import في المستوى الأعلى في ESM؟import().require لتحميل ملف JSON؟JSON.parse()، لتُرجع كائن JavaScript مُحلَّل. ويُستخدم هذا عادةً لتحميل ملفات التكوين.📖 ملخص
- CommonJS هي مواصفة الوحدات النمطية الافتراضية لـ Node.js؛ استخدم
requireللتحميل وmodule.exportsللتصدير - تعد وحدات ES معيارًا رسميًا لـ JavaScript؛ استخدم
import/export، وقم بتفعيلها باستخدام اللاحقة.mjsأو"type":"module" requireترتيب البحث: الوحدات المدمجة → المسارات النسبية/المطلقة → node_modules، مع البحث تصاعديًا مستوىً تلو الآخر- بعد تحميل الوحدة النمطية للمرة الأولى، يتم تخزينها مؤقتًا في
require.cache؛ أما عمليات الاستدعاء اللاحقة لـrequireفتُرجع النسخة المخزنة مؤقتًا مباشرةً. exportsهي إشارة إلىmodule.exports؛ وإعادة تعيينها سيؤدي إلى كسر هذه الإشارة- عند حدوث تبعية دائرية، تُرجع Node.js قيمة
exportsغير مُستوفاة؛ ويمكن تجنب ذلك باستخدام استراتيجيات مثل تأخير استدعاءاتrequire. - لا تتطلب الوحدات المدمجة (مثل fs و path و http و os وغيرها) أي تثبيت؛ ما عليك سوى استخدام
requireلتشغيلها.
📝 تمارين
- قم بإنشاء الوحدة النمطية
calculator.js، وقم بتصدير الدوال الأربعaddوsubtractوmultiplyوdivide، ثم قم باستدعائها واستخدامها فيmain.js - حوّل السؤال السابق إلى نسخة ESM: استخدم صيغة
exportواللاحقة.mjs، وقم بتحميله باستخدامimport. - اكتب كودًا للتحقق من وجود
require.cache: بعد استدعاء وحدة نمطية، اعرض المعلومات المتعلقة بتلك الوحدة النمطية منrequire.cache. - قم عمدًا بإنشاء تبعية دائرية (ملف a.js يتطلب ملف b.js، وملف b.js يتطلب ملف a.js)، وراقب النتيجة، ثم قم بإصلاحها باستخدام أوامر الاستدعاء المؤجلة.
- استخدم الوحدتين
pathوosلطباعة نظام التشغيل الحالي، وعدد نوى وحدة المعالجة المركزية (CPU)، والمسار المطلق للدليل الحالي.