当前位置: 首页 > 后端技术 > Node.js

走进前端 BFF 之 可以看但没必要的 grpc-node 拦截器操作指南

时间:2023-04-03 12:25:05 Node.js

走进前端BFF,看得见却没必要的grpc-node拦截器操作指南先简单说说BFF(Back-endforFront-end)。BFF这个概念大家可能听多了,一些套话我就不在这里复制粘贴了。如果您不明白,可以推荐阅读本文以了解更多信息。简单来说,BFF是一个用于接口聚合和剪枝的http服务器。随着后端go语言的流行,很多大公司都转而去开发微服务。众所周知,go属于Google,自然而然的,同样由Google开发的rpc框架gRPC在go语言中得到了广泛的应用。如果前端BFF层需要连接go后端提供的gRPC+protobuf接口,而不是前端熟悉的RESTfulAPI,那么我们需要使用grpc-node发起gRPC接口调用。这篇文章就是带大家了解如何在grpc-node中使用客户端拦截器(interceptor)?什么是grpc拦截器?什么用途?grpc拦截器类似于我们所知道的axios拦截器。在发送请求之前或响应请求之前,它在请求的每个阶段执行我们的一些处理。例如:为每个请求添加一个token参数,为每个请求响应检查errMsg字段是否有值。如果每个请求都写这些统一的逻辑,那就太可笑了。一般我们会在拦截器中统一处理这些逻辑。grpc-node客户端拦截器在讲grpc-node拦截器之前,我们先假设一个pb协议文件,方便后面大家理解案例。以下所有情况都基于这个简单的pb协议:package"hello"serviceHelloService{rpcSayHello(HelloReq)returns(HelloResp){}}messageHelloReq{stringname=1;}messageHelloResp{stringmsg=1;}客户端拦截器的创建那么如何编写最简单的客户端拦截器呢?//什么都不做,通过拦截器传递所有操作constinterceptor=(options,nextCall:Function)=>{returnnewInterceptingCall(nextCall(options));}是的,根据规范:每个客户端拦截器必须是一个Function,每次创建新拦截器实例的请求都会执行。该函数需要返回一个InterceptingCall实例。InterceptingCall实例可以传递一个nextCall()参数来继续调用下一个拦截器。类似于express中间件的nextoptions参数,描述了当前gRPC请求的一些属性。options.method_descriptor.path:等于/./比如这里是/hello.HelloService/SayHellooptions.method_descriptor.requestSerialize:将请求参数对象序列化到缓冲区中的函数,同时去掉请求参数中不需要的数据的选项。method_descriptor.responseDeserialize:将responsebuffer数据反序列化为json对象。options.method_descriptor.requestStream:布尔值,请求是否流式传输options.method_descriptor.responseStream:布尔值,响应是否流式传输。任何修改都会进行,因为如果后面还有其他拦截器,这会影响下游拦截器的options值。上面的拦截器demo只是简单介绍了拦截器的规范,demo并没有做任何实质性的事情。那么在请求出站前想要做一些花哨的操作怎么办呢?这就需要用到RequesterRequester(出站前拦截处理)在InterceptingCall的第二个参数中,我们可以传入一个request对象来处理发送请求前的操作。const拦截器=(options,nextCall:Function)=>{constrequester={start(){},sendMessage(){},halfClose(){},cancel(){},}returnnewInterceptingCall(nextCall(options),requester);}requester实际上是一个指定参数的对象,结构如下://ts定义如下interfaceRequester{start?:(metadata:Metadata,listener:Listener,next:Function)=>void;sendMessage?:(message:any,next:Function)=>void;halfClose?:(next:Function)=>void;cancel?:(next:Function)=>void;}Requester.start在发起出站调用拦截方法之前调用。开始?:(元数据:元数据,侦听器:侦听器,下一个:函数)=>无效;参数metadata:请求的元数据,可以添加或删除元数据listener:监听器,用于监听入站操作,下面会提到:执行下一个拦截器的requester.start,类似express的next。接下来这里可以传两个参数:metadata和listener。constrequester={start(metadata,listener,next){next(metadata,listener)}}在每个出站消息之前由Requester.sendMessage调用的拦截方法。sendMessage?:(message:any,next:Function)=>void;message:protobuf的请求体next:拦截器调用链,其中next可以传message参数constrequester={sendMessage(message,next){//ForCurrentpbprotocol//message==={name:'xxxx'}next(message)}}Requester.halfClose出站流关闭时调用的拦截方法(消息发送后)。halfClose?:(next:Function)=>void;next:链式调用,无需传参Requester.cancel客户端取消请求时调用的拦截方法。cancel?:(next:Function)=>void;Listener(inbound前的拦截处理)既然有outbound拦截操作,自然就有inbound拦截操作。inbound拦截方法在前面提到的Requester.start方法中的监听器中定义interfaceListener{onReceiveMetadata?:(metadata:Metadata,next:Function)=>void;onReceiveMessage?:(message:any,next:Function)=>void;onReceiveStatus?:(status:StatusObject,next:Function)=>void;}Listener.onReceiveMetadata接收响应元数据时触发的入站拦截方法。constrequester={start(metadata,listener){constnewListener={onReceiveMetadata(metadata,next){next(metadata)}}}}Listener.onReceiveMessage是接收到响应消息时触发的入站拦截方法。constnewListener={onReceiveMessage(message,next){//对于当前pb协议//message==={msg:'helloxxx'}next(message)}}Listener.onReceiveStatus收到状态时触发的入站拦截方法constnewListener={onReceiveStatus(status,next){//调用成功时,status为{code:0,details:"OK"}next(status)}}grpc拦截器执行顺序那么上面介绍了这么多拦截器Outbound拦截相关的方法,那么它们的具体执行顺序是怎样的呢?下面就简单说说吧。单个拦截器:先请求outbound,执行顺序如下:请求后startsendMessagehalfClostinbound,执行顺序onReceiveMetadataonReceiveMessageonReceiveStatusMultipleinterceptorsExecutionorder那么问题来了,如果我们配置多个拦截器,假设配置顺序是[interceptorA,interceptorB,interceptorC],则拦截器的执行顺序为:interceptorAoutbound->interceptorBoutbound->interceptorCoutbound->grpc.Call->interceptorCinbound->interceptorBinbound->interceptorAinbound你可以看到执行顺序类似于一个栈,先进后出,后进先出。看这个流程图,你可能会下意识的认为多个拦截器的执行顺序会是:拦截器A:1.start2.sendMessage3.halfClost拦截器B:4.start5.sendMessage6.halfClost拦截器C:...但实际上并非如此。前面说过,每个拦截器都会有一个next方法,执行next方法其实就是执行下一个拦截器同阶段的拦截方法,例如://InterceptorAstart(metadata,listener,next){//这里执行的next其实就是拦截器B的start方法//next(metadata,listener)}//InterceptorBstart(metadata,listener,next){//这里的metadata和listener是上一个拦截器传入的值next(metadata,listener)}因此,最后多个拦截器的具体方法执行顺序会是:出站阶段:start(拦截器A)->start(拦截器B)->sendMessage(拦截器A)->sendMessage(拦截器B)->halfClost(拦截器A)->halfClost(拦截器B)->grpc.Call->入站阶段:onReceiveMetadata(拦截器B)->onReceiveMetadata(拦截器A)->onReceiveMessage(拦截器B)->onReceiveMessage(拦截器A)->onReceiveStatus(InterceptorB)->onReceiveStatus(InterceptorA)应用场景看了这么多定义,估计看的人一头雾水,大家可能对拦截感兴趣拦截器的作用没有太多概念,来看看在拦截器的实际应用场景中。请求和响应日志可以记录在请求和响应拦截器中onReceiveMessage(resp,next){logger.info(`request:${options.method_descriptor.path}响应主体:${JSON.stringify(resp)}`)next(resp);}});},sendMessage(message,next){logger.info(`发起请求:${options.method_descriptor.path};请求参数:${JSON.stringify(message)}`)next(message);}});};constclient=newhello_proto.HelloService('localhost:50051',grpc.credentials.createInsecure(),{interceptors:[logInterceptor]});mock数据微服务场景最大的好处就是业务切分,但是在BFF层,如果微服务接口未完成,很容易被微服务堵死,就像前端被后台堵死一样——端接口。然后,我们可以用同样的思路实现数据mockconstinterceptor=(options,nextCall)=>{letsavedListener//使用环境变量或者其他判断逻辑判断当前是否需要mock接口constisMockEnv=truereturnnewgrpc.InterceptingCall(nextCall(options),{start:function(metadata,listener,next){//保存监听器,以便后续调用可以响应入站方法savedListener=listener//如果是mock环境,则不需要调用next方法以避免向服务器发出出站请求mockData={hello:'hellointerceptor'}//调用之前保存的监听器响应方法,onReceiveMessage,onReceiveStatus必须全部调用savedListener.onReceiveMetadata(newgrpc.Metadata());savedListener.onReceiveMessage(mockData);savedListener.onReceiveStatus({代码:grpc.status.OK});}else{下一个(消息);}}});};原理很简单,其实就是让request不出去,在response的outbound准备阶段直接调用inbound方法。异常请求fallback有时可能是服务端异常,导致接口异常。您可以在拦截器响应的入站阶段判断状态,以避免应用程序异常。constfallbackInterceptor=(options,nextCall)=>{让savedMessage让savedMessageNext返回新的grpc。InterceptingCall(nextCall(options),{start:function(metadata,listener,next){next(metadata,{onReceiveMessage(message,next){//暂时保存message和next,等待接口响应状态确认,然后thenrespondsavedMessage=message;savedMessageNext=next;},onReceiveStatus(status,next){if(status.code!==grpc.status.OK){//如果接口响应异常,响应预设数据,避免xxxundefinedsavedMessageNext({errCode:status.code,errMsg:status.details,result:[]});//设置当前界面正常next({code:grpc.status.OK,details:'OK'});}else{savedMessageNext(savedMessage);next(status);}}});}});};原理并不复杂,大概捕捉异常状态,响应正常状态和预设数据。结论正如你所看到的,grpc的拦截器概念并没有什么特别或难以理解的地方。和我们常用的拦截器基本一样,比如axios拦截器。方法为请求阶段和响应阶段做一些自定义的统一逻辑处理。本文主要是对grpc-node拦截器的简单解读。希望这篇文章能对正在使用grpc-node作为BFF层的同学有所帮助。