في هذه المقالة ، أريد أن أوضح كيف يتم إنشاء رمز للواجهة الخلفية (وقليلًا من الواجهة الأمامية) في شركتنا ، ولماذا هو مطلوب على الإطلاق وكيف نفعل ذلك بشكل أفضل.
بالضبط ما سوف تولد ليست مهمة جدا.
من المهم أن نصف ثلاثة أنواع من الكائنات التي سنقوم على أساسها بتكوين تفاعل الواجهة الأمامية مع الواجهة الخلفية ، وفي بعض الأماكن سيكون تنفيذ الواجهة الخلفية بالكامل
هذه الأنواع من الكائنات:
1.
الرسائل - الكائنات التي يجري تسلسلها في JSON ، تشارك في تبادل المعلومات بين الواجهة الأمامية والخلفية
2.
نقاط النهاية - URI التي تستدعي الواجهة الأمامية جنبًا إلى جنب مع وصف لطريقة HTTP ومعلمات الطلب ونوع طلب الجسم ونوع المستجيب
3.
الكيانات - هذه هي الرسائل التي لها نقاط نهاية قياسية لإنشاء / تحديث / قائمة / حذف (ربما ليس كل شيء) ، يتم تخزينها في قاعدة البيانات ولهم كائن الوصول إلى البيانات ، أو مستودع تخزين JPA الربيع - بشكل عام يعتمد على التكنولوجيا ، ولكن نوعا من الوصول إلى قاعدة البيانات
لا أفعل الواجهة الأمامية على الإطلاق ، لكن
1) أعرف أنه مكتوب في Typescript ، لذلك نقوم أيضًا بإنشاء فصول typescript
2) معظم متطلبات الواجهة الخلفية تأتي من مطوري الواجهة الأمامية.
متطلبات الرمز
إذن ، ما هي المتطلبات من الواجهة الأمامية؟
1. الراحة واجهة التفاعل مثل
2. ردود رتيبة - json ، الحمولة في حقل "البيانات"
3. الأخطاء الرتيبة إذا حدث استثناء على الواجهة الخلفية ، فمن المستحسن إضافة تتبع مكدس
4. أكواد HTTP "الصحيحة" - 404 إذا لم يتم العثور على الكتاب ، و 400 إذا كان هناك طلب سيء (على سبيل المثال ، غير صالح json) ، إلخ.
سأضيف متطلبات إلى رمز الواجهة الخلفية "بمفردي":
1. خطأ في التعامل في مكان واحد
2. القدرة على وقف التدفق في أي مكان في التعليمات البرمجية وإرجاع رمز HTTP المطلوب
3. أريد أن أكتب بعض منطق الأعمال كحجب ، وبعضها غير متزامن ، اعتمادًا على المكتبات المستخدمة. ولكن كل هذا يجب أن يعمل في إطار واحد غير متزامن
4. من المستحسن أن لا يفكر مطورو الواجهة الخلفية في طلبات HTTP واستجاباتهم ، وطرق Vertx وباس الأحداث ، لكنهم ببساطة يكتبون منطق أعمالهم.
من المستحسن تنفيذ جميع المتطلبات المذكورة أعلاه مع الميراث والتكوين ، وفقط في حالة فشلها ، استخدم إنشاء التعليمات البرمجية
يُنصح أيضًا بإنشاء فصول بالتوازي مع typscript و kotlin بحيث تقوم الواجهة الأمامية دائمًا بإرسال الواجهة الخلفية المطلوبة (وعدم الاعتماد على المطورين أنهم لن ينسوا إضافة حقل جديد إلى الفصل)
ماذا سنولد
على سبيل المثال ، خذ تطبيق ويب افتراضيًا يمكنه حفظ الكتب وتعديلها ، وعرض قائمتهم والبحث بالاسم.
من حيث التكنولوجيا ، فإن واجهة Kotlin الخلفية ، Vert.x ، coroutines. شيء مثل ما عرضته في مقال
"ثلاثة نماذج من البرمجة غير المتزامنة في Vertx"لجعلها أكثر إثارة للاهتمام ، سوف نجعل الوصول إلى قاعدة البيانات على أساس Spring Data JPA.
أنا لا أقول إنك تحتاج إلى مزج Spring و Vert.x في مشروع واحد (على الرغم من أنني أقوم بذلك بنفسي ، أعترف بذلك) ، لكنني آخذ Spring لأن من الأسهل بالنسبة له إظهار الجيل استنادًا إلى الكيانات.
هيكل المشروع مع الجيل
الآن تحتاج إلى إنشاء مشروع للجيل.
سيكون لدينا العديد من المشاريع المهد. الآن سوف أقوم بإعدادها في مستودع واحد ، ولكن في الحياة الواقعية يجب أن يجلس الجميع بمفرده ، لأنهم سيتغيرون في أوقات مختلفة ، وسيكون لديهم إصداراتهم الخاصة.
لذلك ، فإن المشروع الأول هو مشروع يحتوي على تعليقات توضيحية ستشير إلى أجهزة التوجيه الخاصة بنا وطرق HTTP ، إلخ. نسميها
metainfoيعتمد مشروعان آخران على ذلك:
codegen و
apiيحتوي
api على أوصاف لأجهزة التوجيه والرسائل - تلك الفئات التي ستذهب جيئة وذهابا بين الواجهة الخلفية والواجهة الأمامية
codegen هو مشروع لإنشاء الكود (ولكن ليس مشروعًا يتم فيه إنشاء الكود!) - يحتوي على مجموعة من المعلومات من فئات
api ومولدات الأكواد الفعلية.
ستتلقى المولدات جميع تفاصيل التوليد في الوسائط - بدءًا من الحزمة التي تأخذ أوصاف الموجهات ، وإلى أي دليل يتم إنشاؤه ، وما اسم قالب السرعة الذي سيتم إنشاؤه - أي يمكن استخدام
metainfo و
codegen بشكل عام في مشاريع مختلفة تمامًا
حسنًا ، هناك مشروعان سيتم فيهما تنفيذ الجيل:
تم إنشاؤه في
الواجهة الأمامية والذي سنقوم بإنشاء فئة Typescript التي تتوافق مع رسائل kotlin لدينا
والخلفية - مع تطبيق Vertx الفعلي.
لكي "يرى" مشروع ما نتيجة تجميع مشروع آخر ، سنستخدم المكوّن الإضافي لنشر القطع الأثرية في مستودع Maven المحلي.
مشروع Metafinfo :
التعليقات التوضيحية التي سنعرف بها مصادر الجيل - أوصاف نقاط النهاية والرسائل والكيانات:
annotation class EndpointController(val url:String) annotation class Endpoint(val method: HttpMethodName, val param: String = "") annotation class EmptyConstructorMessage /* Make abstract implementation method for endpoint logic asynchronous */ annotation class AsyncHandler /* All the next annotations are for Entities only:*/ annotation class GenerateCreate annotation class GenerateUpdate annotation class GenerateGetById annotation class GenerateList annotation class GenerateDelete /* Make CRUD implementation abstract, so that we will override it*/ annotation class AbstractImplementation /* Generate search by this field in DAO layer */ annotation class FindBy /* This entity is child of another entity, so generate end point like /parent/$id/child to bring all children of concrete parent instead of /child - bring all entities of this type */ annotation class ChildOf(vararg val parents: KClass<*>) enum class HttpMethodName { POST,PUT,GET,DELETE }
بالنسبة لفئات Typescript ، سنحدد التعليقات التوضيحية التي يمكن تعليقها على الحقول والتي ستندرج في فئة Typescript التي تم إنشاؤها
annotation class IsString annotation class IsEmail annotation class IsBoolean annotation class MaxLength(val len:Int)
شفرة المصدر لمشروع metainfoمشروع Api :لاحظ الإضافات noArg و jpa في build.gradle لإنشاء مُنشئات بدون وسيطات
ليس لدي ما يكفي من الخيال ، لذلك سننشئ بعض الأوصاف المجنونة لوحدات التحكم والكيانات لتطبيقنا:
@EndpointController("/util") interface SearchRouter { @Endpoint(HttpMethodName.GET, param = "id") fun search(id: String): String @Endpoint(method = HttpMethodName.POST) @AsyncHandler fun search(searchRequest: SearchRequest)
Api شفرة المصدر للمشروعمشروع Codegen :أولاً نحدد "الواصفات" - تلك الفئات التي سنملؤها من خلال التفكير في مشروع api الخاص بنا:
data class EndPoint( val url: String, val input: String?, val param: String?, val output: String, val method: String, val handler: String, val asyncHandler: Boolean ) data class Router(val name: String, val url: String, val endpoints: List<EndPoint>) class Entity( name: String, val parents: List<String>, val abstractVerticle: Boolean, val crudFeatures: CrudFeatures, fields: List<Field>, var children: List<Entity> ) : Message(name, fields) { fun shouldGenerateRouterAndVerticle(): Boolean { return crudFeatures.generateRouterAndVerticle() } override fun toString(): String { return "Entity(parents=$parents, abstractVerticle=$abstractVerticle, crudFeatures=$crudFeatures, children=$children)" } } data class CrudFeatures( val list: Boolean, val create: Boolean, val update: Boolean, val delete: Boolean, val get: Boolean ) { fun generateRouterAndVerticle(): Boolean { return list || create || update || delete || get } } open class Message(val name: String, val fields: List<Field>) data class Field(val name: String, val type: String, val validators: List<Annotation>, val findBy: Boolean)
يبدو الرمز الذي يجمع المعلومات كما يلي:
class EntitiesCreator(typeMapper: TypeMapper, frontendAnnoPackage:String) { private val messagesDescriptor = MessagesCreator(typeMapper, frontendAnnoPackage) fun createEntities(entitiesPackage: String): List<Entity> { val reflections = Reflections(entitiesPackage, SubTypesScanner(false)) val types = reflections.getSubTypesOf(Object::class.java) return types.map { createEntity(it) } } fun createEntityRestEndpoints(entity: Entity): List<EndPoint> { val name = entity.name val url = name.toLowerCase() val endpoints: MutableList<EndPoint> = mutableListOf() if (entity.crudFeatures.create) { endpoints.add( EndPoint(url, name, null, name, "post", "handleNew$name", false) ) } if (entity.crudFeatures.get) { endpoints.add( EndPoint( "$url/:id", null, "id", name, "get", "handleGet$name", false ) ) } if (entity.crudFeatures.update) { endpoints.add( EndPoint(url, name, null, name, "put", "handleUpdate$name", false) ) } if (entity.crudFeatures.delete) { endpoints.add( EndPoint( "$url/:id", null, "id", "", "delete", "handleDelete$name", false ) ) } if (entity.crudFeatures.list) { if (entity.parents.isEmpty()) { endpoints.add( EndPoint( url, null, null, "List<$name>", "get", "handleGetAllFor$name", false ) ) } } entity.children.forEach { endpoints.add( EndPoint( "$url/:id/${it.name.toLowerCase()}", null, "id", "List<$name>", "get", "handleGet${it.name}For$name", false ) ) } return endpoints } private fun createEntity(aClass: Class<*>): Entity { return Entity( aClass.simpleName, getParents(aClass), isVerticleAbstract(aClass), shouldGenerateCrud(aClass), messagesDescriptor.createFields(aClass), listOf() ) } private fun isVerticleAbstract(aClass: Class<*>): Boolean { return aClass.getDeclaredAnnotation(AbstractImplementation::class.java) != null } private fun getParents(aClass: Class<*>): List<String> { return aClass.getDeclaredAnnotation(ChildOf::class.java)?.parents?.map { it.simpleName }?.requireNoNulls() ?: listOf() } private fun shouldGenerateCrud(aClass: Class<*>): CrudFeatures { val listAnno = aClass.getDeclaredAnnotation(GenerateList::class.java) val createAnno = aClass.getDeclaredAnnotation(GenerateCreate::class.java) val getAnno = aClass.getDeclaredAnnotation(GenerateGetById::class.java) val updateAnno = aClass.getDeclaredAnnotation(GenerateUpdate::class.java) val deleteAnno = aClass.getDeclaredAnnotation(GenerateDelete::class.java) return CrudFeatures( list = listAnno != null, create = createAnno != null, update = updateAnno != null, delete = deleteAnno != null, get = getAnno != null ) } } class MessagesCreator(private val typeMapper: TypeMapper, private val frontendAnnotationsPackageName: String) { fun createMessages(packageName: String): List<Message> { val reflections = Reflections(packageName, SubTypesScanner(false)) return reflections.allTypes.map { Class.forName(it) }.map { createMessages(it) } } private fun createMessages(aClass: Class<*>): Message { return Message(aClass.simpleName, createFields(aClass)) } fun createFields(aClass: Class<*>): List<Field> { return ReflectionUtils.getAllFields(aClass).map { createField(it) } } private fun createField(field: java.lang.reflect.Field): Field { val annotations = field.declaredAnnotations return Field( field.name, typeMapper.map(field.type), createConstraints(annotations), annotations.map { anno -> anno::annotationClass.get() }.contains(FindBy::class) ) } private fun createConstraints(annotations: Array<out Annotation>): List<Annotation> { return annotations.filter { it.toString().startsWith("@$frontendAnnotationsPackageName") } } } class RoutersCreator(private val typeMapper: TypeMapper, private val endpointsPackage:String ) { fun createRouters(): List<Router> { val reflections = Reflections(endpointsPackage, SubTypesScanner(false)) return reflections.allTypes.map { createRouter( Class.forName( it ) ) } } private fun createRouter(aClass: Class<*>): Router { return Router(aClass.simpleName, getUrl(aClass), ReflectionUtils.getAllMethods(aClass).map { createEndpoint(it) }) } private fun getUrl(aClass: Class<*>): String { return aClass.getAnnotation(EndpointController::class.java).url } private fun getEndPointMethodName(declaredAnnotation: Endpoint?): String { val httpMethodName = declaredAnnotation?.method return (httpMethodName ?: HttpMethodName.GET).name.toLowerCase() } private fun getParamName(declaredAnnotation: Endpoint?): String { val paramName = declaredAnnotation?.param return (paramName ?: "id") } private fun createEndpoint(method: Method): EndPoint { val types = method.parameterTypes val declaredAnnotation: Endpoint? = method.getDeclaredAnnotation(Endpoint::class.java) val methodName = getEndPointMethodName(declaredAnnotation) var url = method.name var input: String? = null var param: String? = null val hasInput = types.isNotEmpty() val handlerName = "$methodName${StringUtils.capitalize(url)}" if (hasInput) { val inputType = types[0] val inputTypeName = typeMapper.map(inputType) val createUrlParameterName = inputType == java.lang.String::class.java if (createUrlParameterName) { param = getParamName(declaredAnnotation) url += "/:$param" } else { input = simpleName(inputTypeName) } } return EndPoint( url, input, param, method.returnType.toString(), methodName, handlerName, isHandlerAsync(method) ) } private fun isHandlerAsync(method: Method): Boolean { val declaredAnnotation: AsyncHandler? = method.getDeclaredAnnotation(AsyncHandler::class.java) return declaredAnnotation != null } private fun simpleName(name: String): String { val index = name.lastIndexOf(".") return if (index >= 0) name.substring(index + 1) else name } }
حسنًا ، هناك أيضًا فئات "رئيسية" تستقبل الوسائط - ما هي الحزم التي يجب أن تنعكس عليها ، أي قوالب السرعة التي يجب استخدامها ، إلخ.
إنها ليست اهتمامات ، يمكنك إلقاء نظرة على كل شيء في المستودع:
شفرة المصدرفي المشروعات التي تم إنشاؤها في الواجهة الأمامية والخلفية ، نقوم بأشياء مماثلة:
1. الاعتماد على
api في وقت الترجمة
2. الاعتماد على
codegen في مرحلة البناء
3. توجد قوالب
الإنشاء في دليل
buildSrc والتي تكون الملفات والرموز مطلوبة فيها ، وهي ضرورية في مرحلة الإنشاء ، ولكن ليس في مرحلة التجميع أو وقت التشغيل. أي يمكننا تغيير نمط الجيل دون إعادة ترجمة مشروع
codegen4. تقوم
الواجهة الأمامية بتجميع Typescript الذي تم إنشاؤه ونشره على مستودع حزمة npm
5. في
الخلفية ، يتم إنشاء أجهزة التوجيه التي ترث من جهاز توجيه مجردة غير ولدت الذي يعرف كيفية التعامل مع أنواع مختلفة من الطلبات. يتم أيضًا إنشاء Vertic Verticles التي يجب أن توارث مع تنفيذ منطق العمل نفسه. بالإضافة إلى ذلك ، يتم إنشاء كل أنواع الأشياء الصغيرة التي لا أريد التفكير فيها كمبرمج - تسجيل برامج الترميز وثوابت العنوان في باس الحدث.
شفرة المصدر
للواجهة الأمامية والخلفيةفي
الواجهة التي تم إنشاؤها في
الواجهة ، يجب عليك الانتباه إلى المكون الإضافي الذي ينشر المصادر التي تم إنشاؤها في مستودع npm. لكي يعمل هذا ، تحتاج إلى وضع IP الخاص بمستودع التخزين في build.gradle ووضع رمز المصادقة الخاص بك في .npmrc
الفئات التي تم إنشاؤها تبدو مثل هذا:
import { IsString, MaxLength, IsDate, IsArray, } from 'class-validator'; import { Type } from 'class-transformer';
إيلاء الاهتمام لشروح المصادقة الطبقة ح.
في المشروع الخلفي ، يتم أيضًا إنشاء مستودعات لـ Spring Data JPA ، من الممكن القول أن مشروع معالجة الرسائل في Verticle يحظر (ويتم تشغيله عبر Vertx.executeBlocking) أو غير متزامن (مع coroutines) ، من الممكن القول أن Verticle الذي تم إنشاؤه لـ Entity هو مجردة ومن ثم هناك إمكانية تجاوز الخطافات التي يتم استدعاؤها قبل وبعد استدعاء الأساليب التي تم إنشاؤها. نشر Verticles تلقائيًا على واجهة حبوب الربيع - كثيرًا ، باختصار ، الكثير من الأشياء الجيدة.
وكل هذا سهل التوسيع - على سبيل المثال ، تعليق قائمة الأدوار في نقاط النهاية وإنشاء فحص لدور المستخدم الذي قام بتسجيل الدخول عند الاتصال بنقطة النهاية ، وأكثر من ذلك بكثير - وهو ما يكفي للخيال.
من السهل أيضًا إنشاء ليس Vertx ، وليس Spring ، ولكن شيئًا آخر - حتى akka-http ، يكفي تغيير القوالب في مشروع الواجهة الخلفية فقط.
اتجاه تطوير آخر ممكن هو توليد المزيد من الواجهة الأمامية.
كل شفرة المصدر
هنا .
بفضل Ildar من الواجهة الأمامية للمساعدة في إنشاء الجيل في مشروعنا وكتابة المقال