عندما نستخدم Nodejs للتطوير اليومي، غالبًا ما نستخدم طلب استيراد نوعين من الوحدات، أحدهما هو الوحدة النمطية التي كتبناها بأنفسنا أو文件模块
الطرف الثالث المثبتة باستخدام npm هي الوحدة المضمنة في Node والتي يتم توفيرها لنا لاستخدامها، مثل os
و fs
والوحدات النمطية الأخرى. وتسمى هذه الوحدات核心模块
.
تجدر الإشارة إلى أن الفرق بين وحدة الملف والوحدة الأساسية لا يكمن فقط في ما إذا كانت العقدة مدمجة أم لا، ولكن أيضًا في موقع الملف وعملية التجميع والتنفيذ للوحدة. هناك اختلافات واضحة بين الاثنين . ليس هذا فحسب، يمكن أيضًا تقسيم وحدات الملفات إلى وحدات ملفات عادية أو وحدات مخصصة أو وحدات امتداد C/C++، وما إلى ذلك. تحتوي الوحدات النمطية المختلفة أيضًا على العديد من التفاصيل التي تختلف في تحديد موضع الملف وتجميعه والعمليات الأخرى.
ستتناول هذه المقالة هذه المشكلات وتوضح مفاهيم وحدات الملفات والوحدات الأساسية بالإضافة إلى عملياتها المحددة وتفاصيلها التي يجب الانتباه إليها في موقع الملف أو تجميعه أو تنفيذه.
لنبدأ بوحدة الملف.
ما هي وحدة الملف؟
في العقدة، سيتم التعامل مع الوحدات المطلوبة باستخدام معرفات الوحدة النمطية التي تبدأ بـ .、.. 或/
(أي استخدام المسارات النسبية أو المسارات المطلقة) كوحدات نمطية للملفات. بالإضافة إلى ذلك، هناك نوع خاص من الوحدات النمطية، على الرغم من أنها لا تحتوي على مسار نسبي أو مسار مطلق، وهي ليست وحدة نمطية أساسية، إلا أنها تشير إلى模块路径
عندما تحدد Node هذا النوع من الوحدات模块路径
للبحث عن الوحدة واحدة تلو الأخرى يسمى هذا النوع من الوحدات وحدة مخصصة.
لذلك، تشتمل وحدات الملفات على نوعين، أحدهما عبارة عن وحدات ملفات عادية ذات مسارات، والآخر عبارة عن وحدات نمطية مخصصة بدون مسارات.
يتم تحميل وحدة الملف ديناميكيًا في وقت التشغيل، الأمر الذي يتطلب موقعًا كاملاً للملف وعملية التجميع والتنفيذ، وهو أبطأ من الوحدة الأساسية.
لتحديد موضع الملف، تتعامل Node مع هذين النوعين من وحدات الملفات بشكل مختلف. دعونا نلقي نظرة فاحصة على عمليات البحث لهذين النوعين من وحدات الملفات.
بالنسبة لوحدات الملفات العادية، نظرًا لأن المسار الذي تحمله واضح جدًا، فلن يستغرق البحث وقتًا طويلاً، وبالتالي فإن كفاءة البحث أعلى من الوحدة المخصصة المقدمة أدناه. ومع ذلك، لا تزال هناك نقطتان يجب ملاحظتهما.
أولاً، في الظروف العادية، عند استخدام الأمر require لتقديم وحدة ملف، لا يتم تحديد امتداد الملف بشكل عام، على سبيل المثال:
const math = require("math");
نظرًا لعدم تحديد الامتداد، لا يمكن للعقدة تحديد الملف النهائي. في هذه الحالة، ستقوم Node بإكمال الامتدادات بالترتيب .js、.json、.node
، وتجربتها واحدًا تلو الآخر. وتسمى هذه العملية文件扩展名分析
.
تجدر الإشارة أيضًا إلى أنه في التطوير الفعلي، بالإضافة إلى طلب ملف معين، فإننا عادةً ما نحدد دليلًا أيضًا، مثل:
const axios = require("../network");
في هذه الحالة، ستقوم Node أولاً بتنفيذ الملف إذا لم يتم العثور على الملف المقابل، ولكن تم الحصول على الدليل، فسوف تتعامل العقدة مع الدليل كحزمة.
على وجه التحديد، ستعيد Node الملف المشار إليه بواسطة الحقل main
لـ package.json
في الدليل كنتيجة بحث. إذا كان الملف المشار إليه بواسطة main خاطئًا، أو كان ملف package.json
غير موجود على الإطلاق، فستستخدم Node index
كاسم ملف افتراضي، ثم تستخدم .js
و .node
لإجراء تحليل الامتداد والبحث عن الملف الهدف إذا لم يتم العثور عليه واحدًا تلو الآخر، فسوف يحدث خطأ.
(بالطبع، نظرًا لأن Node لديها نوعان من أنظمة الوحدات النمطية، CJS وESM، بالإضافة إلى البحث عن المجال الرئيسي، ستستخدم Node أيضًا طرقًا أخرى. وبما أنها خارج نطاق هذه المقالة، فلن أخوض في التفاصيل. )
تم ذكرللتو. عندما تبحث Node عن وحدات مخصصة، ستستخدم مسار الوحدة إذن ما هو مسار الوحدة؟
يجب أن يعرف الأصدقاء الذين هم على دراية بتحليل الوحدة أن مسار الوحدة عبارة عن مصفوفة مكونة من مسارات، ويمكن رؤية القيمة المحددة في المثال التالي:
// example.js
نتائج الطباعة
console.log(module.paths);
كما ترون، تحتوي الوحدة في Node على مصفوفة مسار الوحدة، والتي يتم تخزينها في module.paths
وتستخدم لتحديد كيفية عثور Node على الوحدة المخصصة التي تشير إليها الوحدة الحالية.
على وجه التحديد، ستقوم Node باجتياز مصفوفة مسار الوحدة، وتجربة كل مسار واحدًا تلو الآخر، ومعرفة ما إذا كانت هناك وحدة نمطية مخصصة محددة في دليل node_modules
تتوافق مع المسار، وإذا لم يكن الأمر كذلك، فسيتم تكرارها خطوة بخطوة حتى تصل إلى دليل node_modules
في الدليل الجذر حتى يتم العثور على الوحدة المستهدفة، سيتم ظهور خطأ إذا لم يتم العثور عليها.
يمكن أن نرى أن البحث بشكل متكرر node_modules
خطوة بخطوة هو استراتيجية Node للعثور على وحدات مخصصة، ومسار الوحدة هو التنفيذ المحدد لهذه الإستراتيجية.
في الوقت نفسه، توصلنا أيضًا إلى استنتاج مفاده أنه عند البحث عن وحدات مخصصة، كلما كان المستوى أعمق، زاد استهلاك البحث المقابل للوقت. لذلك، مقارنة بالوحدات الأساسية ووحدات الملفات العادية، فإن سرعة تحميل الوحدات المخصصة هي الأبطأ.
بالطبع، ما تم العثور عليه بناءً على مسار الوحدة هو مجرد دليل، وليس ملفًا محددًا. بعد العثور على الدليل، ستقوم Node أيضًا بالبحث وفقًا لعملية معالجة الحزمة الموضحة أعلاه، ولن يتم وصف العملية المحددة مرة أخرى.
ما ورد أعلاه هو عملية تحديد موضع الملف والتفاصيل التي يجب الانتباه إليها بالنسبة لوحدات الملفات العادية والوحدات المخصصة. بعد ذلك، دعونا نلقي نظرة على كيفية تجميع هذين النوعين من الوحدات وتنفيذهما.
عندماوتحديد موقع الملف المشار إليه بواسطة الطلب، فإن معرف الوحدة عادة لا يحتوي على امتداد. وفقًا لتحليل امتداد الملف المذكور أعلاه، يمكننا أن نعرف أن Node تدعم تجميع الملفات وتنفيذها ثلاثة ملحقات:
ملف جافا سكريبت. تتم قراءة الملف بشكل متزامن من خلال وحدة fs
ثم يتم تجميعه وتنفيذه. باستثناء ملفات .node
و .json
، سيتم تحميل الملفات الأخرى كملفات .js
.
ملف .node
، وهو ملف ملحق يتم تجميعه وإنشاءه بعد الكتابة في C/C++، يقوم Node بتحميل الملف من خلال طريقة process.dlopen()
.
json، بعد قراءة الملف بشكل متزامن من خلال وحدة fs
، استخدم JSON.parse()
لتحليل النتيجة وإرجاعها.
قبل تجميع وحدة الملف وتنفيذها، ستقوم Node بتغليفها باستخدام غلاف الوحدة كما هو موضح أدناه:
(function(exports, require, Module, __filename, __dirname) { //Module code});
يمكن ملاحظة أنه من خلال غلاف الوحدة، تقوم Node بتجميع الوحدة في نطاق الوظيفة وعزلها عن النطاقات الأخرى لتجنب مشاكل مثل تعارض تسمية المتغيرات وتلوث النطاق العالمي الوقت، من خلال تمرير معلمات التصدير والطلب لتمكين الوحدة من الحصول على إمكانات الاستيراد والتصدير اللازمة. هذا هو تنفيذ العقدة للوحدات النمطية.
بعد فهم غلاف الوحدة، دعونا نلقي نظرة أولاً على عملية التجميع والتنفيذ لملف json.
يعد تجميع وتنفيذ ملفات json هو الأبسط. بعد قراءة محتويات ملف JSON بشكل متزامن من خلال الوحدة النمطية fs
، ستستخدم Node JSON.parse() لتحليل كائن JavaScript، ثم تعيينه لكائن التصدير للوحدة، وأخيرًا إعادته إلى الوحدة التي تشير إليه العملية بسيطة جدا وخام.
. بعد استخدام غلاف الوحدة لتغليف ملفات جافا سكريبت، سيتم تنفيذ التعليمات البرمجية المغلفة من خلال طريقة runInThisContext()
(المشابهة للتقييم) لوحدة vm
، مما يؤدي إلى إرجاع كائن دالة.
بعد ذلك، يتم تمرير الصادرات والمتطلبات والوحدة النمطية والمعلمات الأخرى لوحدة JavaScript إلى هذه الوظيفة للتنفيذ. بعد التنفيذ، يتم إرجاع سمة التصدير للوحدة إلى المتصل. هذه هي عملية التجميع والتنفيذ لملف JavaScript.
قبل شرح تجميع وتنفيذ وحدات امتداد C/C++، دعنا أولاً نقدم ما هي وحدة امتداد C/C++.
تنتمي وحدات امتداد C/C++ إلى فئة وحدات الملفات، كما يوحي الاسم، تتم كتابة هذه الوحدات بلغة C/C++، ويكمن الفرق بينها وبين وحدات JavaScript في أنها لا تحتاج إلى التجميع بعد تحميلها بعد تنفيذها مباشرة، يتم تحميلها بشكل أسرع قليلاً من وحدات JavaScript. بالمقارنة مع وحدات الملفات المكتوبة بلغة JS، تتمتع وحدات الامتداد C/C++ بمزايا أداء واضحة. بالنسبة للوظائف التي لا يمكن تغطيتها بواسطة وحدة Node الأساسية أو التي لها متطلبات أداء محددة، يمكن للمستخدمين كتابة وحدات امتداد C/C++ لتحقيق أهدافهم.
إذن ما هو ملف .node
، وما علاقته بوحدات الامتداد C/C++؟
في الواقع، بعد تجميع وحدة ملحق C/C++ المكتوبة، يتم إنشاء ملف .node
. بمعنى آخر، كمستخدمين للوحدة، فإننا لا نقدم مباشرة الكود المصدري لوحدة الامتداد C/C++، ولكن الملف الثنائي المترجم لوحدة الامتداد C/C++. لذلك، لا يحتاج الملف .node
إلى التحويل البرمجي بعد العثور على الملف .node
، فهو يحتاج فقط إلى تحميل الملف وتنفيذه. أثناء التنفيذ، يتم ملء كائن الصادرات الخاص بالوحدة وإعادته إلى المتصل.
تجدر الإشارة إلى أن ملفات .node
التي تم إنشاؤها عن طريق تجميع وحدات امتداد C/C++ لها أشكال مختلفة تحت منصات مختلفة: ضمن أنظمة *nix
، يتم تجميع وحدات امتداد C/C++ في ملفات كائنات مشتركة ذات ارتباط ديناميكي بواسطة مترجمين مثل g++/gcc. الامتداد هو .so
؛ في Windows
يتم تجميعه في ملف مكتبة الارتباط الديناميكي بواسطة برنامج التحويل البرمجي Visual C++، والامتداد هو .dll
. لكن الامتداد الذي نستخدمه في الاستخدام الفعلي هو .node
. في الواقع، فإن امتداد .node
يبدو أكثر طبيعية. في الواقع، هو ملف .dll ضمن Windows
وملف .dll
ضمن ملفات *nix
.so
.
بعد أن تعثر Node على ملف .node
المطلوب، فإنها تستدعي التابع process.dlopen()
لتحميل الملف وتنفيذه. نظرًا لأن ملفات .node
لها نماذج ملفات مختلفة تحت منصات مختلفة، فمن أجل تحقيق التنفيذ عبر الأنظمة الأساسية، فإن طريقة dlopen()
لها تطبيقات مختلفة ضمن منصات Windows
و *nix
، ثم يتم تغليفها من خلال طبقة التوافق libuv
. يوضح الشكل التالي عملية التجميع والتحميل لوحدات امتداد C/C++ ضمن منصات مختلفة:
يتم تجميع الوحدة الأساسية في ملف ثنائي قابل للتنفيذ أثناء عملية تجميع التعليمات البرمجية المصدر للعقدة. عندما تبدأ عملية العقدة، يتم تحميل بعض الوحدات الأساسية مباشرة في الذاكرة، لذلك، عند تقديم هذه الوحدات الأساسية، يمكن حذف خطوتين موقع الملف والتجميع والتنفيذ، وسيتم الحكم عليهما قبل وحدة الملف في المسار. لذا فإن سرعة التحميل هي الأسرع.
تنقسم الوحدة الأساسية في الواقع إلى جزأين مكتوبين بلغة C/C++ وJavaScript. يتم تخزين ملفات C/C++ في دليل src لمشروع Node، ويتم تخزين ملفات JavaScript في دليل lib. من الواضح أن عمليات التجميع والتنفيذ لهذين الجزأين من الوحدات مختلفة.
لتجميع وحدات JavaScript الأساسية، أثناء عملية تجميع كود مصدر Node، ستستخدم Node أداة js2c.py التي تأتي مع V8 لتحويل جميع أكواد JavaScript المضمنة، بما في ذلك وحدات JavaScript الأساسية، في المصفوفات في C++، يتم تخزين تعليمات JavaScript البرمجية في مساحة اسم العقدة على هيئة سلاسل. عند بدء عملية العقدة، يتم تحميل كود JavaScript مباشرة إلى الذاكرة.
عند تقديم وحدة JavaScript أساسية، ستستدعي Node process.binding()
لتحديد موقعها في الذاكرة من خلال تحليل معرف الوحدة واستعادتها. بعد إخراجها، سيتم أيضًا تغليف وحدة JavaScript الأساسية بواسطة غلاف الوحدة، ثم يتم تنفيذها، وسيتم تصدير كائن التصدير وإعادته إلى المتصل.
في الوحدة الأساسية. تتم كتابة جميع الوحدات في C/C++، وبعض الوحدات تحتوي على الجزء الأساسي المكتمل بواسطة C/C++، ويتم تجميع الأجزاء الأخرى أو تصديرها بواسطة JavaScript لتلبية متطلبات الأداء. الوحدات مثل buffer
و fs
os
وما إلى ذلك مكتوبة جزئيًا بلغة C/C++. هذا النموذج الذي تقوم فيه وحدة C++ بتنفيذ النواة داخل الجزء الرئيسي وتقوم وحدة JavaScript بتنفيذ التغليف خارج الجزء الرئيسي هو طريقة شائعة لـ Node لتحسين الأداء.
تُسمى أجزاء الوحدة الأساسية المكتوبة بلغة C/C++ الخالصة بالوحدات المضمنة، مثل node_fs
و node_os
وما إلى ذلك. وعادةً لا يتم استدعاؤها مباشرة من قبل المستخدمين، ولكنها تعتمد بشكل مباشر على وحدة JavaScript الأساسية. لذلك، في عملية إدخال الوحدة الأساسية للعقدة، هناك مثل هذه السلسلة المرجعية:
إذًا كيف تقوم وحدة JavaScript الأساسية بتحميل الوحدة المضمنة؟
هل تتذكر طريقة process.binding()
؟ تقوم Node بإزالة وحدة JavaScript الأساسية من الذاكرة عن طريق استدعاء هذه الطريقة. تنطبق هذه الطريقة أيضًا على وحدات JavaScript الأساسية للمساعدة في تحميل الوحدات المضمنة.
بالنسبة لتنفيذ هذه الطريقة، عند تحميل وحدة مضمنة، قم أولاً بإنشاء كائن تصدير فارغ، ثم قم باستدعاء طريقة get_builtin_module()
لإخراج كائن الوحدة المضمنة، واملأ كائن التصدير عن طريق تنفيذ register_func()
، وأخيرًا إعادته إلى المتصل لإكمال عملية التصدير. هذه هي عملية التحميل والتنفيذ للوحدة المضمنة.
من خلال التحليل أعلاه، لإدخال سلسلة مرجعية مثل الوحدة الأساسية، مع أخذ وحدة نظام التشغيل كمثال، تكون العملية العامة كما يلي:
باختصار، تتضمن عملية إدخال وحدة نظام التشغيل إدخال وحدة ملف JavaScript، وتحميل وحدة JavaScript الأساسية وتنفيذها، وتحميل الوحدة المضمنة وتنفيذها، وهذه العملية مرهقة ومعقدة للغاية بالنسبة لمتصل الوحدة، بسبب الحماية الأساسية للتطبيقات والتفاصيل المعقدة، يمكن استيراد الوحدة بأكملها ببساطة من خلال require()، وهو أمر بسيط للغاية. ودي.
تقدم هذه المقالة المفاهيم الأساسية لوحدات الملفات والوحدات الأساسية بالإضافة إلى عملياتها المحددة وتفاصيلها التي يجب الانتباه إليها في موقع الملف أو تجميعه أو تنفيذه. على وجه التحديد:
يمكن تقسيم وحدات الملفات إلى وحدات ملفات عادية ووحدات مخصصة وفقًا لعمليات تحديد موضع الملفات المختلفة. يمكن تحديد موقع وحدات الملفات العادية مباشرة بسبب مساراتها الواضحة، والتي تتضمن أحيانًا عملية تحليل امتداد الملف وتحليل الدليل؛ ستبحث الوحدات النمطية المخصصة بناءً على مسار الوحدة، وبعد البحث الناجح، سيتم تنفيذ موقع الملف النهائي من خلال تحليل الدليل .
يمكن تقسيم وحدات الملفات إلى وحدات JavaScript ووحدات امتداد C/C++ وفقًا لعمليات التجميع والتنفيذ المختلفة. بعد حزم وحدة JavaScript بواسطة غلاف الوحدة، يتم تنفيذها من خلال طريقة runInThisContext
لوحدة vm
؛ نظرًا لأن وحدة امتداد C/C++ هي بالفعل ملف قابل للتنفيذ تم إنشاؤه بعد التجميع، ويمكن تنفيذه مباشرة وإرجاع الكائن المصدر. للمتصل.
تنقسم الوحدة الأساسية إلى وحدة JavaScript الأساسية والوحدة المدمجة. يتم تحميل وحدة JavaScript الأساسية في الذاكرة عند بدء عملية العقدة، ويمكن إخراجها ثم تنفيذها من خلال طريقة process.binding()
؛ وسيمر تجميع وتنفيذ الوحدة المضمنة عبر process.binding()
. معالجة الدالة get_builtin_module()
و register_func()
.
بالإضافة إلى ذلك، وجدنا أيضًا سلسلة مرجعية لـ Node لتقديم الوحدات الأساسية، أي وحدة الملف->وحدة JavaScript الأساسية->الوحدة المضمنة، وتعلمنا أيضًا أن وحدة C++ تكمل النواة داخليًا، وJavaScript تنفذ الوحدة أسلوب التغليف خارجيًا.