GoGoCode - 像用 Jquery 一樣方便地處理 AST
highlight: vs2015
導讀
本文是寫【gRPC】系列文章孵化出來的,想要實現 grpc-web
的完美封裝,需要對 protoc 生成的 JS/TS 檔案進行二次加工。自然想到用 AST(抽象語法樹)的處理工具,在同事的推薦下,試用了一下 gogocode
,的確很方便。
背景
具體背景詳見【gRPC】Web 請求的 TS 封裝 - 完美版,由於上下文較多,所以本文可以做到即使不爬前文也能看懂 gogocode 怎麼用,各位看官可以安心閱讀。
GoGoCode 官網的介紹:
GoGoCode 是一個基於 AST 的 JavaScript/Typescript/HTML 程式碼轉換工具,但相較於同類,它提供了更符合直覺的 API:一套類 JQuery 的 API 用來查詢和處理 AST、一套和正則表示式接近的語法用來匹配和替換程式碼
另有與 babel
和 jscodeshift
的對比,可見它們是同一類東西。本文會比較偏實踐,直接提出問題,並展示 gogocode 如何解決,不會有太多理論層面的論述。
目標
沒有閱讀上下文的同學可以簡單理解本文需要作如下操作:
1. 修改下列 JS 程式碼,增加一個函式;
js
// 原內容
proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {
return /** @type{?proto.helloworld.Student} */ (
jspb.Message.getWrapperField(this, proto.helloworld.Student, 2));
};
// 待增加內容
proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {
return proto.helloworld.Student;
}
2. 修改下列 TS 程式碼,為 Class 增加一個方法。
ts
export class HelloRequest extends jspb.Message {
// 原內容
getStudentInfo(): Student | undefined;
// 待增加內容
getStudentInfoClass(): typeof Student;
}
正文
處理 helloworld_pb.js 程式碼
分析
注:沒有閱讀上下文的同學可以忽略分析過程,直接看分析結論。
閱讀以下 protoc 生成的 helloworld_pb.js
程式碼
```js
/
* optional Student student_info = 2;
* @return {?proto.helloworld.Student}
*/
proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {
return / @type{?proto.helloworld.Student} */ (
jspb.Message.getWrapperField(this, proto.helloworld.Student, 2));
};
/*
* @param {?proto.helloworld.Student|undefined} value
* @return {!proto.helloworld.HelloRequest} returns this
/
proto.helloworld.HelloRequest.prototype.setStudentInfo = function(value) {
return jspb.Message.setWrapperField(this, 2, value);
};
我們可以發現:
1. 所有複雜型別的 get 方法,都會呼叫 `getWrapperField`,set 也一樣;
2. `getWrapperField` 的引數中有我們需要的 `Student` 類,就是上文中的 `proto.helloworld.Student`;
有了以上兩個前提,理論上我們就可以用 `AST` 工具做以下操作,來達到最終目的:
1. 獲取到所有 `getWrapperField` 呼叫,並取出其第二個引數 `A`;
2. 同時獲取到外層的函式名稱 `getXXX`,上例中為 `getStudentInfo`;
3. 生成 `getXXXClass` 函式返回 `A`,上例為
js
proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {
return proto.helloworld.Student
};
```
實現
直接上核心程式碼: ```js const $ = require("gogocode");
const classMethodList = []; // 收集被命中的 method,為後續 ts 處理提供篩選範圍
const newCode = $(code).find(
$_$0 = function() {
return jspb.Message.getWrapperField(this, $_$1, $_$2);
};
)
.each((item) => {
const getMethod = item.match[0][0].value;
const paramsClass = item.match[1][0].value;
classMethodList.push(getMethod);
item.after(${getMethod}Class = function() {
return ${paramsClass};
}
);
})
.root()
.generate();
簡要說明一下:
1. `code` 即為 protoc 生成的 `helloworld_pb.js` 的程式碼;
2. gogocode 的程式碼風格很像 jQuery,可讀性比較強,`find`、`each`、`after` 等語法,即使沒用過 jQuery,大概也能猜到是什麼意思;
3. 程式碼中的 `$_$x` 是正則佔位符,下面的 `item.match[x][x].value` 就是在讀取它們的值。
處理後生成的 newCode 程式碼片段為:
js
proto.helloworld.HelloRequest.prototype.getStudentInfo = function() {
return /* @type{?proto.helloworld.Student} / (
jspb.Message.getWrapperField(this, proto.helloworld.Student, 2));
};
proto.helloworld.HelloRequest.prototype.getStudentInfoClass = function() {
return proto.helloworld.Student;
}
``
4. 新生成的程式碼(
after` 方法裡的內容)可以根據自己的需求定義,理論上不用非得是一個函式,直接定義一個屬性反而更簡單,本文為了易讀性使用了函式。
處理 helloworld_pb.d.ts 檔案
分析
閱讀以下 protoc 生成的 helloworld_pb.d.ts
程式碼
ts
export class HelloRequest extends jspb.Message {
// other code
getStudentInfo(): Student | undefined;
setStudentInfo(value?: Student): HelloRequest;
hasStudentInfo(): boolean;
clearStudentInfo(): HelloRequest;
// other code
}
我們希望插入一條
ts
getStudentInfoClass(): typeof Student;
利用 gogocode 提取出 getStudentInfo
和 Student
後拼出需要的程式碼就行了。但是,別看修改量不大,想實現也不太容易,需要注意幾點:
1. 理論上這部分修改的程式碼需要依賴修改 js 檔案時,遍歷的 getMethod
資訊進行篩選,所以需要用一個 classMethodList
陣列來收集被命中的方法名,詳見上文的程式碼備註;
2. typeof
這個 ts 的關鍵字目前 gogocode
沒有提供方便的生成 AST 的方法(也可能是用的不熟),所以不得不引入 @babel/types
來生成需要的 AST 物件;
3. 基於上一點,生成程式碼有兩種思路:一是 clone 一份 getStudentInfo
的 AST,修改其屬性;二是直接用 @babel/types
來生成。前者的易讀性更好,但是程式碼量較多;後者程式碼量較少,但是需要具備一定的 babel 知識,各位視情況而定。不過即使是第一種方法,也需要稍微用一下 @babel/types
來解決 typeof 的問題。
實現
```js const $ = require("gogocode"); const t = require("@babel/types");
const tsNewCode = $(tsCode) .find("$$1(): $$2 | undefined") .each((item) => { const callee = item.match[1][0].value; // getStudentInfo const returnNode = item.match[2][0]; // returnType AST
if (returnNode.type === "TSTypeReference" && classMethodList.includes(callee)) {
// 方法一:clone item
const newNode = item.clone();
// t.tsTypeQuery 就是生成 typeof AST 的方法
newNode.attr({
"[0].nodePath.node.key.name": `${callee}Class`,
"[0].nodePath.node.returnType.typeAnnotation": t.tsTypeQuery(
returnNode.typeName // Student 的 AST
),
});
item.after(newNode);
/* 方法二: @babel/types
item.after(
t.tsDeclareMethod(
null,
t.identifier(`${callee}Class`),
null,
[],
t.tsTypeAnnotation(t.tsTypeQuery(returnNode.typeName))
)
);
*/
}
})
.root()
.generate();
最終生成的程式碼片段:
ts
// other code
getStudentInfo(): Student | undefined;
getStudentInfoClass(): typeof Student;
// other code
```
處理 helloworld_grpc_web_pb.js
在《完美版》中還提到,需要對 sayHello
所在的檔案進行處理,讓其能夠返回 sayHello
等 Method 的入參類。思路與方法與上文大同小異,就不贅述了。
值得一提的是本來它內部是有一個物件能夠獲取到這個資訊的,但是因為有 bug,沒有辦法獲取到。見如下程式碼:
js
const methodDescriptor_Greeter_SayHello = new grpc.web.MethodDescriptor(
'/helloworld.Greeter/SayHello',
grpc.web.MethodType.UNARY,
proto.helloworld.HelloRequest,
proto.helloworld.HelloReply,
/**
* @param {!proto.helloworld.HelloRequest} request
* @return {!Uint8Array}
*/
function(request) {
return request.serializeBinary();
},
proto.helloworld.HelloReply.deserializeBinary
);
實際上呼叫 methodDescriptor_Greeter_XXX.getRequestMessageCtor()
就能夠獲取到 HelloRequest
了,但是打包後的程式碼中,該物件並沒有這個方法,所以根本調不到。
這個 bug 是同事發現的(沒錯,就是《完美版》中提到的那個同事),而且據說已經合了相關 PR,估計不久後會釋出新版本吧。如果新版本自帶了這個能力,我們也就不用再加工它了。
結語
本文的內容都是基於最簡單的 demo,所以在用在實踐當中時有可能會有需要調整的地方。所以各位重要的還是理解思想,知道這個思路可行即可,具體問題具體分析。
不過 gogocode
的確是一個好用的操作 AST 的工具,而且它還提供了 Vue2 轉 Vue3 的功能,正好符合團隊訴求,後續如果還有什麼好用的工具會盡量分享出來。另外,簡單瞭解下 Babel
尤其是 @babel/types
的用法其實價效比挺高的。
謹記,你是在尋找最好的答案,而不是你自己能得出的最好答案。——Ray Dalio
- 看我用 Linux 帶娃,培養程式設計興趣
- 【微前端】Qiankun Vue3 配置
- 通用 Form API 協議 - 基礎版
- Final Form 設計思路淺析
- 【低程式碼漫談】 lowcode-engine - Vue Renderer 嘗試
- Redash 設計理念淺析
- Metabase 設計理念淺析
- DataEase 設計理念淺析
- 開源 BI 工具調研:Superset、Metabase、Redash、DataEase(一)- 基本資料
- Ubuntu 一行命令裝軟體——VirtualBox
- 程式設計師怎麼給娃起名?當然是寫個指令碼!
- GoGoCode - 像用 Jquery 一樣方便地處理 AST
- 【gRPC】Web 請求的 TS 封裝 - 完美版
- 【gRPC】2 分鐘學會 Protocol Buffer 語法
- 【gRPC】封裝前端網路請求的核心思想 - TS版
- 如何避免 Vue 的漏洞破壞單向資料流
- 用函數語言程式設計寫出“傻瓜”都能看懂的程式碼
- Vue3 最佳實踐之編碼規範