نظامان للموديولز في لغة واحدة
في البداية، لم تكن جافا سكريبت تمتلك أي نظام للموديولز على الإطلاق. جاءت Node عام 2009 لتسدّ هذه الفجوة عبر CommonJS (بـ require و module.exports)، وظلّ هذا هو الشكل السائد لكود Node لسنوات طويلة. ثم في عام 2015، حصلت اللغة نفسها على نظام موديولز قياسي خاص بها — ES Modules (بـ import و export) — وهو مدعوم اليوم في المتصفحات و Node على حدٍّ سواء.
لذلك ستصادف النظامين معًا في المشاريع الحقيقية. إليك نفس الموديول البسيط مكتوبًا بالطريقتين:
نفس الدالة، لكن بغلافَين مختلفَين. بقية هذه الصفحة مخصّصة لمعرفة متى يهمّك كل غلاف منهما، وأيهما تختار.
الفرق بين require و import في الصياغة
الفروقات اليومية بين الاثنين تكفيها بطاقة بريدية صغيرة:
// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");
module.exports = something;
module.exports.name = value;
exports.name = value;
// وحدات ES
import fs from "fs";
import { readFile } from "fs/promises";
export default something;
export const name = value;
export { name };
require هو استدعاء دالة عادي. أما import فهو تعليمة (statement) — لا يمكن كتابتها إلا في أعلى الموديول، ويجب أن يكون المسار نصًا حرفيًا (string literal). وهذا القيد ليس اعتباطيًا، بل هو ما يتيح لـ ES Modules فعل أشياء يعجز عنها CommonJS.
الفرق الجوهري بين require و import: ساكن مقابل ديناميكي
في CommonJS، يُنفَّذ require() لحظة وصول التنفيذ إلى السطر. يمكنك وضعه داخل if، أو حساب المسار أثناء التشغيل، أو تحميل الموديول بشكل مشروط:
ES Modules تعتمد على التحليل الساكن (static). المحرك يقرأ كل جمل import قبل تنفيذ أي سطر من الكود، يبني شجرة الاعتماديات، ثم يحلّ كل شيء مسبقاً. لهذا السبب لازم يكون المسار نصاً حرفياً، ولهذا السبب أيضاً import ما بيظهر إلا في أعلى الملف.
الفائدة من هالموضوع؟ الأدوات تقدر تشوف شجرة الموديولز كاملة دون ما تنفّذ أي كود. هيك طريقة عمل الـ bundlers للـ tree-shaking (أي التخلّص من الـ exports غير المستخدمة)، وهيك تقدر المحررات تعطيك اقتراحات دقيقة، وهيك يقدر المتصفح يجيب الموديولز بالتوازي.
أما إذا فعلاً احتجت تحميلاً ديناميكياً داخل ESM، استخدم import() — وهو تعبير يشبه الدالة ويرجع Promise:
كيف يُحدِّد Node نظام الوحدات المستخدم في كل ملف
المشروع الواحد في Node قد يحتوي على ملفات من النوعين معًا، ويعتمد Node على أمرين لمعرفة النظام الذي ينتمي إليه كل ملف:
- امتداد الملف: الامتداد
.mjsيعني دائمًا ESM، و.cjsيعني دائمًا CommonJS. - قيمة الحقل
"type"في أقرب ملفpackage.json: فإذا كانت"module"فإن ملفات.jsتُعامَل على أنها ESM، وإذا كانت"commonjs"(وهي القيمة الافتراضية عند غياب الحقل) فإنها تُعامَل على أنها CommonJS.
// package.json
{
"name": "my-app",
"type": "module"
}
مع ضبط "type": "module"، أي ملف عادي مثل hello.js داخل نفس الحزمة سيستخدم import/export. وإذا وضعت بجانبه ملف hello.cjs، فإن هذا الملف بالذات سيعمل بنظام require. بهذه الطريقة يمكن لأي مشروع أن ينتقل تدريجيًا إلى ESM، أو لأي مكتبة أن توفّر النكهتين جنبًا إلى جنب.
انتبه لهذه النقطة إن كنت مبتدئًا: داخل ملف ESM، لا وجود لـ require ولا module.exports أصلًا. وإذا كتبتهما بحكم العادة، فسيواجهك خطأ ReferenceError مباشرة.
التعامل المختلط: دمج النظامين معًا
كثيرًا ما ستحتاج إلى استيراد حزمة CommonJS من داخل ملف ESM، أو العكس. والقواعد هنا ليست متماثلة في الاتجاهين.
استيراد CommonJS داخل ESM يعمل بشكل مباشر، إذ يتحوّل كائن module.exports الخاص بـ CJS إلى تصدير افتراضي (default export):
// app.mjs
import greet from "./greet.cjs";
console.log(greet("روزا"));
الاستيراد بالأسماء من وحدات CommonJS قد يشتغل أحيانًا — إذ يحاول Node التعرّف على الـ named exports بشكل ساكن (static detection) — لكن لضمان الموثوقية، يُفضَّل أخذ الـ default ثم تفكيكه:
import pkg from "./utils.cjs";
const { parse, stringify } = pkg;
استيراد ESM من داخل CommonJS هو الاتجاه المؤلم فعلاً. ما تقدر تستخدم require() مع موديول ES، لأنه راح يرمي لك الخطأ ERR_REQUIRE_ESM. الحل البديل هو import() الديناميكي، اللي يشتغل حتى داخل CommonJS ويرجّع Promise:
نود الحديث (22+) أضاف require() متزامنًا للـ ESM تحت شروط معيّنة، لكن يبقى import() الديناميكي هو الحل المحمول والمضمون.
فروقات سلوكية أخرى تستحق الانتباه
بعيدًا عن الاختلاف في الصياغة (syntax)، يختلف النظامان في تفاصيل صغيرة قد توقعك في مشاكل بين الحين والآخر:
بعض الفروقات الإضافية:
thisفي المستوى الأعلى. في CommonJS، قيمةthisهيmodule.exports، أما في ES Modules فهيundefined. والسبب أن ESM يعمل دائمًا في الوضع الصارم (strict mode).__dirnameو__filename. في CommonJS تحصل عليهما جاهزين دون أي عناء، لكن في ES Modules عليك اشتقاقهما بنفسك منimport.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
- امتدادات الملفات عند الاستيراد. نظام ESM يشترط كتابة الامتداد (
"./utils.js"وليس"./utils") في المسارات النسبية، بينما CJS أكثر تساهلاً في هذا الأمر. - الربط الحي مقابل اللقطات الثابتة. عمليات الاستيراد في ESM تمثّل مراجع حيّة لمتغيرات الموديول المُصدِّر، أما في CJS فتحصل على نسخة مما أُسند إلى
module.exportsلحظة التحميل. في الغالب لن تلاحظ الفرق، لكنه يصبح مهمًا مع التبعيات الدائرية.
أيهما تستخدم؟
للمشاريع الجديدة: ES Modules بلا تردد. أضف "type": "module" في package.json وامضِ قُدمًا. فـ ESM هو المعيار الرسمي للغة، ويعمل بالطريقة ذاتها في المتصفح وفي Node، ويدعم top-level await، والأدوات الحديثة مبنية على أساسه.
أما CommonJS فيبقى خيارًا منطقيًا في الحالات التالية:
- عندك كود CJS قائم وتصيانه مستمر، ولا تستحق الهجرة العناء حاليًا.
- تنشر مكتبة تحتاج لدعم إصدارات Node قديمة جدًا أو مستهلكين لا يقدرون على استخدام ESM.
- تعتمد على مكتبة أساسية تُنشر بصيغة CJS فقط وتكامُلها مع ESM مزعج. (نادر هذه الأيام، لكنه ما زال يحصل.)
حتى في هذه الحالات، ستقرأ كود ESM باستمرار — فكل ما يُنشر على npm خلال السنوات الأخيرة يتجه في هذا الاتجاه. إتقان الاثنين ليس ترفًا، لكن إتقان أسلوب النظام الذي تكتب به فعليًا هو الأهم.
قائمة فحص ذهنية سريعة
عند فتح أي ملف جديد، اسأل نفسك:
- هل هذا الملف يستعمل
import/exportأمrequire/module.exports؟ لا تخلط بينهما. - ماذا يقول أقرب
package.jsonعن قيمة"type"؟ - إذا كنت تستورد حزمة، افتح
package.jsonالخاص بها — هل تُصدِّر ESM أم CJS أم الاثنين؟ - إن ظهر لك خطأ
ERR_REQUIRE_ESM، فأنت في CJS وتحاول تحميل ESM. حوّل إلىimport()الديناميكي أو انقل المستدعي إلى ESM.
تسعون بالمئة من الحيرة حول الموديولز في Node سببها واحدة من هذه النقاط الأربع.
التالي: أساسيات npm
الموديولز هي الطريقة التي توزّع بها كودك أنت على عدة ملفات. الخطوة التالية هي جلب كود كتبه آخرون — وهنا يأتي دور npm. سنتناول تثبيت الحزم، ونطاقات semver، والأجزاء التي تستخدمها فعلًا من سير عمل npm في يومك المعتاد.
الأسئلة الشائعة
ما الفرق بين require و import في JavaScript؟
require هو أسلوب CommonJS لتحميل الوحدات، يعمل بشكل متزامن (synchronous) وينفَّذ في اللحظة التي يُستدعى فيها، ويُرجع ما تم إسناده إلى module.exports. أما import فهو صياغة ES Modules، ثابت (static) ويُرفع إلى أعلى الملف ويُحلَّل قبل تنفيذ الكود. كما يختلفان في قيمة this، وفي طريقة التعامل مع الاعتماديات الدائرية (circular dependencies)، وفي السماح بـ top-level await من عدمه.
في مشروع Node جديد، أستخدم CommonJS أم ES Modules؟
استخدم ES Modules. فقط أضِف "type": "module" في ملف package.json واكتب import/export. ESM هو المعيار الرسمي ويعمل في المتصفح وفي Node ويدعم top-level await. ستظل ترى CommonJS في الحزم والأدوات القديمة، لذا ستقرأه حتى لو لم تكتبه بنفسك.
هل يمكنني خلط require و import في نفس المشروع؟
نعم، لكن بقواعد. ملف بامتداد .mjs أو حزمة فيها "type": "module" تُعامَل كـ ESM، بينما .cjs أو "type": "commonjs" تُعامَل كـ CJS. يستطيع ESM استدعاء import لوحدة CommonJS (ويظهر module.exports كـ default export)، لكن CommonJS لا يستطيع عمل require() لوحدة ESM مباشرةً؛ عليك استخدام import() الديناميكي الذي يُرجع Promise.