前言
今日早讀文章由《React狀態管理與同構實戰》作者@LucasHC投稿分享。
正文從這開始~~
近些年,「NodeJS 應該如何在公司業務中真實落地 」這類問題屢見不鮮。自從 2009 年 NodeJS 誕生之后,搶盡風頭,圈粉無數。但一定有工程師不禁要質疑「NodeJS 真的已經開辟天地,占據架構體系的一席之地了嗎」,「國外聽說 NodeJS 開展如火如荼,國內現在到底是個什么狀態」,「聽到過阿里 NodeJS 扛下雙十一,到底是什么情況」
沒錯,風頭越大,質疑也就越多。當爭議和浮華褪去,技術的落地:就讓上帝的歸上帝,撒旦的歸撒旦。本系列文章,我將會梳理 NodeJS 在我司以及在國內外其他團隊的典型項目案例,并深入探討 NodeJS 的發展前景和最佳實踐。
開篇小茶歇
本篇文章內容較長,涉及到了:端到端測試,NodeJS 服務,中臺能力(docker 鏡像等基礎設施),基于圖片比對的插件實現等。
命中注定的緣分 —— 當 NodeJS 遇見端到端測試困局
端到端測試,也叫做 UI 測試,e2e 測試。用白話說,類似常見的自動化測試,它站在用戶使用的角度,并基于協議或者其他技術手段,打開真實瀏覽器,與瀏覽器中頁面交互。
端到端測試有著肉眼可見的優勢,比如:項目經過不斷的開發,最終肯定會趨于穩定,在適當的時機引入端到端測試能及早發現問題,進而保證產品的質量。這種讓軟件代替人工,實施快速、反復的測試,收益非常明顯。有人總結出端到端收益公式(出處:測試人員的 “救命稻草”):
端到端收益 = 迭代次數 全手動執行成本 - 首次自動化成本 - 維護次數 維護成本
即便收益明顯,且相關領域工具層出不窮,但端到端測試的落地實施目前并不廣泛。有條件接入端到端測試的團隊,端到端測試似乎也沒有扮演到應有的角色,一直難以發揮出最大化作用。其原因除了“項目特點是否適合”以外,我認為還和端到端測試在開發上線流程中進行接入的階段有密不可分的關系。
具體來說,相當多的團隊會將端到端測試放到本地執行,和項目代碼強耦合。比如,本地通過 npm script 腳本來實施端到端測試。端到端測試需要保證最新待測試頁面的可訪問性,因此相關腳本需要優先保障本地服務的成功搭建。以 npm run e2e 這樣的 npm script 為例,相關流程如下圖:
本質上這是一種“按心情”執行。一般需要開發者在本地開發完畢之后,自覺執行腳本并觀察端到端測試結果。“按心情”的事情,自然是無法正規化、流程化、平臺化的,注定是比較雞肋的存在。
順著上述思路,我們可以想到:借助 huskey,將上述端到端測試放到 pre-commit 或者 pre-push 階段強制執行。這樣一來我們使得端到端測試流程化,將“按心情”執行改為了強制執行。
但進一步思考,在 pre-commit/pre-push 階段執行的弊端也很明顯:增加了額外的 git hooks,延長了代碼提交流程,直接影響到了上線效率。如果是一個 hot-fix 緊急修改 bug,這樣的時間殤是我們無法接受的。同時,本地階段執行端到端測試的一個前提就是先要保證本地服務的可用性,暫且不談開啟本地服務的時間開銷,一個更尷尬問題就在于:本地服務和線上環境是有天然差別的,本地執行的端到端測試難以冪等于線上真實效果。
基于上述情況,端到端測試在團隊技術體系中,要么漸漸地成為一個“漂亮的玩具”,華而不實;要么就是一個開發者“眼見心煩的累贅”,最終淪為雞肋。
這么看來,端到端測試想要破局并突破,勢必應該在執行流程上進行創新。為此,我們認為:端到端測試應該搬到“容器”上進行,融合到 CI/CD 階段實施,徹底做到自動化、服務化。
這里插入一個知識點:什么是 CI/CD 階段呢?
它們都是現代互聯網應用編譯和發布流程當中的常用詞語,分別對應:持續集成(Continuous Integration)和持續部署(Continuous Deployment),實際上我們還有一個持續交付(Continuous Delivery)的概念,這里不再詳細展開,我們來聚焦到持續集成和部署。
在持續集成環境中,開發人員將代碼提交到主干 master,并觸發 Gitlab 的 hook,進而自動推進代碼的編譯。不同的團隊對持續集成階段的定義也許會有略微不同,但是并不妨礙我們理解。我司在持續集成階段主要完成:構建項目流程。具體來說,在這個階段中臺團隊使用基礎鏡像啟動容器,拉取最新代碼,安裝必要依賴,執行單測腳本,并最終 commit 出下一階段(持續部署階段)的鏡像。
在持續部署階段,中臺使用構建階段(持續集成階段)產出的鏡像,啟動容器,按照一定 pipeline 流程,串行發布最新版本應用,最終啟動服務。
了解 CI/CD 的概念之后,將端到端測試后置到 CD/CD 階段,并在真實容器中執行——這似乎是一個很好的嘗試和創新。
NodeJS 實現端到端服務 —— 想說愛你不容易
截止為此,根據我們的“容器中執行端到端測試服務并接入 CI/CD”的設計思想,我們可以畫出來一個簡單的流程分析。
由上圖,衍生出第一個問題:我們應該將端到端服務接入到 CI 階段還是 CD 階段呢?
按照常理:
CI 階段應該重視測試驗證結果,以保障所有的提交在部署前的質量,對可能出現的一些問題進行預警;
CD 階段應該沒有人為干預,只有當一個修改在工作流 pipeline 中構建失敗才能阻止它部署到產品線。
但是,端到端測試需要保證具有一個可訪問的最新版本應用地址,而 CI 階段在我司只是進行代碼的編譯和容器基礎鏡像的生成,并不會開啟應用服務,因而不具備端到端測試所需的地址。我們當然可以“改造” CI 階段,新啟動一個新的進程,進行應用服務的啟動,但這樣的做法顯然粗暴而不合理。
同時 CD 階段我司在金絲雀過程之前,有一個“辦公室階段”(下文統一使用辦公室階段/辦公室環境一詞),即:辦公室內(公司內網環境下)可全量訪問新版本應用。也就是說公司內網下訪問線上地址:www.a.com/b,網關會將該流量全部…
綜合考慮,對于我司來講,在這個“辦公室”階段,應該是最好執行端到端測試的時間點。一旦端到端測試無法通過,將會中斷部署交付流程。
這樣一來,就解決了全量可測頁面的訪問性問題,同時端到端測試的環境完全和線上環境保持一致。
任何一個創新型項目的開展之路,都注定曲折坎坷。設計先行,但在實施過程中,我們還是遇見了較多的阻力和難點。主要問題集中在端到端框架和中臺容器的貼合性、一致性上。下面我舉一些典型例子來進行說明。
融入社區,反推框架的進步和完善
我們選用了目前業界最為流行和活躍的 Cypress 作為端到端測試框架,關于不同端到端框架的對比和技術實現原理這里不再贅述,感興趣的同學可以關注我們的博文,后續將會專門進行解剖。
整體端到端服務流程并不復雜,如下圖:
這只是一個極簡的圖示,粗略地表現了在相關代碼 MR(merge request)成功構建,并部署到辦公室環境時,中臺請求我們的端到端測試服務開啟接口。
這就存在了第一個難題:我們發現,在辦公室部署完成之后,端到端服務接收到 post 請求,執行 cypress.run(),總會得到報錯:Cypress binary is not installed。為什么本地就能順利執行,到了容器上,開啟 NodeJS 服務之后就會得到報錯呢?
翻看 Cypress 代碼實現,究其原因非常有趣,Cypress 會在 npm post-install 過程安裝 Cypress binary 到容器系統路徑下,post-install 是 npm 的一個 hook,它會在 npm install 成功執行后觸發。在執行 cypress.run() 時,Cypress 會先執行 cypress.verify() 驗證 Cypress 的可用性,其中一個驗證標準就是檢查系統路徑下是否存在 Cypress binary。
那為什么我們容器上就找不到 Cypress binary 呢?我依然用圖示還原案發現場:
在第一次構建時,我們的構建腳本執行 npm install,并成功觸發 npm post-install,Cypress 將 Cypress binary 安裝在容器系統路徑:~/.cache/Cypress 當中。
第二(N)次構建時,面對一個“全新”的空容器,中臺為我們緩存了 node_modules,因此 npm install 并不會真正下載依賴,post-install 的 hook 也不會觸發,也就不存在“Cypress 將 Cypress binary 安裝在容器系統路徑:~/.cache/Cypress 當中”這一動作。進而執行時,得到了 Cypress binary is not installed 的報錯。
解決方案也不難,我的第一個直觀想法是構建腳本當中的 npm install 改為 npm ci, 這里插入一下 npm ci 和 npm install 的區別:
npm ci 需要項目必須要含有 package-lock.json 或者 npm-shrinkwrap.json 文件
如果上述兩種 lock 文件和 package.json 聲明的依賴產生沖突,npm ci 命令會強行退出,并報錯,而 npm install 命令會更新 lock 文件
npm ci 命令會全量安裝項目所有的依賴,無法添加單獨依賴項目
如果項目中已經存在 nodemodules,npm ci 命令會刪除 nodemodules 文件,并重新安裝
npm ci 命令不會寫 package.json 內容以及 lock 文件內容
因此不難看出,在構建階段,本就應該使用 npm ci 命令,這也是 npm ci 命令命名的由來。
但使用 npm ci 和中臺緩存 node_modules 的行為又相矛盾,無可避免地增加了構建的耗時。在任何公司的構建部署系統中,npm ci 安裝依賴的時間一定會是不可忽略的大頭之一。
有沒有更“優雅”的方法呢?我堅信“ PR makes world better”。讓我們回到本質,核心問題在于「中臺緩存了 nodemodules,導致 post-install 無法觸發,進而無法安裝 Cypress binary 到容器系統路徑」,如果我們也能緩存 Cypress binary 到指定路徑,且在執行 cypress.run() 以及 cypress.verify() 時,讓 Cypress 去設定的緩存路徑下找 Cypress binary 是不是就能解決問題。那么這個”緩存路徑“當然就是 nodemodules 文件下的某個路徑即可(因為中臺緩存了 node_modules 文件)。
總結一下,關鍵點在于:
Cypress 需要新增可配置環境變量,用于指明 Cypress binary 的安裝路徑
我們設置環境變量 CYPRESSCACHEFOLDER 為 ./node_modules/.cache/cypress/
cypress.run() 觸發 cypress.verify() 執行時,去 CYPRESSCACHEFOLDER 指定的路徑下查找 Cypress binary 是否存在
此時整體流程如圖:
對于增加配置環境變量,使得容器環境執行 Cypress 更加靈活的提議,當然也得到了 Cypress 官方的認可,此問題暫告解決。同時,對于 Cypress 本身體積較大,安裝耗時且不穩定的問題,我們同樣使用一個 CYPRESSINSTALLBINARY 環境變量指明默認的 Cypress 軟件下載地址。我們在公司內網保存一份,內網下載 Cypress 既迅速又可靠。最終的構建部分腳本如下 (采用 yml 格式,不影響讀者理解):
build:
# export cypress variables
- export CYPRESS_CACHE_FOLDER=node_modules/.cache/Cypress&& export CYPRESS_INSTALL_BINARY=http://內網地址/cypress.zip
## application build
- yarn
- yarn build
其中可見在執行依賴安裝(yarn)和構建項目(yarn build)之前,我們聲明并導出了相關環境變量。
前端和中臺化 打通基于 Cypress 的 NodeJS 服務任督二脈
解決了 Cypress binary 安裝問題,我們在執行過程中遇到的第二個問題也很有趣。在 cypress.run() 執行時,得到報錯信息,“CI stage dependency missing in docker”,經過和官方團隊的討論:
我們嚴重懷疑容器上執行 Cypress 出錯的原因在于:容器系統版本過低。中臺當前提供的容器系統版本均為:Debian 8.2(jessie),NodeJS v10.14.0,即 docker 基礎鏡像聲明為:baseimage: nodejs/v10.14.0jessie (debian 8)。為此,我們組織中臺團隊以及公司內部安全組進行溝通,并制作出加入了安全包的新版本 baseimage: nodejs/v12.13.0stretch (debian 9)的基礎鏡像,供項目容器使用。
基礎鏡像的升級并不是簡單制作一個鏡像那么簡單,其中涉及到較多“技術之外”的探索和磨合,這里我們不過多展開。總之,中臺團隊的存在對于各種類型 NodeJS 應用/服務的落地和發展至關重要。同時中臺方面涉及到的能力是傳統前端開發所欠缺的。因此,項目推動能力,跨團隊溝通能力也是 NodeJS 發展甚至任何一項前端技術都不可忽視的一環。
此外 Cypress 作為一個復雜的端到端測試框架,它本身需要很多系統級的依賴,比如 Xvfb(is an X server that can run on machines with no display hardware and no physical input devices/虛擬屏幕虛擬輸入設備)等,這里梳理總結一下必備系統依賴包包括:
xvfb
libgtk-3-dev
libnss3
libxss1
libasound2
xz-utils
到此為止,簡要總結一下“端到端測試上容器”這一過程遇見的關鍵問題以及解決方案:
Cypress binary 安裝問題:提 PR 解決,提供 Cypress binary 緩存安裝路徑
Cypress 安裝超時且不穩定:提 PR 解決,提供內網獲取路徑,從內網下載
容器系統不兼容:制作鏡像并推動中臺升級容器系統基礎鏡像
當然以上問題并不是全部,但極具代表性,也能總結出任何一個前端團隊在公司內推廣落地新技術時可能會遇見的問題。具體的挫折可能來自 NodeJS 服務自身,也可能來自于和已有技術體系的不兼容,解決方案有技術方向的努力,也有項目推動方向的嘗試。
到此,我們涉及了服務粗略設計以及基礎環境的搭建。接下來,我將重點介紹一下容器化端到端測試服務的技術體系架構設計。
一個完善、易擴展的 NodeJS 服務設計
文章主題圍繞著如何開發一個“容器上運行的端到端測試系統”展開,前面也提到過,其實就是在合適的時機去觸發端到端框架的執行,想來就這么簡單。但是我們在設計一個系統,一個平臺時,應該考慮更多問題,比如:
橫向多項目擴展能力
平臺化服務能力
運行效率極致化設計方案
通報與預警中斷機制
合理選型技術方案以及存儲方案
我們的端到端服務起名為「Goalkeeper」,意為“守門員”,希望它像一名優秀的守門員一樣,守衛著我們產品質量的最后一道防線。
橫向多項目擴展能力
Goalkeeper 目前已經進入成熟階段,從立項的角度來說,該 NodeJS 服務不能只服務于一個項目測試,理想地它應該具備支持公司內所有產品接入的能力,并將接入過程和復雜度降到最低。
當辦公室環境部署完成后,中臺請求 Goalkeeper Post 接口 https://api.goalkeeper.com/run,這個接口提交數據字段包括:
{
"stage_name": "office",
"description": "style: 1221 活動頁樣式兼容低版本安卓",
"mr_iid": 2049999,
"app_name": "xen",
"author": "houce",
"event_name": "deployment_finished",
"candidate_id": 6666,
"deploy_id": 6666
}
app_name 字段為唯一的項目名稱,配合其他表意字段(應該不難理解,這里不再一一說明),這樣的接口設計自然支持全公司所用應用的接入,僅從接口設計上,具備先天擴展能力。接下來的說明也將進一步就橫向擴展來展開。
Goalkeeper 首頁儀表盤頁面,選擇查看應用項目:
運行效率極致化設計
為了最高效地進行端到端測試,我們分析:對于不同的應用,應該多核多進程執行端到端測試,保證不同應用測試任務執行的并行性,即對于多個項目的部署,端到端測試不會發生阻塞,不排隊;對于同一個項目應用,必須要避免短時間內多次不同部署之間的互相影響,對于這些端到端測試執行任務應該正交化設計,串行展開。
具體實施就需要一個消息隊列,不同應用采用不同消息隊列 tube,相同應用在同一個 tube 中串行生產和消費。因為 Goalkeeper 是一個服務內的消息隊列設計,因此我選用了輕量且兼具強大功能的 Beastalkd 作為消息隊列的技術選型。
Goalkeeper 某個應用下部署列表頁面,點擊查看具體信息:
平臺化服務能力
具備了支持多應用的能力,接下來很自然地就想到:「開發者如何查看測試報告和了解測試細節呢」?
Goalkeeper 的設計包含了非常重要的一塊內容 —— 平臺化展示。這其實就是一個典型的:基于 Koa 的 NodeJS 后端服務,前端采用 React 作為多頁面應用方案。具體來說,每次端到端測試服務完成之后,將產生的所有測試報告類數據存入 Redis,開發者訪問 https://www.goalkeeper.com/dashboard,Koa 基于服務端渲染,獲取相關數據進行單頁面應用的平臺化展示。
這些相關數據,不僅包含了每個端到端測試的 case 內容、case 執行信息和結果,還包含了測試產生的富媒體文件地址(包括測試錄像,測試截圖等)。關于測試產生的富媒體文件,我們采用了容器持久化技術進行存儲,并對外提供靜態服務。
換句話說,Goalkeeper 在 NodeJS 的服務層面提供了:
容器上的端到端測試
整套單頁應用服務(包括查詢平臺和富媒體靜態服務等)
比如對于某項目應用某次部署測試信息,可查詢:
視頻信息以及截圖信息:
通報與預警中斷機制
為了更好地服務線上應用,我們也設計了高效的通報與預警機制。通報機制是指在一次提交部署開始,對應相關的端到端測試完成之后,通過企業微信和郵件將測試信息和測試平臺展示地址發送給提交人或負責人。預警中斷機制是指在端到端測試發現異常結果時,阻斷上線流程,并強通知給提交人和負責人。
我們的異常結果不僅包含測試 case 的失敗,更具特色的是也包含了視覺比對測試(visual testing)的異常。基于 Cypress,我們封裝了一套視覺測試插件,它能夠在任意節點自動對測試頁面進行全量截圖,并保存為對比基準圖片。在下一次測試進行時,對當前最新測試的相同節點進行頁面全量截圖,并進行和基準圖片的比對,如果兩幅圖片的不同像素超過一定百分比或一定像素閾值,則認為視覺比對失敗。如圖:
視覺比對測試,能夠大大解放測試 case 編寫的復雜度,非常適合樣式類測試的回歸。當然對于預期之中的圖片對比失敗,比如是正常的頁面 UI 改版,我們提供了「跳過視覺比對并更新基準圖片」的能力。
在任何一種測試失敗時,我們都會出發預警中斷流程。如圖,
架構和流程再梳理
我們通過分析一個請求的流程,再來總結梳理一下整個設計過程:
如圖,簡單示例:
更細節一點的圖示例:
當開發者提交代碼被合并,Merge Request id 為 123 的相關部署到內網環境之后,觸發中臺 hook,中臺會請求端到端測試 Goalkeeper 服務接口 ./run,該服務會為每一個應用創建一個進程處理,利用消息隊列機制跑該次部署的端到端測試,并最后將測試狀態結果(running/success/fail)寫入 Redis 當中。在這個過程中,中臺可以根據輪詢接口 ./consult,該接口查詢 Redis 中相關 Merge Request id 的測試狀態,中臺根據該結果值進行解鎖上線流程或繼續鎖定上線。
同時,在該次部署所對應的端到端測試結束時,會更新測試報告平臺內容,方便開發者訪問最新部署產生的端到端測試報告以及錄像等富媒體信息。相應的通知和預警機制也會在該階段觸發。
整套系統的關鍵依賴的服務項如圖:
整個 Goalkeeper 平臺主要依靠 Koa,Koa-static,Koa-router 來處理測試服務請求,并提供可查詢的測試報告平臺后臺服務。Cypress 是主要的端到端測試框架,它提供了豐富的插件和擴展能力,我們在 Cypress 的基礎上,封裝了大量貼合自己業務的插件和擴展,比如實現視覺比對測試的 @kfe/goalkeeper-image-snapshot,@kfe/goalkeeper-image-snapshot-runner。Cypress 對應的測試腳本 cases 我們用一個單獨的 Gitlab 倉庫維護,每次在部署發生并啟動端到端測試時,拉取最新的測試 cases 代碼。最后,@kfe/goalkeeper-report-generator 是整個可查詢平臺的倉庫,它是一個完整的基于 React SSR 的單頁面應用,根據 React-router,提供了:
首頁,儀表盤頁面(/dashboard)該頁面展示了所有已接入的應用項目基本信息
應用項目詳情頁(/:app)該頁面展示了當前應用項目的基本信息
項目部署列表頁(/:app/mrList)該頁面展示了當前應用項目下,所有的部署信息列表
測試詳情頁(/:app/mrList/:mrId)該頁面展示了當前部署對應的測試信息
測試媒體查詢頁面(/:app/:mrId/media)該頁面展示了當前部署對應測試的富媒體信息,包括測試錄屏、應用截圖等
總結
這篇文章我們介紹了 NodeJS 助力傳統端到端測試,最終實現破局和創新的雙贏項目案例。具體實施上,文章分析了如何實現容器上執行測試,如何接入 CI/CD pipeline,如何打造一個面向任何項目的 Goalkeeper 平臺。
對于前端開發者來說,學習并實施 NodeJS,最關鍵的就是格局。我們要熟知 NodeJS 特性,更要有所謂的“后端”思維,架構思維。我相信 NodeJS 的發展和落地,不是因為它一定具有了某種與生俱來的力量,而是它的某些特點符合技術發展趨勢或自然更迭規律。
我相信自己,生來如同璀璨的夏日之花,不凋不敗,妖冶如火,承受心跳的負荷和呼吸的累贅,樂此不疲。—— 泰戈爾 Tagore
我想,開發者們一定會承受心跳的負荷和呼吸的累贅,但對于技術的發展樂此不疲。請關注我們,后續會帶來更多前端技術實踐和各種知識!
關于本文 作者:@LucasHC 原文:https://juejin.im/post/6883397368155373576
@LucasHC曾分享過
【第2026期】「可視化搭建系統」——從設計到架構,探索前端領域技術和業務價值
【第1706期】不只是同構應用(isomorphic 工程化你所忽略的細節)
歡迎自薦投稿,前端早讀課等你來