基於 GitLab CI 搭建前端頁面預覽服務
前端在粗放開發模式下的痛點
前端業務在近幾年迎來一個很好的發展,但關於前端的基礎設施並沒有跟上前端業務的迅速擴充套件。業務擴張之後,我們不能再像小作坊一樣進行粗放的開發:開發前如何快速規範的初始化專案?開發中如何保證多人高效的合作開發?開發完成後如何保證正確快速的上線?上線後如何管理諸多業務穩定的執行?圍繞這些問題,筆者列舉一些相關的基礎設施:完整的構建打包流程/服務(統一的腳手架、上線服務等)、完整的測試環境、前端錯誤日誌管理系統(收集、統計、報警)、前端資源離線化管理、前端資源增量下載服務以及針對Node應用的日誌(完整呼叫鏈)、效能和錯誤監控平臺等等。
其中,針對前端業務在上線前,我們一直有這樣的一個痛點:基於現有專案在繼續開發時,本地開發完成後,需要啟動本地服務,預覽給PM檢視檢查,但有時PM或者團隊其他人員想看下效果,而自己又不方便操作電腦,就總是需要協調時間。如果自己開發完成後,可以直接上線到一個測試環境,將連結丟到群裡,會非常方便別人隨時預覽效果。
所以,本文的目標是:針對前後端分離的前端專案,git push 之後,能夠直接推到測試環境,可以線上預覽效果。
gitlab CI 簡介
從v 7.1.2 之後,gitlab支援通過配置.gitlab-ci.yaml檔案支援CI/CD。
具體配置可參考文件:https://docs.gitlab.com.cn/ee/ci/yaml/
為了使專案能夠執行yaml檔案中配置的task,還需要我們首先部署安裝相應的環境: install runner && register runner, 參考:https://docs.gitlab.com/runner/#using-gitlab-runner 。runner的執行方式有很多種, 目前最流行的就是作為一個docker容器,其內部集成了gitlab的一些基礎環境, 註冊階段就是將其與gitlab主任務做關聯(runner通常不跟gitlab伺服器部署在同一臺伺服器),而yaml中配置的任務,就是在runner中具體執行, 然後將結果傳送回gitlab伺服器。
最後專案需要在setttings中開啟enable shared runner或者specific runner.
基於Node搭建前端業務的預覽服務
使用Node搭建服務,託管靜態資源,以及代理請求的轉發。
基本流程
- git push
-
runner中執行yaml中的task
-
資源構建
針對測試環境打包:npm run build -e test
-
上傳資源到node 伺服器。
將該服務抽離為npm 包, 執行festaging-scripts
命令,上傳的資源有兩類:- 構建出的靜態資源
- 必要的請求代理配置(預設讀取根目錄下的.festaging.config.js, 下文會解釋為何需要這個)
-
資源構建
基本流程比較好理解,但囿於公司現有基礎設施的限制,一些問題變得複雜一些:
-
node服務部署是基於公司現有的容器管理方案,支援動態擴容或者銷燬。node服務叢集沒有固定的IP,需要首先獲取所有例項ip地址,然後上傳靜態資源;
-
公司load balance服務是統一管理,nginx 配置不支援泛域名解析。所以針對不同專案,不能共用二級域名,如(aa.xxx.com, bb.xxx.com),只能共用一個域名,如
festaging.xxx.com
。但是為了區分不同的專案,我們需要增加路徑資訊, 如festaging.xxx.com/aa/branch1/
,這樣會帶來兩個問題:-
介面代理增加難度: 倘若支援泛域名解析,針對每一個專案的請求,就可以根據域名中的資訊進行相應的代理(每個專案會配置其後端介面訪問地址的實際域名):
aa.xxx.com/api/
->aa.config.origin/api/
;但現在每個專案請求的介面地址仍然會是/api/**,node端如何區分是哪個專案發出的請求,進而對其進行正確轉發? -
多路由專案的支援:node中 配置好靜態資源的路徑之後,瀏覽器輸入
festaging.xxx.com/aa/branch1/
能夠訪問到該專案的主頁,但是點選按鈕,切換路由之後,網址就會變為festaging.xxx.com/tab2
等形式,並且在瀏覽器中只能通過festaging.xxx.com/tab2訪問到該路徑,而不是festaging.xxx.com/aa/branch1/tab2
, 不能保證同一個專案在url上的統一。
-
介面代理增加難度: 倘若支援泛域名解析,針對每一個專案的請求,就可以根據域名中的資訊進行相應的代理(每個專案會配置其後端介面訪問地址的實際域名):
靜態資源的上傳
上面說到,需要首先獲取部署了node服務的所有例項地址,然後進行上傳, 如何上傳呢?
- scp: 這可能是機器間最普遍的傳輸方式了, 但首次連線需要ssh 認證,需要明文寫密碼到指令碼中,而部署了node服務的的容器連線密碼我們並不知道。
- 基於http的網路服務傳輸:操作簡單,但需要node服務提供上傳介面
使用後者作為解決方案:
- 在runner中執行的script負責: 構建-> zip -> post到node服務(這個功能抽離為npm包, yaml配置檔案的script中只要執行該npm對應的命令)
- node服務提供介面: 接受post的zip包, 解壓, 移動到指定位置。
PS: koa 的async, await與操作檔案時的stream配合總覺得有點tricky: 需要將stream的操作形式轉為promise, 如:
function pipe(from, to, options) { return new Promise((resolve, reject) => { from.pipe(to, options) from.on('error', reject) from.on('end', resolve) }) } async function processZipFiles(input, output) { const reader = fs.createReadStream(input); const upStream = fs.createWriteStream(output); await pipe(input, output); }
介面代理的處理
每個專案都需要指定其真實後端的請求域名,這樣才能夠對專案中的請求進行轉發。初次之外,還需要支援將某些介面代理到其他指定地址,如webpack dev server所支援的那樣。
所以我們支援兩種方式,
-
可以指定proxy target為一個json檔案,其內容格式為
{ proxyApi: { '/api/xx': 'https://www.baidu.com', 'default': 'https://v.qq.com' } }
使用這種方式,還可以繼續支援以後新增除了proxyApi的配置,為以後業務的擴充套件提供了餘量。
-
執行script 時, 配置引數
--target "https://www.baidu.com''
如何在介面請求中注入專案的相關資訊?
因為所有專案公用一個域名,緊靠路徑來區分不同專案,但是介面請求時卻都是域名+介面進行拼接,所以我們需要針對不同專案,在其介面中新增關於專案資訊的字首:針對測試環境,在打包時,將其請求介面地址由/api/xxx
改為/project1/branch1/api/xxx
。但是實際修改檔案中的每個地址是不現實的,我們無法準確識別哪些地方是需要新增字首的。而前端進行網路請求的方式就兩種XMLHttpRequest和fetch, 所有我們只要在html檔案最前面對其方法進行改寫即可。
function buildUrl(prefix) {} var originXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { return originXHROpen.call(this, method, buildUrl(url, '${prefix}'), async, user, password); }; if (window.fetch) { var originFetch = window.fetch; window.fetch = function () { var input = arguments[0]; if (typeof input === 'string') { arguments[0] = buildUrl(input, '${prefix}'); } return originFetch.apply(this, arguments); }; }
該配置如何傳送到node服務?
- 作為檔案傳送,node端也從該資料夾下的所有檔案讀取轉發配置:當該資料夾下內容變化時,重啟node服務。
-
作為引數傳送,node 端收到請求後,修改記憶體中的變數: node端維護一個proxy config物件,收到請求後修改其內容,無需node服務重啟, config 格式如下:
{ project1: { `branch1`: { proxyAPI: { '/api/xx': 'https://www.baidu.com', 'default': 'https://v.qq.com' } } } }
後者顯然為更優方案。
靜態資源、proxy config例項化
上文提到靜態資源是直接傳送到每臺例項,proxy config也是傳送到每個node例項,然後直接修改記憶體中的config。倘若node服務重啟,docker容器新建,這些東西不就全部丟失了嗎?所以需要對其進行靜態化儲存,當node重啟服務時,從此讀取初始值。
${project}_${branch}.zip
多路由業務的支援
因為團隊現在統一使用react技術棧,所以對於多路由的支援就圍繞react-router-dom
進行。通常會使用的路由元件是BrowserRouter
或者StaticRouter
, 而其
basename引數 可以用來對url地址新增字首,這跟上文中我們需要的專案相關資訊完全符合,所以我們可以通過修改其basename引數實現對多路由的支援。
- fork一個react-router-dom倉庫, 對其basename進行修改,然後針對測試環境構建時,新增alias,將react-router-dom resolve為我們修改後的倉庫: 優點是修改足夠簡單,缺點也很明顯:我們需要同步更新fork的倉庫,以及可能對低版本支援不足。
-
在webpack構建之後,新增外掛, 解析ast,檢查如果使用了
BrowserRouter
或StaticRouter
,然後修改basename的值,返回新的code。
選用方案2。 基於webpack4 提供的parser api 來解析被webpack處理過的每個module, 類似useStrictPlugin.js 實現, 只是在得到ast後再利用babel的‘traverse‘和'generate'包生成修改了basename的方法。