本文面向的前端小夥伴:前端
- 有前端 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