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