【gRPC】Web 請求的 TS 封裝 - 完美版

語言: CN / TW / HK

highlight: vs2015

導讀

【gRPC】Web 請求的 TS 封裝 (下文簡稱《封裝》)一文提供了一版對 gRPC 的封裝,雖然已經可以在實踐中使用了,但是還存在兩處處不完美的地方。一是請求的某個引數為複雜型別(object)時,還是需要在 services 宣告層使用 new RequestClass() 這種方式處理入參;二是在 services 層必須要傳入 requestClass,理論上應該是可以省略的。

本文致力於解決以上兩個問題,達到 【gRPC】封裝前端網路請求的核心思想 - TS版 中要求的宣告層極簡的效果。但是實現方法要利用 AST 處理工具對 protoc 生成的程式碼進行二次處理,所以具有一定的技術複雜度。不過也算是學習 AST 工具的好契機,不過為了保證文章的聚焦,AST 處理將會另起一文。

背景

如導讀中說的,《封裝》方案中存在兩處不完美的地方:複雜型別入參的處理必須傳入 requestClass。這樣說還是有些抽象,我們直接上程式碼:

假設《封裝》中的 helloworld.proto 檔案新增瞭如下內容: ```js // 新增 Student 型別 message Student { string name = 1; int32 age = 2; }

// 修改 HelloRequest 引數,加入 Student message HelloRequest { string name = 1; Student student_info = 2; } `HelloRequest` 多了一個 `student_info` 的引數,是一個複雜型別(物件)。這時,**services** 層的程式碼就會變成:ts import { grpcPromise, client } from "./request"; import { HelloRequest, Student } from "./helloworld_pb.js";

export const sayHello = (data: HelloRequest.AsObject) => { / 處理 Student start / const { studentInfo, ...rest } = data; const { name = "", age = 0 } = data.studentInfo ?? {}; const student = new Student(); student.setName(name); student.setAge(age); / 處理 Student end / return grpcPromise({ method: "sayHello", requestClass: HelloRequest, data: { ...rest, studentInfo: student }, }); }; `` 有一大堆的處理Student` 的程式碼,這非常的不優雅,不夠完美。這就是上文提到的複雜型別入參的處理問題。

另外,此處還必須引入 HelloRequest,並且作為引數傳入 grpcPromise,這樣也很不優雅。理論上傳入了 sayHello 就能夠得到其入參類 HelloRequest 的資訊才對。這就是上文提到的第二個問題:必須傳入 requestClass

想要解決這兩個問題,必須對 protoc 生成的 JS/TS 檔案進行二次加工,需要用到 AST 的處理工具。有一定的複雜度,具體實現的方詳見筆者的另一篇文章 GoGoCode - 像用 Jquery 一樣方便地處理 AST。為保證思路的連貫,本文假設已經解決了上述問題,聚焦展現完美版的封裝程式碼應該是什麼樣。

假設

以下假設也是《GoGoCode》一文需要實現的目標: 1. 引數例項(Message)上有 getXXXClass 方法能夠獲取到 XXX 類,如上例中呼叫 getStudentInfoClass() 能夠返回 Student 類; 2. 服務例項(Service)上有 getXXXParamsClass 方法能夠獲取到 XXX 方法入參的類,如上例中呼叫 getSayHelloParamsClass() 能夠返回 HelloRequest 類。

正文

如果滿足以上假設,services.ts 將會滿足《思想》一文中的要求: ```ts import { grpcPromise } from "./request"; import { HelloRequest } from "./helloworld_pb.js";

// 不需要處理 Student; 不需要傳入 HelloRequest; // 此處的 HelloRequest.AsObject 只作為 ts 使用 export const sayHello = (data: HelloRequest.AsObject) => grpcPromise({ method: "sayHello", data }); ``` 很優雅,完美。接下來就是重頭戲,如何修改 request.ts 的程式碼。

複雜型別入參的處理

因為呼叫 getStudentInfoClass 方法就能夠獲取到 Student 類,所以上文中把 JSON 處理成 Student Instance 的程式碼就可以抽象到 request.ts 中了。另外,可以很自然的想到,這是一個典型的遞迴場景,所以關鍵點就在實現遞迴函式,直接上程式碼: ts const transJson2ClassInstance = <C>( className: ConcreteClass<C>, data: Record<string, any> ) => { const result = new className(); // 抽象成了引數 Object.entries(data).forEach(([key, val]) => { const method = upperFirst(camelCase(key)); const setMethod = `set${method}` as keyof C; const setFunc = result[setMethod]; if (typeof setFunc === "function") { if (typeof val === "object" && !Array.isArray(val)) { // 呼叫 getXXXClass 方法獲取到 XXX 類 const subClassName = result[`get${method}Class` as keyof C](); setFunc.call(result, transJson2ClassInstance(subClassName, val)); } else { setFunc.call(result, val); } } }); return result; };

必須傳入 requestClass

接下來就是根據 sayHello 獲取到 HelloRequest 類,並作為 transJson2ClassInstance 的初始引數傳入。有了上文的鋪墊,這裡就比較好理解了。直接呼叫 getSayHelloParamsClass 方法即可: ```ts interface GrpcPromiseParams { method: M; // requestClass: ConcreteClass>; 不需要了 data: Partial["toObject"]>>; metadata?: Metadata; }

export const grpcPromise = ( params: GrpcPromiseParams ) => { const { method, data, metadata = {} } = params; // 呼叫 getXXXParamsClass 獲取到 XXX 入參的類 const getClassMethod = get${upperFirst(method)}ParamsClass; const requestClass = clientgetClassMethod as ConcreteClass< RequestClass

; const request = transJson2ClassInstance(requestClass, data);

const result = clientmethod .then((res) => res.toObject()) .catch((err) => console.log(err)) as GrpcPromiseReturn; return result; }; `` 主要邏輯就是呼叫getXXXParamsClasstransJson2ClassInstance` 實現 request 的生成。到這就已經達到一個比較完美的狀態了,起碼宣告層呼叫層已經很簡潔好用了。

PS:這部分由於會有拼接字串的邏輯,這種弱邏輯較難處理 TS,所以適當的 @ts-ignore 一下吧,好在這一層的 ignore 不會對使用層造成影響。

結語

本文的靈感是受到同事的啟發,他為了能夠達到這個效果,去研究了 protoc 的原始碼,並且也達成了上述的效果。但是這樣我們就只能用私人定製版的 protoc 了。不過他的研究給了筆者靈感,既然改原始碼有諸多限制,那麼用指令碼對生成物進行二次加工總可以了吧,於是就有了這篇文章。

最近給團隊提出了願景:極致,創新,無邊界。我對其的理解是:

極致是創新的前提,創新是極致的結果;無邊界可以在更大範圍內尋找最優解,是創新的催化劑。

用這個例子來說,我的同事首先追求極致,才想到要去修改 protoc 原始碼。作為前端去修改 C 的程式碼,敢想敢幹,不設邊界。最終的成果目前來看可以說是業內的獨一份,肯定算得上是創新了。而且也切切實實的為團隊帶來了收益,試想下,如果團隊中都是這樣的好同志,團隊的戰鬥力將是何等強悍。

在激烈競爭中,我們往往會發現,取勝的系統在最大化或者最小化一個或幾個變數上走到近乎荒謬的極端。——《窮查理寶典》