关于我们如何在客户端和服务器开发团队之间同步工作时遇到问题的简短文章。 我们如何连接Thrift以简化团队之间的互动。
谁在乎我们如何做到这一点,以及我们发现了哪些“副作用”,请仔细观察。
背景知识
2017年初,当我们开始一个新项目时,我们选择了EmberJS作为前端。 当组织应用程序的客户端和服务器部分的交互时,几乎自动地使我们致力于REST方案。 因为
EmberData提供了一个方便的工具,用于分离后端和前端命令的工作,并且使用适配器,您可以选择交互的“协议”。
最初,一切都很好-Ember为我们提供了实现服务器请求仿真的功能。 用于模拟服务器模型的数据放在单独的功能文件中。 如果某个地方在不使用Ember Data的情况下开始工作,那么Ember允许您在附近编写端点处理程序仿真器并返回此数据。 我们已达成协议,后端开发人员应对这些文件进行更改,以使数据保持最新状态,以使前端开发人员能够正常工作。 但是像往常一样,当一切都建立在“协议”之上(并且没有检查它们的工具)时,有时会出现“出问题的地方”。
新的需求不仅导致新数据在客户端上的出现,而且导致了旧数据模型的更新。 最终导致了这样一个事实,即在服务器上及其在客户端源文件中的仿真中维持模型的同步变得非常昂贵。 现在,通常在服务器存根准备好后开始进行客户端部分的开发。 而且开发是在生产服务器之上进行的,这使团队工作复杂化并增加了新功能的发布时间。
项目开发
现在我们放弃了EmberJS,转而使用VueJS。 作为迁移决策的一部分,我们开始寻找解决此问题的方法。 制定了以下标准:
- 与旧版和新版协议的兼容性
- 前端开发人员在“无需服务器”的情况下最大的便利
- API描述与测试数据的分离
- 轻松的电话签名同步
- 清晰的签名说明
- 前端和后端开发人员都易于修改
- 最大的自主权
- 强类型的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,可以使用TJSONProtocol通过TServlet访问。 为了使Thrift与Spring无缝集成,我不得不花点时间跳舞。 为此,我必须以编程方式在ServletContainer中创建并注册一个Servlet。
@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 / thrift”中找到。 并打开“ / api / v2 / thrift-ns”。 对于这些区域,使用了不同的过滤器。 在第一种情况下,当通过cookie访问服务时,将形成一个对象,该对象定义进行呼叫的用户。 如果不可能形成这样的对象,则会抛出401错误,并在客户端进行正确处理。 在第二种情况下,过滤器将跳过对服务的所有请求,并且如果确定已发生授权,则在操作完成后,它将用必要的信息填充cookie,以便可以向保护区发出请求。
要连接新服务,您必须编写一些额外的代码。
@Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler)
并注册处理器
@Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() }
通过在所有处理器上悬挂一个附加接口可以简化代码的最后一部分,这将使您立即获得具有一个设计器参数的处理器列表,并负责处理器对处理器本身的访问密钥的值。
在“无服务器”模式下的工作进行了一些更改。 前端部分的开发人员提出了一个建议,使其可以在PHP存根服务器上工作。 他们自己为服务器生成类,这些类实现所需协议版本的签名。 他们用必要的数据集实施服务器。 所有这些使他们能够在服务器端的开发人员完成工作之前进行工作。
客户端的主要处理点是我们编写的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), } } }
为了使该插件正常工作,您需要连接生成的类。
在客户端上调用服务器方法如下:
thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) })
在这里,我不会深入研究VueJS本身的功能,在这些功能中保留调用服务器的代码是正确的。 该代码既可以在组件内部也可以在路由内部使用,也可以在Vuex-action内部使用。
与客户端一起工作时,从内部节约集成进行智力迁移后,必须考虑一些限制。
- Javascript客户端无法识别空值。 因此,对于可以为空的字段,必须指定可选标志。 在这种情况下,客户端将正确接受此值。
- Javascript不知道如何使用长值,因此必须在服务器端将所有整数标识符都强制转换为字符串
结论
向Thrift的过渡使我们能够解决在使用旧版接口时服务器与客户端开发之间的交互中存在的问题。 允许在一处处理全局错误。
同时,由于API的严格类型以及严格的数据序列化/反序列化规则,额外的好处是,对于大多数请求,在客户端和服务器上的交互时间(在通过REST和THRIFT交互比较相同的请求时,从请求发送到服务器的时间到收到响应为止)