去哪兒網前後端分離實踐
作者|興百放
編輯|覃雲
來源 | 前端之巔
本文是對去哪兒網前端業務方向負責人興百放在今年 GMTC 大會上的演講整理。
前後端分離方案
去哪兒網主要有三種前後端的分離方案。
第一種是項目分離,承載頁面分離。他的特點是簡單,快速,前端只關注瀏覽器方面,除瀏覽器端之外都是後端負責。當然缺點是溝通成本高,前期,前端需要使用 ng 或者代理工具調試,後期,還要把頁面給到後端,並且新建一個對應的路由。這樣來來回回,調試非常的複雜,一旦前後端同學涉及到跨部門,跨樓層合作,這些成本又會相應的增加。
第二種方式還是項目分離,只是後端的頁面,放到了前端項目裏,後端只需要配置路由,最終上線時,由發佈系統負責把前端中的頁面,自動同步到後端相應的目錄中。其中相應的目錄需要前後端提前約定,不然後端在渲染頁面的時候,就會找不到相應的文件。相比第一種方案,稍微有點進步。溝通成本會有一定的降低。不過如果需要在頁面裏做一些業務邏輯處理,還需要前端同學掌握和學習 velocity 語法,對於新同學而言看似掌握的了一門新語法,但實際操作起來並非想象中的流暢。另外考慮使用 React SSR 做頁面同構直出,這個方案還有一定的難度。
第三種方案是使用 Nodejs 作爲頁面渲染層,後端只負責數據的生產工作。這也是目前階段主要的使用方式。它的優點是前端同學對於整個頁面的生命週期有完全的控制權,包括開發,調試,部署,上線以及後期的性能監控,應用監控等等。可做的事情也更多,比如使用 React SSR 做同構渲染。當然,使用此種方案,對於前端同學的要求也會很高,除學習前端知識外,還要學習後端知識。另外由於整個應用都是由前端統一負責,所以還需要接收報警電話或者短信,7*24 小時,都在待命狀態。
靜態資源離線包方案(qp)
在三種方案演變的過程中,爲了讓用戶快速的看到頁面,我們還設計了一個靜態資源包的方案,這是它的整體的流程圖:
如果某項目想使用離線包,只需要簡單的兩步。
第一步,在項目的根目錄中,新建 index.yaml 配置文件。主要配置唯一的 ID,面向的 iOS 和 Android 版本,打包的內容,忽略的內容等;
第二步,進入打包平臺,選擇相應的項目,即可通過自動化工具生成 qp 文件,並且自動上傳到 qp 存放服務器中,其中會涉及到壓縮,加密,打包等一系列操作,無需人工幹預;
當用戶進入到客戶端,如果網絡環境是 wifi,會自動拉取所有的離線包,非 wifi 網絡,會選擇性的下載相應的離線包。在進入相應的頁面之前,會檢查本地是否有對應的離線包,如果沒有,會自動下載,走線上環境,反之,直接使用離線包中的資源。
用戶對離線包是完全無感知和透明的。
大家從整個流程上看相關的一些功能,可能覺得很簡單,不復雜,但實際上考慮的事情非常多:
1. 如何保證資源的安全性,不被中間人惡意篡改?
主要體現在 “傳輸安全”和“存儲安全”上。這裏我們採用的 RSA 加密方式,在打包平臺,使用私鑰對 qp 文件求 MD5,在客戶端使用公鑰對 qp 文件求 MD5 ,並和服務端所返回的 MD5 值進行對比校驗,若相等,則校驗通過。
2. 如何快速的回滾?
起初,採用的是假回滾的機制,簡單來說,一旦離線包有 BUG ,在重新發一版。這種流程看起來或聽起來沒有什麼問題,但實際操作起來,成本很高。因爲按照重發的思路,會重新從線上拉取代碼,如果這時線上代碼變了,打出的包內容也會變。
3. 如何下線和強制更新
下線:當某次發版的 qp 包有 BUG 時,可以進行下線操作。針對的是當前指定版本 qp 包。
強制更新:當某個 qp 包希望用戶下載到時,可以是用此操作,針對的是將要下載的 qp 包。
4. 如何提高更新率
不論架構多麼簡單或多麼複雜,更新率問題是最能體現出框架的好與壞。上面提到,有強制更新和普通更新,由於兩者的更新機制不同,最終的效果也不同。
最後,關於更新率的效果:
強制更新和普通更新這兩個機制實現的方式不一樣,所以它的更新效果也不一樣,強制更新的效果最明顯,它能在兩個小時之內達到一個 90% 的水平,普通更新得七八個小時之後才能穩定到 75% 左右。
Node.js 實踐
爲什麼 Node 沒有大規模使用呢?我總結了大概的原因:
- 一些前端開發,只關注瀏覽器端,服務器端開發關注很少,或者根本就不關注 ;
- 認爲 Node.js 只適合開發一些工具類的功能,對於後端開發是個玩具 ;
- Node.js 的生態不如其他後端語言生態健全 ;
- 涉及到後端開發的知識面比較廣,在沒有這些基礎知識或者經驗積累的基礎上,考慮問題比較片面,最終做出的系統問題比較多,容易被後端鄙視 ;
- 對於 Node.js 開發後端,對項目負責人要求比較高(項目的目錄規範,開發規範,系統的安全性,穩定性,可靠性,擴展性,維護成本等);
- 以往前端不需要 7 x 24 保持待命狀態,但是接觸後端後,需要接收報警短信,有時出現問題還需要馬上隨時隨地解決 ;
看似問題很多,但實質上只有兩個原因,一方面,自身知識儲備不夠。第二方面,對 Node.js 瞭解不深,不敢應用在生成環境中,即使應用到生產環境,一旦出現問題,不能快速及時的處理,導致高層認爲還不如其他後端語言穩定,降低了我們的話語權。
Node.js 到底能解決我們哪些的問題和痛點呢?
首先,提高開發效率,因爲有了 Node 之後就不需要配置 Nginx 了,也不需要配置一些代理工具了,所有的頁面生命週期都是由前端統一去管理的,這時候不需要其他人進行合作。
第二,降低溝通成本,除了接口格式外,不需要和後端進行交互了;
第三,前後端職責也更爲清晰,因爲這時候,界限更爲清晰了,後端只負責生產數據,它只提供數據就可以了,至於數據怎麼消費,以及怎麼用,都由前端去做;
第四,可以同時使用 React SSR 技術,做到首屏渲染,提高用戶體驗,除了首屏之外,還可以做異步的加載、SEO 等操作。
最後,Node.js 可提供一些服務,不僅能讓我們使用,還可以對外使用,如 RESTful API,這樣就不用有求於後端了。
三年前,公司內部就搞了一套基於 Express 的 Node.js 解決方案,包含日誌收集,監控,多進程,異常,模板等插件,方案本身也很全面,但在實際項目使用過程中,或多或少的有些不便,主要體現:
- 如何確定項⽬目⽬目錄劃分的規範,命名規範 (view or views);
- 確定規範後,如何保證⼤大家都認可,並且嚴格遵守;
- 如何保證系統的安全性、穩定性和擴展性,怎麼保證和我們內部系統做很無縫的去對接,這就要求有很好的擴展性;
- 守護進程程序的選擇 (pm2 or supervisor);
- 怎麼保證多環境運⾏行行規則 (local / beta / prod),因爲在我們實際項目中,可能對我們的 Local 或者對 Bata 或者對 PID 都有不同的規則,如果這時候沒有去做這件事,就有可能對我們的實際應用有可能造成一定的障礙;
- 如何利利⽤用系統 cpu 多核,以及多進程之間的通信。
針對這些問題,內部也進行了一些改進,但有些功能還是有些不盡人意。
在 17 年 4 月份,團隊內部又重新開始 Review 和調研。發現國內有兩個框架做的比較好,一個是 360 團隊的 Thinkjs ,另一個是阿里的 Eggjs ,兩個框架實現目的也是一致,只是使用的方式有些差別。
團隊內部針對這兩款框架,分別做了不同嘗試,最終從框架擴展的易用性,插件數量,以及部署等方面,選擇使用的是 Eggjs 作爲團隊內部的框架,以替代之前的框架。
插件開發
爲了對接我們的內部系統,我們還開發了不同功能的一些插件。
- egg-qversion,作用是關聯前後端靜態資源版本號
- egg-qconfig,對接公司內部的 qconfig 系統
- egg-qwatcher,對接公司內容的 watercher 系統
- egg-accesslog,產生 access.log 日誌
- egg-swift,對接 swift 系統
- egg-healthcheck,系統健康檢查
- egg-checkurl,應用存活檢查
去哪兒網原來的部署流程(service 方式) 問題
- 不能利⽤發佈系統中相應的端口和⽬錄字段,只能在 qunar_xx 服務中寫死, 不友好
- 不能區分多環境策略如 beta 環境和 prod 環境配置規則不一樣
- 啓動過程中出現錯誤,不方便定位問題,需要到機器上排查
- 寫系統服務需要了解 shell 命令和系統服務格式,對於前端開發同學,成本稍高
- 除了端口、項目路徑、運行環境,node.js 啓動方式外,處理邏輯相似
改進過的部署方式
- 在項目中建立 deploy_scripts 目錄,新增 start.sh (名稱可以隨便命名)
- 在 start.sh 中填⼊Node.js 啓動邏輯,比如 node index.js (之前是 N 行,如今最多兩⾏)
- 在發佈系統選擇 node 發佈方式,填⼊端⼝號,發佈路徑,以及啓動腳本名稱(start.sh),停止腳本填入發佈系統內置的 stop.sh(按照端口殺掉進程)
這是 start.sh 的一個樣例:
React SSR 實踐:
這是大致的結構:
這裏我們沒有使用高大上的技術,只是簡單使用了 Redux ,原因有兩個。一方面,學習成本底,不管對於新同學還是老同學,都能快速上手。第二方面,即使不使用 SSR ,前端代碼照常能運行。
這是 reactRender 的寫法。這裏額外附加了一個嗅探功能,以便前端能提前獲取設備信息。
再看視圖的寫法。
這裏把狀態數據,掛在到了 window 全局變量上了,當然這也是一個缺點吧。
React SSR 遇到的問題
共享代碼如何處理請求
因爲前後端共同使用一個 action,後端 dispatch 的時候,需要同步的自身調用自身,所以在請求時,需要配置一個完整的請求 URL 。同樣是自身調用自身,本身是沒有 cookie 等信息的,所以還需要透傳這些信息,方便後端使用。比如判斷登陸等
共享代碼如何處理錯誤
同樣,同一個 action 可能被後端調用,也可能被前端調用,如果不處理異常的話,對於定位和處理問題也是非常棘手。我們的做法是,最後一個參數,傳遞後端的 context ,在處理異常時,區分環境,有針對性的處理。
後端代碼獲得設備信息
有時,在後端渲染的時候,需要明確知道一些環境信息,比如是否在 APP 內,是否是是 IphoneX 等等,以便在初始渲染的時候,設置額外的信息。所以這裏使用的是高階組件,把這些檢測信息統一注入到組件中。這樣開發同學就不用在每個頁面重複寫這些信息了。
性能監控
對於性能方面,我們做得不是太多,因爲 eggjs 本身已經經歷過淘寶雙十一的洗禮, 相信在阿⾥內部對這塊已經做了不少優化,所以簡單使用的是公司級別的機器監控。
應用監控,分兩個方向。
- 第一個方向,是應用程序級別,比如應用程序錯誤數,請求後端接口時間消耗和異常信息,accesslog 日誌等等。
- 第二個方面,是前端頁面級別,比如腳本全局錯誤,靜態資源文件加載的錯誤,異步接口錯誤,頁面渲染時長等等
針對這兩方面的錯誤,我們有兩套系統,一個是日誌系統,一個是 Watcher 系統,這兩個系統是搭配合作的。
Watcher 系統,它主要的功能是打數,計數,圖形化展示,以及設置報警等功能。實時主動的提醒我們系統運行情況,能夠在第一時間發現問題,使故障影響範圍降到到最小。
大家可以看出,雖然在特定的時間點報出問題,但只限數量上的程度,具體什麼問題,Watcher 系統就不行了,還需要藉助第二套「日誌系統」。通過 kibana 可以實時查看所有錯誤信息。