如何從 0 到 1 搭建全鏈路構(gòu)建系統(tǒng)
正文如下

1.分享話題
本次分享話題主要是站在中小型團(tuán)隊(duì)的視角上,偏系統(tǒng)和技術(shù)性地分享政采云前端團(tuán)隊(duì)是如何從零到一搭建前端工程構(gòu)建發(fā)布系統(tǒng)——云長系統(tǒng)。
2.分享大綱
通過本次的早早聊大會分享,你將從實(shí)現(xiàn)背景、系統(tǒng)基座、系統(tǒng)設(shè)計(jì)實(shí)現(xiàn)、未來規(guī)劃 4 個(gè)方面了解到政采云前端團(tuán)隊(duì) ZooTeam 在構(gòu)建部署方面所做的探索。
3.背景
前端構(gòu)建部署的發(fā)展已經(jīng)歷經(jīng)多年,從早期前后端應(yīng)用不分離,進(jìn)行整體式部署,發(fā)展為前后端應(yīng)用分離并通過打包工具產(chǎn)出靜態(tài)產(chǎn)物,通過 CDN、靜態(tài)資源服務(wù)器等托管最終產(chǎn)物,進(jìn)而演進(jìn)到通過 CI/CD 以及 Docker 等容器化環(huán)境進(jìn)行持續(xù)集成以及獨(dú)立應(yīng)用級部署。
構(gòu)建部署的方式日新月異,但在 19 年政采云的構(gòu)建系統(tǒng)還是通過比較原始的方式進(jìn)行著,前端的所有應(yīng)用都接入了 Jenkins,通過預(yù)先在 Jenkins 系統(tǒng)中預(yù)設(shè)的流水線命令行腳本,針對不同的應(yīng)用運(yùn)行不同的腳本,有通過 Webpack CLI 打包的,也有 Gulp 打包的,還有自己編寫的腳本打包的,可謂百花齊放。
但與此同時(shí),也產(chǎn)生了很多問題。第一,使用體驗(yàn)不友好,全體應(yīng)用都混合展現(xiàn)在一起,有不同業(yè)務(wù)線不同職能團(tuán)隊(duì)和不同類型的應(yīng)用,查找十分耗時(shí)且各發(fā)布環(huán)境區(qū)分不明顯易誤操作,界面設(shè)計(jì)也比較原始,作為前端工程師,不能忍。第二,腳手架煙囪林立,需要維護(hù)多套腳手架以及不同的打包方式,難維護(hù),兼容性差。第三,權(quán)限控制方式較為粗獷,只能按人員維度進(jìn)行控制,無其他維度如應(yīng)用、部門、時(shí)間等,發(fā)布權(quán)限通常集中在 2-3 個(gè)人身上,所有發(fā)布任務(wù)均通過這 2-3 人執(zhí)行,一是加重相關(guān)同學(xué)負(fù)擔(dān),二是不靈活,夜間發(fā)布時(shí)也需要聯(lián)系相關(guān)同學(xué)。第四,無法滿足數(shù)據(jù)、安全、連通場景訴求,如進(jìn)行工程化、研發(fā)方面數(shù)據(jù)統(tǒng)計(jì),或者上線前執(zhí)行代碼檢測,甚至與其他工程化工具打通如 GUI 客戶端,IDE 等都顯露出一定短板。

所以,我們決定搭建一套應(yīng)用構(gòu)建系統(tǒng)來解決以上痛點(diǎn)。這套系統(tǒng)需要滿足一定要求:易用性較好;支持統(tǒng)一化構(gòu)建方式,以及符合工程規(guī)范的應(yīng)用直接接入;嚴(yán)格的權(quán)限控制,有一定的授權(quán)機(jī)制,通過審批機(jī)制臨時(shí)下放某次發(fā)布的權(quán)限;最后支持?jǐn)?shù)據(jù)統(tǒng)計(jì),與多系統(tǒng)的對接集成。

4.系統(tǒng)基座
構(gòu)建部署系統(tǒng)的基座其實(shí)就是工程、編碼、工具的標(biāo)準(zhǔn)化規(guī)范化,有了統(tǒng)一的規(guī)范,才能夠減少開發(fā)以及認(rèn)知上的差異,系統(tǒng)也可以按照特定的流水線去執(zhí)行應(yīng)用構(gòu)建的過程,而且遵循規(guī)范的應(yīng)用可以快速接入。
工程標(biāo)準(zhǔn)化
在工程標(biāo)準(zhǔn)化方面,我們進(jìn)行了項(xiàng)目結(jié)構(gòu)的整理,針對以前多套項(xiàng)目、組件腳手架進(jìn)行了統(tǒng)一整理,將目錄結(jié)構(gòu)、命名規(guī)范、工程依賴、生成入口等合而為一。其次針對不同框架類型,參考了業(yè)界的標(biāo)準(zhǔn)進(jìn)行了統(tǒng)一編碼規(guī)范制定,產(chǎn)出了相同的 ESLint 約束集。再次,抽離了之前配置在項(xiàng)目中的 Webpack Babel 等與構(gòu)建相關(guān)的公共配置文件,由統(tǒng)一的 CLI 工具進(jìn)行維護(hù)。最后,針對不同類型的項(xiàng)目,我們做差異化配置的支持,使不同項(xiàng)目運(yùn)行與配置對開發(fā)者透明,同時(shí)也開放了自定義配置的入口,保留一定靈活性。

命令行工具
有了統(tǒng)一的標(biāo)準(zhǔn),那么還需要一個(gè)工具來將標(biāo)準(zhǔn)化的項(xiàng)目給 'Run' 起來,這些主要交由 CLI 工具來負(fù)責(zé)。CLI 工具主要基于 Webpack Babel Node.js 編寫,主要完成項(xiàng)目的本地啟動、構(gòu)建、發(fā)布、創(chuàng)建、升級等操作。上述核心操作的上層則是不同插件的加載過程,例如,針對 React 項(xiàng)目加載配套的 React-CLI-Service 插件,注入對應(yīng)的操作命令,開發(fā)同學(xué)即可進(jìn)行項(xiàng)目相關(guān)操作。以此來適配不同類型項(xiàng)目,達(dá)到統(tǒng)一開發(fā)入口的目的。

基礎(chǔ)底座
除了上述部分,我們還產(chǎn)出了統(tǒng)一的 ESLint 規(guī)則集,在本地配合 husky 等開源工具在提交時(shí)做代碼檢查,同時(shí)也提供給服務(wù)端在應(yīng)用上線前做代碼規(guī)范掃描。至此有了標(biāo)準(zhǔn)工程、較為統(tǒng)一的編碼規(guī)范、配套工具,整個(gè)構(gòu)建部署系統(tǒng)的基座已經(jīng)形成。


5.系統(tǒng)設(shè)計(jì)
設(shè)計(jì)目標(biāo)
設(shè)計(jì)系統(tǒng)之前我們也有想過很多方案,有通過 Node 做一個(gè)應(yīng)用管理系統(tǒng)后面構(gòu)建發(fā)布還是走 Jenkins 的,也有通過 Node 完成構(gòu)建部分,最終產(chǎn)物通過 CDN 和 nginx 靜態(tài)資源托管的。但是計(jì)劃沒有變化快,唯一不變是變化,由于架構(gòu)升級的關(guān)系,平臺在推行整體應(yīng)用部署的容器化,趁著這趟東風(fēng),我們也決定通過 Node.js + Docker 的能力進(jìn)行前端應(yīng)用的構(gòu)建部署。
系統(tǒng)分層
有了系統(tǒng)大致技術(shù)層面的定性后,接下來就要考慮功能層面的設(shè)計(jì)。首先針對系統(tǒng)做了架構(gòu)分層,包含核心層、服務(wù)層、展示層。最底部核心層提供數(shù)據(jù)以及工具,包含 CLI Service,Lint 規(guī)則集,核心的構(gòu)建流程 Build-Core,以及應(yīng)用、人員、工程等元數(shù)據(jù)。服務(wù)層主要的功能包含應(yīng)用管理、構(gòu)建部署服務(wù)、發(fā)布、權(quán)限、日志等基礎(chǔ)服務(wù),展示層主要對應(yīng)運(yùn)行在瀏覽器上頁面功能,包括數(shù)據(jù)總覽、應(yīng)用管理、發(fā)布流程、權(quán)限申請審批這四大塊。

展示層部分
展示層部分功能詳細(xì)可見下圖,其中核心功能為應(yīng)用管理與發(fā)布流程。從功能上講,應(yīng)用管理主要包含了應(yīng)用移交、分支管理、成員管理、變更記錄等。成員管理為開發(fā)、測試等人員信息管理,以方便在發(fā)布流程中定位到對應(yīng)人員;應(yīng)用移交主要為應(yīng)用管理員權(quán)限的轉(zhuǎn)讓;分支管理對應(yīng) git 分支的變更;變更記錄為變更應(yīng)用信息的操作日志記錄。發(fā)布流程模塊主要是從開發(fā)、測試、預(yù)發(fā)到生產(chǎn)環(huán)境的鏈路式構(gòu)建發(fā)布,以及權(quán)限申請。

服務(wù)端部分
服務(wù)端整體功能亦參考下圖,其中構(gòu)建部署與發(fā)布流程管理為主要功能模塊。構(gòu)建部署主要包含分支代碼拉取更新、NPM 依賴安裝,代碼規(guī)范檢測、Webpack 打包構(gòu)建,構(gòu)建鏡像的推送以及鏡像滾動更新這幾個(gè)過程。發(fā)布流程管理則主要包含發(fā)布流程的申請、流程節(jié)點(diǎn)簽批、流轉(zhuǎn)和完結(jié)通知、臨時(shí)發(fā)布授權(quán)以及最終上線后進(jìn)行代碼的歸檔。

整體框圖
結(jié)合上述梳理的系統(tǒng)功能模塊,我們大致設(shè)計(jì)出最終的系統(tǒng)框圖,展示層主要是基于 Vue + Vuetify 實(shí)現(xiàn),而服務(wù)層是基于 Node.js + Koa2 + MySQL 來實(shí)現(xiàn),下面我們主要看下服務(wù)層的具體流程。

API 網(wǎng)關(guān)
API 網(wǎng)關(guān)是服務(wù)端的最外層,主要負(fù)責(zé)對連接到服務(wù)的請求做一些認(rèn)證與處理。請求連接時(shí)針對訪問來源,通過 CORS 和域名白名單來提供基本的跨域訪問支持,然后通過存儲在系統(tǒng)中的公鑰來解析請求頭中的 Token,獲取對應(yīng)用戶身份做基本的接口訪問鑒權(quán),同理,通過 WebSocket 的連接也需要做鑒權(quán)。認(rèn)證完成后,系統(tǒng)最后決定是否提供服務(wù)或者建立會話,最終返回結(jié)果,但是考慮到存在服務(wù)異常的情況,網(wǎng)關(guān)的外層做了兜底,確保返回正確的狀態(tài)碼與可讀性較好的提示。

構(gòu)建隊(duì)列
進(jìn)行構(gòu)建服務(wù)之前,考慮到服務(wù)器資源有限,且可能會存在多個(gè)應(yīng)用同時(shí)構(gòu)建的情況,需要一個(gè)隊(duì)列來存儲待運(yùn)行的構(gòu)建任務(wù),排隊(duì)執(zhí)行減少資源開銷。由于服務(wù)是多個(gè) docker 實(shí)例部署運(yùn)行,所以需要在多服務(wù)之間共享構(gòu)建隊(duì)列,這里連接單獨(dú)部署的 Redis 實(shí)例,通過 list 結(jié)構(gòu)來存儲任務(wù)數(shù)據(jù),通過 rpush 和 lpop 命令來實(shí)現(xiàn)入隊(duì)出隊(duì)操作,同時(shí)將正在運(yùn)行的構(gòu)建任務(wù)通過 HashMap 在 Redis 中存儲。

工作進(jìn)程
有了構(gòu)建隊(duì)列,系統(tǒng)可以在隊(duì)列中取出任務(wù)依次執(zhí)行,但是由于構(gòu)建任務(wù)是 CPU-Sensitive 的應(yīng)用場景,如果直接在主進(jìn)程中執(zhí)行任務(wù)會長時(shí)間阻塞而無法處理其他請求,所以要借助 Child_Process 來作為構(gòu)任務(wù)運(yùn)行的宿主。主進(jìn)程通過消息將構(gòu)建任務(wù)信息發(fā)送給子進(jìn)程,子進(jìn)程執(zhí)行構(gòu)建任務(wù),將構(gòu)建日志以及產(chǎn)物寫入對應(yīng)存儲媒介,同時(shí)通過 IPC 信道將構(gòu)建任務(wù)運(yùn)行中產(chǎn)生的日志信回傳給主進(jìn)程,由主進(jìn)程通過 WebSocket 推送給瀏覽器。

任務(wù)創(chuàng)建
新建一個(gè)構(gòu)建任務(wù)時(shí),通常攜帶著構(gòu)建信息,包含應(yīng)用 ID、Git 分支、CommitId、構(gòu)建任務(wù) ID 以及環(huán)境信息等。當(dāng)這些信息被提交到服務(wù)端后,會根據(jù)應(yīng)用 ID 與 CommitId 檢查數(shù)據(jù)庫中是否已經(jīng)存在構(gòu)建好的鏡像,如果有就直接使用已存在的鏡像進(jìn)行部署。反之,則通過 Redis 判斷任務(wù)是否已在構(gòu)建或正在隊(duì)列中,防止任務(wù)重復(fù)入隊(duì)執(zhí)行。

構(gòu)建部署
當(dāng)任務(wù)被成功創(chuàng)建后,接下來就是重頭戲構(gòu)建部署流程,主要包含四大塊:代碼檢查、在線構(gòu)建、產(chǎn)物存儲以及部署分發(fā)。

代碼檢查代碼檢查主要涵蓋 ESLint 編碼規(guī)范檢查、NPM 依賴檢查、性能檢查、安全檢查。編碼規(guī)范檢查主要是依托上面提到的項(xiàng)目維度的編碼規(guī)范規(guī)則集以及 ESLint 提供的 Node API 來進(jìn)行。NPM 包檢查主要內(nèi)容為分析是否直接引用了在黑名單中的依賴包,性能檢查則是使用內(nèi)部的基于 Lighthouse 實(shí)現(xiàn)的頁面性能評分系統(tǒng),杜絕性能較差的頁面發(fā)布。最后安全檢查,則是針對代碼中出現(xiàn)的不安全的引用如 HTTP 鏈接,特定域名,通過在 React / Vue AST 樹解析過程中的檢查特定代碼片段,來定位到存在安全風(fēng)險(xiǎn)的內(nèi)容。

應(yīng)用打包通過了代碼檢查,則進(jìn)入到應(yīng)用打包的過程,本質(zhì)上也就是 Webpack Build 的過程。構(gòu)建過程中會傳入應(yīng)用的上下文內(nèi)容,加載特定的 Webpack 插件,最終生成一份 Webpack 配置文件,通過運(yùn)行 Zoo CLI 提供的 Node API 生成初步的構(gòu)建產(chǎn)物。

依賴注入拿到初步生成的構(gòu)建產(chǎn)物,其實(shí)也就是 HTML / JS / CSS 等靜態(tài)資源文件,我們需要針對頁面注入一些附加的資源,如添加埋點(diǎn)、監(jiān)控等 SDK 和全局變量腳本,抑或是業(yè)務(wù)資源如全站吊頂?shù)取Mㄟ^ html parser 解析頁面 HTML 結(jié)構(gòu),在 head body 等位置插入對應(yīng)腳本片段,然后回寫到對應(yīng)文件中,至此便生成了最終的靜態(tài)資源產(chǎn)物。

鏡像生成由于前端應(yīng)用托管在 docker 容器平臺,所以還需要生成最終的鏡像 image 供部署使用。這一步比較簡單,通過將宿主機(jī)的 docker.sock 套接字文件鏈接到容器內(nèi)部,便可以通過 docker client 在容器內(nèi)部的構(gòu)建服務(wù)中直接調(diào)用 docker 命令。然后獲取應(yīng)用需要的 Dockerfile,在產(chǎn)物目錄下運(yùn)行 docker build 即可生成應(yīng)用鏡像。

鏡像部署生成出最終鏡像之后通過 docker auth 驗(yàn)證,執(zhí)行 docker push 命令推送鏡像到私有倉庫存儲。部署時(shí)調(diào)用 Kubernetes 的 API Server 觸發(fā)對應(yīng)應(yīng)用 Pod 內(nèi)部鏡像更新,由于鏡像滾動更新是異步過程,所以需等待一段時(shí)間方可生效。至此,應(yīng)用的整體構(gòu)建部署流程基本上已經(jīng)講完了。

6.流程閉環(huán)
構(gòu)建部署只是研發(fā)鏈路上的一個(gè)小環(huán)節(jié),站在研發(fā)的鏈路上,我們尋求的是更多規(guī)范化可提效的系統(tǒng)建設(shè),以及上下游環(huán)節(jié)的串聯(lián),以達(dá)到從需求設(shè)計(jì)到發(fā)布上線,再通過數(shù)據(jù)分析完成產(chǎn)品優(yōu)化的流程閉環(huán)。

7.更多
未來,我們會繼續(xù)打磨系統(tǒng),在規(guī)范化的前提下通過自動化部署、移動端功能的補(bǔ)齊、以及與工程化 GUI 客戶端 和 IDE 的集成來豐富整個(gè)構(gòu)建系統(tǒng)的能力,讓開發(fā)變得更加“傻瓜化”,解放出更多的人力做更有價(jià)值的事。

8.歡迎加入 ZooTeam
如果你想改變一直被事折騰,希望開始能折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個(gè)結(jié)果,卻不需要你;如果你想改變你想做成的事需要一個(gè)團(tuán)隊(duì)去支撐,但沒你帶人的位置;如果你想改變既定的節(jié)奏,將會是“5 年工作時(shí)間 3 年工作經(jīng)驗(yàn)”;如果你想改變本來悟性不錯(cuò),但總是有那一層窗戶紙的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業(yè)務(wù)騰飛的過程,親手推動一個(gè)有著深入的業(yè)務(wù)理解、完善的技術(shù)體系、技術(shù)創(chuàng)造價(jià)值、影響力外溢的前端團(tuán)隊(duì)的成長歷程,我覺得我們該聊聊。任何時(shí)間,等著你寫點(diǎn)什么,發(fā)給 [email protected]。
加微信 codingdreamer ,備注進(jìn)群,加入大會專屬內(nèi)推群,及講師團(tuán)隊(duì)釘釘群
掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。

