摘要:同構的關鍵要素完善的屬性及生命周期與客戶端的時機是同構的關鍵。的一致性在前后端渲染相同的,將輸出一致的結構。以上便是在同構服務端渲染的提供的基礎條件。可以將封裝至的中,在服務端上生成隨機數并傳入到這個中,從而保證隨機數在客戶端和服務端一致。
原文地址
React 的實踐從去年在 PC QQ家校群開始,由于 PC 上的網絡及環境都相當好,所以在使用時可謂一帆風順,偶爾遇到點小磕絆,也能夠快速地填補磨平。而最近一段時間,我們將手Q的家校群重構成 React,除了原有框架上存在明顯問題的原因外,選擇React也是因為它確實有足夠的吸引力以及優勢,加之在PC家校群上的實踐經驗,斟酌下便開始了,到現在已有頁面在線上正常跑起。
由于移動端上的網絡及環境迥異,性能偏差。所以在移動端上用 React 時,遇到了不少的坑點,也花了一些力氣在上面。關于在移動端上的優化,可看我們團隊的另一篇文章的 React移動端web極致優化
一提到優化,不得不提直出
關于這塊可以查看 Web性能優化之 “直出” 理論與實踐總結,這篇文章較詳細的分析直出的概念及一步步優化,也結合了 手Q家校群使用快速的數據直出方式來優化性能的總結與性能數據分析
一提到 React,不得不提同構
同構基于服務端渲染,卻不止是服務端渲染。
服務端渲染的方案早在后臺程序前后端包辦的時代上就有了,那時候使用JSP、PHP等動態語言將數據與頁面模版整合后輸出給瀏覽器,一步到位
這個時候,前端開發跟后端揉為一體,項目小的時候,前后端的開發和調試還真可以稱為一步到位。但當項目龐大起來的時候,無論是修改某個樣式要起一個龐大服務的尷尬,還是前后端糅合的地帶變得越來越難以維護,都很難過。
前后分離前后端分離后,服務端渲染的模式就開始被淡化了。這時候的服務端渲染比較尷尬,由于前后端的編碼語言不同,連頁面模板都不能復用,只能讓在前后端開發完成后,再將前端代碼改為給后端使用的頁面模板,增大了工作量。最終也還是跟后臺包辦殊途同歸。
語言變通Node 駕著祥云騰空而來,谷歌 V8 引擎給力支持,眾前端拿著看家本領(JavaScript)開始涉足服務端,于是服務端渲染上又一步進階
由于前后端時候的相同的語言,所以前后端在代碼的共用上達到了新的高度,頁面模版、node modules 都可以做成前后通用。同構的雛形,只是共用的代碼還是有局限。
前后同構有了Node 后,前端便有了更多的想象空間。前端框架開始考慮兼容服務端渲染,提供更方便的 API,前后端共用一套代碼的方案,讓服務端渲染越來越便捷。當然,不只是 React 做了這件事,但 React 將這種思想推向高潮,同構的概念也開始廣為人傳。
關于 React 網上已有大多教程,可以查看阮老師的react-demos。關于 React 上的數據流管理方案,現在最為火熱的 Redux 應該是首選,具體可以查看另一篇文章 [React 數據流管理架構之Redux](),此篇就不再贅述,下面講講 React 同構的理論與在手Q家校群上的具體實踐總結。
React 同構 React 虛擬 DomReact 的虛擬 Dom 以對象樹的形式保存在內存中,并存在前后端兩種展露原型的形式
客戶端上,虛擬 Dom 通過 ReactDOM 的 Render 方法渲染到頁面中
服務端上,React 提供的另外兩個方法:ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 可將其渲染為 HTML 字符串。
React 同構的關鍵要素完善的 Compponent 屬性及生命周期與客戶端的 render 時機是 React 同構的關鍵。
DOM 的一致性
在前后端渲染相同的 Compponent,將輸出一致的 Dom 結構。
不同的生命周期
在服務端上 Component 生命周期只會到 componentWillMount,客戶端則是完整的。
客戶端 render 時機
同構時,服務端結合數據將 Component 渲染成完整的 HTML 字符串并將數據狀態返回給客戶端,客戶端會判斷是否可以直接使用或需要重新掛載。
以上便是 React 在同構/服務端渲染的提供的基礎條件。在實際項目應用中,還需要考慮其他邊角問題,例如服務器端沒有 window 對象,需要做不同處理等。下面將通過在手Q家校群上的具體實踐,分享一些同構的 Tips 及優化成果
以手Q家校群 React 同構實踐為例手Q家校群使用 React + Redux + Webpack 的架構
同構實踐 Tips 1. renderToString 和 renderToStaticMarkupReactDOMServer 提供 renderToString 和 renderToStaticMarkup 的方法,大多數情況使用 renderToString,這樣會為組件增加 checksum
React 在客戶端通過 checksum 判斷是否需要重新render
相同則不重新render,省略創建DOM和掛載DOM的過程,接著觸發 componentDidMount 等事件來處理服務端上的未盡事宜(事件綁定等),從而加快了交互時間;不同時,組件將客戶端上被重新掛載 render。
renderToStaticMarkup 則不會生成與 react 相關的data-*,也不存在 checksum,輸出的 html 如下
在客戶端時組件會被重新掛載,客戶端重新掛載不生成 checknum( 也沒這個必要 ),所以該方法只當服務端上所渲染的組件在客戶端不需要時才使用
2. 服務端上的數據狀態與同步給客戶端服務端上的產生的數據需要隨著頁面一同返回,客戶端使用該數據去 render,從而保持狀態一致。服務端上使用 renderToString 而在客戶端上依然重新掛載組件的情況大多是因為在返回 HTML 的時候沒有將服務端上的數據一同返回,或者是返回的數據格式不對導致,開發時可以留意 chrome 上的提示如
3. 服務端需提前拉取數據,客戶端則在 componentDidMount 調用平臺上的差異,服務端渲染只會執行到 compnentWillMount 上,所以為了達到同構的目的,可以把拉取數據的邏輯寫到 React Class 的靜態方法上,一方面服務端上可以通過直接操作靜態方法來提前拉取數據再根據數據生成 HTML,另一方面客戶端可以在 componentDidMount 時去調用該靜態方法拉取數據
4. 保持數據的確定性這里指影響組件 render 結果的數據,舉個例子,下面的組件由于在服務端與客戶端渲染上會因為組件上產生不同隨機數的原因而導致客戶端將重新渲染。
Class Wrapper extends Component { render() { return ({Math.random()}
); } };
可以將 Math.random() 封裝至Component 的 props 中,在服務端上生成隨機數并傳入到這個component中,從而保證隨機數在客戶端和服務端一致。如
Class Wrapper extends Component { render() { return ({this.props.randomNum}
); } };
服務端上傳入randomNum
let randomNum = Math.random() var html = ReacDOMServer.renderToString(5. 平臺區分);
當前后端共用一套代碼的時候,像前端特有的 Window 對象,Ajax 請求 在后端是無法使用上的,后端需要去掉這些前端特有的對象邏輯或使用對應的后端方案,如后端可以使用 http.request 替代 Ajax 請求,所以需要進行平臺區分,主要有以下幾種方式
1.代碼使用前后端通用的模塊,如 isomorphic-fetch
2.前后端通過webpack 配置 resolve.alias 對應不同的文件,如
客戶端使用 /browser/request.js 來做 ajax 請求
resolve: { alias: { "request": path.join(pathConfig.src, "/browser/request"), } }
服務端 webpack 上使用 /server/request.js 以 http.request 替代 ajax 請求
resolve: { alias: { "request": path.join(pathConfig.src, "/server/request"), } }
3.使用 webpack.DefinePlugin 在構建時添加一個平臺區分的值,這種方式的在 webpack UglifyJsPlugin 編譯后,非當前平臺( 不可達代碼 )的代碼將會被去掉,不會增加文件大小。如
在服務端的 webpack 加上下面配置
new webpack.DefinePlugin({ "__ISOMORPHIC__": true }),
在JS邏輯上做判斷
if(__ISOMORPHIC__){ // do server thing } else { // do browser thing }
4.window 是瀏覽器上特有的對象,所以也可以用來做平臺區分
var isNode = typeof window === "undefined"; if (isNode) { // do server thing } else { // do browser thing }6. 只直出首屏頁面可視內容,其他在客戶端上延遲處理
這是為了減少服務端的負擔,也是加快首屏展示時間,如在手Q家校群列表中存在 “我發布的” 和 “全部” 兩個 tab,內容都為作業列表,此次實踐在服務端上只處理首屏可視內容,即只輸出 “我發布的” 的完整HTML,另外一個tab的內容在客戶端上通過 react 的 dom diff 機制來動態掛載,無頁面刷新的感知。
7. componentWillReceiveProps 中,依賴數據變化的方法,需考慮在 componentDidMount 做兼容舉個例子,identity 默認為 UNKOWN,從后臺拉取到數據后,更新其值,從而觸發 setButton 方法
componentWillReceiveProps(nextProps) { if (nextProps.role.get("identity") !== UNKOWN && nextProps.role.get("identity") !== this.props.role.get("identity"))) { this.setButton(); } }
同構時,由于服務端上已做了第一次數據拉取,所以上面代碼在客戶端上將由于 identity 已存在而導致永不執行 setButton 方法,解決方式可在 componentDidMount 做兼容處理
componentDidMount() { // .. 判斷是否為同構 if (identity !== UNKOWN) { this.setButton(identity); } }8. redux在服務端上的使用方式 (redux)
下圖為其中一種形式,先進行數據請求,再將請求到的數據 dispatch 一個 action,通過在reducer將數據進行 redux 的 state 化。還有其他方式,如直接 dispatch 一個 action,在action里面去做數據請求,后續是一樣的,不過這樣就要求請求數據的模塊是 isomorphism 即前后端通用的。
設計好 store state 是使用 redux 的關鍵,而在服務端上,合理的扁平化 state 能在其被序列化時,減少 CPU 消耗
10. 兩個 action 在同個component中數據存在依賴關系時,考慮setState的異步問題 (redux)客戶端上,由于 react 中 setState 的異步機制,所以在同個component中觸發多個action,會出現一種情況是:第一個 action 對 state 的改變還沒來得及更新component時,第二個action便開始執行,即第二個 action 將使用到未更新的值。
而在同構中,如果第一個 action (如下的 fetchData)是在服務端執行了,第二個 action 在客戶端執行時將使用到的是第一個 action 對 state 改變后的值,即更新后的值。這時,同構需要做兼容處理。
fetchData() { this.props.setCourse(lastCourseId, lastCourseName); } render() { this.props.updateTab(TAB); }11. immutable 在同構上的姿勢 (immutable/redux)
手Q家校群上使用了 immutable 來保證數據的不可變,提高數據對比速度,而在同構時需要注意兩點
1.服務端上,從 store 中拿到的 state 為immutable對象,需轉成 string 再同HTML返回
2.客戶端上,從服務端注入到HTML上的 state 數據,需要將其轉成 immutable對象,再放到 configureStore 中,如
var __serverData__ = Immutable.fromJS(window.__serverData__); var store = configureStore(__serverData__);12. 使用 webpack 去做 ES6 語法兼容 (webpack)
實際上,如果是一個多帶帶的服務的話,可以使用babel提供的方式來讓node環境兼容好 E6
require("babel-register")({ extensions: [".jsx"], presets: ["react"] }); require("babel-polyfill");
但如果是以同一個直出服務器,多個項目的直出代碼都放在這個服務上,那么,還是建議使用 webpack 的方式去兼容 ES6,減少 babel 對全局環境的影響。使用 webpack 的話,在項目完成后,可將 es6 代碼編譯成 es5 再放到真正的 server 上,這樣也可以減少動態編譯耗時。
13. 不使用 webpack 的 css in js 的方式使用webpack時,默認是將css文件以 css in js 的方式打包起來,這種情況將增加服務端運行耗時,通過將 css 外鏈,或在webpack打包成獨立的css文件后再inline進去,可以減少服務端的處理耗時及負荷。
14. UglifyJsPlugin 在服務端編譯時慎用上面提及使用webpack編譯后的代碼放到真正的server上去跑,在前端發布前一般會進行代碼uglify,而后端實際上沒多大必要,在實際應用中發現,使用 UglifyJsPlugin 后運行服務端會報錯,需慎用。
15. 糾正 __dirname 與 __filename 的值 (webpack)當服務端代碼需要使用到 __dirname 時,需在 webpack.config.js 配置 target 為 node,并在 node 中聲明__filename和__dirname為true,否則拿不到準確值,如在服務端代碼上添加 console.log(__dirname); 和 console.log(__filenam );
在服務端使用的 webpack 上指定 target 為 node,如下
target: "node", node: { __filename: true, __dirname: true }
經 webpack 編譯后輸出如下代碼,可看出 __dirname 和 __filename 將正確輸出
而不在webpack上配置時,__dirname則為 / ,__filename則為文件名,這是不正確的
使用 webpack 將一個模塊編譯后將形成一個立即執行函數,函數中返回對象。如果需要將編譯后的代碼也作為一個模塊供其他地方使用時,那么需要重新將該模塊暴露出去( 如當業務上的直出代碼只是作為直出服務器的其中一個任務時,那么需要將編譯后的代碼作為一個模塊 exports 出去,即在編譯后代碼前重新加上 module.exports =,從而直出服務將能夠使用到這個編譯后的模塊代碼 )。寫了一個 webpack 插件來自動添加 module.exports,比較簡單,有興趣的歡迎使用 webpack-add-module-expors,效果如下
編譯前
編譯后( 不含module.exports )
使用 webpack-add-module-expors編譯后自動將模塊exports出去
當服務端上不想處理樣式模塊或一些瀏覽器才需要的模塊(如前端上報)時,需要在服務端上將其忽略。嘗試 webpack 自帶的 webpack.IgnorePlugin 插件后出現一些奇奇怪怪的問題,重溫 如何開發一個 Webpack Loader ( 一 ) 時想起 webpack 在執行時會將原文件經webpack loaders進行轉換,如 jsx 轉成 js等。所以想法是將在服務端上需要忽略的模塊,在loader前執行前就將其忽略。寫了個 ignored-loader,可以將需要忽略的模塊在 loader 執行前直接返回空,所以后續就不再做其他處理,簡單但也滿足現有需求。
優化成果服務端上的耗時增加了,但整體上的首屏渲染完成時間大大減少
服務端上增加的耗時服務端渲染方案將數據的拉取和模板的渲染從客戶端移到了服務端,由于服務端的環境以及數據拉取存在優勢(詳見 Web性能優化之 “直出” 理論與實踐總結),所以在相比下,這塊耗時大大減少,但確實存在,這兩塊耗時是服務端渲染相比于客戶端渲染在服務端上多出來。所以本次也做了耗時的數據統計,如下圖
從統計的數據上看,服務端上數據拉取的時間約 61.75 ms,服務端render耗時為16.32 ms,這兩塊時間的和為 78 ms,這耗時還是比較大。所以此次在同構耗時在計算上包含了服務端數據拉取與模板渲染的時間
首屏渲染完成時間對比服務端渲染時由于不需要等待 JS 加載和 數據請求(詳見 Web性能優化之 “直出” 理論與實踐總結),在首屏展示時間耗時上將大大減少,此次在手Q家校群列表頁首屏渲染完成時間上,優化前平均耗時約1281.39 ms,而同構優化后平均耗時為 552.82 ms,有了 728ms 的優化,提升約 56.7% 的性能,秒開搓搓有余!
在Chrome上頁面展示情況對比1.優化前
2.優化后(同構直出)
可明顯看出同構直出后,白屏時間大大減少,可交互時間也得到了提前,產品體驗將變得更好。
總結服務端渲染的方式能夠很好的減少首屏展示時間,React 同構的方式讓前后端模板、類庫、以及數據模型上共用,大大減少的服務端渲染的工作量。
由于在服務端上渲染模板,render 時過多的調用棧增加了服務端負載,也增加了 CPU 的壓力,所以可以只直出首屏可視區域,減少Component層級,減少調用棧,最后,做好容災方案,如真的服務端掛了( 雖然情況比較少 ),可以直接切換到普通的客戶端渲染方案,保證用戶體驗。
以上,便是近期在 React 同構上的實踐總結,如有不妥,懇請斧正,謝謝。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86344.html
摘要:前戲補上參會的完整記錄,這個問題從一開始我就是準備自問自答的,希望可以通過這種形式把大會的干貨分享給更多人。 showImg(http://7xqy7v.com1.z0.glb.clouddn.com/colorful/blog/feday2.png); 前戲 2016/3/21 補上參會的完整記錄,這個問題從一開始我就是準備自問自答的,希望可以通過這種形式把大會的干貨分享給更多人。 ...
摘要:我們的目標是讓的頁面也能夠擁有般的體驗,如果你還在尋求什么技術能夠讓老板虎軀一震拯救你的,那么這篇文章或許能夠幫助到你。這是一個使用編寫的頁面,運行于多端,包括企鵝輔導手機手機瀏覽器。經過我們的測試發現安卓基本上都是支持的,需要以上才支持。 本文由云+社區發表作者:思衍Jax showImg(https://segmentfault.com/img/remote/1460000017...
摘要:我們的目標是讓的頁面也能夠擁有般的體驗,如果你還在尋求什么技術能夠讓老板虎軀一震拯救你的,那么這篇文章或許能夠幫助到你。這是一個使用編寫的頁面,運行于多端,包括企鵝輔導手機手機瀏覽器。經過我們的測試發現安卓基本上都是支持的,需要以上才支持。 本文由云+社區發表 作者:思衍Jax 天下武功,唯 (wei) 快(fu) 不(bu) 破(po)。 隨著近幾年的前端技術的高速發展,越來越多的...
摘要:同構和直出服務端渲染出首屏,主要為了減少用戶等待的時間,縮短白屏時間,在移動數據網絡情況下能夠獲得較好的用戶體驗。在優化渲染時間的時候監控頁面情況很有用。 @(StuRep)2016.06.11 react+node同構和直出 服務端渲染出首屏,主要為了減少用戶等待的時間,縮短白屏時間,在移動數據網絡情況下能夠獲得較好的用戶體驗。 了解了一下react實現同構和直出的方案,收藏了一些還...
閱讀 3960·2021-11-24 09:38
閱讀 1225·2021-10-19 11:42
閱讀 1829·2021-10-14 09:42
閱讀 2154·2019-08-30 15:44
閱讀 544·2019-08-30 14:04
閱讀 2888·2019-08-30 13:13
閱讀 1949·2019-08-30 12:51
閱讀 956·2019-08-30 11:22