js反編譯成ts JS反編譯



文章插圖
js反編譯成ts JS反編譯

文章插圖

為了解決從 JavaScript 逐步遷移到 TypeScript 過程中遇到的痛點,FreeWheel 核心業務團隊評估并提出了一套由 Protobuf 文件自動化生成 TypeScript 類型聲明文件的流程,支持 Protobuf 文件的變化觸發類型聲明文件的自動更新 。所有的 TypeScript 類型聲明文件以微服務為單位儲存,集中維護在公司級別的 TypeScript 中心化倉庫里 。
1背景
FreeWheel 核心業務團隊前后端開發現狀
FreeWheel 核心業務系統采用微服務架構,并使用 Go 語言作為微服務的開發語言,基于 gRPC 進行服務的遠程調用 。微服務之間的數據接口采用 Protobuf 進行定義,使用 protoc 自動生成相應的 RPC 接口代碼 。
微服務需要對外提供 Restful 接口用于 Web 前端和 Open API,而基于 protoc 生成的服務一般用于集群內部通信 。為了兼容 HTTP 調用,FreeWheel 使用 grpc-gateway 進行 Restful 接口的轉化和代理轉發 。
目前 Web 前端基于 React 組件化開發,以 JavaScript 為官方語言 。JavaScript 是一種弱類型語言,在運行時才明確變量的類型,由當前的值決定當前的類型 。在前后端交互需要了解變量類型時,前端開發人員只能通過查看 Protobuf 文件的定義來得到當前變量的類型,開發體驗不好且影響開發效率 。
中心化 TypeScript 類型庫的需求
基于該現狀,FreeWheel 核心業務前端開發團隊正在逐步將前端開發語言從 JavaScript 向 TypeScript 切換 。這么做的原因主要在于,TypeScript 作為 JavaScript 的類型化超集,彌補了靜態、弱類型的 JavaScript 的缺陷,具有靜態類型聲明,可以減少不必要的類型判斷和人工查看類型的成本,開發過程中進行靜態類型檢查和類型提示,對提高開發效率有正向作用 。
但在這個切換過程中,大量基礎類型聲明的構建成了一道必須跨越的鴻溝,主要體現在以下兩點:
缺少內部公共庫的類型定義 。目前線上一些比較老舊的 JavaScript 庫,不太可能用 TypeScript 改寫,對這部分文件如果能夠提供一份公用的類型定義會更合適 。缺失基于后端 Protobuf 定義對應的前端類型聲明文件 。目前整個微服務代碼倉庫已累積超過 700 個Protobuf接口定義文件、15k+ 個message定義 。單純靠開發人員手寫實現轉換并不現實 。而且Protobuf接口仍在不斷增加和修改,相應的類型聲明文件也需要及時得到更新 。
因此維護一個基于公司微服務層面的 TypeScript 類型中心化倉庫的需求便呼之欲出 。這個倉庫既支持內部公共庫的類型聲明,還支持所有微服務的類型聲明文件 。通過發包共享給整個公司的同事使用,降低重復開發成本 。
這一靈感來源于 TypeScript 社區最為熱門的開源項目 DefinitelyTyped,它提供了很多 npm 上常用的包的類型聲明文件,同時對于一些沒有提供聲明文件的包,也支持獨立開發人員自行實現后上傳到 DefinitelyTyped 里共享給大家使用,極大地促進了TypeScript的推廣 。但DefinitelyTyped 中并不包含 Protobuf 文件對應前端類型聲明文件的解決方案 。為了早日在團隊內部完成 TypeScript 的使用推廣,亟需解決這一痛點 。
2自動化 TypeScript 類型庫生成方案的技術選型與設計
DefinitelyTyped 珠玉在前,我們參考其思路并結合 FreeWheel 開發現狀,設計并實現了一套自動維護中心化類型庫 @fw-types 的方案 。
一方面支持自動化地由 Protobuf 文件生成 TypeScript類型聲明文件 。當Protobuf 文件發生更改后觸發生成 TypeScript類型文件的自動化流水線,[email protected],然后觸發 npm 發包流水線將新的類型包上傳到內部的 Artifactory 倉庫中,從而保證能夠追蹤由 Protobuf文件的更改而引起的類型聲明文件的變化 。另一方面支持前端開發人員可以給較老的前端庫補充類型定義,提交 Pull Request 合并到中心化庫里,共享給大家使用 。
技術選型
目前 GitHub 上由Protobuf文件生成 TypeScript 文件的工具有很多,我們分別調研并試用了這些工具,對比情況如下表所示 。
由于我們期望使用interface語法定義的類型,要求可以保留原始字段的蛇形命名,同時能夠生成Protobuf 定義依賴的其他文件類型,最終選擇proto-loader作為開發流程中的生成工具 。對于變量名的轉化,有三個工具是將Protobuf文件里的蛇形命名轉化為駝峰命名 。AsObject 指的是有一類工具轉化TypeScript包的語法中,以命名空間 namespace 的形式為主,對于空間本身定義成一個 AsObject 對象,命名空間可以有效的阻隔重名問題,但是每個類型在調用的過程中就需要添加 .AsObject 來使用 。另一類轉化以接口interface的形式轉化,目前以interface形式的較少 。d.ts文件是集中管理的類型聲明文件,但實際我們關心的是類型聲明文件的內容,內容符合預期的話,.ts文件和d.ts文件對項目來說沒有本質區別 。對于import的文件,只有兩個工具可以生成其對應的.ts文件 。在社區活躍度上,這些工具均比較活躍,最近一個月內都有相關commit 。
架構設計
整體解決方案的架構圖如下圖,從 @fw-types 代碼倉庫的入口來看可以劃分為兩個部分,一個是由于Protobuf文件的變化引發的自動由Protobuf文[email protected],另一個是和DefinitelyTyped一樣,支持開發人員在本地實現類型聲明文件并上傳到共享庫中,提供給大家使用 。
整個流水線按照功能來說可以劃分為三個階段,分別是:
捕獲接口定義文件改動接口定義文件生成類型聲明文件類型聲明文件發包
這三個階段的工作將會在下一章節中詳細介紹 。
3持續集成流水線的實踐詳解
捕獲接口定義文件改動
由Protobuf轉向TypeScript化的關鍵點在于維護好每個版本Protobuf文件定義和類型聲明文件的一一對應關系 。因此從Protobuf 文件的生成開始,就需要持續集成流水線的介入 。
捕獲接口定義文件改動是整個流水線的第一階段,如下圖所示 。后端開發人員提交Protobuf 文件改動,當對應微服務的持續集成測試通過之后,會被合并到主分支 。我們在微服務代碼倉庫的合并事件里增加了鉤子(webhook) 。每當合并事件觸發,該鉤子會檢測發生變化的文件里是否包含Protobuf文件,如果包含則觸發下一階段的任務 。
接口定義文件生成類型聲明文件
這一階段的核心工作是由Protobuf文件生成TypeScript類型聲明文件,[email protected] 里 ??紤]到 git 可以很直觀地給出被改動文件的細節,因此這部分的重點只需要關注類型聲明文件的生成和提交 。
類型聲明文件的生成
在技術選型時,我們對比了目前比較熱門的一些開源項目,最終選擇proto-loader作為開發流程中的生成工具 。但工具本身只提供了初步的轉化能力,我們還有一些額外的工作:
工具最終生成的是以.ts后綴的文件,包含了我們所需要的變量類型聲明 。但在我們的使用場景中還需要對外暴露index.d.ts文件以方便前端開發人員使用,因此需要將.ts文件統一在index.d.ts文件中向外export 。對于生成的.ts文件,我們還設計了相應的開頭注釋,體現當前文件是由工具自動生成的,并且顯式地列出當前.ts 文件的Protobuf來源,方便溯源 。
//DON'T EDIT. THIS IS GENERATED CODE!//package: 微服務名//source: / 主倉庫 / 微服務 /proto/Protobuf 文件/***Generated by @fw-types.*Designed by @[email protected]**/提交生成文件到中心化倉庫
在提交文件改動之前,[email protected]:
以微服務為單位,每個微服務維護一個目錄,包含當前服務所有的.d.ts文件,以及統一向外暴露的index.d.ts文件 。除此以外每個微服務目錄下還有一個package.json文件,這個文件是在接口定義文件生成類型步驟使用npm init生成得到的,該文件包含了當前服務的版本、依賴、名稱等內容,提供給后續類型文件發包步驟使用 。commonTypes為一些基礎的類型聲明文件,例如 Protocol Buffers 本身定義的一些基礎 Protobuf 文件和內部定義的一些公共 Protobuf 文件 。鑒于這些 proto 依賴幾乎每個微服務都會用到,我們對此做了特殊處理,單獨發包管理 。
@fw-types|__serviceA|----index.d.ts|----type1.d.ts|----type2.d.ts|____package.json|__serviceB|__serviceC|__commonTypes當類型聲明文件生成之后,通過 git status命令可以獲取到被改動文件列表,這里存在兩種情況:
a. 對于新的微服務服務,對應的類型包還沒發布,因此不存在 package.json 文件,我們通過 npm init 生成,并配置上相應的參數 。
b. 對于已有的微服務,則需要對 package.json 文件中的 version 字段進行更新,詳細內容將在后續包版本管理中介紹 。
當全部改動都準備就緒,便可以調用 git commit 命令向遠端倉庫提交改動 。我們對于 commit message 進行了特殊設計,將對應的 commit branch 也包含在其中,從而方便通過commit message里對類型聲明文件和對應的Protobuf文件更改進行回溯 。
類型聲明文件發包
Freewheel 目前采用 Artifactory 進行制品內容(Artifacts)的管理與存儲 。Artifactory 是 JFrog 的一個產品,不但可以管理二進制包文件,還可以對市面上幾乎所有語言的包依賴進行管理 。這一階段的類型聲明文件的發包操作也有賴于 Artifactory 對 npm 包的支持 。具體流程如下所示:
[email protected] webhook 檢測到 push 事件時,會觸發向 Artifactory 發包的任務,包以微服務為單位進行管理 。去每個服務下進行版本比較,拉取遠端當前服務的最新版本與現在庫里的版本比對,當不匹配時,說明當前代碼倉庫下的版本有所更新,需要調用 npm publish發新包 。
這里需要注意,第一次和第 N 次發包是有區別的 。第一次發包的時候 Artifactory 上并沒有該服務的包,如果讀取版本會直接報錯中斷流程,因此這里需要對是否是第一次發包進行判斷,結合第二階段生成類型聲明文件的任務,對第一次發包的版本進行特殊處理 。
最終在 Artifactory 上以微服務為單位的目錄結構如下:
————————————————Artifactory——[email protected]|————service A|———— -|[email protected]|—————— service A-0.0.0.tgz|—————— service A-0.0.1.tgz|—————— service A-0.1.0.tgz|—————— service A-0.2.0.tgz|————service B|————service C|————service D使用情況
目前這套流程已經支持了 20+ 微服務,平均每天約有 5 個由于Protobuf文件變化自動觸發的任務 。平均每個 protobuf 改動合并之后能夠在 30 分鐘內從 Artifactory 下載到對應的包文件 。
在 Web 前端的項目中也已經有 3 個項目開始逐步接入這些類型包,大大改善了團隊前端工程師的開發體驗 。
下圖為使用生成的 TypeScript 文件替換原先手寫的類型 。
4落地應用的問題與解決方案
最終代碼提取
我們從一開始生成.ts文件到最終可用的.ts文件提取流程如下圖所示,包含工具生成和二次轉化兩部分 。其中二次轉化包含了冗余代碼去除、命名變化和引用路徑變化,下面逐個進行介紹 。
冗余代碼去除
proto-loader的設計會生成很多文件:
對于每個message生成一個.ts文件對于 rpc 接口生成相應的 .Service.ts文件用于運行時 protobuf 類型獲取的 ProtoGrpcType 文件
對于 FreeWheel 的業務場景,我們只關心與message相關的.ts文件。此外,對于每一個message所生成的interface還會有一個額外的__Output類型,這個類型對于我們來說也是無用的 。因此需要對這些冗余的代碼進行刪減,并根據情況對import里對引入進行調整 。
命名變化
proto-loader以message名作為.ts文件名,有可能會出現文件名重名問題 。例如當一個微服務下的兩個protobuf文件里包含一個僅大小寫存在差異的message,此時生成的.ts文件僅大小寫存在差異,存儲在同一路徑下 。一些不區分大小寫的文件系統里會最終只保留其中一個文件 。因此需要對于生成的文件名進行重復檢測和重新命名,使用其所在的Protobuf文件名來區分 。
生成文件import路徑的變化
使用proto-loader生成的類型聲明文件里,存在對其他類型聲明文件的引用 。直接生成的結果里import的路徑采用原先各個服務Protobuf文件的路徑關系,存放在proto子路徑下,例如下圖所示import ../proto/ 。
————————————————proto——————————————————micro_services_repo|————service A|—————— proto|—————— a.proto|—————— b.proto|—————— c.proto|————service B|————service C|————service D[email protected],沒有 proto子目錄,因此import的 .ts 文件路徑如果和原先proto的路徑一致的話,會無法正確讀取,需要對其生成的文件import的路徑進行更改,[email protected],import ./ 。
————————————————typescript———[email protected]|————service A|—————— a.ts|—————— b.ts|—————— c.ts|—————— index.d.ts|————service B|————service C|————service D除此之外,由于部分文件命名的變化,也需要更改對應文件的import的路徑,才能最終實現正確類型的引入 。
包版本管理
對于每一個微服務服務的類型聲明文件包,其版本在每次d.ts文件存在更新后,都需要進行版本號的更新,并將更新后的版本信息一起作為 commit message [email protected],我們采用語義化版本(SemVer)規范 。其命名規則是以 x.y.z 的形式:
X 表示主版本號,當 API 兼容性變化時,X 遞增Y 表示次版本號,當存在不影響兼容性的功能增加時,Y 遞增Z 表示修訂號,當存在不影響兼容性的 Bug 修復時,Z 遞增
目前 FreeWheel 主要使用 proto3 版本,基于默認前向兼容的情況下,我們暫時只對 x 和 y 位進行更新 。因為對于Protobuf很難界定 bug 修復的行為,所以只存在兼容性變化和新特征的添加,具體結合Protobuf的改動來確定最終得到的版本,x 位表示無法兼容的變更,y 位表示新字段新功能的增加 。
前端庫的類型支持
本解決方案旨在維護一個公司級別的TypeScript類型中心化倉庫,除了對于Protobuf文件生成TypeScript類型聲明文件以外,還期望覆蓋一些前端庫的類型聲明 。因此,我們也支持前端開發人員在 @fw-types倉庫里以 Pull Request 的形式提交對目前公司內部使用的JavaScript庫手寫的類型聲明文件,共享給全公司的同事使用,期望在公司層面維護一個活躍的TypeScript 生態 。
5未來計劃與展望
基于前文其實可以看出,雖然proto-loader 解決了大多數問題,但也引入了不少額外工作 。我們計劃基于proto-loader定制一版專門應對 FreeWheel 需求的生成工具,降低二次轉化部分代碼的維護成本 。這一部分工作已經在進行之中 。
此外,目前生成的代碼尚未被 lint 和格式化,為了保證統一的生成文件樣式,我們還需要加入對 lint 和格式化的支持 。
【js反編譯成ts JS反編譯】最后,@fw-types 倉庫的推廣使用還需要提供更加精簡的接入步驟,繼續增加對更多微服務和前端庫的支持,使 JavaScript 往 TypeScript 的遷移更為簡單和順利 。