導語

本文通過React語法從v15自動升級爲v16的方案,闡述了AST的概念及其在前端項目中的應用與探索,介紹了若干適合AST技術在前端落地的場景。

背景

通常一箇中後臺系統至少有三到五年的生命週期。 

在立項之初一般會採用一些成熟穩健的技術,然而隨着時間的流逝,原有技術棧必將逐漸變得老舊、難以維護:比如 vue1.x 升級至 2.x、swift的歷次升級。 

那麼我們應當如何處理這些老舊的代碼,是另起爐竈推倒重來,亦或是硬着頭皮在原來的基礎上繼續打補丁? 

對於上述問題,本文主要介紹一種基於修改抽象語法樹(Abstract Syntax Tree,AST)實現自動化升級老舊項目技術棧的方案。案例爲React 15 升級至 React 16。

應用AST技術實現自動化升級React技術棧的解決方案

1、 方案概述

該方案的主要步驟如下:

  • 通過原始代碼生成 AST;

  • 按照一定規則修改裁剪 AST;

  • 根據修改後的 AST 生成新代碼。

該方案所需解決的主要問題如下:

  • 模塊的引入/導出改爲 ES6 module 的方式;

  • React15 的 createClass 語法改爲 React16 的 class 組件:其中需要重點關注的內容有:

    a)class語法轉換;

    b)createClass組件內函數 this 自綁定轉換;

    c)getInitialState和getDefaultProps轉換爲新的state和props聲明;

  • 部分生命週期如 componentWillMount 修改爲 UNSAFE_componentWillMount;

確定了方案和問題後,讓我們一起揭開AST的面紗。


2、 初識AST
我們知道,即使是解釋執行的語言,也是需要編譯的。詞法分析會把語句分解成詞法單元,即 Token。語法分析會把 Token 轉化成抽象語法樹,即 AST。

AST流水線

通過工具astexplorer可以查看查看語句let a = 2 所轉換的AST:

let a = 2 生成的AST

樹狀結構

簡單解釋一下AST:

  • type值爲Program通常表示根節點

  • body是一個包含多個Statement的數組,每一個Statement相當於一個語句

    A)type值爲VariableDeclaration說明該語句是個變量聲明語句

    B)declarations中:

    C)VariableDeclarator代表變量聲明的描述

    a)id代表變量名

    b)init代表變量初始值

    D)kind聲明變量類型let

  • sourceType爲module代表它是一個模塊

完整的AST文檔可以參考AST Spec 。

3、 使用Babel進行代碼轉換
實際工程中我們不需要從頭去實現詞法分析器、語法分析器,因爲工程化的前端項目都會用到Babel ,而@babel/core 已經爲我們提供了強大的parser、traverse等工具,利用它們可以快速生成並修改AST。
在使用這些工具之前,我們先比較一下原始代碼和目標代碼:

原始代碼與目標代碼對比

使用 astexplorer 來將原始代碼和期望代碼生成AST,進行對比:

AST對比

通過對比可以發現兩份AST的結構非常相似。對比語句var React = require("react");和Import React from "react";所生成的Statement可以發現,原始AST稍加修改就能得到目標AST:

首先修改Statement的type,不同的Statement其結構也不盡相同,ImportDeclaration的結構如下:

其中關鍵部分爲specifiers(說明符)和source(源):

  • specifiers表示引入的變量;

  • source表示從哪個源引入。

解析VariableDeclaration可以發現我們需要的specifiers和source在VariableDeclaration中都有對應的部分:

  • specifiers.local對應declarations.id;

  • source對應declarations.init.arguments。

因此稍加修改原始AST就能夠轉換成目標AST。而所需要修改的部分和我們第一步總結的大同小異:

  • 以類型 VariableDeclaration 聲明的 require 語句需要轉化爲 ImportDeclaration。

  • 以 VariableDeclaration 聲明的 createClass 語句其type需要轉化爲 ClassDeclaration。

  • 在createClass語法中 JSXElement 上使用諸如onClick等事件來調用成員函數時,this是自綁定的,而Class語法中則需要顯式綁定:

解決思路是判斷節點屬性爲React事件且node.value.expression.type是 MemberExpression ,則修改 node.value.expression.type 爲 CallExpression 併爲其增加 callee 屬性。

  • 處理getInitialState:刪除該節點,將該節點內容增加在 constructor 中。

  • 處理getDefaultProps:刪除該節點,將該節點內容添加在最外層 body 中,類型爲ExpressionStatement。

  • module.epxorts ExpressionStatement 轉爲 ExportDefaultDeclaration。

轉換流程如下圖:

轉換流程

部分示例代碼如下:

查看輸出的代碼後發現空格、縮進等格式會有些亂,這是由於我們沒有對 start、end 等表示位置的屬性進行處理。可以用prettier-eslint進行處理以獲得格式化後的代碼(prettier也是基於AST實現的)。 
這樣就實現了老項目煥發新生。

4、 實踐項目

信安獵人人工審覈系統始建於2016年,該系統前端部分至2018年底共計有9 個人工審覈模板(即子系統),接入300餘個場景。全部模板均fork了採用 React15 的 createClass 語法編寫,通過 webpack2 進行構建的初始模板。隨着產品功能的不斷演進、接入場景的不斷增加,在維護該項目的過程中我們總結了幾個亟待解決的問題:

  • 我們在產品技術演進過程中爲保證中臺系統體驗的一致性積累了許多業務組件,因爲採用了React16 hooks等特性導致不能在該系統中直接複用,複用成本高;

  • 系統所用到的公共組件散落在各個模板中進行維護,維護成本高;

  • 代碼語法與主流技術棧產生了一定程度的割裂,不利於後續維護。

應用該方案升級改造後以上幾個問題都得到了解決:

  • 升級後可以直接複用部分業務組件,降低了複用成本、也降低了後續的維護成本;

  • 部分組件可以用業務組件代替、另外部分組件可以提取到業務組件庫,進行統一管理,提高了複用率,降低了維護成本;

  • 代碼語法已經升級到React16,降低了維護成本。

AST的其它應用場景

回到AST技術本身,其應用場景是非常廣泛的,甚至可以說是前端項目的基石:Babel轉譯、代碼壓縮、PostCSS、lint等工具都是基於AST。以下列舉幾個適合應用AST技術的場景:

1、跨平臺/框架組件互轉

在小程序百花齊放的今天,我們的業務需要支持各個小程序平臺。如果採用原生開發方案,那麼單一功能/組件需要在各個平臺重複實現,不僅開發效率低,維護成本也成倍增加。使用AST進行轉換可以顯著幫助開發者降低開發維護成本。

對比不同小程序的模板文件、樣式處理以及屬性、事件、生命週期,並且統計出功能近似的部分。通過對比我們可以發現,各個平臺提供的主要能力大部分都是接近的,這就是我們能夠通過AST進行小程序轉換的基礎。

另外,在蘋果向開發者發佈“更新使用網頁視圖的App”通知的前提下,許多大量使用WebView的App都需要進行更新。如果將原Webview功能用Objective-C/Swift重新實現,那麼成本對於大多數團隊都高到無法接受。這個時候我們可以考慮使用ReactNative來替換原有Webview,通過AST來自動轉換React/Vue組件爲ReactNative組件,可以極大的降低切換成本。當然這個方案也有其自身的侷限性:

  • React和Vue存在部分生命週期、高階函數、Fragment等無法兼容之處;

  • ReactNative的樣式僅能支持部分CSS子集,部分樣式可能需要修改;

  • 部分Web組件需要重新開發爲ReactNative組件。

雖然跨平臺/跨框架的組件轉換方案仍然存在着若干不足,但是對於以上兩個適用的場景,效率的提升還是非常明顯的。

2、結合Markdown生成時序圖

當我們接手維護或者擴展一個業務項目時,如果其實現邏輯相對複雜,那麼即使有完善的文檔、註釋來支撐,在修改時也需要花費一定的時間精力去梳理。這時候我們可以通過AST獲取函數調用關係,再將其通過Markdown生成爲時序圖。通過時序圖來協助我們理清調用邏輯。示例如下:

Markdown代碼

上述代碼生成的時序圖

3、webpack插件

舉例來說:在開發環境中我們通常會增加許多調試相關的代碼,而這些代碼對於業務邏輯並沒有價值,因此有必要在生產環境構建時移除掉這些代碼,而手動移除費時費力還容易疏漏。這時就可以編寫webpack插件,通過AST來找到debug相關代碼並將其移除,既能保證生產環境的清潔,又能在開發環境充分利用debug信息。

總結

通過對以上幾個方案的分析我們可以看出,所有的解決方案都有其閃光/不足之處,我們工程師的職責就是根據不同的場景定製相對最優的解決方案。這也應了軟件工程中的那句老話:“沒有銀彈”。

隨着AST在前端的應用場景越來越多,它的重要性也不斷提升,因此前端工程師也有必要掌握AST相關知識,在適合的場景運用這項技術來解決問題。對此,希望這篇文章能夠幫助到你。Happy Hacking !

參考文獻

1、AST Spec 

2、babel-parser/ babel-traverse


作者簡介

王亮,本地服務事業羣前端工程師,具備JavaScript、Java、Python、Objective-C等語言開發經驗。相信並踐行“職位有前後端之分但工程師沒有”。

閱讀推薦

相關文章