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