SREWorks前端低程式碼元件生態演進:monorepo架構重構和遠端元件載入實踐

語言: CN / TW / HK

作者:王威(地謙)

文章結構

  • 專案背景
  • 演進分析
  • monorepo架構演進
    • Webpack與Rollup
    • 如何平滑遷移
    • 構建優化
  • 元件的可擴充套件與可插拔
  • 演進總結
  • 版本動態

專案背景

SREWorks是一個面向企業級複雜業務的開源雲原生數智運維平臺,是大資料SRE團隊多年工程實踐的錘鍊及沉澱。前端統一託管工程(frontend)作為平臺的重要一環,提供了一套serverless體驗的配置化前端低程式碼技術方案:低程式碼、配置化是前端低程式碼方案的基礎特性。

frontend工程採用React+antd為主的技術框架,設計了一套元件對映、編排、解析、渲染的工程體系:以antd元件為自由編輯粒度,使用者在前端設計器通過視覺化互動或者json編輯的方式,依據運維工作的實際使用場景,對元件進行屬性配置/元件巢狀拼裝;同時根據使用景目標需求對頁面元件進行佈局的編排、資料來源的繫結以及在合適點位插入Dynamic Logic,完成頁面節點的設計工作,形成節點模型nodeModel,經模板解析引擎進行解析渲染。由於之前已對架構設計做過一篇詳細的介紹,在此不再贅述,詳情可移步這一篇 https://mp.weixin.qq.com/s/_kqItPbivVmIrOVXvEaVlg

image.png

我們開源這套前端工程的願景是:沉澱更多的使用場景,整合更多的使用者需求,與社群一起共建一個豐富的前端運維元件生態。在過去的半年中,為了讓前端元件生態更好地演進,frontend 針對 “可擴充套件、方便插拔”這兩個關鍵點進行架構升級:

  1. 架構層面進行了monorepo模式重構;
  2. 前端元件支援遠端動態載入;

在文字中,我們對整個迭代過程中陸陸續續碰到過一些問題,以及技術方案的選擇與思考做一個階段性的歸納和總結。

演進分析

關注我們開源動態的同學應該知道,我們初版開源的frontend程式碼量近10萬之多,在沒有可靠詳實文件的幫助之下,想要快速理清楚整套工程的設計理念,結構機理進而能參與貢獻還是有一定難度的。同時工程內部細分來看,設計器,模型層,元件層各自又有相差很大的更新頻率,在開發時一個小小的改動都需要整個工程進行構建。

另外一方面,frontend來源於公司內部工程實踐的版本,兩者雖然同源,但是由於公司內部和開源場景都在快速功能演進,最初設計的公共框架層和元件層共享機制已經有些舉步維艱,這也為後續這次演進迭代埋下了伏筆。結合我們打造開源生態“可擴充套件、方便插拔”兩大目標,綜合來看需要解決以下問題:

  • 基座層面支援runtime遠端元件的載入,解決使用者的多樣化的場景與訴求
  • 結構上對大而全的工程進行細粒度拆分
  • 提取framework框架層和widget元件層並能夠單獨構建
  • share-tools工具可共享
  • 內部業務程式碼剝離
  • 在適配內部業務運轉的前提下升級各依賴版本,便於新技術引入與演進
  • 構建工具的升級與配置調優,有效提升構建效率降低構建體積

針對以上待解決的問題,對演進方案進行了技術調研,也參考和採納了社群同學的建議,我們決定採取下面兩個方案來解決上文提到的問題:

  1. 架構層面,採用monorepo模式進行重構提取出framework框架層、widget元件層、shared-tools等幾個子依賴包,以webpack5(主應用包)+rollup(子依賴包)作為構建工具進行過調優構建;
  2. 針對無法進入程式碼庫的元件,提供遠端元件腳手架,支援將遠端元件打包umd格式並以動態script標籤形式進行動態引入和移除,做到runtime載入擴充套件;

下面我們來詳細分享下這兩項方案的實施:

Monorepo架構演進

Monorepo即單倉(repository)多包(package),大型前端工程專案採用這種模式進行開發管理,能帶來諸多的開發和管理便利:

  • 更加清晰的模組結構和依賴關係
  • 更細粒度的獨立構建單元便於協作開發和不同更新頻率的子包單獨發版
  • 更加高效的程式碼複用等

在v1.4版本中採用lerna + yarn workspace 的技術方案進行了Monorepo的架構實踐:將原工程拆分為@sreworks/app主包應用,和@sreworks/components、@sreworks/widgets、@sreworks/framework、@sreworks/shared-utils四個npm子依賴包。目錄結構變動如下圖所示:

image.png

通過lerna+yarnworkspace的方案,將各子包配置進入workspace空間,workspace空間的各子依賴包的更新,會實時同步到主應用包的node_module,無需釋出npm,且能選擇針對特定子包單獨釋出npm版本或者各個包同步釋出新的版本,可以更小粒度的更新主應用依賴,便捷高效。

Webpack與Rollup

在設計好子包的拆分之後,就開始著手進行檔案結構的改造、元件引入掛載方式的變更、遠端元件載入的處理優化,主題式樣的遷移等等問題(由於篇幅有限,本文僅對大的通用環節進行介紹,對處理細節感興趣或者想討論溝通的同學可以加入文末的交流群)。

在處理完工程結構程式碼後,我們開始著手工程的構建:構建工具的選擇,關於構建工具,webpack,gulp,rollup以及後來的vite,綜合我們實際的情況,最後選定了Webpack和Rollup作為備選方案:Webpack和Rollup本質都是對非ES5程式碼的轉義與打包,一個功能強大的compiler函式,通過配置入口讀取目標檔案,然後輸出轉義檔案;要完成整個工程的打包,還需要babel-loader,React和Vue等loader的處理和一系列plugin的適時掛載處理才能完成對諸如圖片,css檔案、JSX及Vue template等型別檔案的處理,及js掛載html的工作。

Webpack與Rollup的特點:

image.png

根據以上特點對比以及參考業內優秀開源專案實踐,frontend選擇了主包應用使用Webpack5作為構建工具,Rollup作為子包應用構建工具的方案,主包應用HMR對於日常開發而言是剛需,因此選擇Webapck;對子包依賴而言,更便捷的配置和更小的輸出才是更佳的選擇。

image.png

image.png

如何平滑遷移

整個這麼大的工程體量,在沒有完全進行程式碼層面準確無誤的拆解並構建的情況下,是跑不起來的,一個很小的錯誤都會造成整個專案拋錯。且二方包使用了sourcemap也是沒有用的,經過了主包構建,很難排查出哪裡除了問題,於是就又要推倒重來……在開始的實時過程中,耗費了很多的時間,疊加每個子包的修改排查,主應用包的構建等驗證週期很長,探索性改造的難點就在於此。

那麼能更小粒度的驗證和遷移嗎?遠端元件的載入給了啟發思路。嘗試性將元件包@SREWorks/widgets打包成esm格式並在原來大而全的工程中直接修改node_modlues引入依賴包打包檔案, 和相應載入機制,在能執行起來的工程上去驗證各子依賴包,配合sourcemap, 問題排查瞬間提速。就這樣,又依次進行@SREWorks/framework等其他包的驗證,矇眼構建排查問題得解。

式樣問題比較頭疼,在此採用的方案是通用式樣在主包保留,子包由於式樣重置覆蓋的場景較少,採用了css-module的方式進行隔離構建。

構建優化

經過各子依賴包在原有工程上進行平滑驗證,來到主應用包的構建環節,構建體積竟然達到了驚人的5.5M,還是gzip壓縮後的體積:

初版本.png

通過分析這張圖,該版本構建存在以下問題:

  • 同名依賴多次出現,各子依賴包存在重複的依賴
  • 部分依賴包構建體積偏大,如BizCharts

針對以上存在的問題對@sreworks/app整體進行三個維度的優化處理:

第一,通過統一子包依賴排查合併依賴版本,優化至2.8M

image.png

const namespace = {
  appRoot: path.resolve('src'),
  appAssets: path.resolve('src/assets'),
  // 減少子依賴包內部重複依賴
  '@ant-design': path.resolve(process.cwd(), 'node_modules', '@ant-design'),
  'js-yaml': path.resolve(process.cwd(), 'node_modules', 'js-yaml'),
  'ace-builds': path.resolve(process.cwd(), 'node_modules', 'ace-builds'),
  'brace': path.resolve(process.cwd(), 'node_modules', 'brace'),
  'lodash': path.resolve(process.cwd(), 'node_modules', 'lodash')
}
...
  resolve: {
    alias: paths.namespace,
    modules: ['node_modules'],
    extensions: ['.json', '.js', '.jsx', '.less', 'scss'],
  },

第二,抽離部分大依賴包到cdn,如下在externals配置項進行剝離;將體積優化至1.6M。但考慮到某些專有云使用場景,無法使用外部cdn。於是採用自定義構建指令碼,從node_modules中遷移目標依賴到輸出資料夾並載入至html的方案,降低參與構建的大依賴包數量,同時保證專有云環境對其正常的使用。

  externals: {
    // 剝離部分依賴,不參與打包
    'react': 'React',
    'react-dom': 'ReactDOM',
    "antd":"antd",
    ...
  },

第三,調整關鍵元件路徑和1.48M, 減少體積70%,構建時間由V1.3版本的74秒,優化至23秒,提升68%。

image.png

元件的可擴充套件與可插拔

雖然frontend已內建運維場景常用的基礎元件,圖表元件,landing元件,佈局元件等五十餘個元件。根據開源之後使用者的使用反饋來看,使用者仍然有著定製化,可擴充套件的共性訴求:總的來講大致分為兩大類:

  1. 前端框架也是React,有自己定製化的使用場景,內建元件不能滿足當前需求,需要擴充套件
  2. 前端技術棧是Vue,歷史元件積澱比較多,全部進行React重構成本太大

針對問題一,frontend本來就有提供JSXRender,支援使用者以JSX進行簡單的靜態渲染類的元件自定義擴充套件,但不支援屬性配置以及資料來源及dynamic業務邏輯處理等高階特性。前端外掛化,很容易想到npm包的引入,但是這也只能在工程程式碼開發的場景下才適配,要runtime使用和移除,就要另尋方案。再進一步深入追溯,前端開發從jQuery時代發展到當今的Agular,React,Vue三駕馬車以及各種工程化構建工具的參與,但本質其實並沒有發變化,依然是以html中以script標籤掛載js程式碼進行渲染載入的。因此自然想到以script標籤的形式載入我們的遠端元件,不過這裡要做到動態、批量載入、可移除,即:將遠端元件打包umd格式併發布到雲端,並獲取相應準確路徑,以動態script標籤的形式引入:

(function (){
  let script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = url // 目標元件url
  document.getElementById('targetDomId').appendChild(script)
})()
script.addEventListener('load',callback,false) // 嵌入邏輯

如果要批量載入多個的話,即:

const loadRemoteComp = async () => {
  let remoteCompList = ["url_a","url_b","url_c",...];
  try {
    remoteComList.forEach(item => {
      pros.push(Promise.resolve(loadSingleComp());
    })
    window['REMOTE_COMP_LIST'] = await Promise.all(pros);
  } catch (error) {
    console.log(error);
  }
}
loadRemoteComp()

當然這裡還涉及到瀏覽器終端適配,容錯等細節。在此frontend採用比較成熟的systemjs包進行元件的載入,對以上細節都有做妥善處理,合理借力,省時高效。

針對問題二, 技術棧不同,即異構元件的載入。目前來說frontend暫且只針對Vue元件做了異構相容渲染,在React中使用Vue元件。一開始想到的是使用轉換工具,將vue元件手動轉換為React元件,之後再貼上構建,但這種方式有個很大的缺陷:不同版本api差異較大,手動轉碼一般需要對轉換過後的程式碼進行人工二次排查調整,需要開發人員對於兩種框架的新老版本屬性熟悉瞭解,對於不符合的程式碼或已更新的hooks等進行二次確認,無形中提高了使用門檻。

受到Docker容器的啟發,思考React和Vue雖然屬於不同的技術棧體系,但區別於Java和Golang的差異,Vue和React在本質上都是原生js物件的封裝,所以理論上講是可以在React中進行容器化渲染Vue元件的:即本質是繫結掛載Vue物件的操作:

createVueInstance (targetElement, reactThisBinding) {
  const { component, on, ...props } = reactThisBinding.props
  reactThisBinding.vueInstance = new Vue({
    el: targetElement,
    data: props,
    ...config.vueInstanceOptions,
    render (createElement) {
      return createElement(
        VUE_COMPONENT_NAME,
        {
          props: this.$data,
          on,
        },
        [wrapReactChildren(createElement, this.children)]
      )
    },
    components: {
      [VUE_COMPONENT_NAME]: component,
      'vuera-internal-react-wrapper': ReactWrapper,
    },
  })
}

通過frontend遠端元件腳手架@sreworks/widget-cli,對React元件和Vue元件進行打包併發布到cdn,然後在物料開發處,進行編輯即可便捷進行遠端元件runtime載入和移除,解決了問題一和問題二,達成了“擴充套件性”和“可插拔”的目標。

演進總結

到這裡,基本解決了開篇列舉的一系列問題,為構建前端運維元件生態鋪設好了共建路徑,可以做到:

  • 從@sreworks/widgets包開發,JSXRender自定義元件,使用@sreworks/widget-cli開發遠端元件三個維度擴充套件元件應用豐富度
  • 更加清晰的結構依賴關係,降低學習和貢獻參與這套低程式碼工程的門檻
  • 以更小粒度的更新單元,更短的構建時間,便捷日常協作開發
  • 子包拆分後,為後續小規模分步引入TS提供了條件

版本動態

我們會根據工作專案節奏,持續對功能進行完善優化和升級,當前主要是前端低程式碼功能的輸出,後續API低程式碼編輯編排已納入版本規劃,以覆蓋全鏈路低程式碼使用,大家有比較好的建議歡迎多提issue,同時也歡迎更多的開發者能夠參與到我們的生態建設中來(@小助手,也可以直接前端@地謙)

SREWorks開源地址:

https://github.com/alibaba/sreworks/paas/frontend