這家獨角獸公司主要是面向企業的業務,對於前端的需求來講,項目是一個很是大的管理平臺,當時前端架構也很是很是古老,先後端並無分離,總體架構大致是這樣的。css
一個很是大的Java項目,用的阿里開源的JavaWeb框架Webx,而後用了相似JSP之類的東西,也就是velocity模板來渲染頁面,而前端須要編寫jQuery腳本和CSS腳原本完成功能,這些腳本放在Webx的靜態資源目錄,在velocity模板中引入對應的腳本。html
能夠看到,這種模式對於如今的咱們來講,很是有年代感,經典的MVC模式,缺點很明顯。前端
因爲項目相對來講已經比較成熟了,全部內容推倒重作是不可能的,並且當時前端在公司的地位很是的低,沒有影響力,老闆是不會容許前端亂搞的,因此,只能一步一步的想辦法,改變現狀。react
對當時的咱們來講,最大的痛點是前端在開發過程當中,必須啓動一個後端項目,而隨着後端項目的愈來愈龐大,每次啓動都至少須要四五分鐘,開發體驗極差。爲何必需要啓動後端項目?一個是項目開發依賴於velocity渲染的html結構,第二是由於項目請求的數據接口依賴於後端項目。webpack
爲了解決開發體驗的問題,咱們想到了個一箭雙鵰的辦法,既能夠更新技術棧,又能夠提高開發體驗。那就是對於老的、已完成的模塊頁面,先放着無論,後續有時間在重構,而對於新的需求頁面,使用React進行工程化編寫。git
對於velocity渲染的html依賴問題,咱們只須要約定好在velocity中渲染對應ID的DOM節點和初始化數據,而後在React項目中ReactDOM.render對應ID的節點並將初始化數據傳遞進去,這樣就能夠解決渲染後端項目的html渲染依賴問題。web
而接口依賴問題很容易解決,能夠經過mock接口解決,也能夠在webpack中配置代理,將請求代理到後端的測試機器,這樣就能夠解決後端項目啓動的問題。redis
而在項目發佈時,將React項目的webpack的output目錄指定到Webx的靜態資源目錄,而後在velicity中引入對應的編譯結果就能夠。好比如今有一個新的模塊A,那麼velocity中的模板是這樣的:sql
<link rel="stylesheet" href="/static/xxx_module/moduleA/main.css?v=hash" /> <script src="/static/xxx_module/moduleA/main.js?v=hash"></script> <script> var _velocity_init_data_ = { // 渲染velocity數據 }; </script> <div id="pageA"></div>
而React的項目是這樣的後端
import React from 'react'; import React from 'react-dom'; import App from './App'; // 在開發時,聲明一個帶ID爲pageA的空頁面就能夠 const container = document.getElementById('pageA'); const initData = window._velocity_init_data_; ReactDom.render(<App initData={initData}/>, container);
webpack項目配置:
const path = require('path'); module.exports = { entry: './src/main.js', output: { path: path.resolve('後端項目路徑', 'static', 'moduleA') // 對應的模塊目錄 }, // ...其餘配置 }
此時,項目的架構以下:
能夠看到,將頁面使用React工程化編寫之後,前端代碼與後端代碼的耦合性大大下降了,後端只須要爲前端提供初始化數據,前端可以使用初始化數據完成相應的頁面渲染。
隨着公司業務的發展,整個後端項目愈來愈龐大,項目的單次更新部署至少都須要二三十分鐘,並且因爲業務場景要求,後端項目必須提高其高可用性和穩定性,這使得後端不得不將項目拆分,將各個模塊各自單獨開發,而且根據其訪問狀況,單獨部署不一樣的機器、容器數量。這樣的模塊拆分,能夠理解爲後端項目在想微服務架構演進,各個模塊有各自的路由,它們之間內部會經過http、rpc或者kafka進行通訊。而當時前端在公司的影響力也並不大,以致於當時錯過在後端項目拆分過程當中的能夠接過路由讓前端管理的機會。
在後端向微服務架構演進的過程當中,前端也無可奈何變成了一個微前端架構,由於公司當時沒有專門作前端架構的人,因此由當時開發這部分的前端拍腦殼定了一個iframe的方案。
頁面狀況大致是這樣,平臺有一個主入口路由,這個路由由本來的Webx項目控制,這個路由渲染頁面左側的菜單欄和右側的內容區域,全部的頁面的權限控制、路由分發由本來的Webx項目完成,右側內容區域渲染一個iframe節點,iframe根據左側的菜單欄的選中項來加載不一樣模塊的頁面。
後端模塊拆分後,大部分項目框架用的Spring Boot,而模板引擎,也從velocity切換到了freemarker,完成後架構以下:
因爲剛開始沒有通過詳細的考慮,iframe式的微前端架構缺點也慢慢暴露出來,和社區裏講的同樣:
因爲項目爲toB項目,更注重項目的可用性,而不是性能,因此咱們當時忽略了頁面加載性能問題。對於用戶體驗問題,咱們經過在iframe內實時計算搞定,並用postMessage發送到主頁面,主頁面動態設置iframe的高度,從而解決了高度塌陷問題。而iframe內的fixed節點樣式受限問題,只能見招拆招,好比前面提到的Antd的message組件和Modal組件,在設置了主頁面和子頁面的域解決了跨域問題後,經過設置組件的getContainer
方法,將fixed節點渲染到主頁面去,而後在主頁面中添加對應的css樣式。
iframe式微前端完成後,爲了提升前端影響力,個人導師當時率先提出了前端獨立發佈的想法,將須要發佈的前端的靜態資源從後端服務中抽離出來你,部署到公司的CDN中(印象裏個人導師好像是華爲雲的前員工,聽說這個前端獨立發佈的想法是他在華爲雲提出並實踐過)。
針對當時前端的狀況,咱們的難點很明顯,路由是由後端項目來分發的,HTML的渲染也是由右端控制的,假如前端資源抽離單獨發佈,那麼在只發布前端的時,必須保證在HTML不變的狀況下(HTML決定了加載哪些CSS、JS),更新須要加載的前端資源,也就是更新須要加載的JS和CSS。
爲了解決這個難點,咱們實現了一個前端資源獨立發佈系統,項目代號prelude。每一個項目的模塊須要prelude在定義應用app,模塊bundle,在資源發佈時,應用以模塊爲粒度,將對應版本(版本號必須遵循Semantic Versioning規範)的靜態資源發佈到對應的CDN文件夾。好比,應用appA的模塊bundleA,發佈的V1.1.0版本,那麼資源請求的路徑應該是:
https://static.xxx.com/appA/bundleA/1.1.0/
在prelude控制檯中,須要配置該模塊bundleA初始化須要加載的資源,也能夠配置的前置依賴模塊,好比初始化配置了須要加載vender-chunk.js、main.js、vender-chunk.css和main.css,那麼若是須要使用這個模塊,則須要加載如下資源:
<link rel="stylesheet" href="https://static.xxx.com/appA/bundleA/1.1.0/vender-chunk.css" /> <link rel="stylesheet" href="https://static.xxx.com/appA/bundleA/1.1.0/main.css" /> <script src="https://static.xxx.com/appA/bundleA/1.1.0/vender-chunk.js"></script> <script src="https://static.xxx.com/appA/bundleA/1.1.0/main.js"></script>
知道了模塊須要加載的資源後,prelude向外暴露了一個loader接口,這個接口接收app、bundle、version三個參數,而後渲染一段js腳本,用來向頁面中注入對應app/bundle/version配置好的須要加載的全部資源,例如前面的例子,只須要在velocity或者freemarker中引入一下腳本:
<script src="https://prelude.xxx.com/preluer-loader?app=appA&bundle=bundleA&version=V1.1.0"></script>
loader接口渲染的腳本大致以下:
var assets = { css: [ 'https://static.xxx.com/appA/bundleA/1.1.0/vender-chunk.css', 'https://static.xxx.com/appA/bundleA/1.1.0/main.css' ], js: [ 'https://static.xxx.com/appA/bundleA/1.1.0/vender-chunk.js', 'https://static.xxx.com/appA/bundleA/1.1.0/main.js' ] }; // 加載CSS assets.css.forEach(href => { var link = document.createElement('link'); // 其餘邏輯 link.rel = 'stylesheet'; link.href = hrefs; document.head.appendChild(link); }); // 加載JS assets.css.forEach(src => { var script = document.createElement('script'); // 其餘邏輯 script.async = false; // 順序執行 script.src = src; document.body.appendChild(script); });
對此還不夠,由於接口的version參數是寫死的V1.1.0,若是前端發佈更新了版本,那麼還須要後端應用去發佈更新velocity或者freemarker中的script標籤的version參數,這不符合需求。因而咱們將version參數進行了升級,可使用規範的通配符,好比傳入version=*,表明永遠取該模塊的最新版本,那麼velocity或者freemarker引入的腳本就變成了下面這樣:
<script src="https://prelude.xxx.com/preluer-loader?app=appA&bundle=bundleA&version=*"></script>
因而,流程差很少通了,在技術方案評審過程當中,收到了來自經理的疑問:假如先後端同時發佈的狀況下,如何保證先後端發佈的同步?
那既然是經理的疑問,該解決仍是要解決,否則方案評審不給過怎麼辦?針對在這個問題,咱們能夠先後端作好約定,約定version參數只容許有第三位版本號的使用通配符,例如只能使用V1.1.*
,這種狀況,loader只會加載V1.1.*
的最新版本,而後在作好發佈的版本更新約定。
例如,當先後端同時發佈時,前端先發布更新版本到V1.2.0
,後端沒發佈時,一直用的是V1.1.*
的版本,當後端發佈後,更新version參數爲V1.2.*
,上線後就自動加載爲V1.2.0的版本了。
整個項目由有三我的完成,我主要負責平臺的全部配置和配置Mysql入庫,個人導師負責loader接口的開發和對應redis的讀寫、項目基建等,另一個同事負責CDN的對接操做,歷時大概一個月左右就完成了。
項目完成並實施後,架構已經將前端慢慢的解耦出來了。
當prelude項目完成後,咱們利用prelude的優點,經過新的方式彌補將iframe式的微前端架構的缺點,好比在Webx項目路由分發時,用渲染prelude-loader標籤的形式代替iframe標籤,製造一個僞iframe微前端的架構。
項目完成後,因爲個人導師功勞巨大(期間還不斷組件公司組件庫的建設之類的),他直接被掉到了公司的基礎架構組作前端架構。2019年12月,我從這家獨角獸離職,後續偶爾有和個人導師聊兩句,他說prelude的發展挺好的,獲得了公司的承認,目前也和持續集成平臺打通了,雖然我沒有問細節,可是大致我能夠想象到如今prelude的樣子。
我離職的時候,prelude的狀態是,模塊production編譯發佈是在我的電腦上進行的,編譯後經過手動或者webpack插件的形式上傳到prelude平臺,由prelude代發到CDN中,操做極其繁瑣。
現在打通了持續集成後,能夠作很是多的事情,例如靜態類型檢查、編碼風格檢測等,固然這都不是重點,重點是怎麼利用持續集成,將前端代碼的交付規範起來,而不是本來的在我的電腦上完成。
首先,前端項目方在gitlab中,咱們須要規定每一個項目有一個編譯產出的出口目錄,而後流水線按照約定好的出口目錄獲取產出,併發布到prelude。
有了持續集成以後,全部production環境的代碼咱們不須要在本身的電腦進行build,只須要在項目根目錄新建build.sh腳本,這個腳本內包含了全部編譯構建的命令,讓持續集成平臺去執行,編譯後在根目錄產出output.tar.gz,而後將產出包包含app/bundle/version參數描述文件或者在產出發佈時候直接傳參給prelude,讓prelude代發到CDN,這樣就能夠完成版本發佈。
到了這一步,整個前端架構基本已經徹底解耦出來。
整個架構的發展大概經歷了兩到三年的時間,中間也遇到了不少坎坎坷坷的問題,雖然咱們沒有全局最優的方案,可是基本都用了局部最優的方案來解決問題,這兩三年時間,我也從一個前端菜鳥變成了一個還能夠的前端精神小夥,仍是很是感謝當時的導師。