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

فاسيا هي المسؤولة عن الواجهة الأمامية ، Fedya - عن الواجهة الخلفية. خط تحديد المسؤولية يمتد أفقيا. يحتوي هذا المخطط على عيوب أي بنية رأسية - من الصعب توسيع نطاقه ولديه تسامح منخفض مع الخطأ. إذا كان المشروع يتوسع ، فسيتعين عليك اتخاذ خيار صعب: من الذي سيعمل على تقوية Vasya أو Fedya؟ أو إذا مرضت Fedya أو تركته ، فلن يتمكن Vasya من استبداله.
يسمح لك النهج المقترح هنا بتوسيع خط تقسيم المسؤولية بمقدار 90 درجة وتحويل الهيكل العمودي إلى أفقي.

مثل هذه البنية أسهل في القياس وأكثر تسامحًا مع الأخطاء. فازيا و Fedya أصبحت قابلة للتبديل.
من الناحية النظرية ، يبدو جيدًا ، دعونا نحاول تنفيذ كل هذا في الممارسة العملية ، دون أن نفقد كل ما يمنحنا الوجود المنفصل للعميل والخادم على طول الطريق.
بيان المشكلة
ليس لدينا مطلقًا خادم عميل متكامل في المنتج. على العكس من ذلك ، فإن مثل هذا القرار سيكون ضارًا للغاية من جميع وجهات النظر. تتمثل المهمة في أنه في عملية التطوير ، سيكون لدينا قاعدة كود واحدة لنماذج البيانات للواجهة الخلفية والواجهة الأمامية ، لكن الإخراج سيكون عميلًا وخادمًا مستقلين. في هذه الحالة ، سوف نحصل على جميع مزايا النهج القياسي ونكتسب وسائل الراحة المذكورة أعلاه لتطوير ودعم المشروع.
الحل
لقد جربت عملية دمج العميل والخادم في ملف واحد لبعض الوقت. حتى وقت قريب ، كانت المشكلة الرئيسية هي أنه في JS القياسية ، كان الاتصال بوحدات الطرف الثالث على العميل والخادم مختلفًا جدًا: يتطلب (...) في node.js ، كل سحر AJAX على العميل. لقد تغير كل شيء مع ظهور وحدات ES. في المتصفحات الحديثة ، تم دعم "الاستيراد" لفترة طويلة. Node.js متأخرة قليلاً في هذا الصدد ويتم دعم الوحدات النمطية ES فقط مع تمكين علامة "--experimental-modules". من المأمول أنه في المستقبل المنظور ، ستنجح الوحدات النمطية في منطقة العقدة. بالإضافة إلى ذلك ، من غير المرجح أن يتغير شيء كثيرًا ، لأن في المتصفحات ، كانت هذه الوظيفة تعمل بشكل افتراضي لفترة طويلة. أعتقد أنه يمكنك الآن استخدام وحدات ES-ليس فقط على العميل ولكن أيضًا من جانب الخادم (إذا كان لديك حجج مضادة بشأن هذا الموضوع ، فاكتب في التعليقات).
مخطط الحل يشبه هذا:

يحتوي المشروع على ثلاثة كتالوجات رئيسية:
الخلفية
المحمية ؛
الواجهة
العامة ؛
مشترك - المشتركة نماذج خادم العميل.
تراقب عملية المراقبة المنفصلة الملفات الموجودة في الدليل المشترك ، ومع أي تغييرات ، تقوم بإنشاء إصدارات من الملف الذي تم تغييره بشكل منفصل للعميل وبشكل منفصل عن الخادم (في الدلائل المحمية / المشتركة والعامة / المشتركة).
التنفيذ
النظر في مثال بسيط في الوقت الحقيقي رسول. سنحتاج إلى node.js جديدة (لدي الإصدار 11.0.0) و Redis (تثبيتها غير مغطى هنا).
استنساخ مثال:
git clone https://github.com/Kolbaskin/both-example cd ./both-example npm i
تثبيت وتشغيل عملية المراقبة (مراقب في الرسم التخطيطي):
npm i both-js -g both ./index.mjs
إذا كان كل شيء على ما يرام ، فسيقوم المراقب بتشغيل خادم الويب ويبدأ في مراقبة التغييرات على الملفات الموجودة في الدلائل المشتركة والمحمية. عند إجراء تغييرات على المشاركة ، يتم إنشاء إصدارات متوافقة من نماذج البيانات للعميل وللخادم. عند إجراء تغييرات على الحماية ، سيقوم المراقب تلقائيًا بإعادة تشغيل خادم الويب.
يمكنك رؤية أداء برنامج المراسلة في المتصفح بالنقر فوق الارتباط
http://localhost:3000/index.html?token=123&user=Vasya
(الرمز المميز والمستخدم تعسفي). لمحاكاة عدة مستخدمين ، افتح نفس الصفحة في متصفح آخر عن طريق تحديد رمز مميز ومستخدم مختلف.
الآن رمز صغير.
خادم الويب
المحمية / server.mjs
import express from 'express'; import bodyParser from 'body-parser';
هذا هو خادم سريع منتظم ، لا يوجد شيء مثير للاهتمام هنا. ملحق mjs مطلوب لوحدات ES في node.js. للتناسق ، سوف نستخدم هذا التمديد للعميل.
العملاء
public / index.html
<!DOCTYPE html> <html lang="en"> <head> ... <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="/main.mjs" type="module"></script> </head> <body> ... <ul id="users"> <li v-for="user in users"> {{ user.name }} ({{user.id}}) </li> </ul> <div id="messages"> <div> <input type="text" v-model="msg" /> <button v-on:click="sendMessage()"></button> </div> <ul> <li v-for="message in messages">[{{ message.date }}] <strong>{{ message.text }}</strong></li> </ul> </div> </body> </html>
على سبيل المثال ، أستخدم Vue على العميل ، لكن هذا لا يغير الجوهر. بدلاً من Vue ، يمكن أن يكون هناك أي شيء يمكنك فيه فصل نموذج البيانات إلى فصل منفصل (الضربة القاضية ، الزاوي).
العامة / الرئيسية
main.mjs هو البرنامج النصي الذي يربط نماذج البيانات مع وجهات النظر المقابلة. لتبسيط التعليمات البرمجية ، يتم إنشاء نماذج من العروض التقديمية لقائمة المستخدمين النشطين وموجز الرسائل في index.html مباشرةً
نموذج البيانات
مشترك / رسائل / نموذج / dataModel.mjs
تقوم هذه الطرق المتعددة بتنفيذ جميع وظائف إرسال واستقبال الرسائل في الوقت الفعلي. تخبر التوجيهات! #Client و! #Server عملية المراقبة بالطريقة التي يتم من خلالها تحديد الجزء (العميل أو الخادم). في حالة عدم وجود هذه التوجيهات قبل تحديد الطريقة ، تكون هذه الطريقة متاحة على كل من العميل والخادم. تعتبر علامات التعليق المائلة قبل التوجيه اختيارية وموجودة فقط لمنع IDE القياسي من أداء اليمين عند أخطاء بناء الجملة.
يستخدم السطر الأول من المسار البحث & الجذر. عند إنشاء إصدارات العميل والخادم ، سيتم استبدال & الجذر بالمسار النسبي إلى الأدلة العامة والمحمية ، على التوالي.
نقطة مهمة أخرى: من طريقة العميل ، يمكنك الاتصال فقط بطريقة الخادم ، والتي يبدأ اسمها بـ "$":
...
يتم ذلك لأسباب أمنية: من الخارج يمكنك فقط اللجوء إلى أساليب مصممة خصيصًا لهذا الغرض.
دعونا نلقي نظرة على إصدارات نماذج البيانات التي أنشأها المراقب للعميل والخادم.
عميل (عام / مشترك / رسائل / نموذج / dataModel.mjs)
import Base from '/lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"}
من جانب العميل ، يكون النموذج من سلالة فئة Vue (عبر Base.mjs). وبالتالي ، يمكنك العمل معها كما هو الحال مع نموذج بيانات Vue العادي. أضاف المراقب أسلوب __getFilePath__ إلى إصدار العميل من النموذج ، والذي يُرجع المسار إلى ملف الفصل واستبدل رمز أسلوب خادم send sendessage ببنية ، في الأساس ، سوف تستدعي الطريقة التي نحتاجها على الخادم من خلال آلية rpc (__runSharedFunction معرّفة في الفئة الأصل).
خادم (محمي / مشترك / رسائل / نموذج / dataModel.mjs)
import Base from '../../lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"} ... ...
في إصدار الخادم ، تمت إضافة طريقة __getFilePath__ وتمت إزالة أساليب العميل التي تحمل علامة التوجيه!
في كلا الإصدارين من النموذج ، يتم استبدال كل الصفوف المحذوفة بصيغ فارغة. يتم ذلك بحيث يمكن بسهولة العثور على رسالة الخطأ على مصحح الأخطاء السطر في مشكلة التعليمات البرمجية المصدر للنموذج.
التفاعل بين العميل والخادم
عندما نحتاج إلى استدعاء بعض أساليب الخادم على العميل ، فإننا نفعل ذلك فقط.
إذا كانت المكالمة داخل نفس النموذج ، فكل شيء بسيط:
... !#client async sendMessage(e) { await this.$sendMessage(this.msg); this.msg = ''; } !#server async $sendMessage(msg) {
يمكنك "سحب" نموذج آخر:
import dataModel from "/shared/messages/model/dataModel.mjs"; var msg = new dataModel(); msg.$sendMessage('blah-blah-blah');
في الاتجاه المعاكس ، أي استدعاء بعض طريقة العميل على الخادم لا يعمل. من الناحية الفنية ، هذا ممكن ، ولكن من الناحية العملية لا معنى له ، لأنه الخادم واحد ، ولكن هناك العديد من العملاء. إذا احتجنا إلى بدء بعض الإجراءات على الخادم على العميل ، فإننا نستخدم آلية الحدث:
تأخذ طريقة fireEvent 3 معلمات: اسم الحدث ، ومعالجته ، والبيانات. يمكنك تعيين المستلم بعدة طرق: الكلمة الأساسية "الكل" - سيتم إرسال الحدث إلى جميع المستخدمين أو في الصفيف لسرد الرموز المميزة للجلسة للعملاء الذين تم توجيه الحدث إليهم.
لم يتم ربط الحدث بمثيل معين من فئة نموذج البيانات وسيتم إطلاق المعالجات في جميع مثيلات الفئة التي تم استدعاء fireEvent فيها.
التحجيم الخلفي الأفقي
يجب أن تفرض قيود غير متجانسة لنماذج خادم العميل في التطبيق المقترح ، للوهلة الأولى ، قيودًا كبيرة على إمكانية القياس الأفقي لجزء الخادم. ولكن هذا ليس كذلك: من الناحية الفنية ، لا يعتمد الخادم على العميل. يمكنك نسخ الدليل "العام" في أي مكان وإعطاء محتوياته عبر أي خادم ويب آخر (nginx ، apache ، إلخ).
يمكن توسيع جانب الخادم بسهولة عن طريق تشغيل مثيلات خلفية جديدة. يتم استخدام نظام قائمة الانتظار Redis و Kue للتفاعل مع الحالات الفردية.
API وعملاء مختلفين إلى خلفية واحدة
في المشروعات الحقيقية ، يمكن لعملاء الخادم المتنوعين استخدام واجهة برمجة تطبيقات خادم واحد - مواقع الويب وتطبيقات الأجهزة المحمولة وخدمات الطرف الثالث. في الحل المقترح ، كل هذا متاح دون أي رقصات إضافية. تحت غطاء محرك السيارة من استدعاء أساليب الخادم هو RPC القديم الجيد. خادم الويب نفسه هو تطبيق سريع كلاسيكي. يكفي إضافة مجمّع للطرق مع استدعاء الأساليب اللازمة لنماذج البيانات نفسها.
آخر النص
لا يتظاهر النهج المقترح في المقالة بأي تغييرات ثورية في تطبيقات خادم العميل. إنه يضيف القليل من الراحة فقط إلى عملية التطوير ، مما يتيح لك التركيز على منطق العمل الذي تم تجميعه في مكان واحد.
هذا المشروع تجريبي ، اكتب التعليقات إذا كان ، في رأيك ، يستحق مواصلة هذه التجربة.