Ein kurzer Artikel darüber, wie wir auf Probleme bei der Synchronisierung der Arbeit zwischen Client- und Server-Entwicklungsteams gestoßen sind. Wie wir Thrift verbunden haben, um die Interaktion zwischen unseren Teams zu vereinfachen.
Wen interessiert es, wie wir das gemacht haben und welche „Nebenwirkungen“ wir haben, schauen Sie bitte unter die Katze.
Hintergrund
Anfang 2017, als wir ein neues Projekt starteten, haben wir EmberJS als Frontend ausgewählt. Dies führte fast automatisch dazu, dass wir beim Organisieren der Interaktion der Client- und Serverteile der Anwendung am REST-Schema arbeiteten. Weil
EmberData bietet ein praktisches Tool zum Trennen der Arbeit der Backend- und Frontend-Befehle. Mit dem Adapter können Sie das „Protokoll“ der Interaktion auswählen.
Anfangs war alles in Ordnung - Ember bot uns die Möglichkeit, die Emulation von Serveranforderungen zu implementieren. Daten zum Emulieren von Servermodellen wurden in separate fuxtures-Dateien abgelegt. Wenn wir irgendwo angefangen haben zu arbeiten, verwende ich keine Ember-Daten. Mit Ember können wir einen Endpoint-Handler-Emulator in der Nähe schreiben und diese Daten zurückgeben. Wir hatten eine Vereinbarung getroffen, dass Backend-Entwickler Änderungen an diesen Dateien vornehmen sollten, um die Daten auf dem neuesten Stand zu halten, damit die Frontend-Entwickler ordnungsgemäß arbeiten können. Aber wie immer, wenn alles auf „Vereinbarungen“ basiert (und es kein Werkzeug gibt, um sie zu überprüfen), kommt der Moment, in dem „etwas schief geht“.
Neue Anforderungen führten nicht nur zum Erscheinen neuer Daten auf dem Client, sondern auch zur Aktualisierung des alten Datenmodells. Was letztendlich dazu führte, dass die Aufrechterhaltung der Synchronisation von Modellen auf dem Server und ihrer Emulation in den Client-Quelldateien einfach teuer wurde. Jetzt beginnt die Entwicklung des Client-Teils in der Regel, nachdem der Server-Stub fertig ist. Die Entwicklung erfolgt auf dem Produktionsserver. Dies erschwert die Teamarbeit und verlängert die Release-Zeit für neue Funktionen.
Projektentwicklung
Jetzt geben wir EmberJS zugunsten von VueJS auf. Im Rahmen der Migrationsentscheidung suchten wir nach Lösungen für dieses Problem. Folgende Kriterien wurden entwickelt:
- Kompatibilität mit älteren und neueren Versionen des Protokolls
- Maximaler Komfort für Frontend-Entwickler bei der Arbeit „ohne Server“
- Trennung der API-Beschreibung von den Testdaten
- Einfache Synchronisierung der Anrufsignatur
- klare Unterschriftenbeschreibung
- Einfache Änderung durch Frontend- und Backend-Entwickler
- maximale Autonomie
- Stark typisierte APIs sind wünschenswert. Das heißt, die schnellste Erkennung einer Tatsache einer Protokolländerung
- Einfaches Testen der Serverlogik
- Integration mit Spring auf der Serverseite, ohne mit Tamburinen zu tanzen.
Implementierung
Nach dem Nachdenken wurde beschlossen, bei
Thrift anzuhalten. Dies gab uns eine einfache und klare API-Beschreibungssprache.
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) }
Für die Interaktion verwenden wir TMultiplexedProcessor, auf den über TServlet mit TJSONProtocol zugegriffen werden kann. Ich musste ein bisschen tanzen, um diesen Thrift nahtlos in Spring zu integrieren. Dazu musste ich programmgesteuert ein Servlet in ServletContainer erstellen und registrieren.
@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 } }
Was ist hier zu beachten. In diesem Code werden zwei Servicebereiche gebildet. Geschützt, verfügbar unter der Adresse "/ api / v2 / thrift". Und offen, verfügbar unter "/ api / v2 / thrift-ns". Für diese Bereiche werden unterschiedliche Filter verwendet. Im ersten Fall wird beim Zugriff auf den Dienst über Cookies ein Objekt gebildet, das den Benutzer definiert, der den Anruf tätigt. Wenn es nicht möglich ist, ein solches Objekt zu bilden, wird ein 401-Fehler ausgegeben, der auf der Clientseite korrekt verarbeitet wird. Im zweiten Fall überspringt der Filter alle Anforderungen für den Dienst. Wenn er feststellt, dass eine Autorisierung erfolgt ist, füllt er die Cookies nach Abschluss des Vorgangs mit den erforderlichen Informationen, damit Anforderungen an den geschützten Bereich gestellt werden können.
Um einen neuen Dienst zu verbinden, müssen Sie einen zusätzlichen Code schreiben.
@Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler)
Und registrieren Sie den Prozessor
@Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() }
Der letzte Teil des Codes kann vereinfacht werden, indem an allen Prozessoren eine zusätzliche Schnittstelle aufgehängt wird, mit der Sie sofort eine Liste der Prozessoren mit einem Designerparameter abrufen und dem Prozessor selbst die Verantwortung für den Wert des Zugriffsschlüssels für den Prozessor übertragen können.
Die Arbeit im Modus "Kein Server" hat sich geringfügig geändert. Die Entwickler des Frontend-Teils machten einen Vorschlag, dass sie auf einem PHP-Stub-Server arbeiten würden. Sie selbst generieren Klassen für ihren Server, die die Signatur für die gewünschte Protokollversion implementieren. Und sie implementieren einen Server mit dem notwendigen Datensatz. All dies ermöglicht es ihnen zu arbeiten, bevor die Entwickler der Serverseite ihre Arbeit beenden.
Der Hauptverarbeitungspunkt auf der Client-Seite ist das von uns geschriebene Thrift-Plugin.
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), } } }
Damit dieses Plugin ordnungsgemäß funktioniert, müssen Sie die generierten Klassen verbinden.
Das Aufrufen von Servermethoden auf dem Client lautet wie folgt:
thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) })
Hier gehe ich nicht auf die Funktionen von VueJS selbst ein, bei denen es richtig ist, den Code beizubehalten, der den Server aufruft. Dieser Code kann sowohl innerhalb der Komponente als auch innerhalb der Route und innerhalb der Vuex-Aktion verwendet werden.
Bei der Arbeit mit der Client-Seite gibt es einige Einschränkungen, die nach der mentalen Migration von der internen Sparsamkeitsintegration berücksichtigt werden müssen.
- Der Javascript-Client erkennt keine Nullwerte. Daher müssen Sie für Felder, die null sein können, das optionale Flag angeben. In diesem Fall akzeptiert der Client diesen Wert korrekt.
- Javascript kann nicht mit langen Werten arbeiten, daher müssen alle Ganzzahlbezeichner auf der Serverseite in Zeichenfolgen umgewandelt werden
Schlussfolgerungen
Durch den Übergang zu Thrift konnten wir die Probleme lösen, die bei der Interaktion zwischen Server- und Cliententwicklung bei der Arbeit an einer alten Version der Schnittstelle auftreten. Erlaubt die Verarbeitung globaler Fehler an einem Ort.
Gleichzeitig erhielten wir mit einem zusätzlichen Bonus aufgrund der strengen Typisierung der API und damit der strengen Regeln für die Serialisierung / Deserialisierung von Daten für die meisten Anfragen eine Erhöhung der Interaktionszeit auf Client und Server um ~ 30% (beim Vergleich derselben Anfragen durch REST- und THRIFT-Interaktion). ab dem Zeitpunkt, an dem die Anforderung an den Server gesendet wurde, bis die Antwort empfangen wurde)