走進前端 BFF 之 能夠看但不必的 grpc-node 攔截器操做指南

前言

本文面向的前端小夥伴:前端

  • 有前端 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 攔截器是什麼?有啥用?

grpc 攔截器和咱們所知道的 axios 攔截器相似,都是在請求發出前,或者請求響應前,在請求的各個階段進行咱們的一些處理。json

例如:給每一個請求加上 token 參數,給每一個請求響應都校驗下 errMsg 字段是否有值。axios

這些統一的邏輯,每一個請求都寫一遍就太扯了,通常咱們都會在攔截器裏統一處理這些邏輯。後端

grpc-node client interceptor

在講 grpc-node 攔截器以前,咱們先假定一個 pb 協議文件,方便後面你們理解案例。微信

下面全部的案例都以這個簡單的 pb 協議爲基準:

package "hello"

service HelloService {
    rpc SayHello(HelloReq) returns (HelloResp) {}
}

message HelloReq {
    string name = 1;
}

message HelloResp {
    string msg = 1;
}

Client Interceptor 的建立

那麼最簡單的一個 client 攔截器怎麼寫呢?

// 沒有幹任何事情,透傳全部操做的攔截器
const interceptor = (options, nextCall: Function) => {
  return new InterceptingCall(nextCall(options));
}

沒錯,根據規範:

  • 每一個 client interceptor 必須是個函數,每次請求都會執行一遍來建立一個新的攔截器實例
  • 函數須要 return 一個 InterceptingCall 實例
  • InterceptingCall 實例能夠傳遞一個 nextCall() 參數,來繼續調用下一個攔截器,相似的 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

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;
}

Requester.start

在啓動出站調用以前調用的攔截方法。

start?: (metadata: Metadata, listener: Listener, next: Function) => void;

參數

  • metadata: 請求的 metadata,能夠對 metadata 進行增添刪除操做
  • listener: 監聽器,用於監聽入站操做,下面會講到
  • next:執行下一個攔截器的 requester.start, 相似 express 的 next。 此處的 next 能夠傳遞兩個參數:metadata 和 listener。
const requester = {
    start(metadata, listener, next) {
        next(metadata, listener)
    }
}

Requester.sendMessage

在每一個出站消息以前調用的攔截方法。

sendMessage?: (message: any, next: Function) => void;
  • message: protobuf 的請求體
  • next:攔截器調用鏈,此處 next 可傳遞 message 參數
const requester = {
    sendMessage(message, next) {
        // 對於當前 pb 協議
        // message === { name: 'xxxx' }
        next(message)
    }
}

Requester.halfClose

當出站流關閉時(在消息發送後)調用的攔截方法。

halfClose?: (next: Function) => void;
  • next: 鏈式調用,無需傳參

Requester.cancel

從客戶端取消請求時調用的攔截方法。比較少用到

cancel?: (next: Function) => void;

Listener (入站前攔截處理)

既然出站攔截操做,天然有入站攔截操做。

入站攔截方法在前面提到的 Requester.start 方法中的 listener 進行定義

interface Listener {
  onReceiveMetadata?: (metadata: Metadata, next: Function) => void;
  onReceiveMessage?: (message: any, next: Function) => void;
  onReceiveStatus?: (status: StatusObject, next: Function) => void;
}

Listener.onReceiveMetadata

接收響應元數據時觸發的入站攔截方法。

const requester = {
    start(metadata, listener) {
        const newListener = {
            onReceiveMetadata(metadata, next) {
                next(metadata)
            }
        }
    }
}

Listener.onReceiveMessage

接收到響應消息時觸發的入站攔截方法。

const newListener = {
    onReceiveMessage(message, next) {
        // 對於當前 pb 協議
        // message === {msg: 'hello xxx'}
        next(message)
    }
}

Listener.onReceiveStatus

接收到狀態時觸發的入站攔截方法

const newListener = {
    onReceiveStatus(status, next) {
        // 成功調用時, status 爲 {code:0, details:"OK"}
        next(status)
    }
}

grpc interceptor 執行順序

那麼上面描述了那麼多個攔截器入站出站的攔截相關方法,那麼具體他們的執行順序是怎麼樣的呢,下面簡單說下, 單個攔截器:

  1. 請求先出站, 執行順序以下:

    1. start
    2. sendMessage
    3. halfClost
  2. 請求後入站,執行順序

    1. onReceiveMetadata
    2. onReceiveMessage
    3. onReceiveStatus

多攔截器執行順序

那麼問題來了,若是咱們配置了多個攔截器,假設配置順序是 [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)

應用場景

看了那麼多定義,估計人都懵了,你們可能對攔截器的做用沒有太大的概念,下面看下 攔截器的實際應用場景。

請求與響應的 log

能夠在請求與響應攔截器中,記錄日誌

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]
  });

mock 數據

微服務場景最大的好處是業務分割,可是在 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); 
      }
    }
  }); 
};

原理很簡單,其實就是讓請求不出站,直接在出站準備階段,調用入站響應的方法。

異常請求 fallback

有時候可能 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 層的同窗一些幫助。




本文首發於 github 博客
如文章對你有幫助,你的 star 是對我最大的支持
其餘文章:


插播信息:
深圳 Shopee 長期內推
崗位:前端,後端(要轉go),產品,UI,測試,安卓,IOS,運維 全都要。招聘詳情具體看拉勾哈
薪酬福利:20K-50K😳, 7點下班😏,免費水果😍,免費晚餐😊,15天年假👏,14天帶薪病假。 簡歷發郵箱:chenweiyu6909@gmail.com 或者加我微信:cwy13920
相關文章
相關標籤/搜索