做者:李強,騰訊web開發工程師
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。
原文連接:http://wetest.qq.com/lab/view/348.htmljavascript
直出這個名詞是在node出現後纔有的,在node出現前叫作服務端渲染。css
因此能夠把直出定義爲:「以node做爲後端語言實現的服務端渲染並輸出HTML字符串到客戶端的一項技術」。這樣瀏覽器渲染首屏的過程就由非直出下的先請求HTML,再請求js、css,最後再請求後臺數據。改成直出下的直接向node服務器發起請求,而後經過內網獲取到首屏數據後,組裝成HTML直接返回給瀏覽器。這裏說明下:直出並不必定就比非直出快,可是它能保證用戶在不一樣機型、不一樣網絡條件下都有一個比較好的體驗。html
那什麼是同構呢?前端
同構就是解決直出的一種思想,node出現後使得javascript腳本也能夠在服務器端執行,經過維護一套項目代碼,實如今先後端均可以執行的目的。java
QQ興趣部落擁有頁面80多個,開發人員14個,參與改造直出人力2個,使用同構的作法無疑能夠最大程度上下降改造和維護成本。node
億萬級用戶意味着什麼呢?目前部落用戶註冊和使用量達億萬級, 這樣大量的用戶意味着存在高併發,服務隨時都有可能掛掉的風險。前端頁面做爲整個web服務中最直接面向用戶的,一旦服務不可用就將是件讓全部人都很崩潰的事情了。
react
本文的目的在於解決兩個問題:webpack
一、 部落是怎樣從一個純前端項目改形成同構直出項目的web
二、在訪問量這麼大的狀況下,如何保證直出服務的可用性的問題。ajax
首先明確同構直出要作好哪些工做,總結下來有三點,可稱之爲同構直出三要素。
一、保證DOM的一致性,若是說原本瀏覽器經過純客戶端代碼渲染出來的頁面結構是下圖這樣,服務端渲染出來卻少了一個dom節點,那確定會致使頁面顯示有問題。
二、保證先後端數據的一致性,服務端不能執行dom操做,因此像綁定事件這樣的工做,就須要瀏覽器拉取到js腳本後才能進行,若是使用服務端獲取到的數據渲染出來的HTML結構與前端綁定事件時用到的數據不一致,就會致使問題。
三、保證路由的一致性,不能讓用戶訪問a頁面的時候,返回b頁面給用戶。
這樣就能夠明確作同構直出的方向,對於部落來講,原來的項目中就使用了react和redux,因此接下來會使用這兩個框架進行講解。
同構直出是一種優化的思想,不受任何框架限制,理解其中的原理纔是最重要的。那麼問題就來了,如何使用react來保證dom一致性,又如何使用redux保證數據一致性?先來看一下dom一致性的實現。
在使用react作同構直出時,很關鍵的一個因素就是它提供了虛擬DOM的支持,是一種在內存中的對象數,使其能夠支持在瀏覽器和node環境下執行,這也是代碼能夠同構的關鍵所在。在瀏覽器端經過render方法生成虛擬dom並掛載到真實DOM上。在服務端經過renderToString方法將虛擬dom拼裝成HTML字符串。使用這兩個方法就能夠解決dom一致性的問題了,來看一下具體的實現。
首先服務端經過調用rendertostring方法將react組件渲染爲html字符串,可是經過react組件渲染出來的並非標準的html格式,須要將其嵌入HTML模板中才可以被瀏覽器解析。當瀏覽器向直出服務器發起請求後,服務端將渲染好的html字符串返回,瀏覽器收到響應後進行渲染。瀏覽器經過解析html拉取到js腳本後,會執行render方法,在render方法處理過程當中會校驗節點中的checksum屬性,該屬性是在服務端調用rendertostring方法時追加的,用於前端校驗dom一致性,當校驗一致時,直接執行腳本中後續的綁定事件等行爲,若是不一致,將會進行虛擬DOM的diff操做,而後再進行增量更新DOM、綁定事件。在紅框處,能夠看到同構代碼的部分。
可是,Node環境和瀏覽器環境畢竟仍是不同的,有這麼多前端代碼是不能直接在node端執行的,應該怎樣在同構代碼上作好平臺區分呢?
解答這個問題以前,再來看一下數據一致性是如何保證的。
Redux使用單一的Store對象保存、管理頁面中的全部狀態,和虛擬dom同樣,是一種駐在內存中的對象,代碼徹底能夠同構。
保證數據一致性的原理其實很簡單。只要在最後組裝HTML字符串時,將服務端的狀態經過script標籤一塊兒輸出給前端,而後在前端初始化 Store 時使用該數據,便可完成了數據的傳遞和共享,達到保證數據一致性的目的。
這裏其實也存在一點問題,頁面的狀態大都來自於後臺數據,而發送異步請求的方法在前端是ajax方法,在node端是使用http模塊的request方法,這樣,咱們又該怎樣保證代碼的同構呢?
接下來能夠了解下怎樣解決上面遇到的一些問題,以及部落同構直出的改造方案。
整個解決問題和改造的過程我把它比做是一次裝修房子的過程,在裝修房子過程當中有這樣一些關鍵的角色,戶型結構圖、設計師、經過設計師設計出來的效果圖、還有房子,若是此時又買了一套戶型結構徹底同樣的房子須要裝修,那就和先後端須要渲染出來的HTML結構同樣是相似的場景了。因此能夠就戶型結構圖看作是源碼,設計師看作構建工具,效果圖看作構建打包後的bundle,已經裝修好的房子看作瀏覽器,等待裝修的房子看作node服務器。你們還記得咱們前面提到的第一個問題嗎?前端代碼中有些代碼是不能在node端執行的,該怎麼解決呢?
先來看一下若是在設計過程當中,想去掉一些東西該怎麼作?
是否是隻須要在戶型結構圖上作些標識,而後告訴設計師紅圈中的內容表示想去掉這部分的內容就能夠了?
就是按照這種思路,咱們在源碼中作了些標記,而後告訴構建工具被這個標記包裹的代碼是打包node端代碼時須要刪掉的,讓構建工具識別這個標籤的方法可使用自定義webpack loader或者babel插件。
而後回想下第二個問題,發送異步請求前端使用的是ajax方法,node端使用的是http模塊的request方法,這個問題怎麼解決?一樣的,在設計過程當中,若是想改個門,是否是直接告訴設計師就能夠了? 都不必在原始圖上進行任何修改了。
藉助這種思考方式,經過構建工具處理,就不須要對源碼進行任何更改。源碼中使用的是ajax方法,同時在node服務器上在全局變量下實現了一個window.ajax的方法,這樣經過自定義babel插件,在對源碼打包時,將ajax方法名替換成爲window.ajax方法名,問題就獲得瞭解決。
到了這一階段——結束了設計工做,有了效果圖,也就是已經打包出了一份能夠在node端執行的bundle,就下來就是須要到房子裏面去還原設計稿的時候了。
施工的話,單憑咱們本身確定不行,因此須要一個施工隊。
施工隊裏面有包工頭,負責承接項目,分發任務給各個工種按照設計稿進行施工。
一樣的原理,咱們在node服務器上引入了直出框架機的概念,幫咱們統一管理直出服務。框架機的第一層就是玄武和TSW(不理解玄武的同窗,這裏能夠把它當作是起了一個koa的server,負責監聽端口,接受請求並轉發到業務邏輯層按照打包好的bundle去處理。)爲了讓業務邏輯層沒必要針對每一個頁面作兼容,因此須要打包出來的server bundle具備固定的結構,那咱們就來看一下bundle是怎樣的一個結構。
源碼的結構大體是這樣子的,你們能夠看到這裏面有一個前端程序的打包入口,實現上是這樣的,裏面有對store和main組件入口的引用。由於源碼中沒有對服務端程序的打包入口,因此須要對store和main進行單獨打包。
最終構建出來的目錄大體是這樣的,以a頁面爲例,有HTML模板、組件入口腳本、建立store對象的腳本,最後還有一個首屏action的腳本。
這個腳本是作什麼的呢?
在action的腳本中封裝了全部異步請求的方法,對於頁面來講,由不少組件構成,每一個組件調用各自的action方法更新自身狀態,可是,首屏並不必定須要渲染全部組件,可能只須要展現組件1和組件2,因此這時就須要提取出首屏所需的action creator方法了,咱們把它封裝在了名爲firstAction腳本中以便構建工具打包後在服務端進行調用。這樣打包後的bundle中每一個頁面就都有了相同的結構。
這時就能夠在框架機中的業務邏輯層統一對直出頁面作處理了。當瀏覽器發起對頁面A的請求時,經過玄武將請求轉發到業務邏輯層,首先進行路由解析,確保路由一致性,這裏使用正則匹配獲取url中的模塊名,經過模塊名獲取頁面A的存放路徑。
而後爲請求建立沙箱環境,讓每一個請求都能在獨立的上下文環境中執行,實現上使用的是node的vm模塊,若是以前沒有接觸過的話能夠把框架機想象成是瀏覽器,每當有一個請求過來就會新開一個tab頁,請求處理完後關閉tab頁。
接着就是初始化一些全局對象,好比前面提到的window.ajax方法。而後將頁面A的腳本引入,經過store腳本建立store對象,經過firstAction腳本獲取首屏所需數據,執行rendertostring方法渲染組件,最後讀取A頁面的HTML模板,組裝成HTML字符串輸出給瀏覽器。這就是框架機基本的一個工做流程了。
最後對直出改造方案進行一下總結。首先是在node服務器上部署了一個直出框架機的服務,使用單獨的代碼倉庫進行維護和發佈。
而後經過打包構建工具構建出客戶端的bundle和服務端的bundle。因爲客戶端和服務端的一些差別,須要在源碼中使用特定的標籤將node端不能執行的代碼作個標記,同時還要新增一個供服務端使用的封裝了首屏action的腳本,在構建工具中新增server端的打包配置,並加入一些自定義的loader和babel插件幫助咱們構建出server端的bundle。
而後將server bundle發送到node服務器上,當瀏覽器發起請求後,框架機幫咱們組裝首屏html字符串並輸出給瀏覽器。瀏覽器進行渲染後,引入前端的js腳本,進行後續的dom更新和綁定事件等工做。
以上就是改造直出的整套方案。
首先要講的是本地開發調試在保證服務可用性方面的問題。
前面提到了框架機,那就先來講一下框架機的開發調試模式。本地開發是以tnpm命令行工具包的形式。對於本地開發調試模式也是和命令行工具包同樣,使用 tnpm link命令,創建命令的全局連接。Tnpm其實就是npm,只不過是企業內部私有npm倉庫,外部訪問不到。
有人說,平時開發時我連這一步也不想要怎麼辦?因而咱們增長了自動化測試。
能夠利用Mocha + Chai 幫助咱們實現一些代碼邏輯上的測試。
接下來就是容災。在代碼報錯、服務器崩潰的時候,須要一套容災方案來讓業務儘可能正常運做。
興趣部落設計了一套柔性可用的容災方案。當直出報錯的時候,會讓請求自動轉發到靜態資源,讓相對穩定的靜態資源接受用戶的請求,以保證業務不受干擾。
具體的原理是怎麼樣的呢?首先由一羣Nginx服務器集羣去調度用戶的請求,這些請求包括了直出服務器、CDN、後臺等等。一旦直出服務器掛掉了,它會自動將請求轉發到CDN服務器。
上面這裏是Nginx接入集羣的示例代碼。
業務上線前,須要先預估請求的量級,才能預先準備足夠的服務器,以抗住大量用戶的請求。所以須要作好壓力測試。
興趣部落在作同構直出的過程當中,使用了騰訊 WeTest 壓測大師,實現更智能和自動化地壓力測試。上圖是壓測大師的入口界面,能分別從系統角度、用戶角度、業務角度,多角度幫助開發人員發析直出業務的「接客」能力。
瞬時TPS圖表,分析了服務最優的承載能力。
經過服務器性能趨勢圖獲得CPU、內存的性能瓶頸。
還支持報告的一個對比,幫助比對分析每次業務更新後的壓測狀況。
直出順利完成,服務器也準備穩當了,此時就已具有了產品發佈的基本條件。但爲了讓產品對業務成效更有把握,這裏須要先作一個用戶灰度。
興趣部落這裏主要是詳情頁作了同構直出。所以針對業務場景,咱們經過在列表頁作一個區分,經過前端來控制灰度。直出的用戶走帶v2的連接,而非直出用戶則不帶。
產品發佈上線時,還須要對它進行全方位監控,以防出亂子。
以上的這些數據指標,都是須要時刻關注的。
興趣部落同構直出順利落地,成果也是至關不錯的。頁面能達到秒出,慢用戶佔比也從6.8%,降低到1.25%。
爲了幫助開發者發現服務器端的性能瓶頸,騰訊WeTest開放了上文提到的壓力測試功能,經過基於真實業務場景和用戶行爲進行壓力測試,實現針對性的性能調優,下降服務器採購和維護成本。
除了興趣部落之外,壓測大師還服務了包括王者榮耀、龍之谷手遊、軒轅傳奇手遊、火影忍者等多款高星級手遊,也包括QQ、NOW直播等明星產品。
爲了讓外部更多產品可以享受到簡單易用的壓測產品,騰訊WeTest決定將這份服務器測試能力產品化,以產品」壓測大師「的形式,正式對外開放,點擊連接:http://wetest.qq.com/gaps/ 便可使用。
若是對使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:800024531