本文面向的前端小伙伴:前端
- 有前端 BFF 开发经验或对此有兴趣的
- 对 gRPC 和 protobuf 协议有必定理解的
首先简单谈一下
BFF (Back-end for Front-end), BFF的概念你们可能都听滥了,这里就不复制粘贴一些陈词滥调了,不了解的能够推荐看这篇文章了解下。node
那么简单来讲,BFF
就是作一个进行接口聚合裁剪的 http server。ios
随着后端 go 语言的流行,不少大公司的都转向了用 go 开发微服务。而总所周知,go 是 谷歌家的,那么天然,一样是谷歌家开发的 rpc 框架 gRPC 就被 go 语言普遍用了起来。git
若是前端 BFF 层须要对接 go 后端提供的 gRPC + protobuf 接口,而不是前端所熟悉的 RESTful API,那么我们就须要使用 grpc-node 来发起 gRPC 的接口调用了。github
本文就是来和你们一块儿理解下 grpc-node 中的 client interceptor(拦截器) 到底该怎么用?express
grpc 拦截器和咱们所知道的 axios 拦截器相似,都是在请求发出前,或者请求响应前,在请求的各个阶段进行咱们的一些处理。json
例如:给每一个请求加上 token 参数,给每一个请求响应都校验下 errMsg 字段是否有值。axios
这些统一的逻辑,每一个请求都写一遍就太扯了,通常咱们都会在拦截器里统一处理这些逻辑。后端
在讲 grpc-node
拦截器以前,咱们先假定一个 pb
协议文件,方便后面你们理解案例。微信
下面全部的案例都以这个简单的 pb 协议为基准:
package "hello" service HelloService { rpc SayHello(HelloReq) returns (HelloResp) {} } message HelloReq { string name = 1; } message HelloResp { string msg = 1; }
那么最简单的一个 client 拦截器怎么写呢?
// 没有干任何事情,透传全部操做的拦截器 const interceptor = (options, nextCall: Function) => { return new InterceptingCall(nextCall(options)); }
没错,根据规范:
express
中间件的 next
options
参数,描述了当前 gRPC 请求的一些属性
options.method_descriptor.path
: 等于 /<package名>.<service名>/<rpc名>
例如,这里就是 /hello.HelloService/SayHello
options.method_descriptor.requestSerialize
: 序列化请求参数对象成为 buffer 的函数,同时会对请求参数中非必要数据裁剪掉options.method_descriptor.responseDeserialize
: 对响应 buffer 数据反序列化成 json 对象options.method_descriptor.requestStream
: boolean, 请求是否是 流式传输options.method_descriptor.responseStream
: boolean, 响应是否是 流式传输通常状况下,咱们对 options 不会作任何修改,由于若是后面还有其余拦截器,这就会影响到下游的拦截器的 options 值了。
以上的 interceptor demo 只是简单说下 拦截器的规范,demo 没有干任何实质性的事情。
那么若是咱们要在请求出站前作一些骚操做时,咱们应该怎么作呢?
这就要用到 Requester
了
在 InterceptingCall
的第二个参数中,咱们能够传入一个 request 对象,来处理请求发出前的操做。
const interceptor = (options, nextCall: Function) => { const requester = { start(){}, sendMessage(){}, halfClose(){}, cancel(){}, } return new InterceptingCall(nextCall(options), requester); }
requester 其实就是个俱备指定参数的对象, 结构以下:
// ts 定义以下 interface Requester { start?: (metadata: Metadata, listener: Listener, next: Function) => void; sendMessage?: (message: any, next: Function) => void; halfClose?: (next: Function) => void; cancel?: (next: Function) => void; }
在启动出站调用以前调用的拦截方法。
start?: (metadata: Metadata, listener: Listener, next: Function) => void;
参数
const requester = { start(metadata, listener, next) { next(metadata, listener) } }
在每一个出站消息以前调用的拦截方法。
sendMessage?: (message: any, next: Function) => void;
const requester = { sendMessage(message, next) { // 对于当前 pb 协议 // message === { name: 'xxxx' } next(message) } }
当出站流关闭时(在消息发送后)调用的拦截方法。
halfClose?: (next: Function) => void;
从客户端取消请求时调用的拦截方法。比较少用到
cancel?: (next: Function) => void;
既然出站拦截操做,天然有入站拦截操做。
入站拦截方法在前面提到的 Requester.start
方法中的 listener 进行定义
interface Listener { onReceiveMetadata?: (metadata: Metadata, next: Function) => void; onReceiveMessage?: (message: any, next: Function) => void; onReceiveStatus?: (status: StatusObject, next: Function) => void; }
接收响应元数据时触发的入站拦截方法。
const requester = { start(metadata, listener) { const newListener = { onReceiveMetadata(metadata, next) { next(metadata) } } } }
接收到响应消息时触发的入站拦截方法。
const newListener = { onReceiveMessage(message, next) { // 对于当前 pb 协议 // message === {msg: 'hello xxx'} next(message) } }
接收到状态时触发的入站拦截方法
const newListener = { onReceiveStatus(status, next) { // 成功调用时, status 为 {code:0, details:"OK"} next(status) } }
那么上面描述了那么多个拦截器入站出站的拦截相关方法,那么具体他们的执行顺序是怎么样的呢,下面简单说下, 单个拦截器:
请求先出站, 执行顺序以下:
请求后入站,执行顺序
那么问题来了,若是咱们配置了多个拦截器,假设配置顺序是 [interceptorA, interceptorB, interceptorC]
,那么拦截器的执行顺序会是:
interceptorA 出站 -> interceptorB 出站 -> interceptorC 出站 -> grpc.Call -> interceptorC 入站 -> interceptorB 入站 -> interceptorA 入站
能够看到,执行顺序是相似栈,先进后出,后进先出。
那么看这流程图,你们可能会下意识以为多个拦截器的执行顺序会是:
拦截器A: 1. start 2. sendMessage 3. halfClost 拦截器B: 4. start 5. sendMessage 6. halfClost 拦截器C: ......
可是实际上并不是如此。
前面提到,每一个拦截器都会有一个 next
方法,next
方法的执行,其实就是执行下一个拦截器的同一个阶段的拦截方法,例如:
// 拦截器A start(metadata, listener, next) { // 此处执行的next 实际上是执行拦截器 B // 的 start 方法 next(metadata, listener) } // 拦截器 B start(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(拦截器B) -> onReceiveStatus(拦截器A)
看了那么多定义,估计人都懵了,你们可能对拦截器的做用没有太大的概念,下面看下 拦截器的实际应用场景。
能够在请求与响应拦截器中,记录日志
const logInterceptor = (options, nextCall) => { return new grpc.InterceptingCall(nextCall(options), { start(metadata, listener, next) { next(metadata, { onReceiveMessage(resp, next) { logger.info(`请求:${options.method_descriptor.path} 响应体:${JSON.stringify(resp)}`) next(resp); } }); }, sendMessage(message, next) { logger.info(`发起请求:${options.method_descriptor.path};请求参数:${JSON.stringify(message)}`) next(message); } }); }; const client = new hello_proto.HelloService('localhost:50051', grpc.credentials.createInsecure(), { interceptors: [logInterceptor] });
微服务场景最大的好处是业务分割,可是在 BFF 层,若是微服务接口还未完成,就很容易被微服务那边阻塞,就相似前端被后端接口阻塞同样。
那么,咱们就能够用一样的思路,来在拦截器层面实现 grpc 接口的数据 mock
const interceptor = (options, nextCall) => { let savedListener // 经过环境变量,或其余判断逻辑,判断当前是否须要 mock 接口 const isMockEnv = true return new grpc.InterceptingCall(nextCall(options), { start: function (metadata, listener, next) { // 保存 listener, 以便后续调用响应入站的 method savedListener = listener // 若是是 mock 环境,就不须要 调用 next 方法,避免请求出站到 server if(!isMockEnv) { next(metadata, listener); } }, sendMessage(message, next) { if(isMockEnv) { // 根据须要, 构造本身的 mock 数据 const mockData = { hello: 'hello interceptor' } // 调用前面保存了的 listener 响应方法,onReceiveMessage, onReceiveStatus必须都调用 savedListener.onReceiveMetadata(new grpc.Metadata()); savedListener.onReceiveMessage(mockData); savedListener.onReceiveStatus({code: grpc.status.OK}); } else { next(message); } } }); };
原理很简单,其实就是让请求不出站,直接在出站准备阶段,调用入站响应的方法。
有时候可能 server 端异常,致使接口异常,能够在拦截器响应入站阶段,判断状态,避免应用异常。
const fallbackInterceptor = (options, nextCall) => { let savedMessage let savedMessageNext return new grpc.InterceptingCall(nextCall(options), { start: function (metadata, listener, next) { next(metadata, { onReceiveMessage(message, next) { // 暂且保存 message 和 next,等到 接口响应状态 肯定后,再响应 savedMessage = message; savedMessageNext = next; }, onReceiveStatus(status, next) { if (status.code !== grpc.status.OK) { // 若是 接口响应异常,响应预设数据,避免 xxx undefined savedMessageNext({ 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 层的同窗一些帮助。
插播信息:
深圳 Shopee 长期内推
岗位:前端,后端(要转go),产品,UI,测试,安卓,IOS,运维 全都要。招聘详情具体看拉勾哈
薪酬福利:20K-50K😳, 7点下班😏,免费水果😍,免费晚餐😊,15天年假👏,14天带薪病假。 简历发邮箱:chenweiyu6909@gmail.com 或者加我微信:cwy13920