年初接到一個任務將 semi-ui 替換到 antd,但是能人力預算不太夠,所以基於工作量和效率選擇了基於 AST 的替換方案。
項目技術棧是 React + tsx
為什麼要使用 AST?
AST(Abstract Syntax Tree,抽象語法樹)是一種在計算機科學中表示源代碼語法結構的樹狀數據結構。
通過 AST,可以理解代碼的結構和含義,實現代碼分析、轉換和操作。
基於 AST 的替換方案可以快速而準確的對大量代碼進行修改,提高開發效率和代碼質量。
還有什麼方案?
- 固定字符串替換。對於 import 和 tsx 都有嚴重的誤傷。
- 正則替換,編寫複雜,嵌套難以識別。
- 手動替換,重複性工作容易失誤,耗費工時太多,適合小工作量的情況。
方案比對
| 方案 | 工作量 | 誤傷 | import | 組件替換 | 組件屬性替換 |
|---|---|---|---|---|---|
| 固定字符串替換 | ✅ | 😭 | 😭 | 😒 | 😒 |
| 正則 | 😒 | 😭 | 😒 | 😒 | 😒 |
| 手動 | 😭 | ✅ | ✅ | ✅ | ✅ |
| AST | ✅ | ✅ | ✅ | ✅ | ✅ |
Babel 簡介
Babel 是前端領域的必備工具,是一種源碼到源碼的轉譯器。
在項目中可以讓我們使用一些新的語法(展開運算符) 和 api(@babel/core),會在編譯的過程中將 code 轉為目標環境所支持的語法並引入 polyfill
Babel 實際應用場景
- 將
()=>{}轉為function(){} - 為
Array.isArray添加 polyfill
除了上面提到的編譯轉譯代碼之外,babel 也可以用來做靜態分析,分析代碼提取信息,然後生成文檔
如:
- 自動國際化處理
- Linter
- 壓縮混淆,刪除死代碼,變量名混淆。
- 模塊遍歷器,分析 import 移除未被使用的資源模塊
Babel 編譯原理
babel 是 source to source 的轉換,整體編譯流程分為三步:
• parse:通過 parser 把源碼轉成抽象語法樹(AST)
• transform:遍歷 AST,調⽤各種 transform 插件對 AST 進⾏增刪改
• generate:把轉換後的 AST 打印成⽬標代碼,並⽣成 sourcemap
Babel 中常⻅節點
- Literal 是字⾯量的意思,比如説
'are you sure?' - Statement 是語句
- Identifer 是標識符的意思
- Declaration 聲明語句是⼀種特殊的語句,它執⾏的邏輯是在作⽤域內聲明⼀個變量、函數、class、import、export 等。
- Expression 是表達式,特點是執⾏完以後有返回值,這是和語句 (statement) 的區別。
- Element 是 JSX,分為 JSXElement、JSXOpeningElement、JSXClosingElement 等等
Babel 中常⽤ API
- @babel/parser :babel parser 叫 babylon,是基於 acorn 實現的,擴展了很多語法,可以⽀
持 es next(現 在⽀持到 es2020)、jsx、flow、typescript 等語法的解析。babel parser 默認只
能 parse js 代碼,jsx、flow、typescript 這些⾮標準的語法的解析需要指定語法插件。 - @babel/traverse :parse 出的 AST 由 @babel/traverse 來遍歷和修改,⽀持指定要遍
歷的 AST 節點,指定 visitor 函數。babel 會在遍歷 parent 對應的 AST 時調⽤相應的 visitor 函 - 數。@babel/types :遍歷 AST 的過程中需要創建⼀些 AST 和判斷 AST 的類型,這時候就需要
@babel/types 包。t.isIfStatement 創建,t.assertIfStatement ⽤於判斷。 - @babel/template :⽀持通過代碼來⽣成 ast 進⾏替換。相⽐ parser ,template ⽀持不同粒
度 - @babel/generator :AST 轉換完之後就要打印成⽬標代碼字符串,通過
@babel/generator 包 - @babel/helper-module-imports
替換實戰
存在的問題
- 項⽬技術棧 React + tsx
- 839 個⽂件中,使⽤ 63 個組件,共計 2485 次使⽤,最多⼀個組件被使⽤了 299 次。
- 項⽬周⼀、週三四,上線時間不定。更新頻率較⾼
- 沒有測試同學,研發⾃測。測試環境⾃測,⽆等待時間,即可上線
實戰腳本
替換屬性值&替換屬性名
- Space 不⽀持 vertical 需要改為 direction="vertical"
- 替換 onChange 事件調⽤
- 提取 children 構建為 options
- 替換 Size 改為新的映射關係
- 將 Link 解構賦值
整體⽅案
- 統計待升級組件,確認替換範圍和影響(839 個⽂件中,使⽤ 63 個組件,共計 2485 次使⽤)
- 項⽬格式化 npx eslint --fix ./src , prettier --write '*/.{tsx,ts}' , 暫
不開啓編輯器⾃動格式化 - 分批替換(⻚⾯維度、組件維度)每週替換4個組件,以低頻⾼優作為參考項。
- 與業務⽅溝通確定上線節奏及變更影響範圍,周⼆推 test,週四推 online,提前⼀周在業務羣公佈。替換計劃固定變更影響範圍。
- 測試驗收(⾃測&test環境)功能、樣式
- ⽀持快速回滾。依賴 turbo 實現快速回滾,採⽤臨時 release 分⽀,實現需求迭代和組件替換不衝突,遇到問題也可以快速回滾。
- 加⼊業務 feedback 羣,遇到問題優先回滾⽌損。
- 所有替換⼯作完成後,開啓⾃動格式化,移除⽆效代碼。
優缺點&改進
優點:
• ⽀持增量替換,不阻塞業務開發
• ⽀持快速回滾
• 適合⼤批量重複性⼯作
• 適合 jsx 這種嵌套關係的內容修改,可以減少誤傷
缺點:
• 有⼀定的學習成本(當然可以使⽤ GitHub Copilot 、ChatGpt 減少⼀些⼯作量),
• 需要收集組件差異(這⾥可以考慮提取屬性和⽅法,然後抓取⽂檔中的定義做⽐對)
• 編碼成本(如果替換數量不多,可以考慮⼿動替換)
總結
最後我們再來回顧⼀下 @babel/parser 、 @babel/traverse 、 @babel/types 、
@babel/template 、 @babel/generator 這些庫的作⽤。
也可以想想有什麼應⽤場景是可以落地的?⽐如説
• ⾃動國際化,通過腳本提取所有⽂案。
• 增加⼀些個性化的格式化規則,⾃閉合標籤,優化 imoprt 導⼊順序。
傳送⻔
- Babel 插件通關秘籍 - zxg_神説要有光 - 掘⾦⼩冊
- https://astexplorer.net/
- Stackblitz 測試地址:Express Starter - StackBlitz