Un court article sur la façon dont nous avons rencontré des problèmes de synchronisation du travail entre les équipes de développement client et serveur. Comment nous avons connecté Thrift afin de simplifier l'interaction entre nos équipes.
Qui se soucie de la façon dont nous avons fait cela et des effets «secondaires» que nous avons détectés, veuillez regarder sous le chat.
Contexte
Début 2017, lorsque nous avons démarré un nouveau projet, nous avons choisi EmberJS comme frontend. Ce qui nous a conduit presque automatiquement à travailler sur le schéma REST lors de l'organisation de l'interaction des parties client et serveur de l'application. Parce que
EmberData fournit un outil pratique pour séparer le travail des commandes backend et frontend, et l'utilisation de l'adaptateur vous permet de sélectionner le «protocole» d'interaction.
Au début, tout va bien - Ember nous a fourni la possibilité de mettre en œuvre l'émulation des demandes du serveur. Les données d'émulation des modèles de serveur ont été placées dans des fichiers de fuxtures séparés. Si, quelque part, nous avons commencé à travailler sans utiliser les données Ember, alors Ember vous permet d'écrire un émulateur de gestionnaire de point de terminaison à proximité et de renvoyer ces données. Nous avons convenu que les développeurs principaux devraient apporter des modifications à ces fichiers afin de maintenir les données à jour pour que les développeurs frontaux fonctionnent correctement. Mais comme toujours, quand tout est construit sur des «accords» (et il n'y a pas d'outil pour les vérifier), il arrive un moment où «quelque chose ne va pas».
De nouvelles exigences ont conduit non seulement à l'apparition de nouvelles données sur le client, mais aussi à la mise à jour de l'ancien modèle de données. Ce qui a finalement conduit au fait que le maintien de la synchronisation des modèles sur le serveur et sur son émulation dans les fichiers sources du client est devenu tout simplement coûteux. Maintenant, le développement de la partie client, en règle générale, commence après que le stub du serveur est prêt. Et le développement est effectué au-dessus du serveur de production, ce qui complique le travail d'équipe et augmente le temps de publication de nouvelles fonctionnalités.
Développement de projet
Maintenant, nous abandonnons EmberJS au profit de VueJS. et dans le cadre de la décision sur la migration, nous avons commencé à chercher des solutions à ce problème. Les critères suivants ont été développés:
- Compatibilité avec les versions plus anciennes et plus récentes du protocole
- Confort maximal pour les développeurs frontaux lorsqu'ils travaillent «sans serveur»
- Séparation de la description de l'API des données de test
- Synchronisation de signature d'appel facile
- description claire de la signature
- facilité de modification par les développeurs frontend et backend
- autonomie maximale
- Des API fortement typées sont souhaitables. C'est-à-dire la détection la plus rapide d'un fait d'un changement de protocole
- Facilité de tester la logique du serveur
- Intégration avec Spring côté serveur sans danser avec des tambourins.
Implémentation
Après réflexion, il a été décidé de s'arrêter à
Thrift . Cela nous a donné un langage de description API simple et clair.
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) }
Pour l'interaction, nous utilisons TMultiplexedProcessor, accessible via TServlet, en utilisant TJSONProtocol. J'ai dû danser un peu pour que cette Thrift s'intègre parfaitement avec Spring. Pour ce faire, j'ai dû créer et enregistrer un Servlet dans ServletContainer par programmation.
@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 } }
Ce qu'il faut noter ici. Dans ce code, deux zones de service sont formées. Protégé, disponible à l'adresse "/ api / v2 / thrift". Et ouvert, disponible sur "/ api / v2 / thrift-ns". Pour ces zones, différents filtres sont utilisés. Dans le premier cas, lors de l'accès au service par des cookies, un objet est formé qui définit l'utilisateur qui fait l'appel. S'il est impossible de former un tel objet, une erreur 401 est levée, qui est correctement traitée côté client. Dans le deuxième cas, le filtre ignore toutes les demandes de service, et s'il détermine qu'une autorisation a eu lieu, puis une fois l'opération terminée, il remplit les cookies avec les informations nécessaires pour qu'il puisse faire des demandes à la zone protégée.
Pour connecter un nouveau service, vous devez écrire un petit code supplémentaire.
@Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler)
Et enregistrez le processeur
@Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() }
La dernière partie du code peut être simplifiée en suspendant une interface supplémentaire sur tous les processeurs, ce qui vous permettra d'obtenir immédiatement une liste de processeurs avec un paramètre de concepteur et d'avoir confié la responsabilité de la valeur de la clé d'accès au processeur au processeur lui-même.
Le travail en mode "sans serveur" a subi un petit changement. Les développeurs de la partie frontale ont proposé de travailler sur un serveur de stub PHP. Ils génèrent eux-mêmes des classes pour leur serveur qui implémentent la signature pour la version de protocole souhaitée. Et ils implémentent un serveur avec l'ensemble de données nécessaire. Tout cela leur permet de travailler avant que les développeurs côté serveur ne terminent leur travail.
Le principal point de traitement côté client est le plugin d'épargne que nous avons écrit.
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), } } }
Pour que ce plugin fonctionne correctement, vous devez connecter les classes générées.
L'appel des méthodes serveur sur le client est le suivant:
thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) })
Ici, je ne me penche pas sur les fonctionnalités de VueJS lui-même, où il est correct de conserver le code qui appelle le serveur. Ce code peut être utilisé à l'intérieur du composant et à l'intérieur de la route et à l'intérieur de Vuex-action.
Lorsque vous travaillez avec le client, il y a quelques limitations qui doivent être prises en compte après la migration mentale de l'intégration d'épargne interne.
- Le client Javascript ne reconnaît pas les valeurs nulles. Par conséquent, pour les champs qui peuvent être nuls, vous devez spécifier l'indicateur facultatif. Dans ce cas, le client acceptera correctement cette valeur.
- Javascript ne sait pas comment travailler avec des valeurs longues, donc tous les identifiants entiers doivent être convertis en chaîne côté serveur
Conclusions
La transition vers Thrift nous a permis de résoudre les problèmes présents dans l'interaction entre le développement serveur et client lors du travail sur une ancienne version de l'interface. Autorisé à rendre possible le traitement des erreurs globales en un seul endroit.
Dans le même temps, avec un bonus supplémentaire, en raison du typage strict de l'API, et donc des règles strictes de sérialisation / désérialisation des données, nous avons reçu une augmentation de ~ 30% du temps d'interaction sur le client et le serveur pour la plupart des requêtes (lors de la comparaison des mêmes requêtes via l'interaction REST et THRIFT, à partir du moment où la demande a été envoyée au serveur jusqu'à la réception de la réponse)