阿裏盒馬-如何打造漸進式可擴展、高生產力的前端研發平臺

程序員成長指北 2022-01-07 18:48:46 阅读数:435

打造 前端

大廠技術  高級前端  Node進階

點擊上方 程序員成長指北,關注公眾號

回複1,加入高級Node交流群

本文是 4.10 前端早早聊-前端搞 CI/CD 專場分享的文字整理稿,來自 阿裏盒馬-夜沉 的分享。

我從 2015 年進入阿裏之後就開始了在前端工程化領域的探索,一直非常感興趣。之前在 B2B 主導負責 JUST 工程體系,後面來到盒馬,負責前端工程以及基礎體驗相關的工作,此次 ReX Dev 研發平臺的分享也算是自己對當前盒馬工程體系的一個總結。

此次分享主要都是與平臺架構相關的設計思考,不會有過多的產品功能相關的部分,這有兩個方面的考慮:一是平臺正在高速迭代之中,部分產品能力還在打磨;另一方面,不同團隊的業務場景不同,對工程化的訴求會有較大差別,分享的借鑒意義並不大。

在這篇文章中,我會盡可能的詳細介紹 ReX Dev 在設計過程中對一些架構問題的思考,希望對你有所幫助。

背景介紹

盒馬中後臺技術現狀

我們團隊是盒馬 B 端的前端團隊,主要負責盒馬的中後臺相關業務。在聊具體的架構之前,我們先看一下盒馬的中後臺技術現狀。

圖片

從上圖可以看出,盒馬工作臺是中後臺的核心系統,小二用戶通過不同的終端訪問盒馬工作臺,這個工作臺之下有從供應商到門店、從商品到物流等數十條垂直業務線。

中後臺工程特征

從前端工程化的角度,盒馬的中後臺場景主要有以下特征:

  • 頁面基數大:有 6000+ 存量頁面,3000+ 活躍維護頁面(半年內),這導致了前端側會有巨大的並發開發量, 對新舊應用的研發效率要求非常高,我們需要考慮建設非常高效的研發平臺;
  • 頁面離散化、模式化:由於盒馬的垂直業務線多、業態複雜,這大量的頁面的背後是頁面的離散化,當然也呈現出了模式化特征,模式化的頁面讓我們有機會去嘗試 LowCode/NoCode 等技術
  • 存在多種應用類型:由於一些曆史及特殊場景的原因,盒馬有多種前端應用研發,它們各有自己所適合的場景,比如微應用、多頁應用、單體應用等等,此外考慮未來的業務變化,很可能還會有新的應用形態產生,因此 對研發流程有較高的擴展性訴求

為應對以上工程上的訴求,我們需要一套高生產力、可擴展的研發平臺。

由於盒馬現有的研發流程是基於原 B2B 的 just-flow 系統定制的,目前平臺的可維護性、穩定性都存在較大問題,同時涉及平臺歸屬問題,也不適合針對盒馬場景做大規模重構,因此我們决定構建盒馬自己的研發平臺 ReX Dev。

產品定比特與演進策略

ReX Dev 的平臺定比特為:盒馬前端應用的高生產力研發平臺。這意味著這個平臺只針對盒馬(及關聯業務)服務,只考慮前端應用,並要求高生產力。確定定比特能明確平臺的能力邊界,聚焦產品要解决的核心問題。

作為一個服務於前端應用開發者的平臺,不能只考慮單純的效率提昇,還要考慮研發模式的演進。就盒馬而言,多達  3000+ 活躍維護頁面(還在不斷增長),目前主要的維護方式還是前端參與大部分研發,即前端作為業務資源進行直接支持。

然而,這會存在以下問題:

  • 業務在不斷發展過程中,有大量技術債務的償還,對前端的資源的訴求只增不减,單純的資源型支持方式已難以滿足業務發展;
  • 大量模式化的頁面也很難帶來特別的技術挑戰,大部分人只是重複的寫模式化的頁面,對業務支持同學的價值感、成就感都偏低,也導致人才的流失。

因此,我們需要推動研發模式的改變。

盒馬這種業務形態聚焦、頁面模式化、頁面量大的中後臺場景,為 LowCode/NoCode 帶來了比較大的機會。一旦當研發從 ProCode 向 LowCode/NoCode 演進時,應用的交付成本能降低到一定程度,我們就能讓那些模式化、特定垂直場景的業務交給非專業前端開發人員維護,比如外包、後端,甚至是非開發類的角色。

未來,我們產品的演進目標是通過推動研發模式的改變,讓盒馬前端團隊從【資源型前端】向【服務型前端】而轉變,從授人以魚授人以漁

圖片

技術分層架構

要達到以上目標,需要 ReX Dev 平臺不斷演進迭代,這背後需要有一個穩健的架構支撐。具體而言,ReX Dev 采用如下分層架:

圖片

設計如上架構時,主要考慮了以下幾點:

  1. 平臺的底層要足够穩健,應用的元信息模型能適應未來的業務發展,不因底層模型約束上層業務發展,同時研發流程需要流水線支持,要有强大且靈活的流水線引擎;
  2. 考慮業務在持續變化的,我們的 研發流程一定要足够的靈活,能滿足不同場景的擴展需求,即使未來有新的應用形態出現時也能快速的支持;
  3. 研發流程只能解决協同效率,而要提昇研發效率,推動研發模式轉變,因此首先在 研發平臺上層需要提供就是更高效的 LowCode 研發能力

總結下來就是穩健的底層、靈活的中層、高效的上層,接下來我們會從這三層分別展開。

穩健的底層

研發平臺的底層,也就是研發平臺的核心,如果我們看一個極簡的 CI/CD 模型,它其實主要由兩分部組成:應用與迭代循環。研發平臺的核心能力就是為滿足這兩個部分存在的,因此我們有兩大目標:

圖片

針對應用,有要穩健的應用元信息模型;針對迭代循環,要有靈活的流程調度能力。

元信息架構設計

如下圖所示,為了便於管理,我們一般會將應用元信息按 domain/product/app 這三層進行抽象(不同平臺可能名稱不同但結構是相似的):

圖片

在傳統的設計中,一般會對 domain+product+app 這三者進行唯一約束,這是一種比較嚴謹的做法,在早期我們也是按這種方式設計的。

但經過實際的業務落地之後,發現這種唯一性約束會帶來較大的靈活性問題:盒馬的業務在不斷發展過程中,產品線是會持續發生變化的,而這種唯一性約束會被相關工程方案形成間接依賴,導致後面要切換產品線時成本非常高,甚至要進行數據訂正。

因此,我們在這種信息架構基礎之上,讓 app 只與 domain 形成依賴,構建唯一約束,而與 product 是弱依賴的關系(product 靈活可變):

圖片

基於這種關聯關系,我們能做到一鍵轉移應用至其它產品線,而不會對應用的研發造成任何影響。

元信息模型的設計,看起來雖然很簡單,但由於它屬於基礎的數據結構,會被上層依賴,設計上一旦考慮不周會導致重構成本大幅上昇,因此要特別慎重考慮。我建議在設計時,一定要面向未來考慮,思考未來變與不變的部分。

這裏可能有人會問,為何 app 不直接全局唯一,而還是要依賴 domain?這主要是綜合考慮變化的可能性,以及沖突帶來的成本問題:

  1. 盒馬這類業務,app 的數量非常大,不同 domain 下的 app 非常容易沖突重名,全局唯一只會導致用戶在 app 的 name 上自行添加前綴,管理和理解成本都會非常高;
  2. 一般來說,跨 domain 的應用遷移並不多見,幾乎很少會有一個 app 同時負責於多個 domain 的,同時  domain 也比較穩定,因此 app+domain 的約束綜合管理成本最低。

流水線引擎設計

除了元信息之外,底層架構另外一個重要的部分就是流水線調度引擎。作為底層的一部分,我們希望設計一個通用化的流水線調度引擎,它要能做到:

  • 實現任意异步任務的調度執行,支持執行節點狀態共享、輸入輸出管道化
  • 支持靈活的任務流程定制,任務可串行、並行執行
  • 支持全鏈路异常捕獲,任務重試與調度恢複,實時的任務日志

下面是一個典型的迭代流程:

圖片

要引擎應該如何設計呢?下面我們分別來看它的關鍵設計。

流水線狀態機

從上圖可以看出,流程本質上就是一個有向圖,流程的流轉本質上就是一個有限狀態機。當然,不同於傳統的狀態機模型,流水線執行節點本身是异步的,而异步節點內部本身又帶有多個狀態,這使得流水線的狀態機模型更像一種分形結構:一個狀態節點又可以拆分成若幹個子狀態。

下面是對狀態節點的一個簡單抽象,它就像一個 Promise 一樣,包含 3 個狀態。將流水節點定義為 stages,執行動作定義為 actions,那麼執行引擎就是 run() 函數:

圖片

可以看到,這個執行引擎邏輯非常簡單:讀取當前狀態,執行對應的動作,獲取下一個狀態,然後遞歸執行相同的邏輯。

上下文與管道

通過狀態機模型,我們可以將流水線執行起來了。在真實世界中,流水線的節點是分布式執行的,它們往往是獨立的,很可能是在不同機器、不同進程中執行。因此,就需要解决各個節點之間數據狀態的通信問題。

為了便於通信,我們需要提供兩種模型:上下文狀態共享管道化支持

  • 上下文可以解决多個節點數據共享問題,適合持久存儲的數據;
  • 管道解决了兩個相鄰節點之間的通信問題,上一個節點的輸出是當前節點的輸入;

下圖提供了一個狀態節點的上下文及管道數據的流轉過程:

圖片

在右側的代碼中,一個狀態節點會接收 context 和 payload 作為參數,經過處理後會返回 context 和 payload,它的外層代碼類似如下結構:

async function runStageAction (pipeline, stage, payload{
  // read context
  const context = await pipeline.readContext();
  
  // run action
  const result = await stage.runCurrentAction(context, payload);
  
  // write context
  await pipeline.saveContext(result.context);
  
  // schedule next action
  stage.scheduleNext(result.payload);
}

在最後一步,調度器會觸發下一個 stage action 的執行,傳入的參數即為當前 action 的輸出。

异常自動恢複

由於流水線引擎是流水線服務的核心模塊,在設計上為保障穩定性,流水線引擎本身只會做任務的調度,具體的執行會派發到相應的執行服務上。因此,流水線的异常會分為兩種情况:

  • 流水線節點執行的异常,這個會比較常見,一般都是執行邏輯錯誤或三方依賴存在問題;
  • 流水線調度异常,這一般會很少見,只會出現在服務器故障、重啟等場景才會發生。

對於前者,我們在流水線節點設計了 stageError 函數,它會在節點出錯時被調用,這允許流程開發者可以自行决定錯誤處理邏輯。

而調度异常,通過引入 DTS 定時任務機制,定時對調度失敗的任務進行檢查,就可以在出現問題時進行流程重新調度。

流程實時日志

由於流水線節點的執行往往是在後臺异步進行的,如果節點執行异常排查將會非常困難,因此我們需要記錄節點執行的詳細日志,有些節點執行時間較長,還需要實時日志展示。

我們基於原 just 的實時日志進行了簡單封裝,在節點執行前後進行日志記錄:

async function execute (stage, action, context, payload{
  const logger = createRealtimeLogger();
  const executor = loadExecutor(logger);
  
  // start log
  await logger.start();
  
  // execute stage action
  const result = await executor.execute(stage, context, payload);
  
  // end log
  await logger.end();
  
  return result;
}

在 execute 中調用 this.logger 以及向 logger 寫入日志流,都會將日志實時同步至日志服務上。

流水線引擎架構

基於以上設計,我們可以將整個流水線引擎架構總結成下圖:

圖片

上圖中的要點如下:

  1. pipeline schema 定義了流水線的狀態機模型,它是一個有向圖;
  2. pipeline engine 基於 pipeline schema 執行輸入的 stage 實例任務,它是一個讀取、執行、調度的循環;
  3. execute 函數的具體執行由 Task Executor 服務執行,在 execute 前後通過 realtime log service 進行實時日志的輸出;
  4. pipeline engine 在執行過程中會持續向 pipeline & stage instance 中寫入相應的 context 和 I/O 數據,進行數據持久化;
  5. scheduler 調度器通過 metaq/http 等方式進行流程的分布式調度,並通過 DTS 進行檢查和自動恢複。

流水線產品化

以上只是流水線調度引擎的架構實現,基於這套引擎,我們可以通過不同層次的產品封裝實現許多上層能力,比如:

圖片

目前,我們還未做 High Level 的封裝,只 Low Level 的形式為如下場景提供了流程調度的支持:

圖片

靈活的中層

有了穩健的元信息架構和强大的流水線調度引擎,我們就可以在此之上建設各類應用的研發流程了,這就是中層的職責:為不同場景下的 CI/CD 流程提供靈活、低成本的定制能力

下圖展示了當前盒馬不同應用的迭代流程:

圖片

在當下我們有三種應用類型需要支持,同時考慮到未來新應用類型的接入,應該用怎樣的架構去支撐呢?

流程抽象:定制的基礎

要讓流程可擴展,首先要進行流程的抽象。對於一個迭代,可以將它大致劃分為如下幾個階段:

  • 創建迭代:主要是創建迭代實例和開發分支,所有流程基本都一樣;
  • 配置迭代:這一步是為了讓迭代在正式開發之前,做資源的初始化,比如環境配置、代碼配置等,不同應用類型可能會不同;
  • 開發迭代:包含實際的編碼、調試以及相關的卡片檢查等,不同應用類型差异會比較大,同時線上線下都有;
  • 部署迭代:將開發的代碼部署至相應的環境中,用於聯調、灰度、正式發布等,不同應用流程差异也會比較大;
  • 上線迭代:在完成正式部署之後,更新迭代的狀態,這一步基本不需要定制;

因此,可以將一個迭代研發流程抽象成如下形式:

圖片

基於這種抽象,各類應用流程的定制方案示意如下:

圖片

在上圖的流程定制方案中,每一種應用類型都有一個定制包,它們是對抽象流程的擴展和實現。每一個定制包都包含以下幾個部分:

  • 構建器:負責將應用的源碼編譯成待發布的產物,會作為應用的依賴安裝;
  • WebUI:即應用在研發流程中對應的操作界面,每種應用都能定制自己的 UI,操作效率會更高;
  • 部署流程:即應用的 CI/CD 部署流程,它們基於流水線引擎定制;
  • 應用服務:即應用的基礎服務以及內部邏輯封裝,比如微應用和單體應用的創建服務是不同的;
  • 流程卡點:即應用研發流程的檢查卡點,比如 CR、封網、Lint、安全檢查等;

以上定制中,構建器部分非常簡單,本質上就是不同構建工具的封裝(如各種 *pack),就不做單獨說明了;部署流程是基於前面的流水線調度引擎定制的,它天然就是可擴展的,這裏也不再贅述。

因此,接下來流程的擴展我們主要圍繞 應用服務WebUI、流程卡點 的擴展來介紹。

服務擴展基礎:SPI

對服務的擴展,理想情况下要做到無侵入的執行邏輯替換。要這樣這種能力有多種方式,但就研發流程這類場景而言,可以考慮將服務實現邏輯外置,由三方系統實現,流程本身只做服務的調用,要實現這種模式的最佳方案就是 SPI。

維基百科對  的定義為:

Service provider interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.

傳統的 API 模式,Server 端負責接口定義和服務實現,在 SPI 模式下,Server 端只負責接口定義和接口調用,服務實現由三方服務提供,如下圖所示:

圖片

這種範式,其實在 JS 語言裏大量存在,你可以認為 SPI 就是某種類型的 callback。在 ReX Dev 平臺的具體實踐中,以微應用服務擴展為例,其服務擴展架構如下:

圖片

在上圖中:

  • ReX Dev 負責研發流程的節點抽象和接口定義,包含創建應用、創建迭代、配置、部署等多個環節;
  • 微應用擴展包以獨立的 FaaS 應用封裝,基於統一的 egg-plugin 提供相關 SPI 的實現;
  • 微應用服務的元信息會向 ReX Dev 的 SPI Registry 注册,在具體的節點執行時,通過 SPI Invoker 來調用。

這樣,無論 ReX Dev 要擴展多少流程,其本身的核心架構和服務穩定性是不會受影響的。

WebUI 擴展

與服務側擴展 SPI 類似,WebUI 的擴展架構是相似的。本質都是基礎的 WebUI 框架提供擴展槽比特,具體的應用流程提供擴展模塊,我們將這些提供具體功能的擴展組件稱為 FPC(Feature Provider Component):

圖片

這種設計方案,在之前 B2B 工程平臺 JUST Flow 和 JUST WebUI 中也早已經過實踐,被證明是一種解决 UI 擴展時相對靈活的方案。

流程卡點擴展

常見的研發流程卡點包含 CodeReview、Lint、安全檢查、封網、測試等,這些卡點都有一個共同的特點,即:卡點邏輯一般由三方系統實現,卡點的觸發和檢查由研發流程負責。

這樣的話,任意流程都可能會對接某個三方服務進行卡點檢查,為避免重複實現卡點邏輯,我們需要一個通用的卡點模型,讓三方系統可以快速封裝,同時研發流程也可以低成本接入。我們需要做以下抽象:

  • 統一卡點模型,包含數據模型和卡點接口,所有三方系統都按相同接口封裝;
  • 定義標准卡點事件,所有卡點都只綁定標准卡點事件,比如 gitcommit、build;
  • 提供事件觸發 SDK,讓研發流程在合適的時機觸發標准事件。

基於這種抽象,卡點擴展方案如下:

圖片

在上圖中,所有的卡點任務都基於 BaseTask 進行封裝,有 run() 和 callback() 方法,每個卡點都會注册至統一的  Task Pool 中,當研發流程觸發標准事件時,會從 Task Pool 中尋找匹配的 Task 並執行,同時 Task 實例將與當前流程關聯。

另外,每個卡點 Task 實例往往都需要有 UI 操作行為(比如 CodeReview 提交、封網申請),因此每個卡點任務都會有對應的 UI 模塊來實現,這個通過上一節提到的 FPC 就可以實現。

對流程定制的思考

在這一章中,我們介紹 ReX Dev 如何通過合理的架構設計實現不同流程擴展,同時不影響自身的穩定性。一般來說,只有當研發模式有重大區別時,才應該采用這種擴展形式。畢竟,開新的流程本身有一些的開發工作量,同時也會有長尾和碎片化的風險,帶來架構治理問題。

除了流程定制外,其實可以考慮以下替代方案:

圖片

高效的上層

研發流程覆蓋了項目的全生命周期,而其中最為關鍵的就是編碼,也是一個項目開發過程中最為核心的部分。要在上層提效,關鍵是如何提昇編碼效率。

在盒馬的業務場景下,模式化、輕量化的應用體系,讓低代碼、甚至無代碼開發成為了可能,下面我們來分析不同的研發模式的適用人群與場景:

圖片

從 ProCode -> LowCode -> NoCode,其適用場景是越來越窄的,帶來的研發效率提昇也會更明顯。當代碼越來越少時,以前在 ProCode 上附加的卡點檢查可能也不需要了,比如 Lint、CodeReview 等,又會進一步促進開發效率。

傳統的單一開發模式下,NoCode/LowCode/ProCode 的技術方案一般都是獨立實現的,這導致了單一模式困境:業務早期可基於 LowCode/NoCode 快速實現,但後期需求迭代導致頁面複雜度上昇,導致 LowCode/NoCode 平臺難以支持,最後要麼是將應用基於 ProCode 實現一遍,要麼是向 LowCode/NoCode 平臺不斷的增加功能。

LowCode/NoCode 平臺功能並不是越多越好,因為它們面向的人群是非專業的前端開發者,它的優勢是簡單,膨脹的功能會帶來複雜度,並損害原有用戶的開發體驗。明確一個 LowCode/NoCode 平臺的定比特與能力圈是非常重要的,保持足够的簡單才會讓它在特定場景下足够的高效。

盒馬的選擇:漸進式研發

對於盒馬而言,立志於將盒馬前端團隊從資源型支持,轉到服務型前端團隊,需要讓資深前端開發人員專注在複雜場景的 ProCode 開發上,然後通過 LowCode/NoCode 讓外包、後端、非技術人員來完成應用交付。我們希望一個應用,能按 NoCode -> LowCode -> ProCode 的方式支持,一種模式無法滿足就降級下一種模式,我們稱之為漸進式研發模式

漸進式研發模式在最大優勢在於,通過不同研發模式的優雅降級,讓研發模式在適用範圍裏保持簡單,避免平臺功能膨脹和複雜度的上昇。

下圖展示了盒馬漸進式研發模式的轉換與應用框架的封裝邏輯:

圖片

在上圖中:

  • NoCode 可降級至 LowCode,LowCode 可降級至 ProCode,同時有限的 ProCode 可逆向轉換為 LowCode;
  • 所有的研發模式,底層都是基於同一個應用框架不斷封裝來實現的,LowCode 基於 ProCode 封裝,NoCode 基於 LowCode 模式封裝;

限於篇幅,我們將會重點介紹 ProCode -> LowCode 的降級和逆向轉換邏輯。

LowCode/ProCode 互轉方案

在阿裏集團內部,最流行的 LowCode 方案都是基於 Schema LowCode 模式的。Schema LowCode 就是指 LowCode 可視化搭建的底層是基於一套 schema 或 DSL 實現的,通過操作 schema 來實現 UI 的編輯。

而盒馬選擇的是 JSX-AST LowCode 模式。JSX 本身提供了一套 XML 風格的聲明式語法,我們通過操縱 JSX 編譯後的 AST 來實現 UI 的編輯,相比 schema 化最大的優勢是它可以實現 100% 的逆向轉換。

具體對比如下圖所示:

圖片

盒馬之所有做以下選擇,有幾個主要原因:

  • 盒馬的應用交互形態相對模式化,可以在應用層做 JSX 的模式化約束;
  • 漸進式研發是我們的核心理念,我們會更傾向整個研發模式是可以優雅降級的;
  • 在 JSX-AST 這個領域裏,我們有足够的前期技術積累,有相對成熟的方案;

JSX-AST LowCode 實現機制

基於 JSX-AST 的 LowCode,需要解决一個關鍵問題:尋找一個 UI 元素背後的 AST 節點,並對其進行 AST Patch,實現即時編輯的效果。

具體原理其實並不複雜,大致轉換流程如下所示:

圖片

在編輯態下,JSX 在編譯成 AST 時會經過特定 babel-plugin 的處理,為 JSX 元素添加特定的標記(記錄 AST 節點、源碼比特置等信息),當操作 UI 時會根據這些標記尋找到目標 AST 節點,並將生成的 Patch 替換原來的 AST,然後經過重新編譯和渲染,修改過的 UI 就會實時生效,同時 AST 也會回寫生成 JSX Code。

以上,就是可逆轉換的 ProCode/LowCode 方案。

應用層約束與降級處理

了解了 JSX-AST 原理之後,你可能會好奇,JSX 語法其實是非常靈活的,這會導致實際的 AST 結構非常複雜,最後讓 UI 難以通過可視化的方式來編輯,盒馬是怎樣應對的?

的確,要基於 JSX-AST 來實現 LowCode 搭建,這個問題是必須面對的。事實上,要解决這個問題有兩個選擇:

  • 一種是通過定義一套類 JSX 的 DSL,實現對 UI 的强約束,比如不支持在 JSX 中寫 JS 條件錶達式和循環語句,這在阿裏集團內部有很多方案;
  • 另一種是不擴展 JSX,依然保持原生寫法,但通過特定的應用層寫法的約束來避免 JSX 過於自由;

定義 DSL 雖然並不複雜,但它畢竟是一個方言,一旦引入方言就需要全套的工程支持(IDE 插件、babel 插件、各類工程支持等),對第三方輸出時也非常不友好。綜合考慮盒馬的現狀,我們認為 DSL 的方案過重,並不適合盒馬場景。

最終,我們采用了方案二,對 JSX 的約束寫法如下:

// @lowcode: 1.0 # 錶示啟動低代碼支持,將會做【嚴格模式】檢查

// 模塊引用
import React from 'react';
import styled from 'styled-components';
import { If, ForEach, observer } from '@alilfe/hippo-app';
import { Layout, SearchForm, Table } from '@alife/hippo';
import Model from './model';

// 常量定義
const { Page, Content, Header, Section } = Layout;
const { Item } = SearchForm;

// 樣式定義
const StyleContainer = styled.div`
  height: 100%;
`
;

// 視圖定義(名稱固定為 View,擁有固定的參數:$page/$model)
function View({ $page, $model }{
  return (
    <StyleContainer>
      <Page page={$page}>
        <Header>
          {/* If 條件錶達式:if value = ? then ? */}
          <If value={!$model.loading}>
            <HeaderDetail model={$model.detail} />
          </If>
        </Header>
        <Content>
          <Section>
            <SearchForm model={$model.section1.form} onSearch={$model.search}>
              <Item name="p1" title="條件1" component="input" />
              <Item name="p2" title="條件2" component="input" />
              <Item name="p3" title="條件3" component="input" />
            </SearchForm>
          </Section>
          <Section>
            {/* ForEach 循環組件:通過 FaCC 的方式對 <Item /> 進行迭代 */}
            <ForEach items={$model.data.list} keyName="key">
              {($item, i) => (
                <div className="list-item">
                  <div className="header">
                    <div className="title">{$item.title}</div>
                    <div className="extra">
                      <Button onClick={(v) => $model.show(v, $item)}>加載</Button>
                    </div>
                  </div>
                </div>
              )}
            </ForEach>
            <Pagination
              current={$model.page.pageNo}
              onChange={$model.changePage}
            />

          </Section>
        </Content>
      </Page>
    </StyleContainer>

  )
}

// 導出視圖
export default observer(Model)(View);

除了 JSX 外,還需要約束應用層,具體體現在三個方面:

圖片

總結下來,就是:

  • 應用結構要强約束,過於自由的結構會給 LowCode 帶來非常大的管理和解析成本;
  • 適用場景要約束,我們 不提供自由搭建能力,只針對高頻的場景提供快速搭建,保證 UI 結構的收斂;
  • 采用 嚴格模式對代碼風格和寫法進行强約束,只有符合嚴格模式的應用才能進行 LowCode 搭建模式;

通過以上約束,可以讓應用變得非常規範,能極大降低 JSX-AST LowCode 模式背後的實現複雜度;同時,如何用戶需要用 ProCode 開發,只需要遵循這套規範,寫後的代碼依然可以用 LowCode 搭建。

統一 Node/Web 構建方案

要實現多種研發模式的融合開發,除了應用層的統一外,還需要在工程方案上保持一致性,即無論是 LowCode 還是  ProCode,其底層的構建機制應該是統一的,這樣就保證。

由於 LowCode 搭建產品是基於 Web 的,因此 Web 側需要提供一套與 Node 側一致的構建方案。目前,ProCode 側的構建器還是基於 webpack 實現的,經過一系統的調研和方案選擇之外,我們還是采用了基於 webpack 的 Web  端構建方案。

具體如下:

圖片

以上構建器設計方案中:

  • ProCode 與 LowCode 遵循相似的構建模型,即從目標比特置(磁盤/內存)讀取 JS/CSS,經過構建後生成目標代碼;
  • 通過將 webpack 及相關插件、配置編譯成 bundle,再結合 Nodebowl Runtime 實現在 web 端運行  webpack;
  • 通過依賴的預構建方案,實現依賴的遠程編譯和加載,讓 web 側可以與 local 開發一樣自由的添加、删除依賴;

LowCode/ProCode 融合研發小結

最後,總結一下 LowCode/ProCode 融合研發方案:

  • 選擇最合適的方案:沒有最好的方案,只有最合適的,基於 JSX-AST 的 LowCode/ProCode 互轉方案,更符合我們對漸進式研發的理念,也許你的場景 Schema 模式也够用了;
  • 確定清晰的邊界:LowCode 的高效在於特定細分領域的業務開發,對於不符合 LowCode 的場景采用 ProCode 是更高效的選擇,也有利於平臺保持簡單;
  • 避免從零搭建應用:避免讓用戶從零開始搭建應用,而是通過模板、脚手架、數據模型這類“半成品”開始(就像盒馬售賣的“快手菜”,通過半成品讓一個不會做飯的小白也能做好一道菜)。

思考與總結

為了追求技術的普適性,本次分享聚焦 ReX Dev 研發平臺背後的產品思考與技術實現,對於產品功能的介紹並不多,因為分享盒馬的應用如何創建、如何部署意義並不大,討論如何實現部署流程才是關鍵。

工欲善其事,必先利其器。個人認為團隊在任何時期都需要在支持好業務的同時,考慮如何提昇團隊的整體研發效率,這類基礎性的投入不在於資源的多,而在於持續、穩定的投入。工程化就如同軟件架構一樣,需要持續的演進、面向未來而不斷優化。

由於前端工程是一個複雜的系統性工作,因此作為架構師在進行工程體系的設計時,需要面向未來做長遠的考慮,如果是當前識別到的架構問題,一定要在早期解决掉,避免因為架構設計問題給未來的人留坑,這在我這近年來做前端工程化時深有體會。

就我自己的經驗,在做前端工程化的架構設計時,有以下幾個原則:

  1. 做好分層設計,無論你的產品是全家桶式,還是自由組合式的,核心架構的分層一定要做好。分層設計的核心在於將穩定的部分放在最底層,它應該能應對未來業務 3~5 年的變化,上層方案需要足够的聚焦,解决特定場景的問題。
  2. 擁抱社區趨勢,除非有足够的資源保障和更先進的理念,否則工程方案最好是基於社區方案封裝或改造,社區的趨勢决定了未來方向,自己造輪子最大的問題是可能後面無人維護,最後成為團隊的技術債務。
  3. 產品上收斂,架構上靈活,做前端工程非常忌諱碎片化,强調方案的統一和收斂,但同時又要兼顧靈活性,因此不能把架構設計的過死,基於一個預設的場景做約束,而是在底層上保持一定的靈活性,將約束放在最上層的產品功能上。面對需求應該做到: 底層架構上“都能支持”,但在產品上“選擇不支持”

具體到 ReX Dev 平臺,其分層設計是貫穿始終的,比如整個研發平臺的產品分層、流水線引擎的設計分層到研發模式的分層;同時在選擇 ProCode/LowCode 互轉方案時,我們拋弃了 DSL 的方案,而采用原生 JSX,也是考慮到未來 DSL 的可維護性;我們在架構上提供了靈活的流程擴展支持,但在具體的流程設計上,我們的首要原則其實是收斂的,避免碎片化。

前端工程化是一個非常具有場景化特征的技術領域,不同的前端團隊技術形態的不同,導致背後的工程方案也千差萬別。統一工程平臺,本質上是統一和收斂技術形態,就阿裏集團而言,內部存在大量的工程平臺,往往不同 BU 都有一套,這是各個 BU 在技術形態上的差异導致的。

作為工程領域的架構師,是自建研發平臺還是基於存量平臺擴展定制,就需要你綜合團隊現狀、未來發展、投入產出比等多個方面仔細思考了。

最後,按早早聊的規矩,給大家推薦一本書《架構整潔之道》:

圖片

- 這是底線 -

Node 社群


我組建了一個氛圍特別好的 Node.js 社群,裏面有很多 Node.js小夥伴,如果你對Node.js學習感興趣的話(後續有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回複「Node」即可。


圖片

   “分享、點贊在看” 支持一波

版权声明:本文为[程序員成長指北]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201071848449656.html