原文連接https://www.tuicool.com/articles/z2eQNrI前端
公司內部的公用圖標字體平臺 iconfont 上線一年以來,運行良好,在內部承擔起了客戶端減容、提升前端開發效率的任務。但因爲其臃腫的架構致使的運行效率底下和頁面卡頓等問題也一直給使用者形成諸多不便。咱們對項目老架構致使的許多問題進行了探索總結,並針對問題提出了 react 同構的解決方案。node
在進行舊版平臺開發的時候,咱們採用了當時淘寶推出的先進思想: 中途島 midway 先後端分離方式 ,搭建了一個 node 中間層進行頁面的渲染,以求提高頁面的渲染速度。咱們舊版平臺的結構以下:react
從圖中咱們能夠看到,儘管咱們前端掌握了 server,能夠進行頁面渲染的控制,可是服務端的渲染和前端的渲染和路由依然是割裂的,之間有不少冗餘的內容。致使這些冗餘的主要緣由,其實仍是先後端渲染方式不一致以及先後端代碼的分離。webpack
咱們知道,在傳統的 MVC 架構的項目之中,js 代碼只佔 View 層的很小的一部分。隨着項目的漸進發展,前端功能的複雜度日益增高,致使項目難以維護;同時先後端語言並不一致(咱們都知道 Java 跟 JavaScript 基本上是雷鋒和雷峯塔的關係),不一樣的開發在一個項目裏操做極爲不便,所以才產生了先後端分離。git
可是隨着 js 向服務端的進發,咱們的中間層 server 也採用支持 js 的 node 來進行架構,因此先後端語言不一致的問題基本上抹平了;而前端功能複雜這一點,從剛纔的分析咱們也能夠看到,其實前端和後端在路由、渲染這些功能上是有很大的重合,所以前端的 server 和前端邏輯項目沒有必要進行分離。github
實際上,這裏咱們的先後端分離,已經有傳統意義上前端和後端代碼的分離、服務端和瀏覽器客戶端的分離,演變爲後端數據提供和前端提供渲染的分離。web
若是將先後端代碼糅合在一塊兒,那麼渲染這裏將會是服務端邏輯和客戶端邏輯的一個結合點,它們的模板、渲染方式都必定要一致,才能減小開發的工做量。算法
對於咱們舊版項目來講,服務端採用 handlebars 做爲模板,而前端採用 MVVM 模式的 avalon 的模板,二者在用法和理念上都是有必定衝突的。其中 MVVM 模式在服務端渲染中最棘手的問題就是: 要實現雙向數據綁定,必需要經歷一次 DOM 渲染 。這樣就致使後端只能渲染一箇中間狀態的模板,而後還須要前端在更改一次 DOM,沒法達到『直出』的效果。npm
這個問題看似困難,但在 react 出現以後,卻獲得了完美的解決:react 基於 virtual DOM,不須要掃描 DOM 來創建雙向綁定關係,只須要在每次狀態變更時進行 diff,有變化纔會進行更新。所以,咱們能夠在服務端直接渲染出 DOM 結構,若是前端最終生成的虛擬 DOM 跟後端直出的 DOM 保持一致,那麼就不須要更改 DOM 結構,大幅度提高渲染速度。redux
若是要實現先後端代碼同構,其實只要保證兩個一致便可: 包管理工具 和 模塊依賴方式 的一致。這裏咱們能夠看到,這兩者的一致性都能得以保證:
有了這兩者的保證,咱們就能夠完美的解決同構的問題,剩下須要考慮的就是如何處理服務端渲染了。
react 自誕生之初就對服務端渲染很是重視,它的『全家桶』都對服務端渲染進行了良好的支持。所謂的『全家桶』指的就是你們耳熟能詳的 react 御三家:react、 react-router 和 redux 。
ReactDOM 在這裏提供的支持就是 ReactDOM.render
和 ReactDOM.renderToString
函數,其中前者會在瀏覽器端生成 DOM 結構,後者會在服務端生成對應的 HTML 字符串模板。React 會在生成的 DOM 結構上添加一個 data-react-checksum
的屬性,這是一個 adler32 算法的校驗和,用以確保兩份模板的一致性。
同時 react 的生命週期在先後端渲染過程當中也有所不一樣。前端渲染的組件擁有完整的生命週期,然後端渲染僅有 componentWillMount
的生命週期。這就意味着,若是咱們想進行先後端共同操做的邏輯,如發送數據請求等,能夠放在 componentWillMount
的生命週期中;若是想單獨處理客戶端的邏輯,能夠放在其餘生命週期,如 componentDidMount
中。
react-router 是 react 的路由 - 視圖控制庫,能夠書寫便捷的聲明式路由以控制不一樣頁面的渲染。react-router 自己是一個狀態機,根據配置好的路由規則,和輸入的 url 路徑,經過 match
方法找到對應的組件並進行渲染。
這套機制在前端和後端都是相通的,例如在後端,就是下面這樣一種實現形式來進行渲染:
app.use(async (ctx, next) => {
match({
location: ctx.originalUrl,
routes
}, callback)
// 渲染完成以後,調用 callback 回調
// 將 <RouterContext> 組件 renderToString 返回前端便可
})
對於前端來講,其實也是處理的上面這些邏輯,不過它被很好的封裝在 <Router>
組件中,咱們只須要寫好聲明式的路由,這一切就能夠隨着 url 的變化自動發生。
redux 是 react 的數據流管理庫,它對服務端渲染的支持很簡單,就是 單一 store 和 狀態可初始化 。後端在進行渲染的時候會構建好單一的 store,並將構建好的初始狀態經過以 JSON 格式,經過全局變量寫到生成好的 HTML 字符串模板上。前端經過獲取初始狀態,生成跟後端渲染完成後如出一轍的 store,就能夠保證先後端渲染數據的一致,以確保先後端生成 DOM 結構的一致。
項目在使用了 react 進行同構構建以後,首屏渲染性能獲得了明顯的提高,以前頁面大約 1500ms 才能展現關鍵數據,而以後的頁面只須要 230ms 就能夠進行數據展現了。
同時項目獲得了精簡,由以前的兩個項目合併爲一個,代碼也得以通用。
項目通過 react 同構,以前的許多問題都得以解決:
其實新的 react 服務端渲染架構並非對以前『中途島』的推翻,而是它理念的演進,核心思想都是由服務端來控制渲染,只是這裏將以前互不干涉的先後端項目糅合到了一塊兒,使用同構的方式簡化了渲染層的工做。對於已使用 node 中間層的項目,不妨嘗試一下 react 同構的技術方案,它會使你的開發效率和首屏性能獲得飛速提高。