كيفية البدء بسرعة مع VUE3.0: أدخل وتعلم
يوفر Nest آلية الوحدة النمطية. يتم إكمال حقن التبعية من خلال تحديد الموفرين والواردات والصادرات ومنشئي الموفرين في مصمم الوحدة، ويتم تنظيم تطوير التطبيق بالكامل من خلال شجرة الوحدة. لا توجد مشكلة على الإطلاق في تشغيل التطبيق مباشرةً وفقًا لاتفاقيات الإطار نفسه. ومع ذلك، بالنسبة لي، أشعر أنني أفتقر إلى فهم أكثر وضوحًا ومنهجية لحقن التبعية، وانعكاس التحكم، والوحدات، والموفرين، والبيانات الوصفية، والديكورات ذات الصلة، وما إلى ذلك التي أعلن عنها إطار العمل.
- لماذا هناك حاجة لعكس السيطرة؟
- ما هو حقن التبعية؟
- ماذا يفعل مصمم الديكور؟
- ما هي مبادئ تنفيذ مقدمي الخدمات والواردات والصادرات في الوحدات النمطية (@Module)؟
يبدو أنني قادر على فهم ذلك وتقديره، لكن دعني أشرحه بوضوح منذ البداية، لا أستطيع أن أشرحه بوضوح. لذلك قمت ببعض الأبحاث وتوصلت إلى هذه المقالة. من الآن فصاعدا، نبدأ من الصفر وندخل النص الرئيسي.
1.1 Express, Koa
عملية تطوير اللغة ومجتمعها التقني يجب أن تُثري وتتطور تدريجيًا من الوظائف السفلية إلى الأعلى، تمامًا مثل عملية جذور الأشجار التي تنمو ببطء إلى فروع ثم مليئة بالأوراق. في وقت سابق، ظهرت أطر عمل خدمات الويب الأساسية مثل Express وKoa في Nodejs. قادرة على توفير القدرة على الخدمة الأساسية للغاية. بناءً على هذا الإطار، بدأ ظهور عدد كبير من البرامج الوسيطة والمكونات الإضافية في المجتمع، مما يوفر خدمات أكثر ثراءً للإطار. نحن بحاجة إلى تنظيم تبعيات التطبيق وبناء دعامات التطبيق بأنفسنا، وهي مرنة ومرهقة وتتطلب أيضًا عبء عمل معين.
في وقت لاحق من التطوير، ولدت بعض الأطر ذات الإنتاج الأكثر كفاءة وقواعد أكثر توحيدًا، إيذانًا بمرحلة أحدث.
1.2 EggJs وNestjs
من أجل أن نكون أكثر قدرة على التكيف مع تطبيقات الإنتاج السريع، وتوحيد المعايير وإتاحتها خارج الصندوق، تم تطوير أطر عمل مثل EggJs، وNestJs، وMidway. يلخص هذا النوع من إطار العمل تنفيذ التطبيق في عملية عالمية وقابلة للتوسيع من خلال تنفيذ دورة الحياة الأساسية. نحتاج فقط إلى اتباع طريقة التكوين التي يوفرها إطار العمل لتنفيذ التطبيق بشكل أكثر بساطة. ينفذ إطار العمل عملية التحكم في البرنامج، ونحتاج فقط إلى تجميع أجزائنا في الموقع المناسب. يبدو هذا أشبه بعمل خط التجميع، ويتم تقسيم كل عملية بشكل واضح، ويتم توفير الكثير من تكاليف التنفيذ.
1.3 الملخص
المرحلتان المذكورتان أعلاه مجرد نذير، يمكننا أن نفهم تقريبًا أن ترقية الإطار تعمل على تحسين كفاءة الإنتاج، وسيتم تقديم بعض أفكار وأنماط التصميم في Nest. حقن التبعية، ومفاهيم البرمجة الوصفية، دعونا نتحدث عنها أدناه.
2.1 حقن التبعية
التطبيق هو في الواقع العديد من الفئات المجردة التي تحقق جميع وظائف التطبيق من خلال استدعاء بعضها البعض. مع زيادة تعقيد كود التطبيق ووظائفه، ستصبح صيانة المشروع بالتأكيد أكثر صعوبة نظرًا لوجود المزيد والمزيد من الفئات والعلاقات بينها أصبحت أكثر تعقيدًا.
على سبيل المثال، إذا استخدمنا Koa لتطوير تطبيقنا، فإن Koa نفسها تنفذ بشكل أساسي مجموعة من إمكانيات خدمة الويب الأساسية، وفي عملية تنفيذ التطبيق، سنحدد العديد من الفئات، وطرق إنشاء مثيلات هذه الفئات، وسوف نقوم بذلك جميعًا. يتم تنظيمها والتحكم فيها بحرية من قبلنا في منطق الكود. يعد إنشاء مثيل لكل فئة أمرًا جديدًا يدويًا بواسطتنا، ويمكننا التحكم فيما إذا كان سيتم إنشاء مثيل للفئة مرة واحدة فقط ثم مشاركتها، أو ما إذا كان سيتم إنشاء مثيل لها في كل مرة. تعتمد الفئة B التالية على A. في كل مرة يتم إنشاء مثيل B، سيتم إنشاء مثيل A مرة واحدة، لذلك بالنسبة لكل مثيل B، A هو مثيل غير مشترك.
الفئة أ {} // ب فئة ب { منشئ () { this.a = new A(); } }
إن لغة C أدناه هي المثيل الخارجي الذي تم الحصول عليه، لذلك تشترك مثيلات C المتعددة في مثيل app.a.
الفئة أ {} // ج تطبيق ثابت = {}؛ app.a = new A(); فئة ج { منشئ () { this.a = app.a; } }
يتم تمرير D التالي من خلال معلمة المُنشئ، يمكنك تمرير مثيل غير مشترك في كل مرة، أو يمكنك تمرير مثيل app.a المشترك (D وF share app.a)، وبسبب الطريقة. لقد أصبحت الآن معلمة تمرير، ويمكنني أيضًا تمرير مثيل فئة X.
الفئة أ {} الفئة العاشرة {} //د تطبيق ثابت = {}؛ app.a = new A(); فئة د { منشئ (أ) { this.a = a; } } فئة واو { منشئ (أ) { this.a = a; } } جديد د (التطبيق أ) جديد F(app.a)
جديد
د(جديد
يعد الحقن عبر المنشئ (التمرير حسب القيمة) طريقة تنفيذ واحدة فقط، ويمكن أيضًا تمريره عن طريق تنفيذ استدعاء الأسلوب المحدد، أو أي طريقة أخرى، طالما أنه يمكن تمرير التبعية الخارجية إلى التبعية الداخلية. انها حقا بهذه البساطة.
الفئة أ {} //د فئة د { سيتديب (أ) { this.a = a; } } كونست د = جديد د () d.setDep(new A())
2.2 الكل في حقن التبعية؟
مع استمرار التكرار، يبدو أن تبعيات B ستتغير وفقًا لشروط مسبقة مختلفة. على سبيل المثال، شرط واحد this.a
يحتاج إلى المرور في حالة A، والشرط الثاني this.a
يحتاج إلى المرور في حالة X. في هذا الوقت، سنبدأ في القيام بالتجريد الفعلي. سنقوم بتحويلها إلى طريقة حقن التبعية مثل D أعلاه.
في الأيام الأولى، عندما قمنا بتنفيذ التطبيق، كنا نطبق طريقة الكتابة للفئتين B وC طالما أنها تلبي الاحتياجات في ذلك الوقت، ولم يكن هذا في حد ذاته مشكلة بعد تكرار المشروع لعدة سنوات لن يتم لمس الكود بالضرورة. وإذا أخذنا في الاعتبار التوسع لاحقًا، فسيؤثر ذلك على كفاءة التطوير وقد لا يكون مفيدًا. لذلك، في معظم الأحيان، نواجه سيناريوهات تتطلب التجريد، ثم نقوم بتحويل جزء من التعليمات البرمجية بشكل تجريدي.
// الفئة ب {قبل التحويل منشئ () { this.a = new A(); } } جديد ب () //الفئة د{ بعد التحويل منشئ (أ) { this.a = a; } } جديد د(جديد أ())جديد د
(تكاليف التنفيذ
الجديدة.
تم تقديم هذا المثال هنا لتوضيح ذلك في نموذج التطوير دون أي قيود أو لوائح. يمكننا كتابة التعليمات البرمجية بحرية للتحكم في التبعيات بين الفئات المختلفة. في بيئة مفتوحة تمامًا، فهي مجانية جدًا. هذا عصر بدائي لزراعة القطع والحرق. نظرًا لعدم وجود نموذج ثابت لتطوير التعليمات البرمجية وعدم وجود خطة عمل عليا، حيث يتدخل مطورون مختلفون أو يكتب نفس المطور التعليمات البرمجية في أوقات مختلفة، ستصبح علاقة التبعية مختلفة تمامًا مع نمو التعليمات البرمجية. ومن الواضح أنه قد يتم إنشاء مثيل مشترك عدة مرات ، إضاعة الذاكرة. من الصعب رؤية بنية تبعية كاملة من الكود، وقد يصبح من الصعب جدًا الحفاظ على الكود.
ثم في كل مرة نحدد فئة، نكتبها وفقًا لطريقة حقن التبعية، ونكتبها مثل D. ثم يتم تطوير عملية تجريد C و B، مما يجعل التوسع اللاحق أكثر ملاءمة ويقلل من تكلفة التحويل. لذلك يسمى هذا All in 依赖注入
، أي أن جميع تبعياتنا يتم تنفيذها من خلال حقن التبعية.
ومع ذلك، تصبح تكلفة التنفيذ المبكر مرتفعة مرة أخرى، ومن الصعب تحقيق الوحدة والمثابرة في تعاون الفريق. وفي النهاية، قد يفشل التنفيذ. ويمكن أيضًا تعريف ذلك على أنه تصميم زائد، لأن تكلفة التنفيذ الإضافية قد لا تكون كذلك تحقيق الفوائد بالضرورة.
2.3 انقلاب التحكم
الآن بعد أن اتفقنا على الاستخدام الموحد لحقن التبعية، هل يمكننا تنفيذ وحدة تحكم المستوى الأدنى من خلال التغليف الأساسي للإطار والاتفاق على قاعدة تكوين التبعية؟ تكوين التبعية الذي حددناه ومشاركة التبعية لمساعدتنا في تحقيق إدارة الفصل. يُطلق على نمط التصميم هذا اسم "انعكاس التحكم" .
قد يكون من الصعب فهم انقلاب السيطرة عندما تسمعه للمرة الأولى. ماذا يعني التحكم؟ ما الذي تم عكسه؟
من المفترض أن يرجع ذلك إلى أن المطورين استخدموا مثل هذه الأطر منذ البداية ولم يختبروا "عصر Express و Koa" الأخير ويفتقرون إلى صدمات المجتمع القديم. إلى جانب الصياغة المعكوسة، يبدو البرنامج مجردًا للغاية ويصعب فهمه.
كما ذكرنا سابقًا، عند تنفيذ تطبيقات Koa، يتم التحكم في جميع الفئات بشكل كامل من قبلنا، لذا يمكن اعتبارها طريقة تقليدية للتحكم في البرنامج، لذلك نسميها: التحكم في الدوران الأمامي. نستخدم Nest، الذي ينفذ مجموعة من وحدات التحكم في الأسفل، نحتاج فقط إلى كتابة كود التكوين وفقًا للاتفاقية أثناء عملية التطوير الفعلية، وسيساعدنا برنامج الإطار في إدارة حقن التبعية للفئات، لذلك نسميها: انقلاب السيطرة.
والجوهر هو تسليم عملية تنفيذ البرنامج إلى البرنامج الإطاري للإدارة الموحدة، ونقل قوة التحكم من المطور إلى البرنامج الإطاري.
التحكم في الدوران للأمام: برنامج التحكم اليدوي البحت للمطور
انقلاب التحكم: التحكم في برنامج الإطار
ولإعطاء مثال حقيقي، يذهب الشخص إلى العمل بنفسه، وهدفه هو الوصول إلى الشركة. إنها تقود نفسها وتتحكم في طريقها الخاص. وإذا سلم السيطرة على القيادة واستقل الحافلة، فما عليه سوى اختيار حافلة مكوكية مناسبة للوصول إلى الشركة. فيما يتعلق بالتحكم وحده، يتحرر الناس ويحتاجون فقط إلى تذكر الحافلة التي يجب أن يستقلوها، كما تقل أيضًا فرصة ارتكاب الأخطاء، ويصبح الناس أكثر استرخاءً. نظام الناقل هو المتحكم، وخطوط الناقل هي التكوينات المتفق عليها.
من خلال المقارنة الفعلية المذكورة أعلاه، أعتقد أنني يجب أن أكون قادرًا على فهم انقلاب السيطرة.
2.4 ملخص
من Koa إلى Nest، ومن JQuery الأمامي إلى Vue React. في الواقع، يتم تنفيذها جميعًا خطوة بخطوة من خلال تغليف الإطار لحل مشاكل عدم الكفاءة في العصر السابق.
يستخدم تطوير تطبيق Koa أعلاه طريقة بدائية للغاية للتحكم في التبعيات وإنشاء مثيل لها، وهي تشبه طريقة تشغيل JQuery في الواجهة الأمامية. وتسمى هذه الطريقة البدائية للغاية إعادة توجيه التحكم، ويشبه Vue React ما يوفره Nest وحدة التحكم، يمكن أن يطلق عليها جميعًا انعكاس التحكم. وهذا أيضًا هو فهمي الشخصي، وإذا كان هناك أي مشاكل، آمل أن ينبهني الله إليها.
دعونا نتحدث عن الوحدة @Module في Nest، حيث يتطلب حقن التبعية وانعكاس التحكم وجودها كوسائط.
تنفذ Nestjs انعكاسًا للتحكم وتوافق على تكوين عمليات الاستيراد والتصدير وموفري الوحدة (@module) لإدارة الموفر، وهو حقن التبعية للفئة.
يمكن فهم الموفرين على أنهم تسجيل وإنشاء مثيل للفئات في الوحدة النمطية الحالية. يتم إنشاء مثيل A وB التاليين في الوحدة النمطية الحالية. إذا كان B يشير إلى A في المنشئ، فإنه يشير إلى مثيل ModuleD الحالي.
استيراد {الوحدة النمطية} من '@nestjs/common'؛ استيراد {ModuleX} من './moduleX'؛ استيراد {أ} من './A'؛ استيراد {B} من './B'؛ @الوحدة({ الواردات: [ModulX]، مقدمو الخدمة: [أ، ب]، الصادرات: [أ] }) فئة التصدير ModuleD {} // ب فئة ب { منشئ (أ: أ) { this.a = a; } }
تشير exports
إلى الفئات التي تم إنشاء مثيل لها في providers
في الوحدة النمطية الحالية كفئات يمكن مشاركتها بواسطة وحدات خارجية. على سبيل المثال، عندما يتم إنشاء مثيل للفئة C من ModuleF، أريد حقن مثيل الفئة A مباشرة من ModuleD. ما عليك سوى تعيين الصادرات A في ModuleD، واستيراد ModuleD من خلال imports
في ModuleF.
وفقًا لطريقة الكتابة التالية، سيقوم برنامج التحكم المقلوب بفحص التبعيات تلقائيًا أولاً، تحقق مما إذا كان هناك موفر A في موفري الوحدة النمطية الخاصة بك، إذا لم يكن الأمر كذلك، فابحث عن مثيل A في ModuleD المستورد تم العثور عليه، واحصل على مثيل ModuleD الذي تم إدخاله في مثيل C.
استيراد {الوحدة النمطية} من '@nestjs/common'؛ استيراد {ModuleD} من './moduleD'؛ استيراد {C} من './C'؛ @الوحدة({ الواردات: [ModulD]، مقدمي الخدمة: [C]، }) فئة التصدير ModuleF {} // ج فئة ج { منشئ (أ: أ) { this.a = a; } }
لذلك، إذا كنت تريد أن تستخدم وحدة نمطية خارجية مثيل فئة الوحدة الحالية، فيجب عليك أولاً تحديد فئة إنشاء مثيل في providers
الوحدة الحالية، ثم تحديد هذه الفئة وتصديرها، وإلا سيتم الإبلاغ عن خطأ.
// صحيح @Module({ مقدمو الخدمة: [أ]، الصادرات: [أ] }) // خطأ @الوحدة النمطية({ مقدمي الخدمة: []، الصادرات: [أ] })
إذا نظرنا إلى الوراء في عملية العثور على مثيلات الوحدةالتكميلية اللاحقة
، فهي في الواقع غير واضحة بعض الشيء. النقطة الأساسية هي أنه سيتم إنشاء مثيل للفئات الموجودة في الموفرين، وبعد إنشاء مثيل لها تصبح موفرين فقط سيتم إنشاء مثيل للفئات الموجودة في الموفرين في الوحدة، والتصدير والاستيراد مجرد تكوينات علاقة تنظيمية. ستعطي الوحدة الأولوية لاستخدام الموفر الخاص بها، وإذا لم يكن الأمر كذلك، فتحقق مما إذا كانت الوحدة المستوردة لديها موفر
مطابق
.
منشئ (خاص أ: أ) { } }
نظرًا لأن TypeScript يدعم معلمات المُنشئ (خاصة، محمية، عامة، للقراءة فقط) ليتم تعريفها ضمنيًا وتلقائيًا كسمات فئة (خاصية المعلمة)، ليست هناك حاجة لاستخدام this.a = a
. هذه هي الطريقة المكتوبة في Nest.
ينعكس مفهوم البرمجة الوصفية في إطار عمل Nest. يمكن أن نفهم بشكل تقريبي أن جوهر البرمجة الوصفية لا يزال برمجة، ولكن هناك بعض البرامج المجردة في المنتصف. يمكن لهذا البرنامج المجرد تحديد البيانات الوصفية (مثل بيانات الكائن في @Module)، وهي في الواقع قدرة توسعة يمكن استخدامها البرامج كبيانات للتعامل معها. عندما نكتب مثل هذه البرامج المجردة، فإننا نقوم بالبرمجة الوصفية.
4.1 البيانات الوصفية
غالبًا ما يتم ذكر البيانات التعريفية في مستندات Nest. قد يكون مفهوم البيانات التعريفية مربكًا عندما تراها للمرة الأولى، ويجب عليك الاعتياد عليها وفهمها مع مرور الوقت، لذلك لا تحتاج إلى الاعتياد عليها متشابكا.
تعريف البيانات الوصفية هو: البيانات التي تصف البيانات، وبشكل أساسي المعلومات التي تصف سمات البيانات، ويمكن أيضًا فهمها على أنها بيانات تصف البرامج.
تعد exports、providers、imports、controllers
التي تم تكوينها بواسطة @Module في Nest كلها بيانات تعريف ، لأنها بيانات تستخدم لوصف علاقات البرامج. معلومات البيانات هذه ليست البيانات الفعلية المعروضة للمستخدم النهائي، ولكن يتم قراءتها والتعرف عليها بواسطة برنامج الإطار.
4.2 Nest Decorator
إذا نظرت إلى الكود المصدري لمصمم الديكور في Nest، فستجد أن كل مصمم ديكور نفسه يحدد فقط البيانات الوصفية من خلال البيانات الوصفية العاكسة.
@وظيفة تصدير الديكور القابلة للحقن
Injectable(options?: InjectableOptions): ClassDecorator { العودة (الهدف: كائن) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }
هناك مفهوم الانعكاس هنا، ومن السهل نسبيًا فهم الانعكاس. خذ مصمم @Module كمثال لتحديد providers
البيانات الوصفية، فهو يقوم فقط بتمرير الفئات إلى مصفوفة providers
، وعندما يتم تشغيل البرنامج فعليًا، فإن الفئات الموجودة في providers
ستفعل ذلك يتم استخدامه تلقائيًا بواسطة برنامج إطار العمل، ويصبح إنشاء مثيل موفرًا، ولا يحتاج المطورون إلى إجراء إنشاء مثيل وحقن التبعية بشكل صريح. تصبح الفئة موفرًا فقط بعد إنشاء مثيل لها في الوحدة النمطية. تنعكس الفئات الموجودة في providers
وتصبح موفرين. إن عكس التحكم هو تقنية الانعكاس المستخدمة.
مثال آخر هو ORM (تعيين علائق الكائنات) في قاعدة البيانات لاستخدام ORM، ما عليك سوى تحديد حقول الجدول، وستقوم مكتبة ORM تلقائيًا بتحويل بيانات الكائن إلى عبارات SQL.
بيانات ثابتة = TableModel.build(); وقت البيانات = 1؛ data.browser = 'الكروم'؛ data.save(); // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":chrome"}]
تستخدم مكتبة ORM تقنية الانعكاس، بحيث يحتاج المستخدمون فقط إلى الانتباه إلى البيانات الميدانية نفسها، والكائن هو انعكاس مكتبة ORM ويصبح عبارة تنفيذ SQL. يحتاج المطورون فقط إلى التركيز على حقول البيانات ولا يحتاجون إلى كتابة SQL.
4.3 تعكس البيانات الوصفية
تعكس البيانات الوصفية عبارة عن مكتبة انعكاس يستخدمها Nest لإدارة البيانات التعريفية. تستخدم البيانات الوصفية العاكسة WeakMap لإنشاء مثيل عام واحد، وتقوم بتعيين البيانات التعريفية للكائن المزخرف والحصول عليها (الفئة، الطريقة، وما إلى ذلك) من خلال أساليب المجموعة والحصول عليها.
// ألقِ نظرة فقط var _WeakMap = !usePolyfill && typeof WeakMap === "function" ? var Metadata = new _WeakMap(); وظيفة تعريف البيانات الوصفية () { OrdinaryDefineOwnMetadata(){ GetOrCreateMetadataMap(){ var targetMetadata = Metadata.get(O); إذا (IsUnified(targetMetadata)) { إذا (! إنشاء) عودة غير محددة؛ targetMetadata = new _Map(); Metadata.set(O, targetMetadata); } var metadataMap = targetMetadata.get(P); إذا (IsUnified(metadataMap)) { إذا (! إنشاء) عودة غير محددة؛ metadataMap = new _Map(); targetMetadata.set(P, metadataMap); } إرجاع خريطة البيانات الوصفية؛ } } }
تقوم البيانات الوصفية العاكسة بتخزين البيانات التعريفية للشخص المُزين في الكائن الفردي العام للإدارة الموحدة. لا تنفذ البيانات الوصفية العاكسة تفكيرًا محددًا، ولكنها توفر مكتبة أدوات للمساعدة في تنفيذ التفكير.
، دعونا نلقي نظرة على الأسئلة السابقة.
لماذا هناك حاجة لعكس السيطرة؟
ما هو حقن التبعية؟
ماذا يفعل مصمم الديكور؟
ما هي مبادئ تنفيذ مقدمي الخدمات والواردات والصادرات في الوحدات النمطية (@Module)؟
1 و 2 أعتقد أنني أوضحت الأمر من قبل، إذا كان لا يزال غامضًا بعض الشيء، أقترح عليك العودة وقراءته مرة أخرى والرجوع إلى بعض المقالات الأخرى لمساعدتك على فهم المعرفة من خلال تفكير المؤلفين المختلفين.
5.1 المشكلة [3 4] نظرة عامة:
يستخدم Nest تقنية الانعكاس لتنفيذ انعكاس التحكم ويوفر إمكانات البرمجة الوصفية. يستخدم المطورون @Module Decorator لتزيين الفئات وتحديد البيانات الوصفية (المزودون/الواردات/الصادرات)، ويتم تخزين البيانات التعريفية في ملف عام. كائن (باستخدام مكتبة بيانات التعريف العاكسة). بعد تشغيل البرنامج، يقوم برنامج التحكم داخل إطار عمل Nest بقراءة وتسجيل شجرة الوحدة النمطية، ومسح البيانات التعريفية وإنشاء مثيل للفئة لتصبح مزودًا، وتوفيرها في جميع الوحدات النمطية وفقًا لتعريف الموفرينالاستيرادالصادرات في بيانات تعريف الوحدة النمطية ابحث عن مثيلات (مقدمي) الفئات التابعة الأخرى للفئة الحالية، وقم بإدخالها من خلال المُنشئ بعد العثور عليها.
تحتوي هذه المقالة على العديد من المفاهيم ولا تقدم تحليلاً مفصلاً للغاية. يستغرق فهم المفاهيم وقتًا طويلاً. إذا لم تفهمها جيدًا في الوقت الحالي، فلا تقلق كثيرًا. حسنًا، هذا كل شيء، لا تزال هذه المقالة تتطلب الكثير من الجهد، ويأمل الأصدقاء الذين يحبونها أن تتمكنوا من الاتصال ثلاث مرات بنقرة واحدة~