حقن التبعية ، جافا سكريبت ، و ES6 وحدات

هناك تطبيق آخر لـ Dependency Injection في JavaScript هو وحدات ES6 ، مع إمكانية استخدام نفس الكود في المستعرض وفي nodejs وعدم استخدام transpilers.


صورة


تحت القصاص هو رأيي في شركة DI ، وموقعها في تطبيقات الويب الحديثة ، والتطبيق الأساسي لحاوية DI التي يمكن أن تخلق أشياء في الأمام والخلف ، وكذلك شرح لما يجب أن يفعله مايكل جاكسون به.


إنني أطلب بشدة من أولئك الذين يجدونها تافهة في المقالة ألا يغتصبوا أنفسهم ولا يقرؤوا حتى النهاية ، بحيث في وقت لاحق ، إذا شعروا بخيبة أمل ، لا تضع "ناقصًا". أنا لست ضد "السلبيات" - لكن فقط إذا كان الطرح مصحوبًا بتعليق ، ما الذي تسبب بالضبط في المنشور في رد فعل سلبي. هذه مقالة تقنية ، لذا حاول أن تتنازل عن أسلوب العرض ، وانتقد المكون التقني لما سبق. شكرا لك


الكائنات في التطبيق


أنا حقًا أحترم البرمجة الوظيفية ، لكنني كرست معظم نشاطي المهني لإنشاء تطبيقات تتكون من كائنات. جافا سكريبت تثيرني بحقيقة أن الوظائف الموجودة بها هي أيضًا كائنات. عند إنشاء التطبيقات ، أفكر في الأشياء ، هذا تشوه احترافي.


وفقًا لمدى الحياة ، يمكن تقسيم الكائنات الموجودة في التطبيق إلى الفئات التالية:


  • دائم - ينشأ في مرحلة ما من التطبيق ولا يتم إتلافه إلا عند اكتمال التطبيق ؛
  • مؤقت - ينشأ عندما يكون من الضروري إجراء بعض العمليات ويتم تدميره عند اكتمال هذه العملية ؛

في هذا الصدد ، في البرمجة هناك أنماط تصميم مثل:



هذا ، من وجهة نظري ، يتكون التطبيق من وحيدون موجودون بشكل دائم والذين يقومون إما بالعمليات المطلوبة بأنفسهم أو يقومون بإنشاء كائنات مؤقتة لتنفيذها.


حاوية الكائن


حقن التبعية هو أسلوب يجعل من السهل إنشاء كائنات في تطبيق ما. أي أنه يوجد في التطبيق كائن خاص "يعرف" كيفية إنشاء جميع الكائنات الأخرى. يسمى هذا الكائن "حاوية كائن" (أحيانًا "إدارة كائن").


حاوية الكائن ليست كائن إلهي ، لأن مهمتها هي فقط إنشاء كائنات كبيرة من التطبيق وتوفير الوصول إلى الكائنات الأخرى لهم. الغالبية العظمى من كائنات التطبيق ، التي يتم إنشاؤها بواسطة الحاوية والموجودة فيها ، ليست لديها فكرة عن الحاوية نفسها. يمكن وضعها في أي بيئة أخرى ، مع توفير التبعيات اللازمة ، وسوف تعمل أيضًا بشكل جيد هناك (يعرف المختبرون ما أعنيه).


مكان التنفيذ


بشكل عام ، هناك طريقتان لحقن التبعيات في كائن:


  • من خلال المنشئ
  • من خلال خاصية (أو ملحقها) ؛

لقد استخدمت أساسًا الطريقة الأولى ، لذا سأواصل الوصف من وجهة نظر حقن التبعية من خلال المُنشئ.


دعنا نقول أن لدينا تطبيق يتكون من ثلاثة أشياء:


صورة


في PHP (هذه اللغة مع تقاليد DI القديمة ، لدي حاليًا أمتعة نشطة ، سأنتقل إلى JS لاحقًا قليلاً) يمكن أن ينعكس موقف مماثل بهذه الطريقة:


class Config { public function __construct() { } } class Service { private $config; public function __construct(Config $config) { $this->config = $config; } } class Application { private $config; private $service; public function __construct(Config $config, Service $service) { $this->config = $config; $this->service = $service; } } 

يجب أن تكون هذه المعلومات كافية حتى يتسنى لحاوية DI (على سبيل المثال ، الدوري / الحاوية ) ، إذا تم تكوينها بشكل مناسب ، أن تنشئ ، بناء على طلبها لإنشاء كائن Application ، تبعياتها Service and Config وتمرير المعلمات إليها إلى مُنشئ كائن Application .


معرفات التبعية


كيف يفهم حاوية الكائن أن مُنشئ كائن Application يتطلب كائنين Config Service ؟ عن طريق تحليل الكائن من خلال واجهة برمجة التطبيقات Reflection API ( Java ، PHP ) أو عن طريق تحليل رمز الكائن مباشرة (التعليقات التوضيحية للكود). وهذا هو ، في الحالة العامة ، يمكننا تحديد أسماء المتغيرات التي يتوقع مُنشئ الكائن رؤيتها عند الإدخال ، وإذا كانت اللغة قابلة للطباعة ، فيمكننا أيضًا الحصول على أنواع هذه المتغيرات.


وبالتالي ، كمحددات للكائنات ، يمكن للحاوية أن تعمل إما بأسماء معلمات الإدخال الخاصة بالمنشئ أو أنواع معلمات الإدخال.


إنشاء كائنات


يمكن إنشاء الكائن صراحة بواسطة المبرمج ووضعه في الحاوية تحت المعرف المقابل (على سبيل المثال ، "التكوين")


 /** @var \League\Container\Container $container */ $container->add("configuration", $config); 

ويمكن إنشاؤها بواسطة الحاوية وفقًا لقواعد محددة. هذه القواعد ، عمومًا ، تنخفض إلى مطابقة معرف الكائن مع الكود الخاص به. يمكن تعيين القواعد بشكل صريح (تعيين في شكل رمز ، XML ، JSON ، ...)


 [ ["object_id_1", "/path/to/source1.php"], ["object_id_2", "/path/to/source2.php"], ... ] 

أو في شكل بعض الخوارزمية:


 public function getSource($id) {. return "/path/to/source/${id}.php"; } 

في PHP ، يتم توحيد قواعد مطابقة اسم فئة مع ملف برمز المصدر الخاص به ( PSR-4 ) ؛ في Java ، تتم المطابقة على مستوى تكوين JVM (أداة تحميل الفئة ). إذا كانت الحاوية توفر بحثًا تلقائيًا عن المصادر عند إنشاء كائنات ، فإن أسماء الفئات تكون معرفات جيدة بما يكفي للكائنات في مثل هذه الحاوية.


مساحات


عادة في مشروع ، بالإضافة إلى الكود الخاص به ، يتم استخدام وحدات الطرف الثالث أيضًا. مع ظهور مديري التبعية (maven ، الملحن ، npm) ، تم تبسيط استخدام الوحدات بشكل كبير ، وزاد عدد الوحدات في المشروعات زيادة كبيرة. تسمح مساحات الأسماء بعناصر الكود التي تحمل نفس الاسم في مشروع واحد من وحدات نمطية مختلفة (فئات ، وظائف ، ثوابت).


هناك لغات يتم فيها بناء مساحة الاسم مبدئيًا (Java):


 package vendor.project.module.folder; 

هناك لغات أضيفت بها مساحة الاسم أثناء تطوير اللغة (PHP):


 namespace Vendor\Project\Module\Folder; 

يسمح لك تطبيق مساحة اسم جيد بمعالجة أي عنصر من عناصر التعليمات البرمجية بشكل لا لبس فيه:


 \Doctrine\Common\Annotations\Annotation\Attribute::$name 

مساحة الاسم تحل مشكلة تنظيم العديد من عناصر البرنامج في المشروع ، وبنية الملف تحل مشكلة تنظيم الملفات على القرص. لذلك ، ليس هناك الكثير من العناصر المشتركة بينهما ، وأحيانًا كثيرة جدًا - في Java ، على سبيل المثال ، يجب إرفاق فئة عامة في مساحة الاسم بشكل فريد بملف مع رمز هذه الفئة.


وبالتالي ، فإن استخدام معرف فئة كائن في مساحة اسم المشروع كمعرفات كائن في الحاوية يعد فكرة جيدة ويمكن أن يكون بمثابة أساس لإنشاء قواعد للكشف التلقائي لرموز المصدر عند إنشاء الكائن المطلوب.


 $container->add(\Vendor\Project\Module\ObjectType::class, $obj); 

رمز بدء التشغيل


في composer PHP composer يتم تعيين مساحة اسم الوحدة النمطية لنظام الملفات داخل الوحدة النمطية في واصف الوحدة النمطية composer.json :


 "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } } 

يمكن لمجتمع JS إجراء تعيين مماثل في package.json إذا كانت هناك مساحات أسماء في JS.


معرفات التبعية JS


أعلاه ، أشرت إلى أن الحاوية يمكن أن تستخدم إما أسماء معلمات مدخلات المنشئ أو أنواع معلمات الإدخال كمعرفات. المشكلة هي أن:


  1. JS هي لغة ذات كتابة ديناميكية ولا توفر تحديد أنواع عند إعلان وظيفة.
  2. يستخدم JS minifiers التي يمكن إعادة تسمية معلمات الإدخال.

يقترح مطورو الحاوية awilix DI استخدام الكائن كمعلمة الإدخال الوحيدة إلى المُنشئ ، وخصائص هذا الكائن كتبعيات:


 class UserController { constructor(opts) { this.userService = opts.userService } } 

قد يتكون معرف خاصية الكائن في JS من أحرف أبجدية رقمية ، "_" و "$" ، وقد لا يبدأ برقم.


نظرًا لأننا سنحتاج إلى تعيين معرفات التبعية على المسار إلى مصادرها في نظام الملفات للتحميل التلقائي ، فمن الأفضل التخلي عن استخدام "$" واستخدام تجربة PHP. قبل ظهور عامل namespace في بعض الأطر (على سبيل المثال ، في Zend 1) ، تم استخدام الأسماء التالية للفئات:


 class Zend_Config_Writer_Json {...} 

وبالتالي ، يمكن أن نعكس تطبيقنا لثلاثة كائنات ( Application ، Config ، Service ) على JS شيء مثل هذا:


 class Vendor_Project_Config { constructor() { } } class Vendor_Project_Service { constructor({Vendor_Project_Config}) { this.config = Vendor_Project_Config; } } class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

إذا نشرنا رمز كل فصل:


 export default class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

في ملفك داخل وحدة مشروعنا:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

بعد ذلك ، يمكننا توصيل الدليل الجذر للوحدة النمطية بـ "مساحة اسم" الجذر للوحدة النمطية في تكوين الحاوية:


 const ns = "Vendor_Project"; const path = path.join(module_root, "src"); container.addSourceMapping(ns, path); 

وبعد ذلك ، بدءًا من هذه المعلومات ، قم بإنشاء المسار إلى المصادر المقابلة ( ${module_root}/src/Config.js ) بناءً على معرف التبعية ( ${module_root}/src/Config.js ).


وحدات ES6


يوفر ES6 تصميمًا عامًا لتحميل الوحدات ES6:


 import { something } from 'path/to/source/with/something'; 

نظرًا لأننا نحتاج إلى إرفاق كائن واحد (فئة) بملف واحد ، فمن المنطقي في المصدر تصدير هذه الفئة افتراضيًا:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

من حيث المبدأ ، من الممكن عدم كتابة مثل هذا الاسم الطويل للفصل ، فقط ستعمل Something أيضًا ، ولكن في Zend 1 كتبوا ولم ينكسر ، ويؤثر تفرد اسم الفصل داخل المشروع بشكل إيجابي على كل من إمكانات IDE (الإكمال التلقائي والمطالبات السياقية) ، لذلك وعند تصحيح الأخطاء:


صورة


يبدو استيراد فئة وإنشاء كائن في هذه الحالة كما يلي:


 import Something from 'path/to/source/with/something'; const something = new Something(); 

استيراد أمامي وخلفي


يعمل الاستيراد في المستعرض وفي العقدة ، ولكن هناك فروق دقيقة. على سبيل المثال ، لا يفهم المستعرض استيراد وحدات nodejs:


 import path from "path"; 

حصلنا على خطأ في المتصفح:


 Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../". 

أي إذا أردنا أن يعمل رمزنا في كل من المتصفح والعقدة ، لا يمكننا استخدام تصميمات لا يفهمها المستعرض أو العقدة. أركز بشكل خاص على هذا ، لأن هذا الاستنتاج أمر طبيعي للغاية بحيث لا يمكن التفكير فيه. كيف تتنفس


مكان شركة DI في تطبيقات الويب الحديثة


هذا هو رأيي الشخصي بحت ، بسبب تجربتي الشخصية ، مثل كل شيء آخر في هذا المنشور.


في تطبيقات الويب ، تأخذ JS مكانها في المقدمة ، في المستعرض ، بدون بديل تقريبًا. على جانب الخادم ، تم حفر جافا ، PHP ، .Net ، روبي ، بيثون ، ... ولكن مع ظهور nodejs ، اخترقت JavaScript أيضًا الخادم. والتقنيات المستخدمة بلغات أخرى ، بما في ذلك DI ، بدأت في اختراق JS من جانب الخادم.


يرجع تطوير JavaScript إلى السلوك غير المتزامن للرمز في المتصفح. عدم التزامن ليس ميزة استثنائية لـ JS ، ولكنه فطري إلى حد ما. الآن فإن وجود JS على كل من الخادم والجبهة لا يفاجئ أي شخص ، بل يشجع على استخدام نفس الأساليب في كلا طرفي تطبيق الويب. ونفس الكود. بالطبع ، تختلف المقدمة والظهر من حيث الجوهر وفي المهام التي لا يمكن حلها لاستخدام نفس الكود هناك وهناك. ولكن يمكننا أن نفترض أنه في تطبيق أكثر أو أقل تعقيدًا سيكون هناك متصفح وخادم ورمز عام.


شركة DI قيد الاستخدام بالفعل في المقدمة ، وفي RequireJS :


 define( ["./config", "./service"], function App(Config, Service) {} ); 

صحيح ، هنا يتم كتابة معرفات التبعيات بشكل صريح وفوري في شكل روابط إلى المصادر (يمكنك تكوين تعيين معرفات في تكوين أداة تحميل التشغيل).


في تطبيقات الويب الحديثة ، يوجد DI ليس فقط من جانب الخادم ، ولكن أيضًا في المستعرض.


ما علاقة مايكل جاكسون بها؟


عندما تقوم بتمكين دعم ES-module في nodejs ( --experimental-modules ) ، يحدد المحرك محتويات الملفات ذات *.mjs أنها وحدات EcmaScript (على عكس وحدات Common-modules مع *.cjs ).


تسمى هذه الطريقة أحيانًا " حل مايكل جاكسون " ، وتسمى النصوص البرمجية نصوص مايكل جاكسون ( *.mjs ).


أوافق على أنه تم حل المؤامرات مع KDPV ، ولكن ... كامون الرجال ، مايكل جاكسون ...


بعد تنفيذ DI آخر


حسنا ، كما هو متوقع ، بنفسك الدراجة وحدة DI - @ teqfw / di


هذا ليس حلاً جاهزًا للقتال ، ولكنه تطبيق أساسي. يجب أن تكون جميع التبعيات عبارة عن وحدات ES وأن تستخدم ميزات شائعة للمتصفح و nodejs.


لحل التبعيات ، تستخدم الوحدة النمطية نهج awilix :


 constructor(spec) { /** @type {Vendor_Module_Config} */ const _config = spec.Vendor_Module_Config; /** @type {Vendor_Module_Service} */ const _service = spec.Vendor_Module_Service; } 

لتشغيل المثال الخلفي:


 import Container from "./src/Container.mjs"; const container = new Container(); container.addSourceMapping("Vendor_Module", "../example"); container.get("Vendor_Module_App") .then((app) => { app.run(); }); 

على الخادم:


 $ node --experimental-modules main.mjs 

لتشغيل المثال الأمامي ( example.html ):


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DI in Browser</title> <script type="module" src="./main.mjs"></script> </head> <body> <p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p> <p>Open browser console to see output.</p> </body> </html> 

تحتاج إلى وضع الوحدة النمطية على الخادم وفتح صفحة example.html في المستعرض (أو استخدام قدرات IDE). إذا قمت بفتح example.html مباشرة ، فسيكون الخطأ في Chrom:


 Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 

إذا سارت الأمور على ما يرام ، فسيكون هناك شيء مثل هذا في وحدة التحكم (المتصفح أو العقدة):


 Create object with ID 'Vendor_Module_App'. Create object with ID 'Vendor_Module_Config'. There is no dependency with id 'Vendor_Module_Config' yet. 'Vendor_Module_Config' instance is created. Create object with ID 'Vendor_Module_Service'. There is no dependency with id 'Vendor_Module_Service' yet. 'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]). 'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]). Application 'Vendor_Module_Config' is running. 

ملخص


AMD ، CommonJS ، UMD؟


ESM !

Source: https://habr.com/ru/post/ar464347/


All Articles