提升前端開發效率,攜程機票定制代碼生成器實踐
作者簡介
Zilin Wang,資深前端開發工程師,擅長前端打雜,專注于Remix、Radix UI、Haskell等領域。
Sheila Dai,資深前端開發工程師,關注前端性能優化、前端智能化。
在日常的研發工作中,編寫前端界面結構占據了一部分工作量。很多UI組件都存在共性,如何減少編寫UI界面的開發時間,以及提取公用的前端組件,從而達到提升研發效能的目的,是我們的重要課題。
在《前端智能化探索,骨架屏低代碼自動生成方案實踐》中,我們曾經探索過一種自動生成骨架屏代碼的方案,在此基礎上,我們設計了一套代碼生成器的定制流程,到達可以定制任意目的代碼的效果。本文將圍繞視覺稿生成任意代碼,探討代碼生成器的原理與細節,最后是落地的效果展示。
一、背景
骨架屏代碼自動生成,作為一個最小 MVP (Minimum Viable Product),驗證了視覺稿自動生成代碼的可行性與實用性。
那么,除了高度重復性質的骨架屏以外,還能夠實現哪些通用組件?甚至能不能實現應用的業務組件?
如圖是視覺稿生成代碼的粗略步驟,我們可以看到,在提取視覺稿信息后使用通用算法,得到中間代碼后,可以生成我們的目的代碼。在這個步驟中,通過編輯中間代碼,我們可以自動生成不同的目的代碼。
我們的預期則是把這一步驟,交付給業務研發自己來實現,通過在 D2C 平臺上插入不同的代碼生成器,實時自動生成需要的目的代碼。
在整個頁面的轉換結果上,頁面上的每個組件都可以通過選擇不同的代碼生成器來得到相應的結果。
二、問題分析
本方案是在骨架屏探索成果上的拓展設計,我們面臨的問題主要有以下三個:
1)平臺角度:如何讓生成器可高度定制?
我們首先需要解決的問題,是讓業務研發可以自行定義代碼生成器。那么我們需要把中間代碼層從封閉的平臺中剝離出來,變成一個開放的插件入口。
2)生成器作者角度:如何快速上手編寫?
雖然針對一個指定目標代碼結果的生成器,只需要一次中間代碼的編寫,即可作為一個公開插件在平臺上提供給其他研發進行使用。但這個中間代碼的編寫過程依然存在一定的門檻,會讓想要使用的人望而卻步。我們需要降低這個門檻,讓業務研發可以隨時發布或者調整自己的代碼生成器。
3)普通使用者角度:如何零成本使用已有生成器?
如果平臺上提供的生成器,已經滿足使用者的需求了,那么他可以在不學習相關知識的前提下進行一鍵生成代碼使用。除此之外,我們需要在平臺上新增一些功能,以便讓使用者在這個過程中可以更加順暢地進行預期的自動代碼生成。
三、解決方案
為了解決上文提到的三個問題,我們從三個方向去解決這些問題:
- 中間代碼插件化
- 生成器編寫模版化
- D2C 平臺優化
3.1 中間代碼插件化
在自動生成代碼的流程中,我們需要把生成器這部分從封閉的平臺中剝離出來,提供給業務研發進行自研。我們主要借助了 UIDL (Universal Interface Definition Language)結構,依據這個 UI 層面的數據結構解析得到我們需要的目的代碼內容。
1 ) UIDL 簡介
UIDL,即通用界面定義語言,一種用于描述 Web、移動和桌面應用程序用戶界面的通用語言,是 teleport 對于 UI 描述的一種定義規范。UIDL是 DSL(Domain Specific Language,解決特定領域問題的語言) 的子集。它與平臺無關,可以應用于任何平臺或者應用程序。
使用 UIDL 的主要目的就是將用戶界面描述成一種機器可讀的格式,以幫助開發人員更加高效地構建、測試和維護用戶界面。UIDL具體定義內容可以參考官方文檔,這里給出一個簡單的示例:
{
"name": "Badge",
"propDefinitions": {
"count": {
"type": "number"
}
},
"node": {
"type": "element",
"content": {
"elementType": "container",
"attrs": {
"class": "badge"
},
"children": [
{
"type": "element",
"content": {
"elementType": "text",
"attrs": {
"class": "badge-text"
},
"children": [
{
"type": "dynamic",
"content": {
"referenceType": "prop",
"id": "count"
}
}
]
}
}
]
}
}
上面的UIDL描述了一個 Badge 組件,它接受一個 number 類型的屬性 count,輸出我們常見的 Badge 組件 UI 結構。通過使用teleport 提供的 React 代碼生成器,我們可以得到下面的結果:
import React from 'react'
import PropTypes from 'prop-types'
const Badge = (props) => {
return (
<div className="badge">
<span className="badge-text">{props.count}</span>
</div>
)
}
Badge.propTypes = {
count: PropTypes.number,
}
export default Badge
UIDL 會給出一個通用的 UI 界面節點描述,通過生成器可以渲染得到我們需要的指定框架代碼。
2 ) 生成器解析
在已有生成器滿足業務研發的需求前提下,可以直接進行使用;不滿足則業務研發通過編輯模版可以得到新的生成器,再通過平臺研發審核發布到已有生成器列表中,開放給所有研發進行代碼的自動生成。
在把中間代碼層插件化的過程中,我們主要借助了 UIDL 的定義結構,使用 teleport 定義的生成器結構來進行代碼轉換,內部包含了 Mappings(UI 組件映射)、Plugins(插件處理邏輯)、PostProcessors(后處理器)。
- Mappings:給出組件映射關系;
- Plugins:處理復雜對應邏輯;
- PostProcessors:美化代碼格式等等。
代碼生成器實際上就是對中間代碼進行解析,不同的解析得到不同的結果。
3 ) 生成器插槽
我們在平臺上提供了選擇不同生成器的入口,根據即時選擇的類型,實時自動生成對應的代碼。
業務研發完成新生成器的編寫后,發布成npm包,通知平臺研發進行審核。審核成功后,相應的生成器版本會展示在平臺的生成器入口處。
其他業務研發可以在這個公開的生成器列表中,選擇適合自己業務場景的生成器,進行代碼的一鍵自動生成。
3.2 生成器編寫模版化
為了降低新生成器編寫者的學習成本,我們提供了可本地預覽的代碼模版,并且封裝了常用前端框架生成器及一些通用方法。
1 ) 開發流程
業務研發clone模版倉庫到本地,模版編寫主要有兩個方向:
a. 編寫某種前端框架下的通用組件:視覺稿 DSL 轉換為需要的 UIDL 結構(調整層級、組件名稱等),再調用對應的框架生成器,生成代碼;
b. 編寫特定的數據結構:獲取 DSL 中的節點數據,構建為新的數據結構。
再在本地進行效果預覽,最后發布成為一個獨立的npm包,通知平臺研發審核后插入到插槽中。
2 ) 本地開發與預覽
業務研發可以根據我們提供的模版來進行定制化輸出,也可以參考其他公開的生成器進行編寫。
以下為提供的模版結構:
custom-generator-template
|----package.json // 發布的 npm 包信息
|----README.md
|----src
| |----index.ts // 編寫的自定義 DSL
|----test // 本地測試輸入與輸出
| |----files // 輸出的文件結構
| |----dsl.json // 輸入的dsl結構
| |----index.ts
|----tsconfig.json // 默認配置源文件為 ts
|---- dist // 輸出的 dist 目錄
模版中內置了輸入與輸出的本地便捷測試:在編輯src/index.ts文件后,執行npm run test即可在test/目錄下看到輸出的內容。
下圖為本地預覽示例,左為 DSL 結構,右為經過生成器轉換的代碼:
本地測試使用的 DSL 結構可以在平臺上快速復制得到。
3 ) 常用 API 封裝
為了降低生成器編寫者的學習成本,我們會處理前序與后序步驟,在實際使用中都是可選的:
- 前序:原始視覺稿 DSL 轉換為 UIDL 結構;
- 后序:調用 API 生成相應框架代碼。
普通的 DSL 代碼生成模型如下:
export type GenerateCustomFiles = (dsl: DSL) => Promise<CompiledComponent>
// CompiledComponent 是定義好的輸出結構,包含代碼依賴和代碼文件
對于頁面級別的 DSL,引進了 Design Tokens 和共享樣式的概念,會在普通的 DSL 上添加一些屬性:
export type GenerateCustomFiles = (rootDSL: RootDSL, options: GenerateCustomOptions) => Promise<CompiledComponent>;
// options 是傳入的生成文件參數,當前有樣式參數(CSS Module、Inline Style等)、布局參數(Pixel、Rem)等
如果調用平臺開發的API,就可以很方便的生成業務定制化的代碼:
const generateFiles: GenerateCustomFiles = async (rootDSL, options) => {
const rootUIDL = rootDSLToRootUIDL(rootDSL, {
layoutUnit: options.layoutUnit,
uidlAdjust,
}) // 前序:使用uidlAdjust進行DSL轉成UIDL過程中屬性的修改
// 核心邏輯:UIDL 結構修改
// ...
const cc = await generateReactFiles(rootUIDL, { layoutUnit: options.layoutUnit, style: options.style })
// 后序:調用前端框架生成API
return cc
}
在后序步驟中,攜程 Design To Code 支持的框架 API 如下:
- generateHtmlFiles
- generateVueFiles
- generateReactFiles
- generateReactNativeFiles
這樣處理是為了讓編寫者無需關心復雜的前端框架自動生成過程,僅通過修改 DSL 結構層次或名稱即可到達自己定制的目的。
3.3 D2C 平臺優化
為了讓普通使用者零成本地在 D2C 平臺上進行代碼的一鍵自動生成,我們對平臺進行了一些優化。接下來介紹其中三個重要功能:
- 節點標簽
- 局部自動生成
- 沙盒預覽模式
1) 節點標簽
點擊界面左側的 DSL 節點的標簽 icon,彈出浮層,可以手動給當前節點添加語義化標簽名稱。
手動打標簽,是在 DSL 節點中新增了一個屬性。在生成器中可以根據該屬性實現不同的效果。
例如在落地方案 Flybirds 測試用例的自動生成中,通過給視覺稿中不同的文案部分做了三個特定標簽識別:render、exist、click,分別對應了以下的執行語句:
- render: 頁面渲染完成出現元素 [機票]
- exist: 存在[機票]的文案
- click: 點擊[text=機票]
從視覺稿信息中來說,這些結構都是文字節點,沒有什么區別。通過手動打標可以在自動生成時補充額外的信息。
2 ) 局部自動生成
下圖中,左邊是 DSL 節點,右邊是視覺稿標注。點擊左側節點會突出顯示視覺稿中內容,同樣地,點擊右側視覺稿局部也會在左側節點中同步突出對應節點。
同時,會在下端實時展示當前選擇的局部畫板通過生成器自動生成的代碼內容。圖示為點選“僅看直飛”按鈕局部后,生成的 React Native 代碼。
在生成器滿足業務研發需求的前提下,可以點選局部后,直接復制下方的代碼到倉庫中進行應用。
在初始進入畫板時,或者點選了 DSL 的根節點,下端會通過選擇的生成器實時渲染整個頁面的代碼。
3 ) 沙盒預覽模式
為了更加真實地預覽自動生成的代碼,我們提供了沙盒模式。React / Vue 等代碼可以直接在 web 端預覽,React Native 我們也通過react-native-web
轉為web端代碼,可以進行實時編輯并查看對應效果。
此外,沙盒模式還提供了一鍵打包下載的功能,使項目可以進行快速部署發布。
四、落地效果
落地效果均為機票實現的生成器,可在平臺中進行一鍵自動生成。我們從三個不同的維度來進行自定義生成器效果展示。
4.1 自動生成指定框架代碼
1 ) 效果演示
圖例為 React Native 代碼自動生成。
2 ) 內部實現
語言框架的應用,是作為自動化轉碼的一個基礎底層代碼內容。在此基礎上,我們可以得到組件化的自動代碼生成。但語言框架轉碼比組件化更為復雜,可以說語言框架轉碼是組件化轉碼的一個超集。
在這個過程中,借助了前文提到的 teleport 定義的 generator 生成器結構,其中需要調整mappings、plugins、postprocessors,來得到我們預期的框架代碼結構。需要處理的具體內容如下:
- 識別抽象 DSL 節點:建立我們自定義的語言框架的首要步驟,需要把抽象的 DSL 映射為基礎的 HTML 節點,識別 elementType 為正確的對應組件名;
- 映射組件:支持 React Native 組件映射及對應引用 package,讀取資源文件生成 React Native組件。在這里最終映射預覽的文件建立在 react-native-web 的基礎上;
- 處理依賴:處理文件之間的依賴關系,加載組件,以便輸出正確文件;
- 樣式表風格化:第一步,將 CSS 風格的樣式表轉換為 React Native StyleSheet;第二步,處理屏幕適配;
- 調整 DSL 結構:處理中間 DSL,減少冗余以及修正轉化錯誤;在有了大體的轉化前后結構內容,依然需要進一步修復轉化過程中的一些冗余,以減少代碼 size,或者降低生成的樣式在不同機型上出錯的可能性。
以 React Native 為例,我們主要需要做到:
(i). 刪除冗余:刪除不需要的樣式節點,例如 fontFamily、letterSpacing、text 節點的 width / height;
(ii). 合并代碼:合并節點,例如 paddingTop 與 paddingBottom 需要合并為 paddingVertical;
(iii). 兼容樣式:刪除或者修改會引起不同機型樣式兼容問題的節點,例如 lineHeight、fontWeight。
- 美化代碼:需要格式化生成的 typescript 代碼;
- 支持在線預覽自動生成的 React Native 代碼:我們需要在 web 頁面進行實時編輯預覽,因此引入了
react-native-web
,以便使用者能實時調試。
4.2 自動生成指定組件代碼
1) 效果演示
圖例為 React Native Label 組件代碼自動生成。
2) 內部實現
我們可以通過編輯中間代碼,來得到預期的業務組件功能代碼,包含動畫效果、交互邏輯等。目標是生成一套在生產環境高可用性、復用性的組件。以標簽組件為例,示范如何生成預期的組件代碼。
在這個過程中,需要使用多個真實場景視覺稿進行代碼渲染,在線預覽效果,進行代碼調試與可用性測試。經過該場景的多次測試,我們需要覆蓋兩個結構不同的轉換場景:
- 帶有漸變背景的標簽處理
- 常規文案處理。
轉換核心代碼如下(抓取對應節點信息,構建為我們預期的代碼結構:
根據當前節點的類型,做不同的層級處理。原始 DSL 與目的代碼前后預覽如下:
4.3 自動生成測試用例代碼
1 ) 演示效果
圖例為 Flybirds 測試用例代碼自動生成。
2) 內部實現
我們使用攜程機票開源的跨端跨框架的BDD UI 自動化測試方案——Flybirds 的用例結構來進行構建該生成器的目的內容。
因為文本節點的結構是一致的,我們需要手動給不同的文本節點賦上不同的語句含義,在此處我們通過在平臺上給視覺稿圖層打標簽進行實現。
在實現上,通過遞歸 DSL 結構中的文本節點進行文案內容的查找與輸出對比。轉換代碼如下:
執行這段代碼,本地運行npm run test
,前后輸出內容對比:
測試用例生成作為一項實驗性實踐,充分說明了自動轉碼的自由性——有了視覺稿的原始信息結構,你可以從中解析出你需要的數據節點,并且修改為需要的目的內容。
五、未來規劃
在后續的開發規劃上,我們側重于完善流程中的細節,以及通過我們官方的示例,增強 D2C 自動代碼內容的實用性,繼續推出更多語言框架,以及適配更多應用平臺,例如小程序等等。
除了平臺細節的完善,在這里主要提出未來的兩個方向上的規劃:
5.1 人工智能:與 copilot 結合的未來
目前的 D2C 方案,存在一個很明顯的劣勢,那就是無法處理復雜的邏輯反饋。我們提供的自定義生成器功能雖然可以高度定制化生成的組件,但是開發使用的內容依然沒有相關的邏輯代碼內容。
人工智能的出現可以彌補這一缺憾。目前人工智能還未推出完善的編寫代碼功能,但能夠根據輸入的自然語言描述或者是環境來推斷開發需要使用的代碼片段。copilot 可以輔助開發人員在開發過程中自動生成一些復雜的代碼,減少開發人員的負擔,提高效率與質量。
5.2 深度定制化:一鍵換膚,Design Tokens + custom DSL
Design Tokens 是一種用于描述設計系統中的基本視覺和品牌屬性的集合。這些屬性包括顏色、字體、間距、邊角半徑等。通過設計標記,我們可以將視覺屬性抽象出來,從而實現統一的設計語言。
實際上目前平臺已經支持了 tokens 的抽象抽取,與對應的主題映射表。除了一些設計元素上的調整,我們也可以在不同環境下使用不同的組件來進行兼容展示,例如在 React Native 中,通過修改 mapping 得到需要的交互組件。
這種深度定制化的方案,不僅可以為用戶提供更好的體驗,同時可以大大提高開發效率。
六、結語
攜程機票開放了視覺稿生成代碼流程中的生成器入口,通過讓業務研發參與生成器的發布與更新,抽象出更多適合業務場景的組件/數據結構。
同時,機票在三個維度上進行了生成器落地示例,多次驗證了該方案的可行性與實用性。在提高項目生產效率與設計稿還原質量的同時,確保了代碼的一致性與可維護性。
視覺稿自動生成代碼,可以極大地提高團隊的效率,減少人為錯誤和重復勞動,從而更加專注于創意和創新。我們相信,在未來的發展中將會為我們帶來更加高效和便捷的工作體驗。