مقال قصير حول كيف واجهنا مشاكل في مزامنة العمل بين فرق تطوير العميل والخادم. كيف ربطنا Thrift لتبسيط التفاعل بين فرقنا.
من يهتم كيف فعلنا ذلك ، وما هي الآثار "الجانبية" التي التقطناها ، من فضلك انظر تحت القطة.
الخلفية
في بداية عام 2017 ، عندما بدأنا مشروعًا جديدًا ، اخترنا EmberJS كواجهة أمامية. الأمر الذي أدى بنا تلقائيًا تقريبًا إلى العمل على مخطط REST عند تنظيم تفاعل العميل والخادم من التطبيق. لأن يوفر
EmberData أداة ملائمة لفصل عمل أوامر الواجهة الخلفية
والواجهة الأمامية ، ويسمح لك استخدام المحول بتحديد "بروتوكول" التفاعل.
في البداية ، كل شيء على ما يرام - قدم لنا Ember القدرة على تنفيذ محاكاة طلبات الخادم. تم وضع بيانات لمحاكاة نماذج الخادم في ملفات fuxlets منفصلة. إذا بدأنا العمل في مكان ما دون استخدام Ember Data ، فإن Ember يسمح لك بكتابة محاكي معالج نقطة النهاية في مكان قريب وإعادة هذه البيانات. كان لدينا اتفاق على أنه يجب على مطوري الواجهة الخلفية إجراء تغييرات على هذه الملفات للحفاظ على تحديث البيانات حتى يتمكن مطورو الواجهة الأمامية من العمل بشكل صحيح. ولكن كما هو الحال دائمًا ، عندما يتم بناء كل شيء على "الاتفاقيات" (ولا توجد أداة لفحصها) ، تأتي اللحظة عندما "يحدث خطأ ما".
أدت المتطلبات الجديدة ليس فقط إلى ظهور بيانات جديدة على العميل ، ولكن أيضًا إلى تحديث نموذج البيانات القديم. الأمر الذي أدى في النهاية إلى حقيقة أن الحفاظ على تزامن النماذج على الخادم ومضاهاة في ملفات مصدر العميل أصبح مكلفًا ببساطة. الآن ، يبدأ تطوير جزء العميل ، كقاعدة ، بعد أن يكون كعب الخادم جاهزًا. ويتم التطوير على رأس خادم الإنتاج ، وهذا يعقد عمل الفريق ويزيد من وقت إصدار الوظائف الجديدة.
تطوير المشروع
الآن نحن نتخلى عن EmberJS لصالح VueJS. وكجزء من قرار الهجرة ، بدأنا في البحث عن حلول لهذه المشكلة. تم تطوير المعايير التالية:
- التوافق مع الإصدارات الأحدث و الأحدث من البروتوكول
- الراحة القصوى لمطوري الواجهة الأمامية عند العمل "بدون خادم"
- فصل وصف API عن بيانات الاختبار
- مزامنة سهلة للمكالمات
- وصف توقيع واضح
- سهولة التعديل من قبل مطوري الواجهة الأمامية والخلفية
- أقصى قدر من الاستقلالية
- واجهات برمجة التطبيقات المكتوبة بقوة هي أمر مرغوب فيه. على سبيل المثال أسرع كشف لحقيقة تغيير البروتوكول
- سهولة اختبار منطق الخادم
- التكامل مع Spring على جانب الخادم دون الرقص مع الدف.
التنفيذ
بعد التفكير ، تقرر التوقف عند
Thrift . أعطانا هذا لغة وصف API بسيطة وواضحة.
namespace java ru.company.api namespace php ru.company.api namespace javascrip ru.company.api const string DIRECTORY_SERVICE= "directoryService" exception ObjectNotFoundException{ } struct AdvBreed { 1: string id, 2: string name, 3: optional string title } service DirectoryService { list<AdvBreed> loadBreeds() AdsBreed getAdvBreedById(1: string id) }
للتفاعل ، نستخدم TMultiplexedProcessor ، يمكن الوصول إليه من خلال TServlet ، باستخدام TJSONProtocol. كان علي أن أرقص قليلاً لجعل هذا التوفير يتكامل بسلاسة مع Spring. للقيام بذلك ، اضطررت إلى إنشاء وتسجيل Servlet في ServletContainer برمجياً.
@Component class ThriftRegister : ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, ServletContextAware { companion object { private const val unsecureAreaUrlPattern = "/api/v2/thrift-ns" private const val secureAreaUrlPattern = "/api/v2/thrift" } private var inited = false private lateinit var appContext:ApplicationContext private lateinit var servletContext:ServletContext override fun onApplicationEvent(event: ContextRefreshedEvent) { if (!inited) { initServletsAndFilters() inited = true } } private fun initServletsAndFilters() { registerOpenAreaServletAndFilter() registerSecureAreaServletAndFilter() } private fun registerSecureAreaServletAndFilter() { registerServletAndFilter(SecureAreaServlet::class.java, SecureAreaThriftFilter::class.java, secureAreaUrlPattern) } private fun registerOpenAreaServletAndFilter() { registerServletAndFilter(UnsecureAreaServlet::class.java, UnsecureAreaThriftFilter::class.java, unsecureAreaUrlPattern) } private fun registerServletAndFilter(servletClass:Class<out Servlet>, filterClass:Class<out Filter>, pattern:String) { val servletBean = appContext.getBean(servletClass) val addServlet = servletContext.addServlet(servletClass.simpleName, servletBean) addServlet.setLoadOnStartup(1) addServlet.addMapping(pattern) val filterBean = appContext.getBean(filterClass) val addFilter = servletContext.addFilter(filterClass.simpleName, filterBean) addFilter.addMappingForUrlPatterns(null, true, pattern) } override fun setApplicationContext(applicationContext: ApplicationContext) { appContext = applicationContext } override fun setServletContext(context: ServletContext) { this.servletContext = context } }
ما يجب ملاحظته هنا. في هذا الرمز ، يتم تشكيل منطقتين للخدمة. محمي ، وهو متاح على العنوان "/ api / v2 / التوفير". ومفتوحة ، متاحة على "/ api / v2 / thrift-ns". بالنسبة لهذه المناطق ، يتم استخدام مرشحات مختلفة. في الحالة الأولى ، عند الوصول إلى الخدمة عن طريق ملفات تعريف الارتباط ، يتم تكوين كائن يحدد المستخدم الذي يجري المكالمة. إذا كان من المستحيل تكوين مثل هذا الكائن ، يتم طرح خطأ 401 الذي تتم معالجته بشكل صحيح من جانب العميل. في الحالة الثانية ، يتخطى المرشح جميع طلبات الخدمة ، وإذا قرر أن التفويض قد حدث ، فبعد اكتمال العملية ، يملأ ملفات تعريف الارتباط بالمعلومات الضرورية حتى يتمكن من تقديم الطلبات إلى المنطقة المحمية.
لتوصيل خدمة جديدة ، عليك كتابة رمز إضافي صغير.
@Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler)
وتسجيل المعالج
@Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() }
يمكن تبسيط الجزء الأخير من التعليمات البرمجية عن طريق تعليق واجهة إضافية على جميع المعالجات ، والتي ستسمح لك على الفور بالحصول على قائمة بالمعالجات بمعلمة مصمم واحدة ، وإعطاء المسؤولية عن قيمة مفتاح الوصول إلى المعالج إلى المعالج نفسه.
شهد العمل في وضع "لا يوجد خادم" تغييرًا طفيفًا. قدم مطورو الجزء الأمامي اقتراحًا بأنهم سيعملون على خادم كعب PHP. هم أنفسهم يولدون فئات لخادمهم الذي ينفذ التوقيع لإصدار البروتوكول المطلوب. وينفذون الخادم مع مجموعة البيانات اللازمة. كل هذا يسمح لهم بالعمل قبل أن ينتهي مطورو جانب الخادم من عملهم.
نقطة المعالجة الرئيسية على جانب العميل هي المكوِّن الإضافي الذي كتبه لنا.
import store from '../../store' import { UNAUTHORIZED } from '../../store/actions/auth' const thrift = require('thrift') export default { install (Vue, options) { const DirectoryService = require('./gen-nodejs/DirectoryService') let _options = { transport: thrift.TBufferedTransport, protocol: thrift.TJSONProtocol, path: '/api/v2/thrift', https: location.protocol === 'https:' } let _optionsOpen = { ... } const XHRConnectionError = (_status) => { if (_status === 0) { .... } else if (_status >= 400) { if (_status === 401) { store.dispatch(UNAUTHORIZED) } ... } } let bufers = {} thrift.XHRConnection.prototype.flush = function () { var self = this if (this.url === undefined || this.url === '') { return this.send_buf } var xreq = this.getXmlHttpRequestObject() if (xreq.overrideMimeType) { xreq.overrideMimeType('application/json') } xreq.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200) { self.setRecvBuffer(this.responseText) } else { if (this.status === 404 || this.status >= 500) {... } else {... } } } } xreq.open('POST', this.url, true) Object.keys(this.headers).forEach(function (headerKey) { xreq.setRequestHeader(headerKey, self.headers[headerKey]) }) if (process.env.NODE_ENV === 'development') { let sendBuf = JSON.parse(this.send_buf) bufers[sendBuf[3]] = this.send_buf xreq.seqid = sendBuf[3] } xreq.send(this.send_buf) } const mp = new thrift.Multiplexer() const connectionHostName = process.env.THRIFT_HOST ? process.env.THRIFT_HOST : location.hostname const connectionPort = process.env.THRIFT_PORT ? process.env.THRIFT_PORT : location.port const connection = thrift.createXHRConnection(connectionHostName, connectionPort, _options) const connectionOpen = thrift.createXHRConnection(connectionHostName, connectionPort, _optionsOpen) Vue.prototype.$ThriftPlugin = { DirectoryService: mp.createClient('directoryService', DirectoryService, connectionOpen), } } }
لكي يعمل هذا البرنامج المساعد بشكل صحيح ، تحتاج إلى ربط الفئات التي تم إنشاؤها.
استدعاء أساليب الخادم على العميل كما يلي:
thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) })
هنا لا أخوض في ميزات VueJS نفسها ، حيث يكون من الصحيح الاحتفاظ بالرمز الذي يستدعي الخادم. يمكن استخدام هذا الرمز داخل المكون وداخل المسار وداخل Vuex-action.
عند العمل مع العميل ، هناك بعض القيود التي يجب أخذها في الاعتبار بعد الهجرة العقلية من تكامل الادخار الداخلي.
- عميل جافا سكريبت لا يتعرف على القيم الخالية. لذلك ، بالنسبة للحقول التي يمكن أن تكون فارغة ، يجب عليك تحديد العلامة الاختيارية. في هذه الحالة ، سيقبل العميل هذه القيمة بشكل صحيح.
- لا يعرف جافا سكريبت كيفية العمل مع القيم الطويلة ، لذلك يجب تحويل جميع معرفات الأعداد الصحيحة إلى سلسلة على جانب الخادم
الاستنتاجات
سمح لنا الانتقال إلى Thrift بحل المشكلات الموجودة في التفاعل بين تطوير الخادم والعميل عند العمل على إصدار قديم من الواجهة. يسمح بإمكانية معالجة الأخطاء العالمية في مكان واحد.
في الوقت نفسه ، بمكافأة إضافية ، بسبب الكتابة الصارمة لواجهة برمجة التطبيقات ، وبالتالي القواعد الصارمة لتسلسل / إلغاء تسلسل البيانات ، تلقينا زيادة بنسبة 30٪ تقريبًا في وقت التفاعل على العميل والخادم لمعظم الطلبات (عند مقارنة نفس الطلبات من خلال تفاعل REST و THRIFT ، من وقت إرسال الطلب إلى الخادم حتى استلام الرد)