البرمجة المعيارية في Node.js: دليل شامل لإنشاء تطبيقات قابلة للتطوير
مقدمة إلى البرمجة المعيارية في Node.js
تُعد الوحدات (Modules) من الميزات الأساسية والجوهرية في بيئة التشغيل Node.js. عند تطوير التطبيقات، ومع تزايد تعقيد الشيفرة، يصبح من غير العملي وضع جميع الأكواد في ملف واحد. لتجنب هذه الفوضى وجعل إدارة الشيفرة أكثر سهولة، يمكنك الاستفادة من نمط الوحدات في Node.js لكتابة أجزاء مختلفة من التطبيق في ملفات منفصلة، ثم تصدير هذه الأجزاء (سواء كانت دوال، كائنات، أو طرق) لاستيرادها واستخدامها في الملف الرئيسي أو في وحدات أخرى.
قد تتساءل: ما هو module بالضبط؟ ببساطة، module ليس سوى ملف JavaScript عادي. هذه هي الفكرة الأساسية! بفضل هذه الوظيفة المعيارية في Node.js، يمكننا استيراد ملفاتنا الخارجية الخاصة، بالإضافة إلى وحدات Node.js الأساسية (Core/Native modules)، ووحدات NPM (مدير حزم Node). في هذا المقال، سنتناول كل نوع من هذه الوحدات بالتفصيل.
استيراد وتصدير الملفات المحلية في Node.js
سنركز في هذا القسم على كيفية تصدير واستيراد ملفاتنا الخاصة. لتوضيح الأمر، سنستخدم ملفين أساسيين: calculate.js، وهو الملف الذي سنقوم بالتصدير منه، وmain.js، وهو الملف الذي سنستورد إليه الوحدة.

لتبسيط الشرح، سنضع كلا الملفين في نفس المجلد.

استيراد دالة (Function)
لنبدأ بتصدير دالة بسيطة. في ملف calculate.js، سنقوم بتعريف دالة add ثم تصديرها باستخدام module.exports:
//---- Exported file [calculate.js] ----
const add = ( a,b )=> {
return a + b
}
module .exports = add
هنا، قمنا بتصدير دالة تسمى add باستخدام module.exports. في Node.js، يُشار إلى كل ملف على أنه module، وexports هي خاصية من خصائص كائن module. بعد ذلك، يتم استيراد هذه الدالة إلى ملف آخر باستخدام طريقة require. يمكننا الآن استدعاء الدالة في الملف الآخر، وهو main.js، عن طريق تمرير الوسائط كما هو موضح أدناه:
//------ Main File[main.js] ----
const add = require ( './calculate' ) //name of the desired file
const result = add( 2 , 4 )
console .log(result); //Output : 6
استيراد كائن (Object)
يمكننا أيضًا تصدير كائن كامل والوصول إلى الطرق المختلفة الموجودة بداخله. لنفترض أننا نريد تصدير كائن يحتوي على دالة result:
//---- Exported file [calculate.js] ----
const add = {
result : ( a,b )=> {
return a + b
}
}
module .exports = add
لقد قمنا بتصدير الكائن add واستوردناه إلى ملفنا الرئيسي باستخدام طريقة require. يمكننا الآن الوصول إلى طريقة result الخاصة بكائن add باستخدام عامل النقطة (. dot operator):
//---- Main file[main.js] ----
const add = require ( './calculate' )
const result = add.result( 5 , 8 )
console .log(result) //Output : 13
طريقة أخرى يمكننا من خلالها تصدير الكائن أعلاه هي بتصدير الطريقة التي نحتاجها فقط بدلاً من الكائن بأكمله. هذه ممارسة جيدة إذا كنت لا تحتاج إلى الكائن بأكمله ولكنك تحتاج فقط إلى بعض طرقه/دواله، كما أنها تجعل شيفرتنا أكثر أمانًا:
//---- Exported file [calculate.js] ----
const add = {
result : ( a,b )=> {
return a + b
}
}
module .exports = add.result
كما ترون، نحن نستورد طريقة result الموجودة داخل الكائن add. لذا يمكن استدعاء هذه الطريقة مباشرة في الملف الرئيسي:
//---- Main file[main.js] ----
const add = require ( './calculate' )
const result = add( 5 , 8 )
console .log(result) //Output : 13
استيراد دالة منشئة (Function Constructor)
تُستخدم الدالة المنشئة (Function Constructor) بشكل أساسي لإنشاء نسخة جديدة من كائن يمتلك نفس خصائص الكائن/الدالة الرئيسية. في الحالة أدناه، سنقوم بإنشاء نسخة جديدة من الكائن 'Add' باستخدام الكلمة المفتاحية new. تُسمى هذه العملية ‘الإنشاء’ (instantiation). ثم نقوم بتصدير هذه النسخة باستخدام module.exports:
//---- Exported file [calculate.js] ----
function Add ( ) {
this .result = ( a,b )=> {
return a + b
}
}
module .exports = new Add()
الآن يمكننا استيرادها إلى ملفنا الرئيسي والوصول إلى طريقة 'result' بداخلها للحصول على القيمة المحسوبة:
//---- Main file[main.js] ----
const add = require ( './calculate2' )
const result = add.result( 1 , 3 )
console .log(result); //Output : 4
بهذه الطريقة، يمكننا تصدير واستيراد دالة منشئة. توجد طريقة أخرى للقيام بذلك، وهي بإنشاء نسختنا الجديدة في الملف الرئيسي بدلاً من الملف المصدر كما هو موضح أعلاه module.exports = new Add(). سنرى كيف يعمل هذا عندما نصدر فئات ES6 Classes التي تعمل بشكل مشابه للدوال المنشئة.
استيراد فئات ES6 (ES6 Classes)
class هو نوع خاص من الدوال حيث تساعد الكلمة المفتاحية class في تهيئتها. تستخدم طريقة constructor لتخزين الخصائص. الآن سنقوم بتصدير الفئة class بأكملها باستخدام module.exports:
//---- Exported file [calculate.js] ----
const Add = class {
constructor (a,b){
this .a = a;
this .b = b;
}
result(){
return this .a + this .b
}
}
module .exports = Add;
الآن في ملفنا الرئيسي، نقوم بإنشاء نسخة جديدة باستخدام الكلمة المفتاحية new والوصول إلى طريقة result للحصول على القيمة المحسوبة:
//---- Main file[main.js] ----
const add = require ( './calculate' )
const result = new add( 2 , 5 )
console .log(result.result()); //Output : 7
استيراد وحدات Node.js الأساسية (Core Modules)
بدلاً من إنشاء وحداتنا المخصصة في كل مرة، يوفر Node.js مجموعة من الوحدات المدمجة لتسهيل عمل المطورين. سنتناول بعض هذه الوحدات هنا، ولكن يمكنك العثور على القائمة الكاملة في وثائق API الرسمية لـ Node.js. استيراد وحدات Node.js الأساسية يشبه إلى حد كبير استيراد وحداتك الخاصة؛ تستخدم نفس دالة require() للوصول إليها في ملفك.
ومع ذلك، هناك بعض الوحدات التي ربما تكون قد استخدمتها دون علم، والتي لا تحتاج إلى استيراد. على سبيل المثال، console.log() – لقد استخدمنا وحدة console عدة مرات دون جلبها في ملفنا المحلي، حيث أن هذه الطرق متاحة عالميًا. لنلقِ نظرة على إحدى الوحدات الأساسية، وهي نظام الملفات (File System) أو fs.
هناك عدد لا يحصى من العمليات التي يمكننا إجراؤها باستخدام وحدة نظام الملفات، مثل قراءة ملف، كتابة ملف، وتحديثه، على سبيل المثال لا الحصر. سنستخدم وحدة fs لقراءة ملف. حتى في هذه الطريقة، هناك طريقتان يمكننا من خلالهما تنفيذ هذا الإجراء: إحداهما باستخدام الدالة المتزامنة fs.readFileSync()، والأخرى باستخدام الدالة غير المتزامنة fs.readFile(). سنتناول الدوال المتزامنة وغير المتزامنة في Node.js في منشورات مستقبلية. اليوم، سنستخدم الإصدار غير المتزامن، وهو fs.readFile().
لهذا المثال، قمنا بإنشاء ملفين: main.js، حيث سنقوم بعملية قراءة الملف، وfile.txt وهو الملف الذي سنقرأه.

يحتوي ملف file.txt على بعض النصوص:
Hello World!
الآن، لنحاول استخدام وحدة fs لقراءة الملف، دون استيرادها، كما هو موضح أدناه:
fs.readFile( './file.txt' , 'utf-8' , ( err,data )=> {
if (err) throw err
console .log(data);
})
ستظهر رسالة خطأ لأن fs غير معرف. هذا لأن وحدة نظام الملفات fs ليست متاحة عالميًا مثل وحدة console. إليك مثال على الخطأ الذي قد يظهر:
ReferenceError: fs is not defined
at Object.<anonymous> (C:\Users\Sarvesh Kadam\Desktop\Training\blog\code snippets\Node Modular Pattern\main.js: 3 : 1 )
at Module._compile (internal/modules/cjs/loader.js: 1256 : 30 )
at Object.Module._extensions..js (internal/modules/cjs/loader.js: 1277 : 10 )
at Module.load (internal/modules/cjs/loader.js: 1105 : 32 )
at Function.Module._load (internal/modules/cjs/loader.js: 967 : 14 )
at Function.executeUserEntryPoint [ as runMain ] (internal/modules/run_main.js: 60 : 12 )
at internal/main/run_main_module.js: 17 : 47
لذلك، نحتاج إلى استيراد جميع البيانات من وحدة نظام الملفات باستخدام دالة require() وتخزين تلك البيانات في متغير fs:
const fs = require ( 'fs' )
fs.readFile( './file.txt' , 'utf-8' , ( err,data )=> {
if (err) throw err
console .log(data);
})
يمكنك تسمية هذا المتغير بأي اسم، ولكنني سميته fs لسهولة القراءة وهو المعيار الذي يتبعه معظم المطورين. باستخدام المتغير fs، يمكننا الوصول إلى طريقة readFile() حيث قمنا بتمرير ثلاث وسائط: مسار الملف، ترميز الأحرف utf-8، ودالة رد الاتصال (callback function) لإعطاء مخرج.
قد تتساءل لماذا نمرر utf-8 كوسيط في readFile()؟ لأنه يقوم بترميز القيمة ويعطي النص كمخرج بدلاً من إعطاء مخزن مؤقت (buffer) كما هو موضح أدناه: <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 21>. دالة رد الاتصال، بدورها، تحتوي على وسيطين: خطأ (err) والمحتوى الفعلي في الملف (data). ثم نقوم بطباعة data في وحدة التحكم (console).
//Output: Hello World!
استيراد وحدات NPM (مدير حزم Node)
ما هو NPM؟
إذن، ما هو مدير حزم Node بالضبط؟ الحزمة (package) هي جزء من الشيفرة يتم إدارته بواسطة مدير الحزم. إنه ببساطة برنامج يدير تثبيت وتحديث الحزم. وفقًا للوثائق الرسمية لـ NPM:
NPMهو أكبر سجل للبرمجيات في العالم. يستخدم مطورو المصادر المفتوحة من كل قارةnpmلمشاركة واستعارة الحزم، وتستخدم العديد من المؤسساتnpmلإدارة التطوير الخاص أيضًا.
لذلك، في NPM، نستخدم شيفرة مفتوحة المصدر لشخص آخر، تتم إدارتها بواسطة NPM، عن طريق استيرادها إلى مشروعنا. عادة ما يأتي NPM مع Node.js عند تنزيله. يمكنك التحقق مما إذا كان NPM مثبتًا على جهازك ببساطة عن طريق تشغيل الأمر npm -v في موجه الأوامر الخاص بك. إذا أعاد رقم إصدار، فهذا يعني أن NPM مثبت بنجاح.
يمتلك NPM سجله الخاص على npmjs.com حيث يمكنك اكتشاف الحزم التي يمكنك استخدامها. لنلقِ نظرة على إحدى الحزم التي تسمى chalk، والتي تُستخدم بشكل أساسي لتنسيق الطرفية (terminal styling).

في الشكل أعلاه، يمكننا رؤية عدد التنزيلات الأسبوعية للحزمة، مما يشير إلى مدى شعبيتها. كما يمكنك أن ترى أن هذه الحزمة تحتوي على تبعيات بداخلها. لذا، فإن هذه الوحدة، التي ستكون بمثابة تبعية في مشروعنا، تعتمد هي نفسها على وحدات أخرى. يتم الاهتمام بعملية الإدارة هذه بالكامل بواسطة مدير الحزم. حتى الشيفرة المصدرية الموجودة على GitHub متاحة لنا، ويمكننا التنقل إليها والتحقق مما إذا كانت هناك أي مشكلات مفتوحة.
فهم الإصدارات الدلالية (Semantic Versioning)
شيء آخر قبل المضي قدمًا: تأتي حزم NPM بإصدارات مختلفة. النمط الذي يتبعه الإصدار هو الإصدار الدلالي (semantic versioning). كما ترون، أحدث إصدار من وحدة chalk عندما كتبت هذا المقال هو 4.1.0. يتبع نمط الإصدار الدلالي Major_changes.Minor_changes.Patch.
Major_changes: كما يوحي الاسم، هي التغييرات الكبيرة التي تم إجراؤها على الوحدة والتي قد تؤثر على شيفرتك الحالية.Minor_changes: هي تحسينات أو ميزات جديدة بالإضافة إلى إصلاحات للعيوب التي تمت إضافتها والتي لا ينبغي أن تؤثر على شيفرتك الحالية.Patch: هي إصلاحات الأخطاء الصغيرة التي لن تتسبب في تعطل شيفرتك الحالية.
يمكنك معرفة المزيد عن الإصدارات الدلالية على semver.org.
كيفية تثبيت حزم NPM
لاستيراد أي حزمة من NPM، تحتاج أولاً إلى تهيئة NPM في مجلد مشروعك المحلي عن طريق تشغيل الأمر التالي في موجه الأوامر:
npm init
بمجرد تشغيل الأمر أعلاه، سيطلب منك بعض البيانات كما هو موضح أدناه، مثل اسم الحزمة، والإصدار، وما إلى ذلك. يمكن الاحتفاظ بمعظم هذه البيانات كقيم افتراضية كما هو مذكور بين الأقواس الدائرية (()). أيضًا، الحقول مثل author وlicense مخصصة للأشخاص الذين أنشأوا حزم NPM هذه. من ناحية أخرى، نحن فقط نستوردها ونستخدمها لإنشاء تطبيقنا الخاص.
package name: (code_npm) code_npm
version: ( 1.0 . 0 ) 1.0 . 0
description: npm demo
entry point: (index.js) index.js
test command: test
git repository:
keywords: npm test
author: Sarvesh
license: (ISC)
بمجرد إدخال جميع الحقول، سيتم إنشاء ملف JSON بالقيم التي تحتوي على الخصائص المذكورة أعلاه، وسيطلب منك تأكيدًا كهذا:
Is this OK? (yes) yes
بمجرد تأكيد yes، سيتم إنشاء ملف package.json بجميع البيانات التي أدخلتها كما هو موضح أدناه:
{
"name" : "code_npm" ,
"version" : "1.0.0" ,
"description" : "npm demo" ,
"main" : "index.js" ,
"scripts" : {
"test" : "echo \"Error: no test specified\" && exit 1"
},
"keywords" : [
"npm" ,
"test"
],
"author" : "Sarvesh" ,
"license" : "ISC"
}
كما يمكنك أن ترى، تمت إضافة كائن script يحتوي على خاصية test. يمكنك تشغيله باستخدام الأمر npm test وسيعطي المخرج المطلوب هكذا:
"Error: no test specified"
الآن، بدلاً من القيام بهذه الطريقة المطولة لتهيئة NPM وإدخال قيم الخصائص المخصصة، يمكنك ببساطة تشغيل الأمر:
npm init -y
بمجرد تشغيل هذا الأمر، سيتم إنشاء ملف package.json مباشرة بالقيم الافتراضية.

الآن لتثبيت أحدث إصدار من حزمة chalk في مشروعك، تحتاج إلى تنفيذ الأمر:
npm install chalk
يمكنك أيضًا تثبيت أي إصدار محدد تحتاجه من chalk بمجرد إضافة @version number كما هو موضح أدناه. أيضًا، بدلاً من install، يمكنك ببساطة وضع العلامة المختصرة i التي تعني التثبيت:
npm i chalk@ 4.0 .0
سيؤدي هذا إلى تثبيت شيئين: مجلد node_modules، وملف package-lock.json.

أيضًا، سيضيف خاصية جديدة تسمى dependencies إلى ملف package.json الخاص بنا، والتي تحتوي على اسم الحزمة المثبتة وإصدارها.
"dependencies" : {
"chalk" : "^4.0.0"
}
يحتوي مجلد node_modules على مجلدات الحزم وتبعياتها. يتم تعديله عند تثبيت حزمة npm. يحتوي ملف package-lock.json على الشيفرة التي تجعل NPM أسرع وأكثر أمانًا.
"chalk" : {
"version" : "4.0.0" ,
"resolved" : "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz" ,
"integrity" : "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==" ,
"requires" : {
"ansi-styles" : "^4.1.0" ,
"supports-color" : "^7.1.0"
}
يحتوي بشكل أساسي على خصائص مثل version، وهو رقم الإصدار الدلالي. خاصية resolved هي الدليل أو الموقع الذي تم جلب الحزمة منه. في هذه الحالة، تم جلبها من chalk. خاصية integrity هي للتأكد من حصولنا على نفس الشيفرة إذا قمنا بتثبيت التبعية مرة أخرى. خاصية الكائن requires تمثل تبعية حزمة chalk.
ملاحظة هامة: لا تقم بإجراء أي تغييرات على هذين الملفين node_modules وpackage-lock.json يدويًا.
كيفية استخدام حزم NPM
الآن بعد أن قمنا بتثبيت chalk في مشروعنا، يمكننا استيراده إلى ملف مشروعنا الرئيسي باستخدام طريقة require(). ثم يمكننا تخزين هذه الوحدة في متغير يسمى chalk.
const chalk = require ( 'chalk' )
console .log(chalk.red( "Hello World" ))
باستخدام طريقة red() من حزمة chalk، قمنا بتلوين نص "Hello World" باللون الأحمر. عند تشغيل الأمر node index.js، نحصل على المخرج التالي:

هناك العديد من الطرق التي يمكنك من خلالها تنسيق مخرج سطر الأوامر الخاص بك باستخدام حزمة chalk. لمزيد من المعلومات، يمكنك الرجوع إلى وثائق Chalk الرسمية على NPM.
أيضًا، يمكنك تثبيت حزم NPM عالميًا (أي على نظام التشغيل الخاص بك) بدلاً من تثبيتها في مشروعك المحلي عن طريق إضافة العلامة -g في سطر الأوامر (والتي تعني global، كما هو مذكور أدناه):
npm i nodemon -g
لن تؤثر هذه الحزمة العالمية على ملف package.json الخاص بنا بأي شكل من الأشكال لأنها لم يتم تثبيتها محليًا. لقد قمنا بتثبيت حزمة nodemon عالميًا، والتي تُستخدم لإعادة التشغيل التلقائي لتطبيق Node.js عند ملاحظة تغييرات في الملفات داخل الدليل. يمكنك الرجوع إلى nodemon لمزيد من المعلومات. يمكننا استخدام حزمة nodemon عن طريق تشغيل التطبيق باستخدام هذا الأمر:
nodemon index.js
يعمل بشكل مشابه لـ node index.js، باستثناء أنه يراقب تغييرات الملفات ويعيد تشغيل التطبيق بمجرد اكتشاف التغييرات.
[ nodemon ] 2.0 . 6
[ nodemon ] to restart at any time, enter `rs`
[ nodemon ] watching path(s): *.*
[ nodemon ] watching extensions: js,mjs,json
[ nodemon ] starting `node index.js`
Hello World
ملاحظة: من المحتمل أن لا يعمل تنسيق chalk عند استخدامك لـ nodemon.
أخيرًا، سنتناول dev dependencies (تبعيات التطوير). هناك بعض حزم أو وحدات NPM التي لن نحتاجها في بيئة الإنتاج (production environment) لمشروعنا، ولكن فقط لمتطلبات التطوير (development requirements). يمكننا تثبيت هذه الوحدات في مشروعنا باستخدام العلامة --save-dev كما هو موضح أدناه:
npm i nodemon - -save -dev
ثم ينشئ خاصية جديدة في ملف package.json تسمى devDependencies:
"devDependencies" : {
"nodemon" : "^2.0.6"
}
الخلاصة
باستخدام نمط الوحدات في Node.js، يمكننا استيراد وتصدير الشيفرة من ملفاتنا الخاصة في أشكال مختلفة، مثل:
- الدوال (
functions) - الكائنات (
objects) - الدوال المنشئة (
function constructors) - فئات
ES6(ES6 classes)
كما يوفر Node.js مجموعة خاصة به من الوحدات الأساسية (Core/Native Modules) التي يمكننا استخدامها. بعضها متاح عالميًا، بينما يحتاج البعض الآخر إلى الاستيراد محليًا في مشروعك/مجلدك.
NPM هو مدير حزم يدير شيفرة مفتوحة المصدر من طرف ثالث يمكننا استخدامها في مشروعنا. قبل استخدام وحدات NPM، تحتاج إلى تهيئة NPM محليًا باستخدام الأمر npm init في سطر الأوامر في جذر مجلد مشروعك. يمكنك تثبيت أي حزمة NPM باستخدام الأمر npm i <package name>. ويمكنك تثبيت حزمة NPM عالميًا باستخدام العلامة -g. كما يمكن جعل الحزمة تعتمد على التطوير باستخدام العلامة --save-dev.
الخلاصة التقنية
تُعد البرمجة المعيارية في Node.js حجر الزاوية في بناء تطبيقات قوية وقابلة للتطوير والصيانة. من خلال تقسيم الشيفرة إلى وحدات مستقلة، نتمكن من تحقيق فصل واضح للمخاوف (separation of concerns)، مما يسهل على المطورين العمل على أجزاء مختلفة من المشروع بشكل متزامن، ويقلل من فرص حدوث الأخطاء. إن القدرة على استيراد الوحدات المحلية، والاستفادة من الوحدات الأساسية المدمجة، ودمج حزم NPM الغنية، تخلق بيئة تطوير مرنة وفعالة. يضمن الفهم العميق لآليات require()، وmodule.exports، ومفاهيم مثل الإصدارات الدلالية، أن يتمكن المطورون من إدارة التبعيات بذكاء، وتحسين أداء التطبيق، والحفاظ على أمانه، مما يمهد الطريق لتطبيقات Node.js مستقرة ومبتكرة.